Rainboids DevDiary: Sound Effect Shenanigans

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.jstools/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 menuClick SFX 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 Params class defaults p_lpf_freq to 1 (LPF wide open). Our partial param objects left it
    undefined → engine treated it as 0 → low-pass at 0Hz zeroed every sample. The generator now merges params onto a fresh new 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.onended fired 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
· · ·

Comments

Leave a Reply

Check also

View Archive [ -> ]

Discover more from afeique.com

Subscribe now to keep reading and get access to the full archive.

Continue reading