engine/audio

PURPOSE — Owns all sound output for the game: a single shared AudioContext with a three-bus mixing architecture (game SFX, UI SFX, music) feeding a master glue-comp / brick-wall limiter / room-reverb / stereo-chorus chain, plus the four sound producers that route through it — procedurally-synthesized micro-SFX for juice events, sample-based one-shots and the heat-driven thrust loop, slot-pitched weapon-fire chimes with optional energy layer, and the in-game music player with weighted shuffle / hearts / skips / crossfade / warp-tween.

OWNS

  • The shared AudioContext (lazy-initialized on first access, gated by browser autoplay policy).
  • The master output chain: master gain, glue compressor, brick-wall limiter, pre-master split, room-IR convolver + reverb send/return, stereo LFO-modulated chorus send/delays/return.
  • Three per-bus input gains and per-bus signal shaping: game SFX (low-pass + compressor), UI SFX (compressor only), music (parallel dry / wet-LPF blend + compressor + separate environment-reverb send and convolver).
  • Master / SFX / music slider volumes, persisted mute state, and the per-domain localStorage keys behind them.
  • Music environment-depth state and the per-frame warp-intensity tween applied to the music LPF cutoff and the current track’s playback rate.
  • Music-player runtime: the canonical track list, the weighted shuffle queue, the back-history, the cache of preloaded HTMLAudioElements with permanently-bound MediaElementSourceNodes, the current-track record, active / playing flags, the hearts/skips sets, and the fade-in / fade-out crossfade envelopes.
  • The procedurally-synthesized micro-SFX primitive library (click / pop / crackle / thud / sizzle / sparkle / balloon-pop / laser), the shared white-noise buffer, the event-name to recipe table, the UI-event allowlist that routes recipes to the UI bus, the XP-tinkle rate limiter, and the per-RAF SFX budget plus its overflow queue (shared with weapon chimes).
  • Sample-SFX loader and decoded AudioBuffer cache, the launch / mission-complete / energy-layer / shield-hit / hull-hit one-shot players, the reward sub-bus (50% dry / 50% wet through its own LPF) for stinger playback, the thrust sub-bus (gain → LPF → compressor) and its main + low-rumble voice pool with heat-driven crossfade state, the heat-warning beep timer, and the medical-UI click namespace.
  • Weapon-chime slot-to-frequency map (root / third / fifth / seventh of a major-seventh chord), the per-family timbre table (oscillator type, detune, envelope, peak gain, HPF cutoff, octave multiplier), micro-timing and gain humanization, the laser-layer sweep mixed under each chime, and the standalone dash chime / whoosh / pop / error cues.

READS FROM

  • engine/core/config for the per-RAF SFX cap that bounds micro-SFX scheduling.
  • @metagame/services/analytics trackEvent for music-player skip / heart / unheart events and the one-time legacy-prefs dump.
  • localStorage for persisted mute state, per-bus slider positions, music hearts/skips sets, and the prefs-dump flag.
  • The browser’s Audio, AudioContext, and BiquadFilterNode / DynamicsCompressorNode / ConvolverNode / OscillatorNode / DelayNode / StereoPannerNode APIs.
  • Supabase Storage public-bucket URLs for the music CDN (60 MP3s under a speed-variant subfolder).
  • fetch + decodeAudioData against the public sample paths under /sounds/ for the WAV/OGG library.

PUSHES TO

  • The browser’s default audio destination (every bus eventually merges into master → destination, with parallel reverb-return and chorus-return also writing direct to destination).
  • localStorage writes for slider positions, music hearts and skips, and the one-shot prefs-dump flag.
  • @metagame/services/analytics for music-skip / music-heart / music-unheart / music-prefs-dump telemetry.

DOES NOT

  • Decide which juice event to fire — engine/effects/juice calls in, this module looks up the recipe and synthesizes the sound.
  • Decide which weapon slot fired this frame or pick a damage tag — the weapons module passes both into WeaponChimes.play.
  • Read or react to gameplay state directly (heat, warpT, isThrusting, absorbed shield, hull damage, mission start/end, reward picks). All of that is pushed in by the bridge / weapons / reward / damage layers as call arguments.
  • Define weapon families, timbres-per-weapon, or the slot-to-pitch policy as data — the timbre table and slot-frequency map are hardcoded in this module.
  • Spatialize sounds in world coordinates. Stereo panning is only used in the XP-tinkle and the master chorus stage; everything else is mono routed to a bus.
  • Render any HUD or UI for the music player — only exposes getState for an external HUD to read.
  • Schedule the per-RAF reset itself — the bridge calls resetFrameCount once per frame, which also drains the overflow queue back through the same dispatch path.
  • Implement an inter-module signal bus. Producers call public methods on the four exports directly; the audio context is a singleton consumed via static getters.

Signals fired / Signals watched — none. The audio surface is a set of static method calls on AudioBus, MicroSfx, SampleSfx, WeaponChimes, and MusicPlayer. No engine signals are emitted or subscribed to.

Entry points

  • AudioBus.getCtx / .getMaster / .getGameSfxBus / .getUiSfxBus / .getMusicBus / .getMusicLowpass — accessors that lazy-init the context and return the appropriate node.
  • AudioBus.resume — re-arms a suspended context from any user-gesture-triggered code path.
  • AudioBus.setVolume / .getVolume / .setSfxVolume / .getSfxVolume / .setMusicVolume / .getMusicVolume — slider plumbing with localStorage persistence.
  • AudioBus.setMusicEnvironment — biome-driven environment-reverb depth, also compensates music gain for added reverb energy.
  • AudioBus.setMusicLowpass — exponential ramp of the music LPF cutoff (used by the reward-screen ducking path).
  • AudioBus.dispose — tears down the whole graph on unmount.
  • MicroSfx.play — looks up a recipe by juice event name, walks the steps, schedules each primitive on the correct bus through the frame limiter / overflow queue.
  • MicroSfx.resetFrameCount — called once per RAF frame to zero the budget and drain queued events.
  • MicroSfx._playXpTinkle — the special-cased XP pickup recipe (rate-limited, stereo-panned, allpass-coloured C6/C7 sines).
  • MicroSfx.playCardBurst — reward-card explosion chime, branched by whether the card is a new weapon or an upgrade.
  • MedClick.click — flavored click cue for medical-UI panels and drawers, delegating to a recipe via MicroSfx.play.
  • SampleSfx.preload — kicks off the lazy fetch + decode pass over the sample library.
  • SampleSfx.playLaunch / .playMissionComplete / .playEnergyLayer / .playArtifactReveal / .playWeaponChestOpen / .playLevelUp / .playLevelUpClick / .playReward / .playGong — one-shot players for UI / reward / weapon-layer cues.
  • SampleSfx.playShieldHit / .playHullHit — damage feedback with volume scaled by fraction of max shield / HP lost.
  • SampleSfx.updateThrust — per-frame state machine that spawns the main thrust voice, the always-on low-rumble loop, runs heat-step and time-based crossfades, hands off to the warning-beep oscillator at high heat, and releases all voices on stop.
  • SampleSfx.stopAll — kill switch used on mission end.
  • WeaponChimes.play — slot-and-timbre-keyed chime fire with humanization, optional detuned partial, and an always-on laser sweep mixed underneath; also routes the optional energy-tagged sample layer.
  • WeaponChimes.playDashChime / .playDashWhoosh / .playDashPop / .playDashError — standalone dash-related cues; the whoosh returns a stop closure.
  • WeaponChimes.setVolume / .dispose — pass-throughs to AudioBus.
  • MusicPlayer.start / .stop — mission-lifecycle start (builds queue, fades in first track) and end (disconnects current track, clears state).
  • MusicPlayer.toggle / .next / .previous / .heart / .volumeUp / .volumeDown / .seek — player controls; next always counts as a skip (removes track + clears heart).
  • MusicPlayer.setWarpIntensity — per-frame tween that drives the music LPF cutoff (log-frequency interpolated) and the current track’s playback rate.
  • MusicPlayer.getState — read-only snapshot of track name, progress, duration, playing / hearted / active flags and music volume for the HUD.

Pattern notes

  • The four sound surfaces (MicroSfx, SampleSfx, WeaponChimes, MusicPlayer) are independent modules but all merge through AudioBus.get*Bus(); there is no central mixer module — each producer creates its own short-lived envelope nodes and connects them directly to the bus input.
  • Per-RAF scheduling is shared: weapon chimes and micro-SFX both call bumpSfxFrame against a single counter and any over-budget micro-SFX events are queued, never dropped, then re-dispatched on the next resetFrameCount call (which the bridge owns).
  • UI vs game routing for micro-SFX is decided by a hardcoded Set of event names — a sound’s character is set by both its recipe and which bus the dispatcher picks for it.
  • Recipes are [primitive, gainDb, delaySec, extra?] tuples; each primitive builds its own envelope and disconnects when the source ends. There is no recipe registry — adding an event is a single object-literal entry.
  • Sample-SFX state (thrust voices, low-rumble source, warning-beep timestamp, voice count, next-heat threshold) is module-level — updateThrust is the only entry that mutates it and is expected once per frame.
  • The thrust path uses a parallel-voice crossfade: each heat step or near-end timeout spawns a louder / higher-pitched copy while the older voice is hard-killed or tailed off, capped at two simultaneous voices, with all of them gluing through a shared sub-bus compressor.
  • The reward sub-bus is built lazily on first use and shapes all stinger one-shots through a parallel dry/wet LPF, so the master comp sees a tilt-down rather than two serial chains.
  • Music tracks bind a MediaElementAudioSourceNode permanently to their HTMLAudioElement; crossfades use per-track fadeGain nodes that are created and disposed each play.
  • The music environment-reverb send is separate from the master reverb send: the master reverb is a flat 10% wet glue, while the music reverb is depth-driven and routes its convolver directly to destination (parallel to the master path), with a small compensating music-gain reduction.
  • The legacy getMaster accessor still returns the game SFX bus input for un-migrated callers; new code should pick the explicit bus.
  • Mute state is read once at module load from localStorage (ss_sound_muted) and applied as a zero starting volume; both MicroSfx.enabled and the master gain read the same flag.