GPU_GUARD_MONOREPO/packages/frontend/public/pcm-playback-processor.js
2026-05-20 21:39:12 +08:00

110 lines
3.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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);