From 709ad1ba7d595187c772233a929e8cae3935f99c Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Fri, 27 Mar 2026 19:31:26 +0400 Subject: [PATCH] fix: revert to scheduled playback with 200ms drift cap Pull-based ScriptProcessor approach broke audio completely. Back to createBufferSource scheduling which worked, but with tighter 200ms max drift (was 300ms). Snaps back when exceeded. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/wzp-web/static/index.html | 54 ++++++++++---------------------- 1 file changed, 17 insertions(+), 37 deletions(-) diff --git a/crates/wzp-web/static/index.html b/crates/wzp-web/static/index.html index 236114c..c9ef2f7 100644 --- a/crates/wzp-web/static/index.html +++ b/crates/wzp-web/static/index.html @@ -173,52 +173,32 @@ function startAudioCapture() { scriptNode.connect(audioCtx.destination); } -// 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; - playbackNode = audioCtx.createScriptProcessor(1024, 1, 1); - playbackNode.onaudioprocess = (e) => { - const output = e.outputBuffer.getChannelData(0); - 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 (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 { - for (let i = 0; i < need; i++) output[i] = 0; - } - }; - playbackNode.connect(audioCtx.destination); -} +// Scheduled playback with aggressive drift correction +let nextPlayTime = 0; function playAudio(pcmInt16) { if (!audioCtx) return; - initPlayback(); const floatData = new Float32Array(pcmInt16.length); for (let i = 0; i < pcmInt16.length; i++) { floatData[i] = pcmInt16[i] / 32768.0; } - // Append to sample buffer - const combined = new Float32Array(playSamples.length + floatData.length); - combined.set(playSamples); - combined.set(floatData, playSamples.length); - playSamples = combined; + const buffer = audioCtx.createBuffer(1, floatData.length, SAMPLE_RATE); + buffer.getChannelData(0).set(floatData); + + const source = audioCtx.createBufferSource(); + source.buffer = buffer; + source.connect(audioCtx.destination); + + const now = audioCtx.currentTime; + + if (nextPlayTime < now || nextPlayTime > now + 0.2) { + // Behind or drifted too far ahead — snap to now + 40ms + nextPlayTime = now + 0.04; + } + source.start(nextPlayTime); + nextPlayTime += buffer.duration; } function startStatsUpdate() {