2026-03-08 22:11:58 +00:00
|
|
|
// 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') {
|
2026-03-12 23:06:22 +00:00
|
|
|
ctx!.resume().catch(err => console.warn('[Audio] context resume failed:', err))
|
2026-03-08 22:11:58 +00:00
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export { MUSIC_VOL, SFX_VOL }
|