The model
barline rests on one idea: the code is the source of truth. A song is a real TypeScript program; the timeline, beat grid, and piano roll are projections of its AST, never independent state. Edit either side and both update. Everything below makes that bidirectional, glitch-free, and crash-proof.
The three timescales
Section titled “The three timescales”The single most important invariant is which code runs when. There are three distinct timescales, and putting work on the wrong one is the most common mistake.
| Timescale | What runs | What it costs to fail |
|---|---|---|
| Composition time | The whole file, on load and hot-reload: imports, top-level code, song.track(), song.section() registration. | The whole load fails → a banner; the last good program keeps playing. |
| Bar time | Section bodies, re-run at every bar boundary (or when a read flag changes). play() writes into a capture buffer. Must be synchronous, cheap, side-effect-free. | One bar → the runtime replays that section’s last good capture. |
| Query time | Pattern.query(window, ctx) at scheduling resolution. The hot path: no allocation-heavy work, no I/O. | One ~25 ms tick of one track. |
Two consequences worth internalizing:
moment().hour()at the top level freezes at load — that is intended; the top level runs once.Math.random()inside a section body or pattern breaks the match between the visualization and playback. Usectx.rng(seed)instead — it’s seeded, so the piano roll and the audio always agree. This is why patterns are required to be pure and deterministic. (See Custom patterns.)
Capture-and-commit
Section titled “Capture-and-commit”Section bodies never make sound directly. At each bar the clock:
- creates a fresh
EventCapturebuffer, - runs the section body — every
play()/automate()/clip()writes into that buffer, - on successful completion, hands the captured assignments to the scheduler.
If the body throws, the capture is discarded and the previous successful capture for that section is replayed. So a syntax slip or a thrown error inside a body degrades to “the last good bar keeps playing.” Failure is always one bar, one section — never the set. This is the “a live set never stops” principle made mechanical.
Hot reload
Section titled “Hot reload”When you edit the code, the runtime re-runs the user module and performs
upserts against a stable Song registry:
song.section("drop", …)replaces the body but keeps the section’s identity and marks it dirty for the next bar.- Bar N plays the old code; bar N+1 plays the new code. No glitch, no re-trigger.
- A syntax error keeps the last good version, surfaces the error, and does not touch the registry.
- Removed tracks fade out at the next bar boundary.
Because the recompile is debounced and applied on the bar grid, you can edit a playing song and hear the change land in time — the core loop the whole tool is built around.
One document, every editor
Section titled “One document, every editor”The authoritative state is a single CRDT document (a Yjs doc) living in a per-song Durable Object. The CodeMirror editor binds to it over a WebSocket; the MCP server mutates the same doc directly; the browser keeps an offline replica.
That uniformity is the payoff: a keystroke, a drag in the piano roll, and an AI
edit are all the same kind of transaction on the same document. Undo,
presence, and conflict resolution work identically across all three. The
studio’s views are read-and-write-back projections of that
one document — which is why dragging a note in the piano roll edits the
notes([...]) literal in your code.