Add src/voice Pipecat pipeline, browser demo at /voice-demo, and config/voice.json. Co-authored-by: Cursor <cursoragent@cursor.com>
105 lines
3.2 KiB
JavaScript
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);
|