PURPOSE
Plays decoded WAV/OGG samples for key gameplay moments — UI launch, mission-complete, energy-weapon layer, player damage feedback, reward stingers, and the heat-driven thrust crossfade loop. Owns the lazy fetch/decode pipeline, the thrust voice graph (sub-bus + LPF + compressor), the reward stinger parallel LPF sub-bus, and an out-of-tune oscillator warning beep that fires above 96% heat.
OWNS
SAMPLE_PATHSregistry mapping sample names to/sounds/*.wavand*.oggfiles served frompublic/sounds/._bufferscache (Map<string, AudioBuffer>) and the_loadingStartedguard for one-shot async fetch +decodeAudioData.- Thrust sub-bus graph:
_thrustBus(GainNode) →_thrustLpf(lowpass 1800 Hz, Q 0.707) →_thrustCompressor(threshold -23 dB, knee 3, ratio 12, attack 0.003 s, release 0.15 s) → game SFX bus. - Reward sub-bus graph:
_rewardBussplit into 50% dry path and 50% wet path through a 4 kHz lowpass (Q 0.707), both summed into the UI bus. - Thrust voice pool:
_voices: ThrustVoice[](each holdssource,gain,startTime,basePitch); per-hold counters_voiceCount,_nextHeatThreshold, and the_thrustActiveflag. - Low rumble layer state:
_thrustLowSource(loopingAudioBufferSourceNode) and_thrustLowGainwith an attack/decay/sustain/release envelope. - Warning-beep cadence state
_lastWarnBeep(AudioContext-time timestamp). - Internal helpers
_ensureLoaded,_playOneShot,_ensureRewardBus,_ensureThrustBus,_spawnVoice,_tailOffVoice,_killVoice,_playWarnBeep,_crossfade. - Public
SampleSfxsingleton with the player-facing API.
READS FROM
AudioBus(./audio-context) —getCtx()for the sharedAudioContext,getUiSfxBus()for UI-routed samples,getGameSfxBus()for thrust/energy layer, andresume()to unlock on first user-gesture-driven play.public/sounds/HTTP assets — each entry inSAMPLE_PATHSis fetched viafetch()and decoded withctx.decodeAudioData. Failed entries leave the buffer absent; callers no-op when the buffer is missing.- Caller-supplied gameplay state —
updateThrust(isThrusting, heat),playShieldHit(absorbed, shieldMax),playHullHit(hullDmg, hpMax)use those values for envelope and gain scaling.
PUSHES TO
- Web Audio graph — connects sources/gains/filters/compressors into the appropriate
AudioBusoutputs. Output never bypasses anAudioBusmixer node. console.warnonce if_ensureLoadeddecoded fewer samples than were requested.
DOES NOT
- Does not own or mutate the master/UI/game/music
GainNodes; those belong toAudioBus. - Does not stream or schedule music — that is the music-engine module’s responsibility.
- Does not read game-state stores directly; all gameplay inputs arrive as method arguments.
- Does not retry failed sample fetches —
_loadingStartedlatchestrueeven when some samples fail. - Does not run a per-frame ticker; consumers must call
updateThrustthemselves each frame for the heat crossfade loop to advance. - Does not despawn voices on
stopAllother than by callingsource.stop()and disconnecting gain — theonendedhandler still removes them from_voices.
Signals
THRUST_BASE_DB = -34,GAIN_STEP_DB = 0.5per successive voice copy.THRUST_LOW_DB = -26, sustain = 50% of peak; attack 0.2 s, decay 0.4 s, release 1.0 s.THRUST_FADE_OUT = 0.3 son release of the heat-driven voices.CROSSFADE_OUT = 2.0 sexponential ramp on tail-off;TIME_CROSSFADE_LEAD = 4.0 sbefore sample end triggers a fallback crossfade.HEAT_STEP = 20— heat delta between successive crossfades (the next threshold isheat + HEAT_STEPat thrust start). Header doc references “every 10%” but the constant is 20.PITCH_STEP = 0.05per voice copy;PITCH_CAP = 1.5.- Warning beep —
WARN_FREQ = 566 Hzsquare withdetune = 15cents,WARN_GAIN = dB(-28),WARN_BEEP_DUR = 0.06 s, beep interval lerps fromWARN_INTERVAL_MAX = 0.35 sat 96% heat toWARN_INTERVAL_MIN = 0.12 sat 100%, starts atWARN_HEAT_START = 96. - Reward sub-bus —
REWARD_LPF_HZ = 4000,REWARD_WETNESS = 0.5. - One-shot gains — launch: -10 dB with 0.2 s fade-in; mission complete: -16 dB; energy layer: -25 dB; shield hit:
dB(-25) * (0.25 + 0.75 * frac); hull hit:dB(-25) * (0.35 + 0.65 * frac); all reward stingers and gong: -20 dB. - Voice cap of 2 —
_crossfadehard-kills the oldest and tail-offs the second when at the cap.
Entry points
SampleSfx.preload()— fires_ensureLoadedasynchronously without awaiting.SampleSfx.playLaunch()— UI bus, -10 dB with 0.2 s exponential fade-in; callsAudioBus.resume().SampleSfx.playMissionComplete()— UI bus one-shot at -16 dB; callsAudioBus.resume().SampleSfx.updateThrust(isThrusting, heat)— per-frame; starts/stops the heat-driven voice pool and the low rumble loop, triggers heat-step and time-based crossfades, respawns if all voices ended between frames, and emits warning beeps onceheat >= 96.SampleSfx.playEnergyLayer()— game SFX bus; picksenergy_1..3uniformly at random and plays at -25 dB.SampleSfx.playShieldHit(absorbed, shieldMax)— UI bus; volume scales by absorbed fraction of max shield (floor 0.25, ceiling 1.0).SampleSfx.playHullHit(hullDmg, hpMax)— UI bus; randomplayer_hull_1..4; volume scales by HP-loss fraction (floor 0.35).SampleSfx.playArtifactReveal(),playWeaponChestOpen(),playLevelUp(),playLevelUpClick(),playReward(),playGong()— all route through the reward sub-bus at -20 dB.SampleSfx.stopAll()— clears the thrust active flag, stops every active voice and the low rumble source, disconnects their gains.
Pattern notes
- Lazy buffer loading runs once per process.
_loadingStartedis a latch — re-callingpreload()after a fetch failure will not retry. - All
_playOneShotandplayLaunchpaths fail silently when theAudioContext, target bus, or required buffer is missing; this is the intended boundary behaviour (audio is non-critical). - Sub-buses are created lazily inside the first call that needs them (
_ensureThrustBus,_ensureRewardBus) and cached for the lifetime of the process. - Thrust voice lifecycle uses both an
onendedhandler (removes from_voices, disconnects gain) and explicitsource.stop()on release; gain envelopes useexponentialRampToValueAtTimeand therefore always target0.0001rather than0. - The thrust voice cap relies on
_crossfade: at length 2 the oldest is hard-killed, then the next-oldest tail-offs, then a new voice is spawned — meaning during a crossfade only two voices share the bus. - The low rumble layer (
thrust_low) is independent of the heat-step crossfade — it is a single looped source with its own ADSR envelope and rides the same thrust sub-bus. - The warning beep deliberately bypasses the game SFX low-pass by routing through the UI bus, and its 15-cent detune is intentional for an unsettling feel.
- Header comment describes “every 10% heat” but the live constant
HEAT_STEP = 20and inline comment (“Every 20% heat gained”) are the source of truth. _voiceCountand_nextHeatThresholdare reset only on the start-of-thrust edge; they are not reset between consecutive holds inside a single mission.- Module state is module-singleton — there is no per-instance/per-scene reset other than
stopAll()and the start-of-thrust edge logic.