PURPOSE
Generates per-shot weapon “chime” audio so a four-weapon loadout sounds like a harmonic chord rather than a noise stack. Each weapon slot is locked to a scale degree of a C major 7th voicing (C / E / G / B), and the weapon family supplies a timbre (oscillator type, envelope, filter, octave shift). A sci-fi laser sweep is layered under every shot, and energy-tagged weapons also trigger a sampled energy layer. Also hosts the dash-related one-shots used by the player traversal system (activation chime, whoosh, pop, error buzz).
OWNS
SLOT_FREQUENCIES— C4 / E4 / G4 / B4 pitch map keyed by slot index (0–3).ChimeTimbreinterface and theTIMBREStable (glass,bronze,crystal,sine,tine,pluck,bell,impact) plusDEFAULT_TIMBREfallback.- Humanization helpers
humanizeFreq(±~3% pitch jitter via± 0.06 / 2) andhumanizeGain(±15% volume jitter). _playLaserLayer— sawtooth sweep from a randomized 1200–1800 Hz down to 200–400 Hz, low-passed at 3000 Hz, mixed atLASER_GAIN_MULT = 0.316(~-10 dB) of the chime gain.WeaponChimesmodule:play,setVolume,dispose,playDashChime,playDashWhoosh,playDashPop,playDashError.
READS FROM
./audio-context—AudioBus.getCtx(),AudioBus.getGameSfxBus(),AudioBus.resume(),AudioBus.setVolume(),AudioBus.dispose()../micro-sfx—bumpSfxFrame()gate that throttles per-frame SFX spam; chime exits early if the gate denies../sample-sfx—SampleSfx.playEnergyLayer()whendamageTag === 'energy'.- Caller-supplied
slotIndex,timbreKey, and optionaldamageTag(timbre key originates fromWeaponCoreSpec.chimeTimbrein weapon data).
PUSHES TO
- Web Audio graph rooted at the shared game SFX bus
GainNodereturned byAudioBus.getGameSfxBus(). All oscillators, gain envelopes, biquad filters, and the dash whoosh noise buffer connect into that bus. - No game-state writes, no store dispatches, no DOM, no telemetry.
DOES NOT
- Does not decide when weapons fire; it only renders audio when called.
- Does not own the audio context lifecycle — it delegates create/resume/dispose to
AudioBus. - Does not throttle itself across frames beyond the single
bumpSfxFrame()gate at the top ofplay; the dash helpers are not gated. - Does not adjust mixer levels per weapon beyond the timbre’s
peakGainand the humanization jitter. - Does not persist anything, does not load samples (the energy sample lives in
sample-sfx). - Does not cancel or pool oscillators — each shot allocates fresh
OscillatorNode,GainNode, andBiquadFilterNodeinstances and schedulesstop()shortly after the envelope ends.
Signals
- Input call signal:
WeaponChimes.play(slotIndex, timbreKey, damageTag?)from the weapons firing path. - Gate signal:
bumpSfxFrame()returns false → chime is skipped entirely (energy layer is also skipped because the early return runs first). - Resume signal: every entry point calls
AudioBus.resume()to satisfy browser autoplay policies before scheduling. - Audio output signal: oscillator + filter + envelope graph terminating at the shared SFX bus, where the main mix routes it to the destination.
- Stop signal:
playDashWhooshreturns a callback that cancels the envelope and stops the noise source early (used when a dash ends prematurely, e.g. terrain collision).
Entry points
WeaponChimes.play(slotIndex: number, timbreKey: string, damageTag?: string): void— main per-shot entry. Picks pitch fromSLOT_FREQUENCIES(clamped to last slot), picks timbre fromTIMBRES(falls back toDEFAULT_TIMBRE = TIMBRES.glass), schedules a primary oscillator through a highpass + envelope, optionally a detuned second oscillator at half gain (no filter), and always layers the sci-fi laser sweep.WeaponChimes.setVolume(v: number): void— proxies toAudioBus.setVolume.WeaponChimes.dispose(): void— proxies toAudioBus.dispose.WeaponChimes.playDashChime(): void— two-note C5 + G5 (perfect fifth) triangle+sine chorus with 1.8 s decay, second note staggered 15 ms.WeaponChimes.playDashWhoosh(): (() => void) | null— looped white-noise buffer through a bandpass sweep (200 → 3000 → 800 Hz over 0.8 s) with a swell envelope; returns a stop function ornullif the audio context isn’t available.WeaponChimes.playDashPop(): void— 300 → 80 Hz sine thud plus 2000 Hz triangle click.WeaponChimes.playDashError(): void— two detuned square waves at 150 Hz and 158 Hz (~90 cents apart) for a dissonant buzz.
Pattern notes
- Slot-to-degree mapping is fixed at module scope (
SLOT_FREQUENCIES); changing the chord voicing means editing this constant.Math.min(slotIndex, SLOT_FREQUENCIES.length - 1)means any out-of-range slot collapses onto the B4 seventh. - Timbre lookup is a string keyed table; unknown
timbreKeyvalues silently fall back toglass. New weapon families add aTIMBRESentry plus a matchingchimeTimbrein weapon data. - Per-shot timing is humanized only via
humanizeFreq(pitch jitter) andhumanizeGain(volume jitter); there is no explicit phase or scheduling offset — the “wind chimes / polyrhythm” feel relies on weapon fire cadence varying naturally. - The detuned second oscillator is gated on
timbre.detuneCents > 0and bypasses the highpass filter, mixing in at half the primary’s peak gain. - The laser layer is unconditional inside
playand shares the parent chime’s attack and decay so it tracks the chime envelope rather than running on its own timeline. - Energy-tag layering is independent of timbre and runs
SampleSfx.playEnergyLayer()once per qualifying shot, after thebumpSfxFramegate. - Oscillators self-terminate via
osc.stop(now + attack + decay + 0.05)— there is no manual cleanup, the browser GCs disconnected nodes once they finish playing. - Dash helpers (
playDashChime,playDashWhoosh,playDashPop,playDashError) live in this file because they share the same Web Audio scaffolding and SFX bus; they are not gated bybumpSfxFrameand are intended to be invoked sparingly from the dash code path. - All numeric constants (slot frequencies, timbre fields, laser sweep range, dash decay, error detune) are declared as named module constants or table fields rather than magic numbers inside functions.