// Generic AudioWorkletProcessor for low-latency raw PCM playback over a // WebSocket (paired with pcm_ws_bridge.py on the server side). Expects // interleaved 16-bit signed little-endian stereo frames delivered via // postMessage as ArrayBuffers; converts to Float32 and plays them back // through a small ring buffer that absorbs network jitter. // // The queue is deliberately capped (see maxQueuedFrames): if the network/ // browser ever delivers data faster than real-time playback consumes it - // which happens routinely on a fast local connection - an uncapped queue // just accumulates that backlog forever, growing steadily out of sync with // the video instead of settling back down. Dropping the oldest excess data // keeps latency bounded at the cost of an occasional small audible glitch // on the drop, which is a fair trade for staying live. class PCMWorkletProcessor extends AudioWorkletProcessor { constructor() { super(); this.queue = []; this.readOffset = 0; this.channels = 2; this.maxQueuedFrames = Math.round(sampleRate * 0.1); // ~100ms cap this.port.onmessage = (event) => { const int16 = new Int16Array(event.data); const float32 = new Float32Array(int16.length); for (let i = 0; i < int16.length; i++) { float32[i] = int16[i] / 32768; } this.queue.push(float32); this._trimBacklog(); }; } _queuedFrames() { let frames = -this.readOffset; for (const chunk of this.queue) frames += chunk.length / this.channels; return frames; } _trimBacklog() { while (this._queuedFrames() > this.maxQueuedFrames && this.queue.length > 1) { this.queue.shift(); this.readOffset = 0; } } process(inputs, outputs) { const output = outputs[0]; const numChannels = output.length; const numFrames = output[0].length; for (let frame = 0; frame < numFrames; frame++) { if (this.queue.length === 0) { for (let ch = 0; ch < numChannels; ch++) output[ch][frame] = 0; continue; } const current = this.queue[0]; for (let ch = 0; ch < numChannels; ch++) { const idx = this.readOffset * this.channels + ch; output[ch][frame] = idx < current.length ? current[idx] : 0; } this.readOffset++; if (this.readOffset * this.channels >= current.length) { this.queue.shift(); this.readOffset = 0; } } return true; } } registerProcessor('pcm-worklet-processor', PCMWorkletProcessor);