Research & Reference

Scoring a game with no audio files

Every game in the collection sounds like something — a break shot, a two-stroke engine climbing its rev range, a knockout — and not one of them ships a single sound file. Here is how you build a soundtrack out of oscillators, noise, and envelopes.

Open any game in the collection with the network tab up and watch what doesn't happen. No .wav, no .mp3, no audio sprite streamed from a CDN. The crack of a break shot, a two-stroke engine dragging itself up through its rev range, a fireball tearing across the screen, a knockout that shakes the frame — every one of them is computed the instant it is heard, from oscillators and noise, by code that ships in the same HTML file as the game.

This is not a stylistic flourish. Each game is one offline page with nothing to install, and an audio file is a fetch, and a fetch is a bet that some server is still there when the player double-clicks the file with the wifi off. Zero files is the only honest way to keep the zero-install promise. So the Audio department on each build — every game here is assembled by an AI studio from a single prompt, department by department — wrote a synthesis engine instead of reaching for a sound library.

The shared spine

The four modules were written independently, for four unrelated games, and they converge on the same skeleton anyway — which is a decent sign it is the right one. An AudioContext is created lazily on the first user gesture, because browsers keep it suspended until then. A master gain node is hard-capped and feeds a DynamicsCompressor before the destination, so a fighting-game combo or a screen full of exploding enemies never clips. A single buffer of white noise is filled once and reused for every gritty sound in the game. And every event is an attack–decay envelope: snap the gain up to near-silence, ramp it to a peak over a few milliseconds, then let it fall exponentially back to nothing.

const AC = window.AudioContext || window.webkitAudioContext;
const ctx = new AC();
const master = ctx.createGain();
master.gain.value = 0.7;                 // hard cap; a compressor catches the peaks
const comp = ctx.createDynamicsCompressor();
master.connect(comp).connect(ctx.destination);

// one noise buffer, filled once, reused everywhere
const noise = ctx.createBuffer(1, ctx.sampleRate * 2, ctx.sampleRate);
const data = noise.getChannelData(0);
for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1;

Add a retrigger guard of thirty to fifty milliseconds so rapid events don't machine-gun into a buzz, disconnect nodes once they have finished sounding, and that is the whole platform. Everything below is a few oscillators and a slice of that noise buffer, shaped.

An impact is a filtered noise burst

A punch, a ball strike, a landing — they are all the same object: a short burst of filtered noise with a fast decay, plus a low sine thump for weight. What makes one a jab and another a haymaker is that the parameters scale with collision energy. The fighter's hit(power) takes a value from 0 to 1 and lets it drive the whole sound.

function hit(power) {                       // power 0..1 = collision energy
  const t = ctx.currentTime;
  const out = ctx.createGain();
  out.gain.value = 0.55 + power * 0.45;             // louder the harder you land it
  const noiseFilter = ctx.createBiquadFilter();
  noiseFilter.type = 'bandpass';
  noiseFilter.frequency.value = 6200 - power * 4200; // heavy hits are duller, meatier
  const g = ctx.createGain();
  g.gain.setValueAtTime(0.0001, t);
  g.gain.linearRampToValueAtTime(0.9, t + 0.002);    // a-few-ms attack
  g.gain.exponentialRampToValueAtTime(0.0001, t + 0.03 + power * 0.05);
  if (power > 0.45) noiseFilter.connect(shaper);     // only big blows distort
}

Harder hits are louder, duller, and longer, and the sine body underneath pitch-drops toward a lower fundamental as power rises. There is a threshold, too: below about 0.45 the smack stays clean, and above it the noise is routed through a WaveShaper whose distortion amount also scales with power, so only heavy blows crunch. The pool game does the identical trick with ball speed on the contact crack. One function, the full dynamic range from a tap to a slam.

The engine that never stops

The motocross engine is the interesting one, because it is not a one-shot at all. startEngine() builds a graph and leaves it running; setThrottle() is called every frame to nudge its parameters. The tone is a sawtooth fundamental plus a square oscillator an octave below for body, both through a lowpass, with a looping band of filtered noise layered on for mechanical grit — and a 19 Hz sine quietly amplitude-modulating the engine gain, which is the idle putput chug. Throttle then maps across every one of those at once.

// called every frame with throttle 0..1
function setThrottle(t) {
  const now = ctx.currentTime;
  const freq   = 55  + t * (190 - 55);      // fundamental tracks RPM
  const cutoff = 450 + t * (2800 - 450);    // filter opens as you rev
  const gain   = 0.045 + t * (0.20 - 0.045);
  // setTargetAtTime = exponential glide, so per-frame updates don't zipper
  osc1.frequency.setTargetAtTime(freq, now, 0.06);
  osc2.frequency.setTargetAtTime(freq * 0.5, now, 0.06);  // octave-down body
  lowpass.frequency.setTargetAtTime(cutoff, now, 0.06);
  engineGain.gain.setTargetAtTime(gain, now, 0.06);
}

The crucial detail is setTargetAtTime with a 60 millisecond time constant. It sets an exponential approach toward each new target rather than jumping, so the discrete per-frame updates smear into a continuous climb instead of a stair-stepped zipper. Rev, and you hear the fundamental rise, the filter open, the grit come up, and the idle chug thin out, all gliding together. Leave the ground and the throttle unhooks from load for a free-rev pitch bump. It is a whole vehicle modelled in about eight parameters.

Whooshes, stingers, and blips

The rest of the vocabulary falls out of the same primitives. A fireball whoosh is three sawtooths detuned a few cents apart, sweeping from 110 to 420 Hz, run through a lowpass whose cutoff sweeps from 280 up to 3200 Hz — the opening filter is the whoosh — with a band of noise riding on top. A KO stinger stacks a deep sine boom pitch-dropping from 190 down to 38 Hz, a square crunch pushed through a distortion curve, and a lowpassed slab of noise, over about a second. UI blips and music cues are just arpeggios: triangle oscillators stepping a C-major chord for a victory flourish, a descending sawtooth line through a lowpass for game over, a two-tone sine plunk for a pocketed ball.

The entire soundtrack of the collection reduces to four things: oscillators, one noise buffer, biquad filters, and gain envelopes. No asset pipeline, no licensing, no loading spinner, nothing that can 404. The sound is the program — which is exactly the point. A game that fits in a single file you can open with the network unplugged has no business phoning home for its own footsteps.