let audioCtx: AudioContext | null = null; function getAudioCtx(): AudioContext | null { if (audioCtx) return audioCtx; try { const Ctx = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext; audioCtx = new Ctx(); return audioCtx; } catch { return null; } } /** Short two-tone ping for chat notifications */ export function playChatSound() { const ctx = getAudioCtx(); if (!ctx) return; if (ctx.state === 'suspended') { void ctx.resume(); } const playTone = (freq: number, start: number, duration: number) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.value = freq; gain.gain.setValueAtTime(0.0001, start); gain.gain.exponentialRampToValueAtTime(0.12, start + 0.02); gain.gain.exponentialRampToValueAtTime(0.0001, start + duration); osc.connect(gain); gain.connect(ctx.destination); osc.start(start); osc.stop(start + duration + 0.02); }; const t = ctx.currentTime; playTone(880, t, 0.12); playTone(1174, t + 0.14, 0.14); } /** Louder alert for student violations (quit / kill app) */ export function playAlertSound() { const ctx = getAudioCtx(); if (!ctx) return; if (ctx.state === 'suspended') { void ctx.resume(); } const playTone = (freq: number, start: number, duration: number) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'square'; osc.frequency.value = freq; gain.gain.setValueAtTime(0.0001, start); gain.gain.exponentialRampToValueAtTime(0.1, start + 0.02); gain.gain.exponentialRampToValueAtTime(0.0001, start + duration); osc.connect(gain); gain.connect(ctx.destination); osc.start(start); osc.stop(start + duration + 0.02); }; const t = ctx.currentTime; playTone(520, t, 0.16); playTone(390, t + 0.18, 0.2); playTone(520, t + 0.4, 0.18); }