chore: complete clean Agent Desk source snapshot
This commit is contained in:
91
web/lib/sound-alert.ts
Normal file
91
web/lib/sound-alert.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user