From cb642238ed1e30cba26d7563a75d7bb8355cbf10 Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Wed, 3 Jun 2026 14:42:57 -0400 Subject: [PATCH] zebra-spaces: leave + step-down chimes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two more Web Audio tones, completing the doorman set: - playToneLeave: E5 -> C5 descending major third (mirror of join's C5 -> E5 ascending). Fires on peer-left (any role leaves the room post hiccup-grace) and peer-booted (mod kicks someone), skipping self on the boot case. - playToneStepDown: single G4 low sine blip — softer than the arrival/departure pair. Fires on role-change for OTHER peers when canSpeak(prev) && !canSpeak(next): they're still in the room, just lost mic privileges. Full notification ladder: C5 -> E5 guest arrived A5 (triangle) hand raised G4 speaker stepped down (still here) E5 -> C5 guest left / was kicked --- web/zebra-spaces.html | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html index c33b982..62db2e4 100644 --- a/web/zebra-spaces.html +++ b/web/zebra-spaces.html @@ -2945,6 +2945,18 @@ function playToneRaise(){ /* single A5 bell-ish tone — light, attention-getting */ playToneBlip(880.0, 260, 'triangle'); } +function playToneLeave(){ + /* E5 → C5 descending major third — mirror of playToneJoin, conveys + * 'departed'. Fires on peer-left + peer-booted. */ + playToneBlip(659.25, 140, 'sine'); + setTimeout(() => playToneBlip(523.25, 200, 'sine'), 120); +} +function playToneStepDown(){ + /* single G4 low blip — softer than join/leave, conveys 'still here + * but stepped off the stage'. Fires on canSpeak -> listener role + * transitions for other peers. */ + playToneBlip(392.0, 220, 'sine'); +} /* ================================================================== * room state mirror (server is source of truth, we mirror locally for @@ -3164,6 +3176,9 @@ async function handleSignal(raw){ const left = members.get(m.uuid); const pub = pubHexFromMsg(m, 'pubkey') || (left && left.pubkey ? pubHexFromMsg({pubkey:left.pubkey}, 'pubkey') : ''); if (left || pub) logLine('', idTag(m.uuid, pub)+' left'); + /* doorman chime — someone actually left (post hiccup-grace). + * server never sends peer-left for self so no self-filter. */ + playToneLeave(); /* if they were sharing screen or camera, drop the tiles */ if (left && left.pubkey){ try { const h = hex(unb64(left.pubkey)); removeScreenTile(h); removeCameraTile(h); } catch(_){} @@ -3293,6 +3308,9 @@ async function handleSignal(raw){ removeCameraTile(subjPub); removeVideoTile('gameshare', subjPub); } catch(_){} + /* 'stepped off the stage' chime — they're still in the + * room but lost mic privileges. */ + playToneStepDown(); } } renderRoom(); @@ -3308,6 +3326,8 @@ async function handleSignal(raw){ || (mm && mm.pubkey ? pubHexFromMsg({pubkey:mm.pubkey},'pubkey') : ''); const byPub = pubHexFromMsg(m, 'by_pubkey'); logLine('', idTag(m.uuid, victPub)+' was removed by '+(by||byPub?idTag(m.by, byPub):'a mod')); + /* same departure chime as peer-left — they're gone either way. */ + if (m.uuid !== myUUID) playToneLeave(); if (m.uuid === myUUID){ /* server will close our socket; surface a clear notice and prevent * the WS reconnect loop from auto-rejoining into a boot loop. Tear @@ -4097,8 +4117,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');