Files
Claro-cases/src/hooks/useSound.ts
T
bryan_garcia 8f044567c0 feat: migrar dashboard a React 19 + TypeScript + Vite + Tailwind v4
- Módulo HITL (/cases): 6 patrones de formularios dinámicos para 53 tipos de caso con validación Zod
- Módulo Monitor (/monitor): streaming token-a-token en tiempo real, auto-scroll y notas internas vía WebSocket
- Arquitectura híbrida: REST (canal autoritativo) + WebSocket (difusión/streaming)
- MSW para desarrollo sin backend, hooks de notificaciones/sonido/título preservados
- Backend legacy movido a legacy/, archivos residuales eliminados de raíz
2026-07-23 18:27:03 -05:00

115 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 twotone chime:
* C5 (523.25 Hz) for 120 ms → E5 (659.25 Hz) for 120 ms,
* with a shared exponential fadeout 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<AudioContext | null>(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 twotone 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 fadeout
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 sinewave 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 };
}