// utils/pcm-player.js export default class PCMPlayer { constructor(options) { this.queue = []; this._options = options this.AllTexts = []; this.audioCtx = null; this.startTime = 0; this._t = null } feed(data) { if (!this.audioCtx) { this.audioCtx = wx.createWebAudioContext(); } this.playPCM(data); } playPCM(pcmBuffer, sampleRate = 16000) { const length = pcmBuffer.byteLength / 2; // 16-bit = 2 bytes per sample const audioBuffer = this.audioCtx.createBuffer(1, length, sampleRate); const channelData = audioBuffer.getChannelData(0); // Float32Array const view = new DataView(pcmBuffer); // 将 16-bit PCM 转换为 Float32 [-1, 1] for (let i = 0; i < length; i++) { const sample = view.getInt16(i * 2, true); // little-endian channelData[i] = sample / 0x8000; // convert to [-1, 1] } this.queue.push(audioBuffer); if (!this.isPlaying) { // 开始播放 this.startTime = this.audioCtx.currentTime ?? 0; this.playNext(); this.isPlaying = true; setTimeout(() => { this.syncLoop(); }); } } // 监听音频播放进度 syncLoop() { const loop = () => { 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(); } playNext() { if (this.queue.length === 0) { // 结束 this.isPlaying = true; return; } this.isPlaying = true; const source = this.audioCtx.createBufferSource(); source.buffer = this.queue.shift(); source.connect(this.audioCtx.destination); source.start(); source.onended = () => { setTimeout(() => { this.playNext(); }); }; } // 增加文字 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() } }