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(); } }