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
localStoragekeys 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-boundMediaElementSourceNodes, 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
AudioBuffercache, 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/configfor the per-RAF SFX cap that bounds micro-SFX scheduling.@metagame/services/analyticstrackEventfor music-player skip / heart / unheart events and the one-time legacy-prefs dump.localStoragefor persisted mute state, per-bus slider positions, music hearts/skips sets, and the prefs-dump flag.- The browser’s
Audio,AudioContext, andBiquadFilterNode/DynamicsCompressorNode/ConvolverNode/OscillatorNode/DelayNode/StereoPannerNodeAPIs. - Supabase Storage public-bucket URLs for the music CDN (60 MP3s under a speed-variant subfolder).
fetch+decodeAudioDataagainst 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).
localStoragewrites for slider positions, music hearts and skips, and the one-shot prefs-dump flag.@metagame/services/analyticsfor music-skip / music-heart / music-unheart / music-prefs-dump telemetry.
DOES NOT
- Decide which juice event to fire —
engine/effects/juicecalls 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
getStatefor an external HUD to read. - Schedule the per-RAF reset itself — the bridge calls
resetFrameCountonce 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 withlocalStoragepersistence.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 viaMicroSfx.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 toAudioBus.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;nextalways 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 throughAudioBus.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
bumpSfxFrameagainst a single counter and any over-budget micro-SFX events are queued, never dropped, then re-dispatched on the nextresetFrameCountcall (which the bridge owns). - UI vs game routing for micro-SFX is decided by a hardcoded
Setof 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 —
updateThrustis 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
MediaElementAudioSourceNodepermanently to theirHTMLAudioElement; crossfades use per-trackfadeGainnodes 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
getMasteraccessor 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; bothMicroSfx.enabledand the master gain read the same flag.