Files
learnharness/public/worklets/pcm-capture.js
T

66 lines
2.7 KiB
JavaScript
Raw 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-capture — the dictation audio path (STT wave, 2026-09-17).
*
* Runs OFF the main thread (AudioWorklet): converts the mic's Float32
* samples to 16-bit little-endian PCM (linear16) and hands buffers to the
* main thread in two streams:
*
* { type: "level", rms } — every render quantum (128 samples ≈ 8ms at
* 16kHz): the quantum's RMS level, 0..1. This
* is the meter's heartbeat; posting per quantum
* is what makes the meter feel instant (the
* dominant latency in the owner's terminal
* version was a 256ms IO buffer — see
* ~/.pi/agent/extensions/dictate).
* { type: "audio", buffer } — Int16Array frames of 2048 samples (128ms),
* zero-copy (transferred). Deepgram recommends
* ~100–200ms chunks; silence is streamed too,
* so the socket never idles (no KeepAlive
* needed — the 10s NET-0001 timeout only bites
* a stream that stops SENDING).
*
* Ships as a static file under public/ so it rides the standalone bundle.
* Plain JS by necessity (worklets run outside the module graph).
*/
class PcmCaptureProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.frame = new Int16Array(2048);
this.count = 0;
// Whole-recording STT must include the final, partially filled frame.
this.port.onmessage = (event) => {
if (event.data?.type !== "flush") return;
if (this.count > 0) {
const tail = this.frame.slice(0, this.count);
this.port.postMessage({ type: "audio", buffer: tail }, [tail.buffer]);
this.count = 0;
}
this.port.postMessage({ type: "flushed" });
};
}
process(inputs) {
const channel = inputs[0] && inputs[0][0];
if (!channel) return true; // no input yet — stay alive
// RMS for the meter (float domain, before conversion).
let sumSquares = 0;
for (let i = 0; i < channel.length; i++) sumSquares += channel[i] * channel[i];
this.port.postMessage({ type: "level", rms: Math.sqrt(sumSquares / channel.length) });
// Float32 → Int16 (clamp first: a hot mic can exceed ±1.0).
for (let i = 0; i < channel.length; i++) {
const s = Math.max(-1, Math.min(1, channel[i]));
this.frame[this.count++] = s < 0 ? s * 0x8000 : s * 0x7fff;
if (this.count === this.frame.length) {
this.port.postMessage({ type: "audio", buffer: this.frame }, [this.frame.buffer]);
this.frame = new Int16Array(2048);
this.count = 0;
}
}
return true;
}
}
registerProcessor("pcm-capture", PcmCaptureProcessor);