// ══════════════════════════════════════════════ // Tone.js lazy load — import('tone') only on first SFX call // ══════════════════════════════════════════════ type ToneModule = typeof import('tone'); let _tone: ToneModule | null = null; let _toneLoading: Promise | null = null; function loadTone(): Promise { if (_tone) return Promise.resolve(_tone); if (!_toneLoading) { _toneLoading = import('tone').then((m) => { _tone = m; return m; }); } return _toneLoading; } /** Synchronously get Tone; returns null if not yet loaded (triggers loading as a side effect). */ function T(): ToneModule | null { if (!_tone) { void loadTone(); } return _tone; } // ══════════════════════════════════════════════ // Tone.js routing — sfxChannel / bgmChannel lazy init // ══════════════════════════════════════════════ let _sfxChannel: any = null; let _bgmChannel: any = null; function getSfxChannel(t: ToneModule) { if (!_sfxChannel) _sfxChannel = new t.Channel({ volume: 0 }).toDestination(); return _sfxChannel; } function getBgmChannel(t: ToneModule) { if (!_bgmChannel) _bgmChannel = new t.Channel({ volume: 0 }).toDestination(); return _bgmChannel; } // ══════════════════════════════════════════════ // AudioContext compatibility // ══════════════════════════════════════════════ export function getAudioCtx(): AudioContext | null { const t = T(); if (!t) return null; return t.getContext().rawContext as AudioContext; } // ══════════════════════════════════════════════ // iOS audio-session bypass — keep the process in the MediaPlayback category so WebAudio // (BGM + SFX) ignores the hardware mute switch (silent mode). // ══════════════════════════════════════════════ // WebKit picks ONE process-wide audio-session category: pure WebAudio → AmbientSound (respects // the mute switch → silent in silent mode); an *audible* HTMLMediaElement → MediaPlayback // (ignores the mute switch). A muted / zero-volume element is treated as inaudible and does NOT // raise the category — that is why the previous muted-