I finally got sound effects working again. It was an involved effort
Randomized Sound EFfects
Originally, I was generating random sound effects at game load. The idea was to generate random effects to make every game experience unique. (Also to try and get a sense of what effects sound good). The approach was interesting, but had technical limitations. Namely, I ran into bugs playing multiple sound effects through JSFXR simultaneously, because that approach was using HTMLAudio elements.
Polyphonic Sound
I moved the sound system to WebAudio to be polyphonic, which required pre-rendering the sounds – something which was a good idea anyway, as it would reduce load time, and offload the JSFXR-based code from the game runtime to being a separate development module. Reducing a runtime library dependency is a meaningful gain.
So I took out the “randomized sound effects” feature and had a script which generated sound effects and saved the waveforms. The problem was, sounds stopped working, and for days, I couldn’t figure out why.
Zero-Byte Sounds
Claude did some meaningful analysis work, analyzing the sound data at the byte-level and finding that, while the sound effects were being generated, for some reason they were pretty much all zeros. That was the first issue.
This was fixed relatively quickly. The problem was, even after generating sound files with actual sound, a few of the sounds were still barely audible.
The Frequency Problem
Analyzing the spectral frequency composition of the problematic waveforms showed the issue: the sounds were primarily sub-bass in composition. These were sounds that were meant to be explosions and have a certain “rumble” to t hem – that’s how Claude had engineered the sounds. Unfortunately, those frequencies aren’t really audible on laptop and mobile speakers. (I’m developing off a seven-year old laptop, for reference – I’m not exactly packin’ any woofers)
By shifting the destruction noises to have more mids and highs (and shift more towards bass, from sub-bass), they are now completely audible.
For the first time in a while, all the sound effects in the game work without any issues.
Timeline
Foundations
- Wired the Kenney sample pack into a working AudioManager (per-event manifest, throttling, layered playback).
- Lifted the SFX volume cap from 20% to 100% — old default was inaudible against the music.
- Fixed the SFX slider UI which was still multiplying by 0.2 from the old cap (slider read “20%” at max).
The big migration
- Restored the offline jsfxr generator pipeline (
sound-defs.js→tools/scripts/generate-sfx.js). Every sound is now 2-3
stacked sfxr voices peak-normalized to a single WAV. - Replaced every Kenney mp3 with an SFXR-generated WAV. Deleted 62 mp3s + the source archive. Game audio is now 100%
synthesized, no third-party sample packs. - 11 new SFX added:
asteroidDestroy,enemyDestroy, all 6 defense skill activations,playerHit_LIGHTNING_ARC, generic
enemy-bullet-hit fallback, plus a new UI tick.
UI clicks
- New delegated capture-phase click listener plays a
menuClickSFX on every button across the document — buttons, tabs,
shop rows, music player, sfx toggles. Auto-skips canvas (gameplay input). Throttled to 50ms so multi-click streaks don’t
buzz.
The silence bug
- Discovered every generated WAV was completely silent — not low, not quiet, zero bytes in the data section. Files looked
fine; spectrum was flat. - Root cause: jsfxr’s
Paramsclass defaultsp_lpf_freqto1(LPF wide open). Our partial param objects left it
undefined → engine treated it as0→ low-pass at 0Hz zeroed every sample. The generator now merges params onto a freshnew Params()so all 27 fields inherit defaults.
Per-enemy destruction sounds
- Each of the 10 enemy types now has its own destruction signature, mass-class-tuned:
- Light (HUNTER, WASP) — sharp pop, ~330ms
- Mid (STALKER, DRIFTER, WEAVER, TANGERINE) — character-coded with phaser/vibrato/repeat-stutter/freq-dramp
- Heavy (SENTINEL, PROWLER, GUARDIAN) — long boom, 600-900ms
- Boss (TITAN) — cataclysmic 1.5s rolling thunder
The “I can’t hear it” saga
- User reported destructions weren’t audible. Wrote a runtime probe to instrument every step of
playSound()— confirmed the
dispatch chain was perfect (BufferSource.onendedfired for every clip). - Wrote a sliding-window FFT spectrum auditor. Found the bug: 87-98% of every destruction’s energy lived below 150Hz — a
band laptop and phone speakers physically cannot reproduce. Audio chain was fine; the speakers were filtering all the content
out. - Moved body energy into the audible 400Hz–3kHz range, then iterated again to land on a 3-layer multi-band design (sub-bass +
mid body + mid noise) that sounds great on a real subwoofer AND on integrated laptop speakers.
Diagnostic tooling
Saved everything to tools/scripts/sound/:
check-wavs.mjs— peak/RMS/nonzero audit per WAV (catches silent files)spectrum.mjs— FFT in 6 frequency bands per WAV (catches band-mismatch issues)- Two Playwright runtime probes for the dispatch chain
- README documenting the bug history each script was written to investigate
Stats
- Versions shipped: 5.68.5 → 5.69.4 (10 patch/minor releases in one session)
- WAVs in the library: 0 → 47
- Total SFX size: ~1.5 MB
- Kenney mp3s remaining in the project: 0



Leave a Reply