Files
agent-desk-ng/web/lib/sound-alert.ts

92 lines
2.1 KiB
TypeScript

let sharedAudioContext: AudioContext | null = null
/**
* Resets the cached AudioContext instance (used for testing).
*/
export function resetAudioContextForTesting(): void {
sharedAudioContext = null
}
function getOrCreateAudioContext(): AudioContext | null {
if (typeof window === "undefined") {
return null
}
const AudioContextClass =
window.AudioContext ||
(window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext
if (!AudioContextClass) {
return null
}
if (!sharedAudioContext || sharedAudioContext.state === "closed") {
try {
sharedAudioContext = new AudioContextClass()
} catch {
return null
}
}
return sharedAudioContext
}
function scheduleTone(
ctx: AudioContext,
freq: number,
startTime: number,
duration: number,
peakGain = 0.15
): void {
const osc = ctx.createOscillator()
const gain = ctx.createGain()
osc.type = "sine"
osc.frequency.setValueAtTime(freq, startTime)
gain.gain.setValueAtTime(peakGain, startTime)
gain.gain.exponentialRampToValueAtTime(0.001, startTime + duration)
osc.connect(gain)
gain.connect(ctx.destination)
osc.start(startTime)
osc.stop(startTime + duration)
}
/**
* Plays a gentle dual-tone chime (587.33 Hz D5 -> 880.00 Hz A5) using the Web Audio API.
* Safely handles SSR/Node environments, unsupported browsers, suspended contexts, and
* browser autoplay policy rejections without throwing errors.
*
* @returns boolean indicating whether the sound playback was successfully initiated.
*/
export function playIncomingMessageSound(): boolean {
try {
const ctx = getOrCreateAudioContext()
if (!ctx) {
return false
}
if (ctx.state === "suspended") {
ctx.resume().catch(() => {
// Traps browser autoplay rejection without crashing
})
}
const now = ctx.currentTime
const note1Duration = 0.08
const note2Duration = 0.16
// Note 1: 587.33 Hz (D5) for 0.08s
scheduleTone(ctx, 587.33, now, note1Duration)
// Note 2: 880.00 Hz (A5) for 0.16s
scheduleTone(ctx, 880.00, now + note1Duration, note2Duration)
return true
} catch {
return false
}
}