GPU_GUARD_MONOREPO/packages/frontend/public/pcm-playback-processor.js

110 lines
3.1 KiB
JavaScript
Raw Normal View History

2026-05-20 21:39:12 +08:00
/**
* 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);