zebra-spaces: listeners adapt down too — 4s → 2s on sustained clean feed

Fox 2026-06-05: "the 4 sec delay doesn't get faster with a better feed
for listeners."

Previously the listener worklet was locked at rate=1.0 (no time-
stretching, music quality protection) so the Double Dragon controller's
retargets couldn't smoothly shrink the buffer. The listener got the
full 4s cushion forever, even with a steady clean feed.

Changes:
1. Remove the explicit lock_rate=1.0 on listener worklet install —
   all worklets now support the ±8% adaptive rate.
2. New constant DD_BASE_LISTENER_TARGET_SEC = 2.0. Listeners start
   at RECV_PLAYOUT_DELAY_SEC (4s) for fresh-attach wiggle-immunity;
   controller shrinks them toward 2s after DD_CLEAN_SAMPLES_NEEDED
   (30s) of clean stats.
3. ddBaseTargetForRole(role) — listener=2s, speaker/cohost/host=0.5s.
4. ddEntry initial currentTargetSec reflects the role's actual
   starting target (4s for listeners, 0.5s for speakers).
5. Controller's SHRINK branch uses the role-aware base.

Music quality during the shrink: ≤8% rate change for ~10-20s, ≤1
semitone brief pitch shift. Audible on sustained tones but acceptable
for the latency improvement — listener feels "more live" when room
is calm. Phase 4 (WSOLA grain processing for true pitch preservation)
still deferred; this gets us 80% of the win without it.
This commit is contained in:
Russell Ballestrini 2026-06-05 08:26:24 -04:00
parent 05064a487e
commit f07052780b
No known key found for this signature in database

View file

@ -1996,14 +1996,13 @@ function installJitterBuffer(uuid, node){
refreshLipSyncForUuid(uuid);
}
};
/* Listener-role worklets never time-stretch — music quality on the
* deep cushion is paramount, and the buffer drifts gradually with
* clock drift. Speaker / cohost / host worklets DO stretch
* adaptively to drive the per-publisher target the Double Dragon
* controller sets. */
if (myRole === 'listener'){
try { jbuf.port.postMessage({ cmd: 'lock_rate', rate: 1.0 }); } catch(_){}
}
/* All roles now allow time-stretching — listeners benefit too
* (their 4s cushion shrinks toward 2s when feed is clean, grows
* back to 4s on instability). The ±8% rate cap means the brief
* pitch shift during adaptation is ≤1 semitone — barely audible
* on speech, briefly audible on sustained music tones. Fox
* 2026-06-05: "the 4 sec delay doesn't get faster with a better
* feed for listeners." */
try { node.src.disconnect(node.gain); } catch(_){}
node.src.connect(jbuf).connect(node.gain);
node.jbuf = jbuf;
@ -2186,15 +2185,23 @@ const DD_JITTER_THRESH_SEC = 0.030; /* > 30ms = unstable */
const DD_UNSTABLE_SAMPLES_NEEDED = 2;
const DD_CLEAN_SAMPLES_NEEDED = 6; /* 6 × 5s = 30s clean → recover */
const DD_TELEMETRY_TICK_SEC = 5;
const DD_BASE_TARGET_SEC = 0.5; /* normal conversational cushion */
const DD_MAX_TARGET_SEC = 4.0; /* maximum cushion under instability */
const DD_BASE_TARGET_SEC = 0.5; /* speaker normal conversational cushion */
const DD_BASE_LISTENER_TARGET_SEC = 2.0; /* listener clean-feed cushion — lower than initial 4s so latency feels less laggy when feed is good; controller grows back to MAX on any instability */
const DD_MAX_TARGET_SEC = 4.0; /* maximum cushion under instability — same ceiling for everyone */
function ddBaseTargetForRole(role){
return role === 'listener' ? DD_BASE_LISTENER_TARGET_SEC : DD_BASE_TARGET_SEC;
}
function ddEntry(pubHex){
let e = doubleDragon.get(pubHex);
if (!e){
/* Initial currentTargetSec matches what attachAudioStreamViaWorklet
* actually set as the worklet's target — playoutDelayForRole(myRole).
* Listeners start at 4s (RECV_PLAYOUT_DELAY_SEC), speakers at 0.5s.
* Controller shrinks listener toward 2s on sustained clean stats. */
e = { state: 'auto', unstableStreak: 0, cleanStreak: 0,
lastLost: 0, lastSeenStatsAt: 0,
currentTargetSec: DD_BASE_TARGET_SEC };
currentTargetSec: playoutDelayForRole(myRole) };
doubleDragon.set(pubHex, e);
}
return e;
@ -2254,6 +2261,7 @@ async function evaluateDoubleDragonForPub(pubHex){
}
});
} catch(_){ return; }
const baseTarget = ddBaseTargetForRole(myRole);
const unstable = (lossRate > DD_LOSS_PER_SEC_THRESH) ||
(jitter > DD_JITTER_THRESH_SEC);
if (unstable){
@ -2269,12 +2277,12 @@ async function evaluateDoubleDragonForPub(pubHex){
} else {
e.cleanStreak++; e.unstableStreak = 0;
if (e.cleanStreak >= DD_CLEAN_SAMPLES_NEEDED &&
e.currentTargetSec > DD_BASE_TARGET_SEC){
e.currentTargetSec = DD_BASE_TARGET_SEC;
ddSetTargetForPub(pubHex, DD_BASE_TARGET_SEC);
e.currentTargetSec > baseTarget){
e.currentTargetSec = baseTarget;
ddSetTargetForPub(pubHex, baseTarget);
logLine('', 'double-dragon pub='+pubHex.slice(0,4)+
' SHRINK target → '+DD_BASE_TARGET_SEC+'s (clean '+
e.cleanStreak+' samples)');
' SHRINK target → '+baseTarget+'s (clean '+
e.cleanStreak+' samples, role='+myRole+')');
}
}
}
@ -6789,9 +6797,9 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');
</script>
<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-04</span><br>
md5 <span class="stamp-md5">42afccff3f68bba3b3a2959cec4adff2</span><br>
sha256 <span class="stamp-sha">b4c8dd2cc581545d770c4c7ae254d298dad91215c64033d9f656658f01ff2224</span><br>
<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-05</span><br>
md5 <span class="stamp-md5">b137f24f08a8bc2a9537e066b58adf6d</span><br>
sha256 <span class="stamp-sha">7aacac5e13460ab999f5c20fb39926e73948dcb6e100f16745af602721e08f91</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>