How every note of a mixtape toy is made in your browser
Small Beats is a mixtape toy: pick a sound, switch parts on and off, pick a buddy for the label, and send the link to someone. Every note they hear is synthesized in their browser as it plays. There are no audio files, no samples and no AI anywhere in it.
The music engine is one JavaScript file of about 47 KB, 17 KB gzipped, built on the Web Audio API. A whole song travels as a link of about 100 characters. This is how it works, and what was hard.
A song is a number
A tape link holds a short JSON array, base64url-encoded: the name, the cassette shell, the buddy, six settings (sound, drums, bass, keys, texture, speed), one integer seed and, if you played along on the keyboard, your two bars of notes (three characters a note: when, which step of the scale, how long).
[1, "GROCERY RUN", 0, 1, 1, 3, 0, 1, 1, 1422687053, 49]
The seed goes through a small hash (FNV-1a) into a seeded random number generator. Everything structural comes from that generator: the key, the mood, the two chord progressions, and the hook. So the same link gives everyone the same song's bones.
The six settings give 3,600 combinations of parts, and the seed has about 2.1 billion values. That is roughly 7.7 trillion possible tapes, though plenty of neighbours sound alike.
One honest caveat: the bones are fixed, but the small stuff is not. Timing jitter, velocities and the improvised riffs between hooks use the ordinary Math.random(). Two people hear the same key, chords and hook, played by a slightly different band each time. I like that: it's closer to a band than a recording.
Why any combination fits
Every part follows the same chord chart, so drums, bass, keys and tune can be switched on in any combination and still agree.
The seed picks one of three moods. Each mood is a scale plus a handful of four-chord progressions, written as root and chord quality per bar:
2m9 7dom9 0maj9 9m7 ii9 – V9 – Imaj9 – vi7, in whatever key the seed chose
0m9 5m7 8maj9 7dom7b9 a minor-key loop with a flat-nine pull back home
A tune uses two progressions: one for the first 16 bars of a 32-bar cycle, another for the second.
Three small rules keep it sounding played rather than generated:
- Voice leading. Each chord is voiced near the previous one: if the average pitch would jump more than four semitones, the voicing is inverted to stay close.
- No mud. Voicings with a minor second inside them are rejected, because that interval reads as a wrong note.
- Melody snaps to the harmony. On strong beats, tune notes are pulled to the nearest chord tone. Off the beat they can pass through other scale notes, as long as they don't rub a semitone against the chord.
The hook, and how a tune develops
Each tune has a two-bar hook built from a tiny cell: two or three notes, the same cell moved up or down a step, and a long note to land on. That shape (statement, answer, resolution) is most of what makes a melody feel intentional.
The hook comes back every eight bars and develops on a four-step cycle:
- As written.
- With a new ending.
- A rest, sometimes filled with a short riff improvised from pieces of the hook.
- Moved up a step.
The second half of the tune gets its own hook, and a fresh one every 64 bars, so a tape left on for an hour keeps moving. Late in each 32-bar cycle the drums drop out for a few bars, leaving only hats, and come back in. That breakdown is the cheapest trick in the engine, and the most effective.
The instruments, all in code
Every voice is built from oscillators, noise and filters at the moment it plays.
| Part | How it's made |
|---|---|
| Rhodes | Two-operator FM: a sine modulating a sine, with the modulation depth decaying fast for the bell-like attack, plus a slightly detuned sine for warmth |
| Guitar | Karplus–Strong plucked string: a burst of filtered noise fed into a short delay loop that averages itself; a comb filter sets where the string is plucked. Each note is rendered once and cached, in three takes, so repeated strums aren't identical |
| Pad | Detuned saw pairs through a lowpass filter that opens and closes over the chord |
| 8-bit | Pulse waves at 12.5% and 25% duty, built from Fourier series with createPeriodicWave |
| Bass | Warm (sine and triangle), deep (a tighter sub) and synth (saw and square through a plucky filter) |
| Kick | A sine swept from 110 Hz to 42 Hz in about 130 ms |
| Snare, clap, rim | Band-passed noise bursts, the snare with a 190 Hz tone under it |
| Hats, shakers, brushes | High-passed and band-passed noise with very short envelopes |
| Keyboard | A two-oscillator synth with a sub, a filter envelope and a glide between quick notes, voiced per style (warm for lo-fi, a stab for house, glassy for ambient, a square lead for 8-bit, dark and echoing for dub). Every key is a note of the song's pentatonic scale, so nothing you play clashes, and it plays through the song's own tempo echo |
| Chip drums | Noise held for six samples at a time, which mimics the stepped noise of old sound chips |
None of this is new. Most of it is textbook synthesis from the 1970s and 80s. What surprised me is how far plain Web Audio nodes go when the envelopes are right.
Feel
Perfect timing sounds like a machine, so three things loosen it up.
- Swing. Every other sixteenth note is pushed late, by a different amount per sound and speed: about 4% of a beat for fast 8-bit, up to 16% for slow lo-fi. TIGHT plays dead straight; SWING leans on the offbeats harder.
- Human timing. Every drum hit lands a few milliseconds early or late, snares slightly behind the beat, and velocities wobble by around 15%.
- The house pump. On house tapes, every kick briefly ducks the keys and pad, the sidechain pumping that defines the genre.
The cassette chain, and weather that never loops
Lo-fi tapes run the whole mix through a small model of a worn cassette:
- soft saturation (a waveshaper driving the signal into gentle clipping, then turned back down),
- a high-pass at 75 Hz and a two-stage roll-off around 6–8.5 kHz,
- a little bandpassed hiss,
- wow (about 6 cents of pitch drift at 0.48 Hz) and flutter (about 2 cents at 6.3 Hz), made by modulating a short delay line.
The textures are generated too, never looped:
- Rain is a soft filtered-noise bed plus individual taps at random intervals, about 3.6 a second on average, sometimes in pairs, and a single droplet plink every few seconds.
- Fire is a slowly swaying low roar of brown noise with sparse crackles, the odd one muffled into a pop.
- Hiss is tape noise with a faint 60 Hz mains hum and an occasional tick of dust on the heads.
Because the timing is random rather than a recording, an hour of rain never repeats a pattern you could notice.
Scheduling on the audio clock
JavaScript timers are too jittery to play music, so nothing is played "now". A plain setInterval wakes every 400 ms and schedules whole bars up to 2.2 seconds ahead, against the audio context's own sample-accurate clock. This is the two-clock pattern from Chris Wilson's A Tale of Two Clocks. A busy main thread, or a tab in the background, never makes the beat stumble.
The same scheduler also reports every note as it's queued. That stream drives the visuals: the buddies on the screen hop on beats 1 and 3, the step lights chase the beat, and the player piano lights each note on the keyboard. Pause is AudioContext.suspend(), so a tune picks up exactly where it stopped.
What was hard, and how I built it
- Making it sound like a band, not a demo. Correct notes are easy; feel is hard. Most of the time went into envelopes, swing amounts and leaving space.
- Switching parts mid-song. Changing a part rebuilds the band, and the new one takes over on the next beat, at the same place in the song, so the groove never restarts.
- Keeping old links alive. Any change to the engine changes every tape ever shared. Next up is versioning the engine, so a tape made today sounds the same in five years.
I'm a guy who likes mixtapes, 808s, retro games and dinosaurs. Small Beats is all four in one tab.
I'm old enough to remember trading cassette tapes in high school. Small Beats is sort of the digital version of that. It's hosted on GitHub, with AWS Amplify pointed at it.
I used Claude (Opus 5.5) to help with the procedural generation algorithms. The music itself is still made by plain code in your browser, with no AI involved when a tape plays.
Try it
It's free, with no account, and it works offline once loaded. Make a tape, roll the dice until the melody grabs you, and send the link to someone. If you make one you like, I'd love to hear it: hello@smallbeats.com.
▶ MAKE A TAPE