// One-shot call sound effects via WebAudio. Generated on the fly so no // binary assets needed. Intentionally short + low-volume — these fire // multiple times per call and shouldn't feel intrusive. type Sfx = 'join' | 'leave' | 'end' | 'mute' | 'unmute' | 'deafen' | 'undeafen'; let ctx: AudioContext | null = null; function getCtx(): AudioContext | null { if (ctx) return ctx; const AudioCtx = window.AudioContext ?? (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext; if (!AudioCtx) return null; ctx = new AudioCtx(); return ctx; } function beep(freq: number, durationSec: number, delaySec: number, gain = 0.15): void { const c = getCtx(); if (!c) return; const osc = c.createOscillator(); const g = c.createGain(); osc.type = 'sine'; osc.frequency.value = freq; osc.connect(g); g.connect(c.destination); const t0 = c.currentTime + delaySec; g.gain.setValueAtTime(0, t0); g.gain.linearRampToValueAtTime(gain, t0 + 0.02); g.gain.exponentialRampToValueAtTime(0.001, t0 + durationSec); osc.start(t0); osc.stop(t0 + durationSec + 0.02); } export async function playSfx(kind: Sfx): Promise { const c = getCtx(); if (!c) return; if (c.state === 'suspended') { try { await c.resume(); } catch { /* ignore */ } } switch (kind) { case 'join': // Rising two-note chirp — someone entered. beep(523.25, 0.12, 0, 0.16); // C5 beep(783.99, 0.18, 0.1, 0.16); // G5 break; case 'leave': // Falling two-note — someone left. beep(659.25, 0.12, 0, 0.14); // E5 beep(329.63, 0.18, 0.1, 0.14); // E4 break; case 'end': // Soft descending thud — call ended. beep(440, 0.18, 0, 0.14); // A4 beep(293.66, 0.26, 0.14, 0.14); // D4 break; case 'mute': // Discord-style: short downward blip when mic goes silent. Quick, low // volume so it doesn't fight whatever the user is listening to. beep(880, 0.06, 0, 0.1); // A5 beep(660, 0.08, 0.04, 0.1); // E5 break; case 'unmute': // Mirror: upward blip when mic comes back. beep(660, 0.06, 0, 0.1); beep(880, 0.08, 0.04, 0.1); break; case 'deafen': // Lower + slightly longer than mute — Discord uses a deeper tone for // deafen so the user can tell the two states apart without looking. beep(660, 0.07, 0, 0.1); beep(392, 0.12, 0.05, 0.1); // G4 break; case 'undeafen': // Mirror of deafen: low → mid. beep(392, 0.07, 0, 0.1); beep(660, 0.12, 0.05, 0.1); break; } } export function playJoinBeep(): Promise { return playSfx('join'); } export function playLeaveBeep(): Promise { return playSfx('leave'); } export function playEndBeep(): Promise { return playSfx('end'); } export function playMuteBeep(): Promise { return playSfx('mute'); } export function playUnmuteBeep(): Promise { return playSfx('unmute'); } export function playDeafenBeep(): Promise { return playSfx('deafen'); } export function playUndeafenBeep(): Promise { return playSfx('undeafen'); }