← Back to devlog

2026-02-28

Procedural audio, take one

Combat scene where the new audio plays
The combat scene where the new SFX and drone live.

The game has sound. Cards click when you play them, attacks land with a thud, the victory chime is a small triumph in a single second.

All of it is procedural — built at click-time from oscillators, noise bursts, and biquad filters routed through a master compressor and a tiny convolver reverb. The whole audio module is one TypeScript file, no audio assets. A SFX call looks like:

function fxAttack(c) {
  const sub = osc({ type: "sine", freq: 220, glideTo: 60, ... });
  const body = osc({ type: "sawtooth", freq: 320, glideTo: 110, ... });
  const crack = noise({ duration: 70, filter: "highpass", ... });
  return [sub, body, crack];
}

Why bother with this when a single mp3 would be done in five minutes? Two reasons. First, asset weight — the whole audio system is <15 KB transferred. Second, every interaction is unique enough that pre-baked samples either sound stale or balloon to dozens of variants.

Combat ambience is a low drone (two detuned sawtooths through a low-pass) that fades in on combat enter. It's ugly on its own, but it sells the "pressure" of the fight when stacked under the SFX.

What's missing: actual music. A drone is not a score. I have an idea for a slow chord-progression pad with per-scene palette — minor on the map, warmer at campfires — but that's its own evening. Today I just wanted the game to stop being silent.

A mute toggle and a reduced-motion toggle landed in Settings today too. Accessibility is non-negotiable; volume sliders coming later.