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:

BusDefault volumeSignal chainUsed by
Game SFX0.8 (slider 8)gain → lowpass(2kHz, Q=0.707) → compressor (-12dB, 8:1, 3ms/100ms) → masterGainweapon-chimes.ts, micro-sfx.ts gameplay sounds
UI SFX0.8 (slider 8, shared with Game SFX)gain → compressor (-8dB, 4:1, 5ms/150ms) → masterGainmenu clicks, reward sounds, stat stamps
Music0.6 (slider 6, ×0.7 scale)gain → 50% dry + 50% lowpass(1kHz, Q=0.707) → compressor (-6dB, 3:1, 10ms/250ms) → masterGainmusic 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:

SliderlocalStorage keyDefaultSetterApplied to
SFXss_sfx_volume0.8setSfxVolume(v)both gameSfxGain and uiSfxGain
Musicss_music_volume0.6setMusicVolume(v)musicGainMUSIC_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 volume
  • depth = 0.5 (default) → 10% wet, music volume ×0.925
  • depth = 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.