A soundtrack that ships as zero bytes
Written in Haskell, played live in TidalCycles, compiled into the app as a table of numbers. No audio file is bundled.
The constraint came first: no audio ships. A two-minute rendered loop would roughly double a 2.1 MB app and be the largest thing in it by an order of magnitude. Impact thocks were already synthesised from oscillators at launch, so the question was whether that scales from one percussive hit to four simultaneous parts. It does, through a strange pipeline.
GameMusicScore.swift, drawn from the same
(startCycle, durationCycles, semitonesFromMiddleC) triples the
synthesiser reads. This is the whole soundtrack, and on disk it is source
code rather than audio.The pipeline
A MIDI file becomes GameMusic.hs — TidalCycles patterns, one per part,
in a live-coding environment where re-evaluating a single line swaps one
instrument at the next cycle boundary while the rest keeps playing.
d1 $ jux rev $ pianoP # s "superpiano"
d4 $ every 3 (fast 2) $ bassP # s "superbass"
d5 $ degradeBy 0.3 $ celloP # s "supersquare" # lpf 900
tools/export-score.hs then queries those same patterns and emits Swift
source — GameMusicScore.swift, 1046 notes across 32 cycles at 136bpm
in 4/4, one bar per cycle. Each part is a flat [Float] of
(startCycle, durationCycles, semitonesFromMiddleC) triples.
Two decisions in that file do real work.
Swift, not JSON. MarbleCore imports simd and nothing else — not
Foundation — so there is no JSONDecoder. Generated source honours that
and drops the bundle resource, which is how the soundtrack costs zero bytes on
disk.
Flat and explicitly typed. Nested or untyped literals at this size make the Swift typechecker crawl.
The exporter is also the only place the legato arithmetic lives. Tidal’s
legato scales an event’s own step length, so sounding duration is step
× legato — exactly where an octave or a note length goes quietly
wrong. Two pieces, one copy of the subtle part.
Playback is a buffer
ScoreRenderer mixes the entire score once, at launch, into a single block of
mono samples that loops. Polyphony resolves offline, so playback needs no
scheduler, no voice allocation and no clock — the audio hardware plays a
buffer.
Cost: about 9.5 MB of RAM for 56.5 seconds of 44.1 kHz float. On disk, nothing.
What that removes: layers cannot fade in with altitude and the underwater passage cannot filter the music, because there is no per-note scheduling against a sample clock to hang either on. That is a larger job, and it wants this one to prove the voices sound right first.
The number that isn’t a bug
A debug build renders 15–45× slower — 0.2s release on a Mac, 2.6s debug, 9.1s in the simulator — because unoptimised Swift keeps every bounds check and inlines nothing. The render runs on a background thread and the game is playable throughout, so it is an annoyance rather than a cost.
Two attempts to close the gap, an exp-free envelope and unchecked buffer
writes, each bought under 60ms in release and were reverted. The debug number
describes the compiler, not the code.