zebra-spaces: classic chimes — guest-joined + hand-raised

Two Web Audio synthesised tones, no asset shipping required:
- playToneJoin: C5 -> E5 ascending major-third chime (~340ms total),
  fires on every peer-joined. Server broadcasts peer-joined to
  everyone except the joiner via broadcastExcept, so existing members
  always hear it for new arrivals only.
- playToneRaise: single A5 triangle blip (~260ms), fires on
  hand-raised UNLESS it's the raiser's own hand (server broadcasts
  hand-raised to all so we filter on m.uuid !== myUUID).

Tones share a lazily-instantiated AudioContext (the user already
gestured to enter the room, autoplay policy is satisfied). Skipped
when document.hidden so background tabs stay quiet. Short
exponential attack + decay envelope avoids click artifacts.
This commit is contained in:
Russell Ballestrini 2026-06-03 13:18:19 -04:00
parent 441c08631a
commit 79ee317ccf
No known key found for this signature in database

View file

@ -2886,6 +2886,56 @@ function startMeter(uuid, stream){
}
function stopMeter(uuid){ meterCtl.delete(uuid); }
/* ==================================================================
* notification tones — synthesised via Web Audio so no asset needed.
*
* playToneJoin() — ascending major-third chime (523→659 Hz):
* friendly 'welcome' signature
* playToneRaise() — single bell-like 880 Hz tone: gentle attention
*
* A single shared AudioContext is lazily created on first use; the
* user already gestured to enter the room (clicked 'enter'), so the
* autoplay policy is satisfied. Each tone uses a short exponential
* fade-out to avoid click artifacts.
*
* Tones are skipped if the page is hidden (background tab) — the
* idea is to notify the LIVE user, not interrupt them when they
* stepped away.
* ================================================================== */
let _toneCtx = null;
function _toneCtxGet(){
if (_toneCtx) return _toneCtx;
try { _toneCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(_){ return null; }
return _toneCtx;
}
function playToneBlip(freq, durationMs, type){
if (typeof document !== 'undefined' && document.hidden) return;
const ctx = _toneCtxGet(); if (!ctx) return;
try {
const t0 = ctx.currentTime;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = type || 'sine';
osc.frequency.value = freq;
osc.connect(gain); gain.connect(ctx.destination);
/* short attack, exponential decay → no click on start or end */
gain.gain.setValueAtTime(0.0001, t0);
gain.gain.exponentialRampToValueAtTime(0.12, t0 + 0.012);
gain.gain.exponentialRampToValueAtTime(0.0001, t0 + durationMs / 1000);
osc.start(t0);
osc.stop(t0 + durationMs / 1000 + 0.02);
} catch(_){}
}
function playToneJoin(){
/* C5 → E5 ascending major third — a classic 'someone arrived' lift */
playToneBlip(523.25, 140, 'sine');
setTimeout(() => playToneBlip(659.25, 200, 'sine'), 120);
}
function playToneRaise(){
/* single A5 bell-ish tone — light, attention-getting */
playToneBlip(880.0, 260, 'triangle');
}
/* ==================================================================
* room state mirror (server is source of truth, we mirror locally for
* rendering). updated by welcome / peer-joined / peer-left / state /
@ -3075,6 +3125,10 @@ async function handleSignal(raw){
case 'peer-joined':
members.set(m.uuid, { uuid:m.uuid, pubkey:m.pubkey, handle:m.handle, role:m.role, joined_at: Date.now()/1000 });
{ const pub = pubHexFromMsg(m, 'pubkey'); logLine('', idTag(m.uuid, pub)+' joined as '+m.role); }
/* classic 'guest joined' chime — server only sends peer-joined
* to OTHER members (broadcastExcept), so we're always notifying
* the existing room about a new arrival. */
playToneJoin();
/* re-announce our mic state so the new arrival's UI reflects
* reality. Server-side mic-state-req covers welcome timing; this
* covers the standard 'a new peer joined while we were already
@ -3153,6 +3207,9 @@ async function handleSignal(raw){
case 'hand-raised':
handraise.add(m.uuid); renderRoom();
{ const pub = pubHexFromMsg(m, 'pubkey'); logLine('', idTag(m.uuid, pub)+' raised hand'); }
/* attention bell for mods — skip if it's our own hand going up
* (server broadcasts to everyone including the raiser). */
if (m.uuid !== myUUID) playToneRaise();
break;
case 'hand-lowered':
handraise.delete(m.uuid); renderRoom();
@ -4029,8 +4086,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">7873563955c603f22be1ccbb9fb2885a</span><br>
sha256 <span class="stamp-sha">d7c50a97445363af36e67d20f719e14ec5b399d3070cc6939eaf5fde3e8ee9fc</span><br>
md5 <span class="stamp-md5">6a4b0c2e116f1101ee2f03dc64b29655</span><br>
sha256 <span class="stamp-sha">54434f25483a558642b63ca0716d303b08df8872e70b3dcaab1fb65a876acb95</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>