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() {