diff --git a/crates/wzp-web/static/index.html b/crates/wzp-web/static/index.html index e552000..236114c 100644 --- a/crates/wzp-web/static/index.html +++ b/crates/wzp-web/static/index.html @@ -173,31 +173,33 @@ function startAudioCapture() { scriptNode.connect(audioCtx.destination); } -// Ring buffer playback using AudioWorklet-style approach -let playbackBuffer = []; +// Pull-based playback with sample-accurate ring buffer +let playSamples = new Float32Array(0); // accumulated float samples let playbackNode = null; +const MAX_BUFFERED_SAMPLES = SAMPLE_RATE / 5; // 200ms max (~9600 samples) function initPlayback() { if (playbackNode) return; - // Use a ScriptProcessorNode as a pull-based audio sink. - // It asks for audio every ~21ms (1024 samples at 48kHz). - // We feed it from our ring buffer of received frames. playbackNode = audioCtx.createScriptProcessor(1024, 1, 1); playbackNode.onaudioprocess = (e) => { const output = e.outputBuffer.getChannelData(0); - // Pull from buffer — drop old frames if we're behind - while (playbackBuffer.length > 10) { - playbackBuffer.shift(); // drop oldest, keeps latency bounded + const need = output.length; // 1024 + + // Drop excess to cap latency + if (playSamples.length > MAX_BUFFERED_SAMPLES) { + playSamples = playSamples.slice(playSamples.length - MAX_BUFFERED_SAMPLES); } - if (playbackBuffer.length > 0) { - const frame = playbackBuffer.shift(); - // frame is 960 samples, output is 1024 — copy what we can - const len = Math.min(frame.length, output.length); - for (let i = 0; i < len; i++) output[i] = frame[i]; - for (let i = len; i < output.length; i++) output[i] = 0; + + if (playSamples.length >= need) { + output.set(playSamples.subarray(0, need)); + playSamples = playSamples.slice(need); + } else if (playSamples.length > 0) { + // Partial — play what we have, pad with silence + output.set(playSamples.subarray(0, playSamples.length)); + for (let i = playSamples.length; i < need; i++) output[i] = 0; + playSamples = new Float32Array(0); } else { - // Underrun — silence - for (let i = 0; i < output.length; i++) output[i] = 0; + for (let i = 0; i < need; i++) output[i] = 0; } }; playbackNode.connect(audioCtx.destination); @@ -211,7 +213,12 @@ function playAudio(pcmInt16) { for (let i = 0; i < pcmInt16.length; i++) { floatData[i] = pcmInt16[i] / 32768.0; } - playbackBuffer.push(floatData); + + // Append to sample buffer + const combined = new Float32Array(playSamples.length + floatData.length); + combined.set(playSamples); + combined.set(floatData, playSamples.length); + playSamples = combined; } function startStatsUpdate() {