From 4de72e2d98a2ab1a3d2e29348daa7e4726b312c5 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Fri, 27 Mar 2026 19:26:59 +0400 Subject: [PATCH] fix: pull-based audio playback eliminates drift + rustls crypto provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Web playback rewritten from push-scheduling to pull-based ring buffer: - ScriptProcessorNode pulls frames from buffer every ~21ms - Buffer capped at 10 frames (~200ms) — drops oldest on overflow - Latency permanently bounded, no drift over time Also: install ring crypto provider for rustls TLS on Linux, build on debian-12 to match mequ glibc. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/wzp-web/src/main.rs | 3 ++ crates/wzp-web/static/index.html | 52 +++++++++++++++++++------------- scripts/build-linux.sh | 2 +- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/crates/wzp-web/src/main.rs b/crates/wzp-web/src/main.rs index 4788faf..39e625c 100644 --- a/crates/wzp-web/src/main.rs +++ b/crates/wzp-web/src/main.rs @@ -32,6 +32,9 @@ struct AppState { #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt().init(); + rustls::crypto::ring::default_provider() + .install_default() + .expect("failed to install rustls crypto provider"); let mut port: u16 = 8080; let mut relay_addr: SocketAddr = "127.0.0.1:4433".parse()?; diff --git a/crates/wzp-web/static/index.html b/crates/wzp-web/static/index.html index 42f4409..e552000 100644 --- a/crates/wzp-web/static/index.html +++ b/crates/wzp-web/static/index.html @@ -173,35 +173,45 @@ function startAudioCapture() { scriptNode.connect(audioCtx.destination); } -let nextPlayTime = 0; +// Ring buffer playback using AudioWorklet-style approach +let playbackBuffer = []; +let playbackNode = null; + +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 + } + 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; + } else { + // Underrun — silence + for (let i = 0; i < output.length; i++) output[i] = 0; + } + }; + playbackNode.connect(audioCtx.destination); +} 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; } - - 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; - const MAX_LATENCY = 0.3; // 300ms max buffer — reset if we drift beyond this - - if (nextPlayTime < now) { - // Fell behind — reset - nextPlayTime = now + 0.04; - } else if (nextPlayTime > now + MAX_LATENCY) { - // Too far ahead — skip to reduce latency - nextPlayTime = now + 0.04; - } - source.start(nextPlayTime); - nextPlayTime += buffer.duration; + playbackBuffer.push(floatData); } function startStatsUpdate() { diff --git a/scripts/build-linux.sh b/scripts/build-linux.sh index bcf4041..2a93bf1 100755 --- a/scripts/build-linux.sh +++ b/scripts/build-linux.sh @@ -12,7 +12,7 @@ SSH_KEY_NAME="wz" SSH_KEY_PATH="/Users/manwe/CascadeProjects/wzp" SERVER_NAME="wzp-builder-$(date +%s)" SERVER_TYPE="cx33" -IMAGE="ubuntu-24.04" +IMAGE="debian-12" REMOTE_USER="root" OUTPUT_DIR="target/linux-x86_64"