92 lines
2.1 KiB
TypeScript
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
|
|
}
|
|
}
|