zebra-spaces: bump playoutDelayHint 400ms -> 700ms for cross-Wi-Fi voice

Reports of robot-voice artifacts from a peer on a different Wi-Fi —
classic symptom of jitter-buffer under-runs (Opus PLC kicks in,
generates synthetic samples that sound vocoder-y). 400ms was tight
enough that bursty inter-arrival on cross-AP / cellular hand-off
networks would exhaust the cushion.

700ms costs barely-audible end-to-end latency (well under the 1s
people perceive as 'walkie talkie'), well above typical Wi-Fi peak
jitter, and matches both the SFU subscribe path and mesh peer path
so the room sounds consistent regardless of how the audio is being
delivered.

Bitrate / FEC / DTX settings unchanged — adding bandwidth would
make a lossy link worse, not better.
This commit is contained in:
Russell Ballestrini 2026-06-03 13:19:38 -04:00
parent 79ee317ccf
commit 1920cf6a55
No known key found for this signature in database

View file

@ -2523,8 +2523,12 @@ function handleRemoteSfuTrack(ev){
logLine('', 'sfu: unknown kind '+kind+' from '+pubHex);
return;
}
/* mic audio — 400ms jitter-buffer target absorbs Wi-Fi peak jitter */
try { if (ev.receiver) ev.receiver.playoutDelayHint = 0.4; } catch(_){}
/* mic audio — 700ms jitter-buffer target absorbs cross-Wi-Fi peak
* jitter and burst packet loss. 400ms was too tight for users on
* different APs / cellular hand-offs — under-runs were producing
* the 'robot voice' artifact. 700ms costs an inaudible bit of
* end-to-end latency in exchange for clean playback under loss. */
try { if (ev.receiver) ev.receiver.playoutDelayHint = 0.7; } catch(_){}
/* cache by full pubkey (already resolved above) so it survives the
* member's session uuid changing across leave/rejoin */
sfuStreamsByPubHex.set(pubHex, ev.streams[0]);
@ -3494,9 +3498,10 @@ async function connectToPeer(uuid, weOffer){
let a = remoteAudio.get(uuid);
if (!a){ a = document.createElement('audio'); a.autoplay = true; document.body.appendChild(a); remoteAudio.set(uuid, a); }
a.srcObject = ev.streams[0] || new MediaStream([ev.track]);
/* 400ms jitter buffer — Wi-Fi peak inter-arrival is multiples of
* the smoothed jitter, so a generous buffer absorbs the bursts */
try { ev.receiver.playoutDelayHint = 0.4; } catch(_){}
/* 700ms jitter buffer — matches the SFU path. Cross-Wi-Fi and
* cellular hand-off jitter ate into the old 400ms cushion and
* produced robot-voice artifacts on tighter networks. */
try { ev.receiver.playoutDelayHint = 0.7; } catch(_){}
stopMeter(uuid); startMeter(uuid, a.srcObject);
};
pc.onicecandidate = (ev) => { /* using waitForIceGathering pattern, candidates ignored */ };
@ -4086,8 +4091,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');
<footer style="margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-03</span><br>
md5 <span class="stamp-md5">6a4b0c2e116f1101ee2f03dc64b29655</span><br>
sha256 <span class="stamp-sha">54434f25483a558642b63ca0716d303b08df8872e70b3dcaab1fb65a876acb95</span><br>
md5 <span class="stamp-md5">319e1c7b25a69f68c0c1b327e3bc24ad</span><br>
sha256 <span class="stamp-sha">fcdbe9ca01318641fbc2af9a9ed32d651cdfedb29e7435b73bbb341050641d3e</span><br>
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify against these hashes; point at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
</footer>