Music as Code¶
Written 2026-10-27 from the project's own docs and code.
The introduction and the general description of Strudel and TidalCycles are from general knowledge of these tools, not freshly checked sources. The sections on the spike and Fboard come from the projects' own docs and code.
Music as code means writing music as text in a programming language instead of placing notes on a piano roll or recording takes. The code describes patterns: rhythms, note sequences and transformations of them. A program turns those patterns into sound, either live while you edit or rendered to a file.
Why do it¶
- Patterns are compact. One short line can describe a drum part that would take dozens of clicks on a piano roll.
- Transformations are cheap. Speeding a part up, reversing it or thinning it out is one more function call, not a re-edit.
- It's text. It diffs, merges and lives in version control like any other code.
- It suits games (adaptive music). A program that generates music from patterns can change what it plays while the game runs: add layers as the action builds, switch sections, nudge the tempo.
Strudel and TidalCycles¶
TidalCycles (by Alex McLean, written in Haskell) is a language for live-coding patterns. Strudel is its port to JavaScript and runs in a web browser. Both measure time in cycles: a pattern describes what happens in one cycle, and the cycle repeats. Strudel is licensed AGPL-3.0, which matters for anything that ships (see below).
A short Strudel example:
s("...")plays sounds by name:bdis a bass drum,sda snare,hha hi-hat.- The string inside is mini-notation, a compact rhythm language. Its words are spread evenly across one cycle, so
"bd ~ bd ~"is four steps. ~is a rest.hh*8repeats the hi-hat eight times within its step, here the whole cycle.stack(...)plays its patterns at the same time..gain(0.6)turns the hi-hats down.
Other mini-notation pieces that appear in the projects below: [ ] groups several steps into one, < > plays one item per cycle in turn, , layers patterns, bd(3,8) spreads 3 hits as evenly as possible over 8 steps (a Euclidean rhythm), and bd:3 picks the fourth sample of the bd bank.
SFZ: sample instruments as text¶
A sampled instrument is built from recordings of a real instrument, one file per note or per few notes, often at several loudness levels. Playing it means picking the right recording for the note and how hard it was struck, then pitch-shifting and shaping it.
SFZ is an open, plain-text format for describing such an instrument. An .sfz file lists regions: each region names a sample file and the conditions under which it plays, written as opcode=value pairs. Typical conditions and settings are:
- the key range the sample covers, and the key it was recorded at (so other keys can be pitch-shifted from it);
- the velocity range (velocity is how hard a note is played, 1–127), so a soft note and a hard note can use different recordings, called velocity layers;
- round robin: several recordings of the same note played in rotation, so repeated notes don't sound machine-identical;
- tuning, volume, envelope (attack and release), loop points and filters.
Many free and commercial sample libraries ship as SFZ, which makes it a practical common format for code that wants to play real instruments.
A Strudel spike over local sample libraries¶
A spike is a short experiment to find out whether an idea works. This one ran a local Strudel REPL (a browser page where you type code and hear it immediately; Ctrl+Enter plays, Ctrl+. stops) and taught its language to play instruments from a personal collection of sample libraries, without copying any of them: a local dev-server route serves the original folders read-only.
What it added to Strudel:
| Addition | What it does |
|---|---|
await loadSfz(id, ..., {preload}) |
Fetches an instrument's region map. preload decodes every sample up front. |
.sfz(id) |
Plays notes through an SFZ instrument, resolving each note like an SFZ player: key range, velocity layer, round robin or random choice, tuning, volume, velocity tracking and curves, velocity/key/controller crossfades, loop points, envelope, a static low-pass filter and pan. Regions that match together are layered. |
s("bank:n") |
Folders of one-shot samples (single hits, effects) as ordinary Strudel sounds. |
sfzList() |
Lists every available instrument. |
A build step indexes the libraries into one JSON region map per instrument, along with a per-instrument report of which regions were kept or skipped and which SFZ opcodes weren't modelled. At the time of writing it indexed 320 SFZ instruments and 731 one-shot banks. A sidebar lists them all, and clicking one inserts it into the code.
What it doesn't model¶
The spike's README lists its gaps plainly:
- Controller knobs are frozen at each library's defaults. There are no live controller streams; velocity stands in for the mod-wheel crossfades some orchestral libraries use.
- Release triggers (samples that play when a key is let go, such as a piano's damper noise) are skipped, and keyswitch patches play only their default articulation.
- Choke groups (e.g. a closed hi-hat cutting off an open one), hold stages, EQ, LFOs and filter, pitch and pan modulation aren't modelled.
- One amplitude opcode is interpreted by the author's reading of the format, not verified against a reference SFZ player.
- Samples load lazily on first hit, and on a slow drive the first hit of each sample can be dropped. Preloading fixes that for small instruments, but a large piano (484 samples of 24-bit audio) is too big to preload casually.
- General MIDI programs can also be loaded from a SoundFont (
.sf2) through Strudel's experimental loader, but that path clips badly and bypasses all effects. The preferred route is to freeze them: render each program to samples at 25 keys × 2 velocities and play the result as an ordinary instrument.
Freezing software instruments¶
The same freezing idea extends to software instruments (plugins that run inside a DAW). A script generates a DAW project per instrument that plays a grid of notes (every 3 semitones at 3 velocities), renders it without opening the DAW's window, and slices the render into one WAV per note. Each slice is measured rather than assumed:
- silent notes are dropped, and an instrument whose loudest note is under −30 dBFS fails outright;
- the sounding pitch is measured (with the YIN pitch-detection algorithm) and corrected in the region map; one instrument turned out to sound a whole octave low and was re-rendered shifted;
- one constant gain is applied per instrument, so its own loud/soft dynamics survive.
Several instruments froze cleanly. Some didn't: two produced silence when run without their window, and for one multi-instrument library, switching patches by pushing saved state didn't work, so every render came out as the same instrument. That was caught by comparing raw waveforms (cross-correlation 0.999 on the same keys) and the key ranges. The README's fix is to save one project per patch by hand and freeze from those.
Licensing¶
Strudel is AGPL-3.0: fine for local use, but anything served to others or shipped inherits AGPL obligations. Some of the sample libraries also forbid redistribution. So the spike is local only and is never published or served beyond the machine it runs on.
On this wiki¶
Fboard uses a pattern engine written in Rust that follows Strudel's model. Its music is written in Strudel syntax, so the same text can be auditioned in the Strudel REPL and played by the game. But none of Strudel's code ships: the engine was written fresh from the published TidalCycles/Strudel documentation, not ported, and plays through the Kira audio library.
To show it really behaves like Strudel, the spike above includes a script that runs real Strudel's pattern engine over a list of expressions and saves every event it produces. Fboard's test suite checks its own engine against that file: 39 expressions, 438 events over 4 cycles, all matching.
The engine is finished enough to drive adaptive music in the game. The music itself isn't: the current score is a placeholder, and the game's musical direction hasn't been chosen yet.
How the engine works, including the parity test, is on its own page: A pattern engine in Rust. For more on Rust in these projects, see the Rust section.