From 9ad21182a8e0df8161b064ddf116e73a98bd2096 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Fri, 27 Mar 2026 18:29:00 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20web=20audio=20playback=20quality=20?= =?UTF-8?q?=E2=80=94=20gapless=20scheduling=20+=20sample=20rate=20debug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Schedule each playback buffer to start exactly where the last ended (was causing gaps/overlaps with fixed 60ms offset) - Log AudioContext sample rate to console for debugging - Reset playback timeline when falling behind Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/wzp-web/static/index.html | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/wzp-web/static/index.html b/crates/wzp-web/static/index.html index 8c3bcc1..f3c6313 100644 --- a/crates/wzp-web/static/index.html +++ b/crates/wzp-web/static/index.html @@ -127,6 +127,8 @@ function stopCall() { function startAudioCapture() { audioCtx = new AudioContext({ sampleRate: SAMPLE_RATE }); + console.log('AudioContext sampleRate:', audioCtx.sampleRate); + setStatus('Connected — mic active (sample rate: ' + audioCtx.sampleRate + 'Hz)'); const source = audioCtx.createMediaStreamSource(mediaStream); // ScriptProcessorNode requires power-of-2 buffer. We use 1024 and @@ -171,10 +173,11 @@ function startAudioCapture() { scriptNode.connect(audioCtx.destination); } +let nextPlayTime = 0; + function playAudio(pcmInt16) { if (!audioCtx) return; - // Convert int16 to float32 const floatData = new Float32Array(pcmInt16.length); for (let i = 0; i < pcmInt16.length; i++) { floatData[i] = pcmInt16[i] / 32768.0; @@ -187,9 +190,14 @@ function playAudio(pcmInt16) { source.buffer = buffer; source.connect(audioCtx.destination); - // Schedule playback with small buffer to reduce latency - const playTime = audioCtx.currentTime + 0.06; // 60ms buffer - source.start(playTime); + // Schedule seamlessly: each buffer starts exactly where the last one ended + const now = audioCtx.currentTime; + if (nextPlayTime < now) { + // Fell behind — reset with small lookahead + nextPlayTime = now + 0.04; // 40ms initial buffer + } + source.start(nextPlayTime); + nextPlayTime += buffer.duration; } function startStatsUpdate() {