/** * PCM 流式播放 AudioWorklet Processor * 动态扩容缓冲区 + 线性插值重采样(24kHz PCM → 系统采样率) * 豆包 TTS PCM 格式:24000Hz / 16bit / mono / little-endian (pcm_s16le) */ class PcmPlaybackProcessor extends AudioWorkletProcessor { constructor() { super(); // 缓冲区:初始 60 秒 @ 24kHz = 1440000 采样,足够容纳 TTS 突发数据 this._bufferSize = 1440000; this._buffer = new Float32Array(this._bufferSize); this._writePos = 0; this._readPos = 0; this._stopped = false; // 重采样:从 24kHz 到系统采样率(sampleRate 是 AudioWorklet 全局变量) this._srcRate = 24000; this._ratio = this._srcRate / sampleRate; this._fractionalPos = 0; this.port.onmessage = (e) => { const { type } = e.data; if (type === 'pcm') { this._enqueuePcm(e.data.buffer); } else if (type === 'clear') { this._writePos = 0; this._readPos = 0; this._fractionalPos = 0; } else if (type === 'stop') { this._stopped = true; } }; } /** 缓冲区中可读采样数 */ _available() { const diff = this._writePos - this._readPos; return diff >= 0 ? diff : diff + this._bufferSize; } /** 将 Int16 PCM 数据转为 Float32 写入缓冲区,空间不足时自动扩容 */ _enqueuePcm(arrayBuffer) { const int16 = new Int16Array(arrayBuffer); const len = int16.length; const avail = this._available(); // 如果剩余空间不够,扩容到当前 2 倍 if (avail + len >= this._bufferSize - 1) { this._grow(Math.max(this._bufferSize * 2, avail + len + this._bufferSize)); } for (let i = 0; i < len; i++) { this._buffer[this._writePos] = int16[i] / 32768; this._writePos = (this._writePos + 1) % this._bufferSize; } } /** 扩容缓冲区,保留已有数据 */ _grow(newSize) { const newBuf = new Float32Array(newSize); const avail = this._available(); // 按顺序拷贝可读数据到新缓冲区头部 for (let i = 0; i < avail; i++) { newBuf[i] = this._buffer[(this._readPos + i) % this._bufferSize]; } this._buffer = newBuf; this._bufferSize = newSize; this._readPos = 0; this._writePos = avail; } process(inputs, outputs) { if (this._stopped) return false; const output = outputs[0][0]; if (!output) return true; for (let i = 0; i < output.length; i++) { // 需要至少 2 个采样做线性插值 if (this._available() < 2) { output[i] = 0; continue; } // 线性插值重采样 const intPart = Math.floor(this._fractionalPos); const frac = this._fractionalPos - intPart; const idx0 = (this._readPos + intPart) % this._bufferSize; const idx1 = (this._readPos + intPart + 1) % this._bufferSize; output[i] = this._buffer[idx0] + (this._buffer[idx1] - this._buffer[idx0]) * frac; // 步进 this._fractionalPos += this._ratio; // 消费已经越过的整数采样 const consumed = Math.floor(this._fractionalPos); if (consumed > 0) { this._readPos = (this._readPos + consumed) % this._bufferSize; this._fractionalPos -= consumed; } } return true; } } registerProcessor('pcm-playback-processor', PcmPlaybackProcessor);