110 lines
3.1 KiB
JavaScript
110 lines
3.1 KiB
JavaScript
/**
|
||
* 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);
|