SEPSIS_ICU_MIMIC / frontend /src /lib /useCriticalAlarm.ts
Expanic's picture
feat: Add About Sepsis tab and ICU Critical Alarm sound
c0d51b8
Raw
History Blame Contribute Delete
6.4 kB
"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,
}
}