PURPOSE

Owns the single shared AudioContext for the game and the three-bus mixing architecture (game SFX, UI SFX, music) routed through a master output chain (glue compressor → brick-wall limiter → dry + master reverb + stereo chorus). All sound-producing code connects into one of the bus inputs exposed by the AudioBus singleton. Lazy-initializes on first getCtx() call so the AudioContext is only created after a user gesture (browser autoplay policy).

OWNS

  • The singleton AudioContext (_ctx) and every Web Audio node hanging off it.
  • The master output chain: _masterGain, _masterComp (glue, 2:1, −18 dB threshold), _masterLimiter (brick wall, 20:1, −1 dB threshold), _preMaster, master reverb send/return/convolver, and master stereo chorus (two delay lines modulated by a 0.5 Hz LFO).
  • The synthetic 0.6-second small-room impulse response generated by _createRoomIR (five early reflection taps plus exponential-decay diffuse tail; shared between master reverb and music reverb convolvers).
  • The three sub-bus input nodes: _gameSfxGain, _uiSfxGain, _musicGain. Each has its own per-bus chain feeding _masterGain.
  • The music-specific environment reverb chain: _musicReverbSend, _musicConvolver, _musicReverbReturn.
  • The music bus low-pass filter (_musicLpf) and its parallel wet/dry blend nodes (_musicLpfWetGain, _musicDryGain).
  • Volume state: _volume (master), _sfxVolume (shared SFX), _musicVolume (music slider), _musicDepth (environment depth).
  • The MUSIC_VOLUME_SCALE constant (0.7) applied to all music slider values.
  • The persisted localStorage keys ss_sound_muted, ss_sfx_volume, and ss_music_volume.

READS FROM

  • window.AudioContext (with webkitAudioContext fallback) for context construction.
  • localStorage for persisted mute and per-bus volume defaults on module load and to write updated SFX/music volumes.
  • Caller-supplied values to setVolume, setSfxVolume, setMusicVolume, setMusicEnvironment, and setMusicLowpass.

PUSHES TO

  • Web Audio _ctx.destination via three parallel paths from _preMaster: dry (through _preMaster), master reverb return, and master stereo chorus return.
  • _ctx.destination separately via _musicReverbReturn for the music-only environment reverb.
  • localStorage keys ss_sfx_volume and ss_music_volume on volume setter calls.
  • Returns GainNode references from getGameSfxBus, getUiSfxBus, getMusicBus, and getMaster for consumers to connect their own sources.
  • Returns the music BiquadFilterNode from getMusicLowpass so callers can directly automate the cutoff (used by the warp-puddle audio tween).

DOES NOT

  • Does not load or decode any audio files; the only sound buffer it generates is the synthetic impulse response.
  • Does not play any sound itself — it only builds the routing graph and exposes bus inputs.
  • Does not subscribe to game state, ticks, or the bridge; all updates are pull-based via setter calls from consumers.
  • Does not handle mute toggling beyond reading the persisted ss_sound_muted flag once on load to set the initial _volume to 0; there is no setMuted API.
  • Does not recreate or rewire nodes after dispose() automatically — the next getCtx() rebuilds from scratch.
  • Does not apply per-source effects (panning, pitch, envelopes); that is the consumer’s responsibility.

Signals

  • Lazy init signal: the first call to getCtx() (or any get*Bus() method) triggers _init(), which constructs the entire graph and starts the chorus LFO oscillator.
  • Volume changes propagate immediately by writing to the gain.value of the relevant nodes.
  • setMusicEnvironment(depth) adjusts _musicReverbSend.gain linearly (0–20% wet) and calls _applyMusicGain to compensate music volume downward by up to 15% at full depth.
  • setMusicLowpass(freq) schedules an exponential ramp on _musicLpf.frequency over 0.3 seconds.
  • The chorus LFO runs continuously from init until dispose() is called.

Entry points

  • AudioBus.getCtx(): AudioContext | null — returns the shared context, lazy-initializing.
  • AudioBus.getMaster(): GainNode | null — backward-compat alias returning the game SFX bus input.
  • AudioBus.getGameSfxBus(): GainNode | null — bus input for gameplay SFX (weapon chimes, micro-sfx).
  • AudioBus.getUiSfxBus(): GainNode | null — bus input for UI sounds (menu clicks, reward stamps).
  • AudioBus.getMusicBus(): GainNode | null — bus input for music tracks.
  • AudioBus.getMusicLowpass(): BiquadFilterNode | null — direct handle to the music LPF for the warp-puddle tween.
  • AudioBus.resume(): void — resumes the context if suspended (call from any user gesture).
  • AudioBus.setVolume(v) / getVolume() — master volume (also applied to reverb and chorus returns).
  • AudioBus.setSfxVolume(v) / getSfxVolume() — shared volume for game and UI SFX buses (persisted).
  • AudioBus.setMusicVolume(v) / getMusicVolume() — music slider value before scale/compensation (persisted).
  • AudioBus.setMusicEnvironment(depth: 0..1) — sets music reverb wetness and compensating gain reduction.
  • AudioBus.setMusicLowpass(freq) — exponential ramp to the given cutoff frequency over 0.3 s.
  • AudioBus.dispose() — stops the chorus LFO, closes the context, and nulls every node reference.

Pattern notes

  • Singleton module-scoped state: every node is held in a private let _foo: T | null = null and accessed only through the AudioBus object literal. The module is its own instance — there is no class.
  • Lazy init guarded by if (!_ctx) _init() at the top of every public getter, so the order of first call does not matter.
  • Per-bus chains all merge into a single _masterGain so the master glue compressor and limiter see the summed mix, not individual buses.
  • Master reverb and master chorus are wired as parallel sends from _preMaster, additive to the dry path rather than replacing it.
  • Music bus uses a parallel dry/wet blend around _musicLpf (separate _musicDryGain and _musicLpfWetGain nodes feeding the same compressor) rather than a single in-line filter, so the wet/dry ratio is independently controllable.
  • The synthetic impulse response is generated in pure JS at init via _createRoomIR; no IR file ship as an asset. Stereo decorrelation comes from per-channel random noise and ±1 ms tap jitter.
  • Chorus uses two DelayNodes with opposite-polarity LFO modulation (+0.002 and −0.002 gain on the LFO send) so the stereo image breathes rather than moving in unison.
  • Volume setters clamp to [0, 1] with Math.max(0, Math.min(1, v)).
  • localStorage reads and writes are wrapped in try/catch so the module works under SSR or when storage is unavailable.
  • Backwards-compat shim: getMaster() returns the game SFX bus input so legacy callers that never migrated to a specific bus still produce sound.
  • dispose() is idempotent-safe: every try { _chorusLfo.stop() } catch {} and _ctx.close().catch(() => {}) swallows already-stopped errors.