Skip to content

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 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.

TimescaleWhat runsWhat it costs to fail
Composition timeThe 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 timeSection 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 timePattern.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. Use ctx.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.)

Section bodies never make sound directly. At each bar the clock:

  1. creates a fresh EventCapture buffer,
  2. runs the section body — every play() / automate() / clip() writes into that buffer,
  3. 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.

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.

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.