Spaces:
Sleeping
Sleeping
| "use client" | |
| import { useCallback, useEffect, useRef, useState } from "react" | |
| /** | |
| * ICU Critical Alarm System | |
| * | |
| * Uses Web Audio API to generate realistic ICU monitor alarm sounds. | |
| * - Critical: Rapid triple-beep pattern (like real ICU cardiac monitors) | |
| * - Warning: Double-beep pattern (medium urgency) | |
| * | |
| * No external audio files needed — pure synthesis. | |
| */ | |
| type AlarmLevel = 'critical' | 'warning' | null | |
| interface UseCriticalAlarmReturn { | |
| /** Play the alarm for the given level */ | |
| playAlarm: (level: AlarmLevel) => void | |
| /** Stop any currently playing alarm */ | |
| stopAlarm: () => void | |
| /** Whether sound is muted */ | |
| isMuted: boolean | |
| /** Toggle mute state */ | |
| toggleMute: () => void | |
| /** Whether an alarm is currently active */ | |
| isPlaying: boolean | |
| } | |
| // Persist mute state across component mounts | |
| let globalMuted = false | |
| export function useCriticalAlarm(): UseCriticalAlarmReturn { | |
| const [isMuted, setIsMuted] = useState(globalMuted) | |
| const [isPlaying, setIsPlaying] = useState(false) | |
| const audioCtxRef = useRef<AudioContext | null>(null) | |
| const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null) | |
| const timeoutRefs = useRef<ReturnType<typeof setTimeout>[]>([]) | |
| const activeNodesRef = useRef<OscillatorNode[]>([]) | |
| // Initialize AudioContext lazily (must be after user interaction) | |
| const getAudioContext = useCallback(() => { | |
| if (!audioCtxRef.current) { | |
| audioCtxRef.current = new (window.AudioContext || (window as any).webkitAudioContext)() | |
| } | |
| if (audioCtxRef.current.state === 'suspended') { | |
| audioCtxRef.current.resume() | |
| } | |
| return audioCtxRef.current | |
| }, []) | |
| // Play a single beep tone | |
| const playBeep = useCallback(( | |
| frequency: number, | |
| duration: number, | |
| volume: number = 0.3, | |
| type: OscillatorType = 'sine' | |
| ) => { | |
| try { | |
| const ctx = getAudioContext() | |
| const oscillator = ctx.createOscillator() | |
| const gainNode = ctx.createGain() | |
| oscillator.type = type | |
| oscillator.frequency.setValueAtTime(frequency, ctx.currentTime) | |
| // Attack-decay envelope for realistic beep | |
| gainNode.gain.setValueAtTime(0, ctx.currentTime) | |
| gainNode.gain.linearRampToValueAtTime(volume, ctx.currentTime + 0.01) | |
| gainNode.gain.setValueAtTime(volume, ctx.currentTime + duration - 0.05) | |
| gainNode.gain.linearRampToValueAtTime(0, ctx.currentTime + duration) | |
| oscillator.connect(gainNode) | |
| gainNode.connect(ctx.destination) | |
| oscillator.start(ctx.currentTime) | |
| oscillator.stop(ctx.currentTime + duration) | |
| activeNodesRef.current.push(oscillator) | |
| oscillator.onended = () => { | |
| activeNodesRef.current = activeNodesRef.current.filter(n => n !== oscillator) | |
| gainNode.disconnect() | |
| } | |
| } catch (e) { | |
| console.warn('Audio playback failed:', e) | |
| } | |
| }, [getAudioContext]) | |
| // ICU Critical alarm pattern: rapid triple beep (like cardiac monitor) | |
| // Frequency ~1000Hz, pattern: beep-beep-beep ... pause ... repeat | |
| const playCriticalPattern = useCallback(() => { | |
| // Triple beep: 3 quick beeps | |
| playBeep(1000, 0.15, 0.35, 'sine') | |
| const t1 = setTimeout(() => playBeep(1000, 0.15, 0.35, 'sine'), 200) | |
| const t2 = setTimeout(() => playBeep(1000, 0.15, 0.35, 'sine'), 400) | |
| // Second harmonic overlay for urgency | |
| const t3 = setTimeout(() => playBeep(1500, 0.08, 0.15, 'square'), 450) | |
| timeoutRefs.current.push(t1, t2, t3) | |
| }, [playBeep]) | |
| // Warning alarm pattern: double beep (medium urgency) | |
| const playWarningPattern = useCallback(() => { | |
| playBeep(800, 0.12, 0.2, 'sine') | |
| const t1 = setTimeout(() => playBeep(800, 0.12, 0.2, 'sine'), 250) | |
| timeoutRefs.current.push(t1) | |
| }, [playBeep]) | |
| const stopAlarm = useCallback(() => { | |
| // Clear all scheduled beeps | |
| if (intervalRef.current) { | |
| clearInterval(intervalRef.current) | |
| intervalRef.current = null | |
| } | |
| timeoutRefs.current.forEach(t => clearTimeout(t)) | |
| timeoutRefs.current = [] | |
| // Stop any active oscillators | |
| activeNodesRef.current.forEach(osc => { | |
| try { osc.stop() } catch { } | |
| }) | |
| activeNodesRef.current = [] | |
| setIsPlaying(false) | |
| }, []) | |
| const playAlarm = useCallback((level: AlarmLevel) => { | |
| if (!level || isMuted) { | |
| stopAlarm() | |
| return | |
| } | |
| // Stop existing alarm before starting new one | |
| stopAlarm() | |
| setIsPlaying(true) | |
| if (level === 'critical') { | |
| // Play immediately | |
| playCriticalPattern() | |
| // Repeat every 1.5 seconds (ICU-style persistent alarm) | |
| intervalRef.current = setInterval(() => { | |
| if (!globalMuted) { | |
| playCriticalPattern() | |
| } | |
| }, 1500) | |
| // Auto-stop after 10 seconds to avoid infinite alarm | |
| const autoStop = setTimeout(() => { | |
| stopAlarm() | |
| }, 10000) | |
| timeoutRefs.current.push(autoStop) | |
| } else if (level === 'warning') { | |
| // Play immediately | |
| playWarningPattern() | |
| // Repeat every 3 seconds (less urgent) | |
| intervalRef.current = setInterval(() => { | |
| if (!globalMuted) { | |
| playWarningPattern() | |
| } | |
| }, 3000) | |
| // Auto-stop after 6 seconds | |
| const autoStop = setTimeout(() => { | |
| stopAlarm() | |
| }, 6000) | |
| timeoutRefs.current.push(autoStop) | |
| } | |
| }, [isMuted, stopAlarm, playCriticalPattern, playWarningPattern]) | |
| const toggleMute = useCallback(() => { | |
| const newMuted = !isMuted | |
| globalMuted = newMuted | |
| setIsMuted(newMuted) | |
| if (newMuted) { | |
| stopAlarm() | |
| } | |
| }, [isMuted, stopAlarm]) | |
| // Cleanup on unmount | |
| useEffect(() => { | |
| return () => { | |
| stopAlarm() | |
| } | |
| }, [stopAlarm]) | |
| return { | |
| playAlarm, | |
| stopAlarm, | |
| isMuted, | |
| toggleMute, | |
| isPlaying, | |
| } | |
| } | |