/** * audio-capture-worklet.js * ------------------------ * Runs in the AudioWorklet thread (dedicated audio thread, zero GC pauses). * * Responsibilities * ---------------- * 1. Receive float32 PCM frames from the mic at AudioContext.sampleRate * (typically 44100 or 48000 Hz). * 2. Downsample to TARGET_SR = 16000 Hz using averaged decimation * (basic anti-aliasing — good enough for speech). * 3. Accumulate until CHUNK_SAMPLES (320 samples = 20 ms at 16 kHz) are * ready, then convert to Int16 and post to the main thread. * * Messages posted to main thread * -------------------------------- * { type: 'audio', pcm: ArrayBuffer } — 640 bytes = 320 int16 samples */ const TARGET_SR = 16_000; const CHUNK_SAMPLES = 320; // 20 ms at 16 kHz → 640 bytes as Int16 class AudioCaptureProcessor extends AudioWorkletProcessor { constructor () { super(); this._buf = []; // Downsampled float32 accumulator this._active = false; this.port.onmessage = ({ data }) => { if (data.type === 'start') this._active = true; if (data.type === 'stop') this._active = false; }; } process (inputs) { if (!this._active) return true; const channel = inputs[0]?.[0]; if (!channel || channel.length === 0) return true; // ── Downsample: average samples within each output window ───────── // const ratio = sampleRate / TARGET_SR; // e.g. 48000/16000 = 3.0 const outCount = Math.floor(channel.length / ratio); for (let i = 0; i < outCount; i++) { const srcStart = i * ratio; const srcEnd = srcStart + ratio; let sum = 0; let n = 0; for (let j = Math.floor(srcStart); j < Math.min(Math.ceil(srcEnd), channel.length); j++) { sum += channel[j]; n++; } this._buf.push(n > 0 ? sum / n : 0); } // ── Emit CHUNK_SAMPLES at a time ─────────────────────────────────── // while (this._buf.length >= CHUNK_SAMPLES) { const slice = this._buf.splice(0, CHUNK_SAMPLES); const int16 = new Int16Array(CHUNK_SAMPLES); for (let i = 0; i < CHUNK_SAMPLES; i++) { int16[i] = Math.max(-32_768, Math.min(32_767, Math.round(slice[i] * 32_767))); } this.port.postMessage({ type: 'audio', pcm: int16.buffer }, [int16.buffer]); } return true; // keep processor alive } } registerProcessor('audio-capture-processor', AudioCaptureProcessor);