No samples, no licensed tracks, no AI audio model, no API. A game-building agent can call the same tiny contract - seed + vibe in, soundtrack out. Two systems share one idea: a song is just a small program. This is exactly what runs in Bandlings and in 170+ shipped games.
1 · Your text becomes a number: the game's name is hashed into a 32-bit genome.
2 · Slices of that number pick the key, the chords, the melody shape and the tempo.
3 · A scheduler walks a 16-step grid and spawns one oscillator per note. No files, no samples, no AI model.
4 · In Bandlings the board IS the band: tier = instrument, merging a monster upgrades the song.
The engine hashes the game's identity (its title and path, or whatever you type into the box) with an FNV-1a-style hash into a 32-bit number. Same text in, same number out, forever - which is why the same seed always plays the same song, on any device.
var h = 2166136261;
for (var i = 0; i < key.length; i++) {
h ^= key.charCodeAt(i);
h = (h * 16777619) >>> 0;
} // "Doom" -> one specific 32-bit genome
Independent bit-slices of that one number make the musical decisions, so two games never make the same combination of choices:
root = roots[(seed >>> 5) % 4] // key (each preset has 4 keys) prog = PROGS[(seed >>> 9) % 8] // chord progression pool contour = SHAPE[(seed >>> 23) % 6] // melody shape pool bpm = base * (0.92 + slice(seed >>> 13) * 0.16) // +/-8% tempo startBar = (seed >>> 17) % prog.length
Progressions are written as scale degrees (I-vi-IV-V, vi-IV-I-V, jazz turnaround, ...), so the same progression automatically colors major or minor depending on the preset's scale.
Six presets define scale (major/minor), oscillator waveforms, drums on/off, loudness and arp
density: cozy, synth, arcade, calm,
idle, tense. When you type a game name on the home page, a small
agent maps keywords to a preset ("Doom" → tense: E-minor scale, sawtooth
leads, square-wave bass, driving kick). The text itself is the seed, so describing the game
IS composing its soundtrack.
One WebAudio AudioContext. A 30ms timer keeps about 250ms of audio scheduled ahead
(the standard look-ahead pattern, immune to frame drops). Each bar is 16 sixteenth-steps:
step 0: pad chord (triad stacked in-scale) + bass root + kick
step 8: bass + kick again (beat 3)
every arp step: lead note walks the chord FOLLOWING THE CONTOUR,
jumping an octave in the bar's second half
odd steps: hi-hat = 35ms burst of white noise through a 7kHz highpass
There are no audio files anywhere. Each note is an
OscillatorNode → gain envelope (exponential attack/release) → optional
lowpass → master gain, created at its scheduled time and destroyed when it ends. The kick
is a sine that pitch-glides 130Hz→48Hz in 160ms. Open devtools, filter Network by media:
zero requests.
In Bandlings the seed is per-player and the orchestration is literally the board state, on one shared 8-bar loop:
tier 1 monsters -> percussion (each layers its own rhythm mask) tier 2 -> bass (same-tier monsters rotate bar by bar) tier 3 -> chords / arpeggio tier 4 -> lead melody (contour with rests) tier 5 -> choir (sustained detuned pair, every 2 bars)
Hatch a monster: a new voice joins. Merge two: their lane upgrades. Lose the run: your song's
earnings bank, the band persists. "Save my song" re-renders the exact same schedule into an
OfflineAudioContext and downloads it as a 16-bit WAV - your collection, as a track.
A 500-line Python "code-DAW" (numpy/scipy) builds richer instruments than the browser engine: band-limited additive saws (harmonics summed below Nyquist - no aliasing), 7-voice detuned supersaws, an FM electric piano, and synthesized drums (pitch-glide kick, tone+noise snare, triple-burst clap, highpassed hats).
Three hand-arranged styles (synthwave 116 BPM, swung lofi 76 BPM, ambient calm 64 BPM) with intro / breakdown / drop sections. The per-game seed transposes the key (-6..+5 semitones), nudges tempo +/-6%, and picks the chord progression and melody from pools - so two synthwave games are different songs, not the same song in a new key.
voices -> sidechain duck (to the kick) -> ping-pong stereo delay
-> convolution reverb (the impulse response is GENERATED:
decaying filtered noise + 3 early reflections)
-> soft-knee compression -> tanh limiter -> 44.1kHz WAV -> ffmpeg mp3
The factory renders a bespoke track per game (gen_soundtrack.sh). In the game,
GF.bgMusic() plays that file on the first tap and, if it is missing or fails to
decode, falls back to the live engine from Part 1 - so no game can ever ship silent.