Audio Buses
Starship Survivors routes all sound through a single shared AudioContext with three sub-bus input nodes plus a master output chain. Each bus has its own character before merging into a master gain stage that adds glue compression, brick-wall limiting, room reverb, and stereo chorus.
Bus topology
There are three sub-buses (game SFX, UI SFX, music) feeding one master bus:
| Bus | Default volume | Signal chain | Used by |
|---|---|---|---|
| Game SFX | 0.8 (slider 8) | gain → lowpass(2kHz, Q=0.707) → compressor (-12dB, 8:1, 3ms/100ms) → masterGain | weapon-chimes.ts, micro-sfx.ts gameplay sounds |
| UI SFX | 0.8 (slider 8, shared with Game SFX) | gain → compressor (-8dB, 4:1, 5ms/150ms) → masterGain | menu clicks, reward sounds, stat stamps |
| Music | 0.6 (slider 6, ×0.7 scale) | gain → 50% dry + 50% lowpass(1kHz, Q=0.707) → compressor (-6dB, 3:1, 10ms/250ms) → masterGain | music player |
The “micro-sfx” bucket isn’t a fourth bus — micro-sfx.ts routes some sounds to Game SFX and others to UI SFX depending on whether they’re diegetic or interface chrome.
Per-bus character
Game SFX runs through a 2 kHz lowpass with no resonance (Butterworth flat) to tame harsh high-frequency synth content from procedurally generated chimes, then through an aggressive 8:1 compressor that acts as a limiter when many sounds stack at once.
UI SFX has no filter and a gentler 4:1 compressor — menu clicks should stay crisp and full-band.
Music has its global volume scaled by a constant MUSIC_VOLUME_SCALE = 0.7 (effectively a -30% trim at all slider positions). The signal splits into a 50% dry path plus a 50% wet path through a music-bus lowpass filter, then a slow 3:1 compressor.
Master output chain
All three buses merge into masterGain, which flows through a fixed chain:
masterGain → masterComp (glue: -18dB, 2:1, 20ms/200ms)
→ masterLimiter (-1dB, 20:1, 1ms/50ms)
→ preMaster ┬→ destination (dry)
├→ reverbSend (10%) → roomConvolver (0.6s small-room IR) → destination
└→ chorusSend (10%) → stereo LFO-modulated delays → destination
The glue compressor’s soft 10dB knee and 20ms attack let transients through while gluing the mix. The limiter’s hard knee and 1ms attack catch any peak that sneaks past glue.
The room reverb uses a synthetically generated 0.6-second impulse response — five discrete early-reflection taps at 5–40 ms simulating wall bounces, then an exponential-decay diffuse tail from 40 ms to 600 ms (RT60 ≈ 0.5 s). Stereo decorrelation comes from per-channel random seeds.
The stereo chorus is two delay lines (7 ms left, 9 ms right base) modulated by a 0.5 Hz sine LFO at ±2 ms depth. Left and right delays receive opposite-polarity modulation so one shortens while the other lengthens — creates width without an audible chorus effect.
Master volume
AudioBus.setVolume(v) (0.0–1.0) applies to masterGain, reverbReturn, and chorusReturn simultaneously so wet and dry paths track together. Default is 0.3. On module load, if localStorage['ss_sound_muted'] === '1', volume initializes to 0 (persisted mute).
Per-bus volume control (settings)
The settings UI exposes two sliders backed by localStorage:
| Slider | localStorage key | Default | Setter | Applied to |
|---|---|---|---|---|
| SFX | ss_sfx_volume | 0.8 | setSfxVolume(v) | both gameSfxGain and uiSfxGain |
| Music | ss_music_volume | 0.6 | setMusicVolume(v) | musicGain (× MUSIC_VOLUME_SCALE × reverb compensation) |
The SFX slider drives both the Game SFX and UI SFX buses with the same value — they don’t have independent volume control. Slider values are clamped to [0, 1] on set and persisted on every change.
Low-pass ducking during freezes
The music bus exposes its lowpass filter via getMusicLowpass() and setMusicLowpass(freq). The setter cancels any scheduled ramp, holds the current value, then exponentialRampToValueAtTime(freq, currentTime + 0.3) for a smooth 0.3-second sweep.
Normal gameplay sits at 5000 Hz. Reward screens drop to 1000 Hz — bass-only, muffled feel that signals UI takeover. The warp-puddle audio tween chops harder, ramping to 400 Hz when the ship is inside a warp zone.
Music environment reverb (depth)
Separate from the master room reverb, the music bus has its own environment send: musicReverbSend → musicConvolver (same 0.6s IR) → destination. Driven by setMusicEnvironment(depth):
depth = 0(close quarters) → 0% wet, full music volumedepth = 0.5(default) → 10% wet, music volume ×0.925depth = 1(deep space) → 20% wet, music volume ×0.85
Volume scales down with wetness via _applyMusicGain() (musicVolume × MUSIC_VOLUME_SCALE × (1 − 0.15 × depth)) to compensate for added reverb energy keeping perceived loudness flat.
AudioContext lifecycle
The AudioContext is a singleton, lazy-initialized on first AudioBus.getCtx() call. Browsers require a user gesture before audio can play, so init happens after the first user interaction — never at module load.
AudioBus.resume() is called from any user-gesture-triggered code path to wake the context if it has been suspended by the browser. Tab-hide / tab-focus suspension and resumption are handled by browser autoplay policy plus resume() calls on gameplay-resuming gestures.
AudioBus.dispose() stops the chorus LFO oscillator, closes the context, and nulls every cached node reference for clean teardown.
Authoring notes
- New gameplay sounds should connect to
getGameSfxBus()and inherit the 2 kHz lowpass + 8:1 compressor automatically. - New UI sounds should connect to
getUiSfxBus()— no filter, gentler dynamics. - New music tracks should connect to
getMusicBus()and inherit the music-bus envelope (LPF dry/wet, scaled volume, environment reverb). - The legacy
getMaster()getter returns the Game SFX bus input for backward compatibility — don’t use for new code.
Related
engine/audio/audio-context.ts— implementationgameplay/concepts/freeze-states.md— what triggers the music lowpass duck