Files
botfights/frontend/src/game/audio/context.ts
T
DorianandClaude Opus 4.6 8afdc2d898 fix: replace raw setTimeout with tracked timers in game audio (BUG-4)
Added audioTimeout() to audio/context.ts — tracked timer set cleared on
scene destroy via clearAllAudioTimers(). Converted 20 sfx.ts + 3 voice.ts
raw setTimeout calls to audioTimeout. Converted FightScene playEntrance
timeout to trackedTimeout. Remaining setTimeout in tts.ts worker layer
and music.ts (already tracked via setMusicTimeout) are self-managing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 09:59:16 +00:00

138 lines
3.6 KiB
TypeScript

// Audio context, gain nodes, volume/mute management
// This module has ZERO imports from other audio modules to prevent circular deps.
let ctx: AudioContext | null = null
let musicGain: GainNode | null = null
let sfxGain: GainNode | null = null
let musicPlaying = false
let musicTimeout: number | null = null
// AudioContext is ONLY created inside ensureAudioContext() (called from user gesture).
// Before that, all SFX calls silently no-op to avoid Chrome autoplay warnings.
let _audioUnlocked = false
let masterMuted = false
const MUSIC_VOL = 0.12
const SFX_VOL = 0.25
export function initCtx() {
if (ctx) return
ctx = new AudioContext()
musicGain = ctx.createGain()
musicGain.gain.value = masterMuted ? 0 : 0.12
musicGain.connect(ctx.destination)
sfxGain = ctx.createGain()
sfxGain.gain.value = masterMuted ? 0 : 0.25
sfxGain.connect(ctx.destination)
}
export function getCtx(): AudioContext {
if (!ctx) initCtx()
if (ctx!.state === 'suspended') {
ctx!.resume().catch(err => console.warn('[Audio] context resume failed:', err))
}
return ctx!
}
// Get the SFX destination node (gain is 0 when muted, so audio is silent but context stays valid)
export function getSfxDest(): AudioNode {
const c = getCtx()
return sfxGain ?? c.destination
}
export function getMusicGain(): GainNode | null {
return musicGain
}
export function getSfxGain(): GainNode | null {
return sfxGain
}
export function isMusicPlaying(): boolean {
return musicPlaying
}
export function setMusicPlaying(v: boolean) {
musicPlaying = v
}
export function getMusicTimeout(): number | null {
return musicTimeout
}
export function setMusicTimeout(v: number | null) {
musicTimeout = v
}
export function isAudioUnlocked(): boolean {
return _audioUnlocked
}
export function setAudioUnlocked(v: boolean) {
_audioUnlocked = v
}
export function isMasterMuted(): boolean {
return masterMuted
}
export function getMasterMuted(): boolean {
return masterMuted
}
export function setMusicVolume(v: number) {
if (musicGain) musicGain.gain.value = Math.max(0, Math.min(1, v))
}
export function setSfxVolume(v: number) {
if (sfxGain) sfxGain.gain.value = Math.max(0, Math.min(1, v))
}
export function setMasterMute(muted: boolean) {
masterMuted = muted
if (muted) {
if (musicGain) musicGain.gain.value = 0
if (sfxGain) sfxGain.gain.value = 0
// Note: kokoroStop and speechSynthesis.cancel are called from the caller
// to avoid circular deps — see index.ts setMasterMute wrapper
} else {
if (musicGain) musicGain.gain.value = MUSIC_VOL
if (sfxGain) sfxGain.gain.value = SFX_VOL
}
}
// Disconnect gain nodes to instantly kill all in-flight oscillators/buffers,
// then reconnect so future sounds still work
export function resetGainNodes() {
if (ctx) {
if (musicGain) {
musicGain.disconnect()
musicGain = ctx.createGain()
musicGain.gain.value = masterMuted ? 0 : MUSIC_VOL
musicGain.connect(ctx.destination)
}
if (sfxGain) {
sfxGain.disconnect()
sfxGain = ctx.createGain()
sfxGain.gain.value = masterMuted ? 0 : SFX_VOL
sfxGain.connect(ctx.destination)
}
}
}
// Tracked audio timers — SFX fire-and-forget delays that must be cleaned on scene destroy
const _audioTimers = new Set<ReturnType<typeof setTimeout>>()
export function audioTimeout(fn: () => void, ms: number): ReturnType<typeof setTimeout> {
const id = setTimeout(() => { _audioTimers.delete(id); fn() }, ms)
_audioTimers.add(id)
return id
}
export function clearAllAudioTimers() {
for (const id of _audioTimers) clearTimeout(id)
_audioTimers.clear()
}
export { MUSIC_VOL, SFX_VOL }