import { useRef, useEffect, useCallback } from 'react'; // ───────────────────────────────────────────────────────────── // Constants // ───────────────────────────────────────────────────────────── /** C5 frequency in Hz (523.25) */ const C5 = 523.25; /** E5 frequency in Hz (659.25) */ const E5 = 659.25; /** Duration of each tone in seconds (120 ms) */ const TONE_DURATION = 0.12; /** Overall gain / volume (0.08 = 8%) */ const VOLUME = 0.08; /** Exponential fade-out duration in seconds (450 ms) */ const FADE_DURATION = 0.45; // ───────────────────────────────────────────────────────────── // useSound // ───────────────────────────────────────────────────────────── /** * Hook for playing a notification chime using the Web Audio API. * * - Initialises an `AudioContext` lazily on the first user gesture * (click or keydown) to comply with browser autoplay policies. * - Exposes `playNotificationSound()` which plays a two‑tone chime: * C5 (523.25 Hz) for 120 ms → E5 (659.25 Hz) for 120 ms, * with a shared exponential fade‑out envelope (0.08 → 0.001 over 450 ms). * - If the `AudioContext` is suspended, a warning is logged and the * call is silently ignored. */ export function useSound() { const audioCtxRef = useRef(null); const initializedRef = useRef(false); // ── Lazy initialisation on first user gesture ────────────── useEffect(() => { const initAudio = () => { if (initializedRef.current) return; try { audioCtxRef.current = new AudioContext(); initializedRef.current = true; } catch (err) { console.warn('[useSound] Web Audio API is not available:', err); } // Remove both listeners after the first gesture window.removeEventListener('click', initAudio); window.removeEventListener('keydown', initAudio); }; // Attach listeners for first gesture window.addEventListener('click', initAudio, { once: true }); window.addEventListener('keydown', initAudio, { once: true }); return () => { window.removeEventListener('click', initAudio); window.removeEventListener('keydown', initAudio); // Close AudioContext on unmount const ctx = audioCtxRef.current; if (ctx) { ctx.close().catch(() => {}); audioCtxRef.current = null; } initializedRef.current = false; }; }, []); // ── Play the two‑tone chime ──────────────────────────────── const playNotificationSound = useCallback((): void => { const ctx = audioCtxRef.current; if (!ctx) { console.warn('[useSound] AudioContext has not been initialised yet'); return; } if (ctx.state === 'suspended') { console.warn('[useSound] AudioContext is suspended — cannot play sound'); return; } const now = ctx.currentTime; // Single shared gain node for both oscillators → unified fade‑out const gain = ctx.createGain(); gain.gain.setValueAtTime(VOLUME, now); gain.gain.exponentialRampToValueAtTime(0.001, now + FADE_DURATION); gain.connect(ctx.destination); // Helper: create and schedule a single sine‑wave tone const playTone = (frequency: number, startTime: number): void => { const osc = ctx.createOscillator(); osc.type = 'sine'; osc.frequency.setValueAtTime(frequency, startTime); osc.connect(gain); osc.start(startTime); osc.stop(startTime + TONE_DURATION); }; // Schedule the two tones playTone(C5, now); // C5 starts immediately playTone(E5, now + TONE_DURATION); // E5 starts after C5 ends }, []); return { playNotificationSound }; }