Files
engine-v5-pipecat-core/examples/webpage/pcm-recorder.worklet.js
2026-05-21 13:08:40 +08:00

105 lines
3.2 KiB
JavaScript

/**
* PCM Recorder AudioWorklet.
*
* Captures mono Float32 mic samples at the AudioContext's native rate,
* resamples them to a target sample rate (default 16 kHz) with linear
* interpolation, then ships PCM16 frames of a fixed duration (default 20 ms)
* to the main thread via `port.postMessage(ArrayBuffer)`.
*
* It also computes a simple RMS level per frame for the UI VU meter so the
* main thread doesn't have to re-process the audio.
*/
class PcmRecorderProcessor extends AudioWorkletProcessor {
constructor(options) {
super();
const opts = (options && options.processorOptions) || {};
this._targetSampleRate = opts.targetSampleRate || 16000;
this._frameMs = opts.frameMs || 20;
this._frameSamples = Math.round(
(this._targetSampleRate * this._frameMs) / 1000,
);
// Resampling state.
// `ratio` is input samples per output sample.
this._ratio = sampleRate / this._targetSampleRate;
this._inputBuffer = new Float32Array(0);
// Float position in `_inputBuffer` for the next output sample.
this._inputOffset = 0;
// Output framing state.
this._frameBuffer = new Int16Array(this._frameSamples);
this._frameIndex = 0;
// VU meter accumulator.
this._rmsSumSquares = 0;
this._rmsCount = 0;
}
process(inputs) {
const input = inputs[0];
if (!input || input.length === 0) return true;
const channel = input[0];
if (!channel || channel.length === 0) return true;
// Append new samples to the input buffer.
const merged = new Float32Array(this._inputBuffer.length + channel.length);
merged.set(this._inputBuffer, 0);
merged.set(channel, this._inputBuffer.length);
this._inputBuffer = merged;
const ratio = this._ratio;
const inLen = this._inputBuffer.length;
let pos = this._inputOffset;
while (pos + 1 < inLen) {
const lo = Math.floor(pos);
const hi = lo + 1;
const w = pos - lo;
const sample =
this._inputBuffer[lo] * (1 - w) + this._inputBuffer[hi] * w;
this._rmsSumSquares += sample * sample;
this._rmsCount += 1;
let s = sample;
if (s > 1) s = 1;
else if (s < -1) s = -1;
this._frameBuffer[this._frameIndex++] =
s < 0 ? Math.round(s * 0x8000) : Math.round(s * 0x7fff);
if (this._frameIndex === this._frameSamples) {
const frame = new Int16Array(this._frameSamples);
frame.set(this._frameBuffer);
const rms =
this._rmsCount > 0
? Math.sqrt(this._rmsSumSquares / this._rmsCount)
: 0;
this.port.postMessage(
{ type: "frame", buffer: frame.buffer, rms },
[frame.buffer],
);
this._frameIndex = 0;
this._rmsSumSquares = 0;
this._rmsCount = 0;
}
pos += ratio;
}
// Trim consumed samples from the input buffer; keep at least the last
// sample we still need to interpolate against on the next call.
const consumed = Math.floor(pos);
if (consumed > 0) {
this._inputBuffer = this._inputBuffer.slice(consumed);
pos -= consumed;
}
this._inputOffset = pos;
return true;
}
}
registerProcessor("pcm-recorder", PcmRecorderProcessor);