You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
168 lines
4.4 KiB
168 lines
4.4 KiB
export default class PcmPlayer {
|
|
constructor(options) {
|
|
this._options = options;
|
|
this.audioCtx = null;
|
|
this.rate = this._options.rate ?? 16000;
|
|
this.ch = this._options.ch ?? 1;
|
|
this.fadeMs = this._options.fadeMs ?? 20;
|
|
this.fadeSamples = (this.rate * this.fadeMs) / 1000;
|
|
this.queue = [];
|
|
this.isPlaying = false;
|
|
this.startTime = 0;
|
|
this._t = null;
|
|
this.AllTexts = [];
|
|
this.totalLen = 0
|
|
}
|
|
|
|
feed(data) {
|
|
if (!this.audioCtx) {
|
|
this.audioCtx = wx.createWebAudioContext();
|
|
}
|
|
|
|
this.queue.push(new Int16Array(data));
|
|
if (!this.isPlaying) this._play();
|
|
}
|
|
|
|
loadAudio = (url) => {
|
|
return new Promise((resolve) => {
|
|
wx.request({
|
|
url,
|
|
responseType: "arraybuffer",
|
|
success: (res) => {
|
|
console.log("res.data", res.data);
|
|
audioCtx.decodeAudioData(
|
|
res.data,
|
|
(buffer) => {
|
|
resolve(buffer);
|
|
},
|
|
(err) => {
|
|
console.error("decodeAudioData fail", err);
|
|
reject();
|
|
}
|
|
);
|
|
},
|
|
fail: (res) => {
|
|
console.error("request fail", res);
|
|
reject();
|
|
},
|
|
});
|
|
});
|
|
};
|
|
|
|
async _play() {
|
|
this.isPlaying = true;
|
|
if (!this.audioCtx) {
|
|
return;
|
|
}
|
|
// 开始播放
|
|
this.startTime = this.audioCtx.currentTime ?? 0;
|
|
setTimeout(() => {
|
|
this.syncLoop();
|
|
});
|
|
while (this.queue.length) {
|
|
const pcm = this.queue.shift();
|
|
const len = pcm.length / this.ch;
|
|
const buf = this.audioCtx.createBuffer(this.ch, len, this.rate);
|
|
|
|
// 1. 归一化(防溢出)
|
|
for (let c = 0; c < this.ch; c++) {
|
|
const data = buf.getChannelData(c);
|
|
for (let i = 0; i < len; i++) {
|
|
const val = pcm[i * this.ch + c];
|
|
data[i] = val > 0 ? val / 0x7fff : val / 0x8000;
|
|
}
|
|
}
|
|
|
|
// 2. 帧边界淡入淡出(防不连续滋滋)
|
|
const fadeSamples = Math.min(this.fadeSamples, len); // 5ms@16kHz
|
|
const data = buf.getChannelData(0);
|
|
for (let j = 0; j < fadeSamples; j++) {
|
|
const gain = j / fadeSamples;
|
|
data[j] *= gain; // fade-in
|
|
data[len - 1 - j] *= gain; // fade-out
|
|
}
|
|
|
|
// 3. 播放并等待结束
|
|
await new Promise((resolve, reject) => {
|
|
if (!this.isPlaying) {return reject()}
|
|
const src = this.audioCtx.createBufferSource();
|
|
src.buffer = buf;
|
|
src.connect(this.audioCtx.destination);
|
|
src.onended = resolve;
|
|
src.start();
|
|
});
|
|
}
|
|
this.isPlaying = false;
|
|
}
|
|
|
|
// 监听音频播放进度
|
|
syncLoop() {
|
|
const loop = () => {
|
|
if (!this.audioCtx) {
|
|
return;
|
|
}
|
|
const now = this.audioCtx.currentTime - this.startTime;
|
|
// 找当前字幕
|
|
const index = this.AllTexts.findIndex(
|
|
(s) => now >= s.beginTime && now < s.endTime
|
|
);
|
|
if (index > -1) {
|
|
const obj = this.AllTexts[index];
|
|
if (this.AllTexts.length == 1) {
|
|
// 最后一条数据
|
|
obj.is_final = true;
|
|
}
|
|
const tmp = this.AllTexts.shift();
|
|
this._options.onPlay && this._options.onPlay(tmp);
|
|
if (!this.AllTexts.length) {
|
|
clearTimeout(this._t);
|
|
return;
|
|
}
|
|
}
|
|
if (!this.AllTexts.length) {
|
|
this._t = setTimeout(loop, 16);
|
|
} else {
|
|
if (now < this.AllTexts[this.AllTexts.length - 1].endTime) {
|
|
this._t = setTimeout(loop, 16);
|
|
} else {
|
|
// 结束
|
|
clearTimeout(this._t);
|
|
console.log("语音播放结束");
|
|
this.destroy();
|
|
}
|
|
}
|
|
};
|
|
loop();
|
|
}
|
|
|
|
// 增加文字
|
|
addText(res) {
|
|
if (res.result && res.result.subtitles && res.result.subtitles.length) {
|
|
const tmpText = res.result.subtitles.map((it) => {
|
|
return {
|
|
...it,
|
|
content: it.text,
|
|
beginTime: it.beginTime / 1000,
|
|
endTime: it.endTime / 1000,
|
|
request_id: res.requestId,
|
|
sessionId: res.sessionId,
|
|
};
|
|
});
|
|
this.AllTexts = [...this.AllTexts, ...tmpText];
|
|
}
|
|
}
|
|
|
|
close() {
|
|
this.isPlaying = false;
|
|
this.audioCtx.close();
|
|
}
|
|
destroy() {
|
|
this.queue = [];
|
|
this.isPlaying = false;
|
|
this.audioCtx && this.audioCtx.close();
|
|
this.audioCtx = null;
|
|
this.AllTexts = [];
|
|
this.startTime = 0;
|
|
this._options.onAudioEnd && this._options.onAudioEnd();
|
|
}
|
|
}
|
|
|