export type CallSoundStyle = 'chime' | 'soft' | 'retro'; let sharedCtx: AudioContext | undefined; const getAudioContext = (): AudioContext | undefined => { try { if (!sharedCtx || sharedCtx.state === 'closed') sharedCtx = new AudioContext(); if (sharedCtx.state === 'suspended') sharedCtx.resume().catch(() => undefined); return sharedCtx; } catch { return undefined; } }; /** * Create and resume the shared AudioContext from within a user gesture * (e.g. clicking "Join"). Browsers block AudioContext playback until it has * been started by a gesture, so join/leave sounds — which fire later without * any gesture — would otherwise be silent. Call this on call entry so every * participant's later membership-change sounds are actually audible. */ export const unlockCallSounds = (): void => { getAudioContext(); }; type Note = { freq: number; /** Offset from now, in seconds */ at: number; /** Duration in seconds */ dur: number; }; const playNotes = (notes: Note[], type: OscillatorType, peakGain: number): void => { const ctx = getAudioContext(); if (!ctx) return; const now = ctx.currentTime; notes.forEach(({ freq, at, dur }) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = type; osc.frequency.value = freq; const start = now + at; // Short attack/decay envelope to avoid clicks gain.gain.setValueAtTime(0, start); gain.gain.linearRampToValueAtTime(peakGain, start + 0.015); gain.gain.exponentialRampToValueAtTime(0.0001, start + dur); osc.connect(gain); gain.connect(ctx.destination); osc.start(start); osc.stop(start + dur + 0.02); }); }; const SOUNDS: Record void; leave: () => void }> = { chime: { join: () => playNotes( [ { freq: 587.33, at: 0, dur: 0.12 }, { freq: 880, at: 0.1, dur: 0.2 }, ], 'sine', 0.25, ), leave: () => playNotes( [ { freq: 880, at: 0, dur: 0.12 }, { freq: 587.33, at: 0.1, dur: 0.2 }, ], 'sine', 0.25, ), }, soft: { join: () => playNotes([{ freq: 523.25, at: 0, dur: 0.4 }], 'triangle', 0.18), leave: () => playNotes([{ freq: 392, at: 0, dur: 0.4 }], 'triangle', 0.18), }, retro: { join: () => playNotes( [ { freq: 440, at: 0, dur: 0.07 }, { freq: 554.37, at: 0.07, dur: 0.07 }, { freq: 659.25, at: 0.14, dur: 0.14 }, ], 'square', 0.1, ), leave: () => playNotes( [ { freq: 659.25, at: 0, dur: 0.07 }, { freq: 554.37, at: 0.07, dur: 0.07 }, { freq: 440, at: 0.14, dur: 0.14 }, ], 'square', 0.1, ), }, }; export const playCallJoinSound = (style: CallSoundStyle): void => SOUNDS[style]?.join(); export const playCallLeaveSound = (style: CallSoundStyle): void => SOUNDS[style]?.leave();