Say where each part plays and how long it lasts, instead of listing every combination of parts as a scene. A pad that runs for sixty-four bars is one region; drums that come and go are two; a filter that opens over twenty-four bars is one gesture, not twelve ramps.
Hear it and change it#
Listen for: the chords never stop. The drums enter at bar 9, step out at bar 17, come back at bar 21 and leave at bar 37 — and the bass filter opens once, slowly, across the twenty-four bars in the middle, while the two-bar bass phrase repeats underneath it twelve times.
tempo = 108bpm
meter = 4/4
tone = macro 620Hz
riverV = patch { source = triangle (freq pitch),
ampEnv = adsr 900ms 1.2s 0.7 2.4s, gain = vel * 0.06 }
siltV = patch { source = saw (freq pitch) |> lpf tone 1.1,
ampEnv = adsr 4ms 140ms 0.55 160ms, gain = vel * 0.34 }
stoneV = patch { source = kick909 0.34 0.22 0.56, gain = vel * 0.46 }
riverIdea = !"<[a3,c4,e4,a4] [f3,a3,c4,f4]>"
siltIdea = !"<[a1 ~ a1 ~ e2 ~ a1 ~] [f1 ~ f1 ~ c2 ~ e1 g1]>"
stoneIdea = !"c2 ~ ~ c2 ~ ~ c2 ~"
opening = automation {
points = [[0bars, 620Hz], [8bars, 1500Hz], [24bars, 5600Hz]],
interpolation = "linear"
}
riverT = play riverV [region 0bars 40bars riverIdea]
stoneT = play stoneV [region 8bars 8bars stoneIdea,
region 20bars 16bars stoneIdea]
siltT = play siltV [region 8bars 24bars siltIdea { id = "silt-run" }]
brightness = automate tone [region 8bars 24bars opening]
riverine = song {
timeline = [riverT, stoneT, siltT, brightness],
length = 40bars,
playback = "once"
}
Positions are zero-based, so 8bars is ruler bar 9.
Make it yours: change region 8bars 24bars siltIdea to region 8bars 12bars siltIdea and play it again. The bass now stops halfway through the filter opening — and the opening keeps its own twenty-four bars, because the curve belongs to brightness, not to the bass. Nothing rescales when a container changes length; that is the whole point of the model.
The nine songs examples/songs/timeline-01-riverine.volta … examples/songs/timeline-09-windlass.volta are written on this surface.
The model#
Six words, and each answers one question.
| Word | What it answers | How it is written |
|---|---|---|
| Content | What happens. A pattern, a control curve or an audio asset, each with its own local clock. | any pattern binding; automation { … }; audio "name" |
| Region | Where and how it plays. One placement: start, length, offset, rate, repeat, edge rule. | region at length content { … } |
| Track | What it plays through. The instrument, the insert chain and the mixer strip — a play binding, as before, or a sound binding where there is no instrument to trigger. | play voice [regions], sound [regions] |
| Lane | What it moves. Curve regions aimed at one destination. | automate target [regions] |
| Timeline | What runs together. Tracks and lanes with absolute placements, and a declared extent. | a list, or stack / concat / repeat / silence |
| Song | The conductor. Tempo, meter, markers, the finite extent and what happens at the end. | song { timeline, length, playback } |
Two things follow from that split and are worth stating plainly:
- Content is stored once however many times it is placed. Two regions of one phrase share one definition and differ only in their cursors, so editing the phrase changes both and moving one changes neither.
- A region never rescales its content. A two-bar idea inside a twenty-four bar region plays twelve times; it does not become a twenty-four bar phrase. Stretch is the explicit
ratekey, never a side effect of resizing.
Constructors#
| Form | What it makes |
|---|---|
region at length content | A placement. at and length are unit-bearing. |
region at length content { … } | The same, with the configuration record below. |
play voice [region …, …] | A region track: the voice, inserts and strip stay on the track; placement and timing belong to the regions. |
audio "name" | A placed audio asset, by bank name. |
sound [region …, …] | An audio track: the same receiver without an instrument. Sample patterns are welcome here too. |
automation { points = ps, interpolation = "linear" } | A finite typed curve. |
automate target [region …] | A lane of curve regions aimed at one destination. |
song { timeline = ts, length = d, playback = "once" } | The written song. |
editRegions t { id = { at = … } } | Keyed placement overrides over a timeline value. |
stack [a, b] | Concurrent timelines. |
concat a b | b after a, by a's declared extent. |
repeat n a | n passes of a finite timeline. |
silence d | Real musical time with nothing in it. |
Arrangement combinators produce Timeline values. stack and concat select this meaning from their operand types: tracks, lanes, timelines and groups. They retain their pattern/list meanings for non-temporal operands. Mixing a pattern or scalar into a temporal composition is a type error, including in an unused declaration. silence is an empty pattern when used bare and a timeline extent when applied to a fixed duration; both forms survive aliases, functions and containers. repeat has only the timeline meaning.
Name a Timeline, return it from a function, keep it in a record/list, or bind it locally before passing it to a song. For example, shift = \t -> concat (silence 2bars) t preserves the leading two bars through form = shift part. concat [a] [b] sequences temporal operands by the first operand's extent; ordinary non-temporal lists still concatenate as lists. Timeline selection does not depend on searching referenced declaration names.
An ordinary play voice pattern track is unchanged, and a song may hold both kinds — but only a region track can appear in a timeline. Naming a pattern track there is an error that says what to do about it.
Reusable region values#
A placement has type Region C, retaining its content type: for example, Region (Pattern Pitch), Region AutomationCurve or Region AudioContent. Bare Region leaves the content type open. Name a list, import it, return it from a function, or build it with map and pass the result to play, sound, or automate:
tempo = 120bpm
voice = patch { source = sine (freq pitch) }
idea = !"c3 e3 g3 ~"
place = \at -> region at 4bars idea
placements : List Region = map place [0bars, 8bars]
part = play voice placements
main = song { timeline = [part], length = 12bars }
Change [0bars, 8bars] to [0bars, 4bars, 8bars] to fill the gap. Records, tuples, aliases, local bindings and compile-time branches also retain placements. A generated empty List Region makes an empty region track; a bare [] has no region element type and is not a pattern.
Naming placements preserves their named content and explicit seed draws. The checker rejects curve regions at play or sound, audio regions at play, and pattern regions at automate, including values passed through functions and containers. Only regions containing a timeline/group can be composed directly as timelines. Coordinate values, curve point units and placement options are validated during realization. A region list is distinct from a pattern. Curves and audio content can themselves be named, returned from functions, or carried in records and lists before placement. To wrap the constructor, use a lambda as above; a bare builtin alias (put = region) does not make a callable constructor.
Computed or reused placements have no direct Arrange drag/resize source anchor. Edit the list or factory in Source; the compiler does not present a shared or imported definition as a writable literal in the receiving track. Inline region literals keep their existing edit anchors.
Placing content#
region at length content. Both magnitudes carry a unit, and the pair must agree: mixing bars with beats or s in one placement is refused, because a meter edit would then have no single answer for where the region goes.
| You write | The placement is anchored in | Resolves to beats through |
|---|---|---|
8bars, or a bare 8 | bars | the meter map |
32beats | quarter-note beats | nothing — beats are the plan's axis |
4c | bars (one base cycle is one notational bar) | the meter map |
12s, 250ms | elapsed seconds | the tempo at beat zero, re-resolved by the engine's conductor |
The authored coordinate is kept beside the resolved beat span. A region written at bar 9 stays at bar 9 after a meter edit; one written at beat 32 stays at beat 32; one written at 12 s stays at 12 s and re-resolves when the tempo changes. See Temporal core for the exact arithmetic.
A region starts at or after song zero and has a positive length. Regions sort by start whatever order they were written in, so a list is a set of placements, not a sequence.
Configuration#
Every key is optional. Anything else is an error with a did-you-mean.
| Key | Value | Default | What it does |
|---|---|---|---|
id | string | derived (below) | Durable identity for selection, Undo and write-back. |
offset | content units (2c, 1bars, a bare number, or beats) | 0 | Where in the source the region starts — slip. |
phase | "entry" / "transport" | "entry" | Whether content cycle 0 sits at the region, or at song zero. |
rate | positive number, or the exact (3/2) | 1 | Content units per parent unit — the explicit stretch. |
repeat | "native" / "once" / [start, length] | "native" (audio: "once") | Native period, one pass, or a source fold. |
boundary | "let-ring" / "clip" | "let-ring" | Whether admitted notes may ring past the region end. |
continuity | string | none | Joins this region to its neighbour across a split seam. |
attach | "region" / "song" / "marker" | "region" | Curve regions only. Whose clock the curve runs on. |
bind | { port = track } | none | Group placements only. Redirect one of the group's receivers. |
seed | non-negative whole number | inherited | Musical variation seed — this placement redraws its own choices (below). |
control | { name = curve } | none | Note regions only. Voice-scoped curves every admitted onset latches (Draw a change, and record one). |
fade | [in, out] durations | [5ms, 5ms] | Audio regions only. Edge fades, each clamped to half the region. |
warp | "off" / "resample" | "off" | Audio regions only. Whether the audio follows the tempo map. |
gap | — | — | Belongs to the lane, not one region: write @gap "hold" on the automate binding. |
A key that does not belong to this region's content kind is refused by name and told what it configures — fade on a pattern region says so rather than being accepted and ignored.
rate written as a ratio is kept exact all the way to the scheduler: rate = (4/3) stores the pair, and a pass length is computed by multiplying by the denominator and dividing by the numerator rather than by the f64. A three-beat idea against a four-beat one therefore does not drift — measured at bar 400, where the f64 spelling of the same rate is more than a sample out (engine/tests/timeline_regions.rs::a_literal_rate_ratio_is_exact_at_the_far_end_of_a_song). A rate the author wrote as a decimal keeps its own value.
phase is the one place the written and the live profiles differ by default. "entry" means this placement is the content's clock origin: the region starts at its own beginning wherever it sits. "transport" reads the phase off song time instead, so a part that stops and comes back later continues its rotation rather than restarting it — which is exactly what a launched scene does, and what examples/songs/timeline-03-cogwheel.volta uses to make an eight-bar absence read as a gap in one continuous motion.
A region track keeps the track-level pipes that shape sound (|> width, |> duck, a mixer strip) and the whole-track clock (|> clock (3/4)). The track-level placement pipes — |> at, phraseOrigin — are errors on it: the regions carry placement, and a second origin would contradict them.
Identity#
A region with no id is given a document-local one derived from the owning binding and its position in the region list, so it survives re-indentation, a comment, and a formatter round trip — formatting cannot change musical identity. A combinator that moves a placement (concat, repeat, a group) makes a new occurrence and tags the id with that placement, so two passes of one phrase are two occurrences sharing one content and one seed.
Write id = "silt-run" when you want to name a placement yourself; two regions on one receiver may not share an id.
The song#
sparkT = play siltV [region 0bars 4bars siltIdea]
main = song {
timeline = [riverT, stoneT, brightness],
length = 32bars,
playback = "loop",
scenes = { lift = [sparkT] }
}
| Field | Meaning | Coexists with |
|---|---|---|
timeline | The written arrangement: region tracks and lanes. | scenes, markers. Not arrange, edges or songGraph. |
length | The finite extent. Omitted, it is the latest region end — so silence at the end must be declared. | timeline only |
playback | "once" (stop) or "loop" (repeat from song zero). | timeline only |
scenes | Launchable bundles for live play. | everything |
arrange, edges | The scripted walk of Arrange a song. | scenes, not timeline |
timeline and arrange are two schedulers for one song and are rejected together; scenes may coexist with either, which is how examples/songs/timeline-04-beacon.volta runs a written bed with three launchable bundles over it.
A song that names scenes or timeline rejects an unknown field, at parse time, so a typo is a named mistake rather than a silent no-op or a confusing "name not in scope" from a field read as a scene. A legacy bare song has no configuration to misspell — every field but arrange and edges is a scene there, including names like length or wedges — and it compiles exactly as it did before this surface existed.
The two profiles#
A written region and a launched slot are the same content on the same clock with different expectations, so each takes its own defaults.
| Default | In a timeline (written) | In scenes / arrange (live) |
|---|---|---|
| Phase | entry — the placement is the origin | transport — join the running phrase |
| Note boundary | let-ring | let-ring |
| Song end | once | loop |
| Two authored writers on one destination | a compile error | resolved at block order, reported as a lint |
Every one of these is an explicit key, so a song can mix them deliberately. Nothing about a legacy song changes: a song without timeline emits no plan at all and runs exactly as it did.
Why once. A written song declares its extent, so reaching the end is the end; a live set runs. playback = "loop" is the other case, and the transport loop brace still loops any range you select.
Why let-ring is still the default. A scene end and a pattern cell end have always stopped admitting new onsets while letting admitted notes finish, and keeping that rule for regions means a split is an identity for notes: the onsets partition and a held note simply rings on. The cost is the familiar DAW gotcha — trimming a region's end does not shorten a note that already started — and boundary = "clip" is there for exactly that case, opt-in, as it is in other DAWs.
Composing timelines#
A timeline is an ordinary typed value.
bassT = play siltV [region 0bars 4bars siltIdea]
hatT = play stoneV [region 0bars 4bars stoneIdea]
verse = stack [bassT, hatT]
form = concat (silence 2bars) (repeat 3 verse)
main = song { timeline = form }
stack [a, b]runs timelines concurrently. A list literal —timeline = [keys, drums]— is the same thing.- A TRACK member is a declared name. Both receivers resolve a track member through its declaration, so a computed one —
stack [make 1],[rack.line],[head parts],[let held = line in held], a function's result — is refused while checking: a track in astackor a song'stimelinemust be a declared top-level name for now. Bind it (part = make 1) and placepart. The rule is about the track, not about computation: a computed TIMELINE still reaches the receiver, and every other timeline value (named, imported, held in a record or a list) always has. concat a bplacesbaftera's declared extent. A tail ringing past the end does not pushblater, and a timeline with no declared extent cannot be the left operand — give it one withsilence, or place a region that ends it.repeat n ais structural:npasses of one timeline, each pass a new occurrence of the same content, with the same seed. A repeat never rerolls and never copies an event array.silence dis musical time with nothing in it — the way to start a form two bars in, or to declare the rest a song needs at its end.
concat and repeat move placements, so the resolved positions are written back onto the tracks and lanes the song names. What you read out of the plan is where things actually play.
Groups#
A group is not a new kind of value: it is a finite timeline placed by region, and bind redirects one of its receivers onto another track.
altT = play stoneV [region 0bars 4bars stoneIdea] |> track.gain 0.6
verse = stack [bassT, hatT]
main = song {
timeline = stack [
region 0bars 4bars verse,
region 8bars 4bars verse { id = "second", bind = { hatT = altT } }
],
length = 16bars
}
Both placements are occurrences of one shared content — the phrase is stored once — and the second plays its hats through altT's instrument and strip instead of hatT's. That is a redirect, not a cloned mixer channel.
The container is a window: a member placement that starts past the group's end does not sound, and one that overruns is shortened. Admitted notes still ring unless their own region says clip.
A binding that names a receiver the group does not have, or names one twice, is an error; so is a group that contains itself. Nesting is bounded at four deep, which is the occurrence-path depth the temporal core stores inline — exceeding it is an error, never a silently truncated identity.
Inside a group, bars count from the GROUP's zero, so composing a group across a meterMap change misplaces its bars; and a group placement's extent is its length, not at + length. A track member of stack/concat is a declared name (timelines::check_named_members). A generator that writes top-level bindings must dodge builtin names (chorus, pulse) and module tags (form).
Source folds#
repeat = [start, length] plays a chosen window of the source, in content units, and loops that window instead of the native period.
escapeV = patch { source = sine (freq pitch), ampEnv = adsr 2ms 900ms 0 600ms,
gain = vel * 0.3 }
escapeIdea = !"<[c6 ~ g5 ~] [e6 ~ b5 ~] [a5 ~ e5 ~] [g5 ~ d5 ~]>"
escapeT = play escapeV [region 8bars 16bars escapeIdea,
region 32bars 12bars escapeIdea { id = "bell-answer",
repeat = [2c, 2c] }]
The bell states its four-bar phrase in full, then comes back looping only its last two bars. The source is not edited and not copied: the fold is a property of the placement, and the stored content still has its four bars. Events inside the window are re-onset against it and the window becomes the new pass length; a note that begins inside the window keeps its full length, because the fold decides what repeats, not where notes end.
A fold that would have to walk more than 256 source passes to collect its window is a compile error, naming the pass count and the ceiling — a fold is an editing gesture over a few bars, not a way to materialise a long song into one pass. The ceiling is one constant, MAX_FOLD_PASSES in ir/src/ir.rs — the IR both sides already read, because the compiler cannot depend on the engine; volta_engine::timeline::MAX_FOLD_PASSES is a pub use of it, so the compiler's refusal and the engine's abandonment cannot name different numbers.
Split and continuity#
Splitting a region is two regions sharing one content, the right half offset into the source by however far the seam is:
kickVoice = patch { source = kick909 0.34 0.22 0.56, gain = vel * 0.3 }
t = play kickVoice [
region 0bars 8bars beat,
region 8bars 8bars beat { offset = 8c }
]
For notes that is already an identity, with no extra machinery: admission is positional, so the onsets partition at the seam and a note held across it rings on from the left half without a second note-on.
A clip region is the case that needs help, because its gates are released at its own edge. Give both halves the same continuity = "…" name and the engine releases the family's gates at the end of the chain, so nothing fires at the seam. The link is checked as real semantic data: the regions it joins must share one content and actually abut. Move or slip one half and the link is refused — which is the compiler saying what a DAW says with a dialog, that the edit broke the join and the join has to go in the same edit.
Automation lanes#
lamp = macro 0.2
sweepOn = automation { points = [[0bars, 0.18], [8bars, 0.9]],
interpolation = "linear" }
sweepOff = automation { points = [[0bars, 0.9], [12bars, 0.18]],
interpolation = "linear" }
@gap "hold"
beam = automate lamp [region 0bars 8bars sweepOn,
region 20bars 12bars sweepOff]
An automate binding is a lane: curve regions aimed at one destination, placed by the same region this page describes and edited by the same rules. A lane goes in the timeline list like a track.
What a lane may aim AT — a macro, a mixer fader, a send, a named insert — what a curve's segments are, how two overlapping base curves compose, and the Read / Touch / Latch / Write / Off recording modes are all Draw a change, and record one. The placement-side facts that belong here:
repeaton a curve region is refused, not ignored. A curve has no source period to fold — its points are its extent, and outside them it holds its endpoint — so a source fold on a lane region is a compile error naming the two spellings that work: a modulation source (lfoTri (5c)) for a repeating control, or the curve region placed more than once for a repeating shape.attachdecides whose clock the curve runs on: its own placement ("region", the default), song time ("song"), or the marker span it starts in ("marker", which snaps the placement to that section).- Between regions a lane plays the destination's declared base;
@gap "hold"on the binding plays the preceding region's final value instead, evaluated analytically — so seeking into the gap reads the same value as playing into it.
Audio#
Audio is content like any other: audio "name" names a bank entry, and a region places it.
takeT = sound [region 8bars 4bars (audio "vox"),
region 16bars 4bars (audio "vox") { id = "answer",
offset = 2.5s,
fade = [10ms, 400ms] }]
sound is the audio track. It is already Volta's receiver without an instrument — inserts and a mixer strip, no synth to trigger — which is exactly what placed audio needs. play gives a track its instrument, so play on audio content is an error that names sound. A sound region list still takes sample patterns; audio is an addition, not a replacement.
The asset is stored once however many regions place it. The placement owns everything else:
| Key | What it does |
|---|---|
offset | where in the source this region starts, in seconds — an audio asset's own clock is elapsed time |
fade = [in, out] | edge fades, 5 ms each by default, each clamped to half the region |
warp | "off" (default) or "resample" |
Audio plays once, and its container gates it. Overlapping audio regions sum before the track's inserts; @overlap "replace" suppresses the covered one (below).
The two warp modes, and the one that is missing#
warp = "off" keeps the audio's own speed. The container is anchored in bars, so a tempo change moves and resizes it around the take, and the gate at the end admits less or more of it. Slow the tempo and bar 9 falls later in seconds: the take still starts at bar 9, still plays at its own speed, and now ends earlier in bars. That is the design's mixed-domain worked example, and it is a test.
warp = "resample" reads the trimmed window at whatever rate fills the container, which changes its speed and therefore its pitch. It follows the tempo map exactly, because the container's seconds re-resolve.
warp = "beats" is refused by name. A pitch-preserving stretch that follows a tempo map would need a real-time TSM; Volta's stretch.rs pre-renders one constant ratio on the control thread, which cannot. The refusal says so and names "resample", rather than accepting the word and quietly resampling.
Splitting audio#
Two audio regions joined by a continuity link are sample-continuous across their shared seam: the one continued into drops its fade-in and the one continued from drops its fade-out, so the split adds nothing, while the family's outer edges keep theirs. Without the link the two clips fade at the seam like any pair of edges, which is what an unrelated adjacency should do.
Overlapping regions#
@overlap on the play (or sound) binding is the receiver's policy:
early = !"c2 ~ ~ ~"
late = !"c2 c2 ~ c2"
@overlap "replace"
t = play kickVoice [region 0bars 12bars early,
region 4bars 4bars late]
| Policy | Where two regions overlap |
|---|---|
"mix" | both sound — note regions layer into the receiver's polyphony, audio sums before the inserts. The default, and every legacy shape. |
"replace" | the LATER region's span suppresses the covered region's onsets there |
Suppression is a set of windows, not a shortened region: a region covered in its middle resumes afterwards. It is positional, like admission itself, so a seek lands the same set of onsets. @overlap on a track that plays a pattern rather than regions is an error saying so — and so is a placement covered by more separate later spans than the ceiling in § Limits, because dropping the extras would fall back to mix with nothing said.
In Studio, placing a region over bars its track already plays adds @overlap "replace" to the binding (unless it states a policy), so what you hear matches the Timeline, which paints the later region on top.
Musical seeds#
seed = 7 on a placement redraws that placement's own choices:
coins = !"c2*8" |> degradeBy 0.4
t = play kickVoice [region 0bars 4bars coins { id = "a", seed = 1 },
region 4bars 4bars coins { id = "b", seed = 2 }]
The two placements of coins now draw differently — degradeBy, a?, [a|b], humanize, shuffle, scramble, someCyclesBy and rwalk all see the seed. A seeded placement stores its own definition (coins@seed1), so:
- two seeds of one idea are two contents, and editing
coinsstill changes both; - the same seed twice is the same music — which is what makes Duplicate and Reroll different gestures;
- an unseeded placement is untouched, byte for byte, which is why no existing song's audio moves.
A content that was already computed into events before it was placed — patternData, a map over a list — has no draws left to make, and says so rather than accepting a key that changes nothing. A curve has no choices at all, and says that.
A seed redraws by re-lowering the content's source: a named binding's pattern is already materialised, so the placement's seed is made ambient (lower::salt::with_region_seed) while the expression lowers again, and every draw mixes it in through mix_region_seed (seed 0 is the identity). A seeded placement interns its own content.
Keyed overrides#
A generated form is not an un-editable one. editRegions is an ordinary typed function over a timeline value, keyed by each region's durable id:
entryT = play siltV [region 4bars 4bars siltIdea { id = "bassEntry" }]
form = stack [entryT, repeat 4 hatT]
main = song {
timeline = editRegions form { bassEntry = { at = 12bars, length = 8bars } },
length = 32bars
}
It moves the placement without evaluating the generator that made it — which is the whole point: the source still reads as "this is the form, and this one placement is different", not as a thousand literal notes.
Most ids are not identifiers, so a key may be quoted — and that is the case the feature exists for. A minted id is t:0; an occurrence repeat, concat or a group makes is t:0@16; the corpus convention is hyphenated. All three are spelled as strings:
main = song {
timeline = editRegions (repeat 4 verse) { "bassT:0@16" = { at = 24bars } },
length = 32bars
}
That moves the second pass of a generated form and leaves the other three exactly where repeat put them — without the generator being asked again. A bare identifier still works where the id happens to be one, and a quoted field name is an editRegions spelling only: everywhere else it is the parse error it has always been.
at, length and offset are what an override may move. Four mistakes name themselves: a key no region carries — and the diagnostic lists every id in the spelling you have to type it — a key twice in one record, a field that is not a placement, and a second override of a field an earlier editRegions already moved, which has no single answer worth guessing.
You rarely type these by hand: Option/Alt-dragging a pass in Arrange writes one, Clear override removes it, and a canvas edit that moves an overridden pass re-keys its entry to the pass's new id (§ In Studio).
Live launch and the written row#
A written song can still be played by hand, and §6 of the design is exact about what that means: a manual launch takes only its explicit destination set. Stop one track of a timeline song and that receiver goes quiet; every other one keeps playing its written row, on the same transport, with no gap.
| Gesture | What it does on a WRITTEN song |
|---|---|
| Stop a track | The hand owns that receiver. Its regions retire; nothing else changes. |
| Start a track that holds regions | Enables its written row from the transport position — not from bar 0, and not as a pattern track. A region receiver's own pattern is empty by construction, so starting it any other way starts silence. |
| Start a track that holds a plain pattern | An ordinary slot launch; the hand owns it. |
| Resume arrangement | Returns every overridden receiver at once, re-seating each one where the song is NOW. |
| Return one track | The same, for one receiver — an affordance in its row, and resume_arrangement(Some(track)) on the handle. |
A legacy arrange song still suspends the whole walk. That is not an oversight: a scripted walk's scenes name track sets, and it has no track identity to hand back one at a time. volta convert --timeline (Import and export music files) is therefore also what grants a song per-track scope — the two halves of that sentence are the same change.
Bundles#
A scene launch is a transaction, not a sequence of starts:
| Key | Values | Default and why |
|---|---|---|
| running | retain / restart | retain — a track already playing keeps its phrase, which is what every shipped scene apply does. |
| blank | stop / keep | stop — "the scene defines the running set exactly", the shipped rule. keep leaves everything the bundle does not name alone. |
| compose | refuse / takeover / crossfade / priority | refuse — see below. |
Automation destinations are reserved before the launch is accepted. A bundle that writes a destination another launch already holds, and declares no way to resolve it, lands nothing at all — not even the receivers it would have started — and says which destination and which launch instance holds it. A winner discovered by callback order halfway through an already-audible launch is exactly what this prevents.
A scene chip (a manual ApplyScene, no target bar) is the exception to the refuse default: it declares takeover, because a clicked scene replaces what is playing rather than contending with the scene it replaces. Every reservation and hand-held receiver is released by Stop, so the next ▶ re-applies the opening scene from a clean slate.
Coordinates#
Four numbers move independently, and a host that collapses them will reroll music that should not have moved:
| Coordinate | Moves on |
|---|---|
epoch | A seek, a song-loop wrap, a recompile, a transport restart. Administrative: it changes event-delivery identity, never a musical seed. |
pass | A song-loop wrap only. The musical repetition coordinate. |
launch | Every accepted bundle launch. |
run_beat | Forward motion only — an unwrapped position a backward seek never rewinds. |
A loop wrap moves the epoch and the pass; a plain seek moves only the epoch. They are separate calls in the engine (wrap_transport_loop beside seek_to_beat) so the two cannot be confused — the same confusion §9 forbids a host from making when it reports every backward jump as a loop. A command stamped with a superseded epoch is refused, so a launch queued a bar ago cannot land into a different song position. They publish every block, through an atomic mirror the control thread reads directly (so a host stamps a launch with the epoch the transport is at now, not the one it last painted) and through the viz frame's LV01 trailer.
Capture performance#
An observed performance converts directly to regions at their actual boundaries — not padded out to scene-length steps. There is no second recorder: the shipped performance log already stamps the sounding set at every boundary inside every block, so engine::capture::rows_on_axis only has to read it, converting each recorded beat to a bar through the song's own bar axis (a capture across a meterMap change writes the bars the compiler reads back). A part that left and came back is two placements; a sliver shorter than a sixteenth of a bar is a recording artefact, not written down. A boundary within one block of a 1/16- or 1/12-beat point is written on it.
What a capture may claim about itself is Provenance, and it is in the generated FILE as well as in the dialog, because the file is what survives:
- lost events (the recorder overran its bounded slot table),
- a discontinuity (the transport jumped during the take),
- a source revision (the song changed under it).
Any of the three and it is not an exact replay, stated in those words. A durable capture drains in bounded chunks with sequence numbers, so a gap in the record reads as a gap rather than as a shorter performance.
Seeking#
| Policy | What it promises |
|---|---|
| Fast seek — the live transport | Notes and automation are exact from here. Onsets are admitted positionally, so the set after a seek is the set uninterrupted playback would have admitted; every lane is evaluated analytically at the target. Effect tails are not reconstructed: a reverb, a delay line and a compressor envelope keep whatever state they had. |
Exact render — the offline export, and a live seek_exact | Every tail is the real one, because the audio that produced it was actually rendered. Offline that is free: the render starts at the song's origin and never seeks. |
A live exact seek renders a silent preroll from the nearest marker. seek_exact(beat) lands on the latest markers position at or before the target — the song's origin always counts as one — and renders forward to the target with the OUTPUT muted, so every processor arrives carrying the state the music actually produced. Markers are the right grid because they are the author's own idea of where the music has sections; a checkpoint every N bars would be cheaper to build and worse to use, because the preroll from it would begin in the middle of a phrase. A marker's bar becomes its beat through the module's bar axis (meter and meterMap, the same transport::MeterAxis the clock installs), so under meterMap [[0, 7/8], [2, 4/4]] a marker at bar 3 is a checkpoint at beat 11 and a seek there prerolls nothing.
The preroll is real rendering, and it costs what rendering costs: in an offline or audition context that is CPU, on a live device it is wall time. EngineHandle::seek_exact returns the preroll length in beats before committing, and checkpoint_preroll(beat) answers the same question without doing it, so a host can say "this will take eighteen bars" rather than appearing to freeze. Fast seek remains what dragging the playhead does; Exact is offered where audition and export differ, which is the only place the difference is worth paying for.
Placed audio is located, not restarted. A take is one event at its region's start that plays once, so the positional rule alone would leave a seek past that start with nothing to admit and the take silent for the rest of the song. A seek into an audio region instead resumes it: the take is fired at the seek position with its source window advanced by how far into the container the transport now is — plus its offset, and scaled by the take's own speed when warp = "resample" is fitting it. Seek to 0:10 of a take that starts at 0:04 of its source and you hear source second 14, at the sample. Whatever was already sounding on that receiver is cut click-free first, so a seek never leaves two copies of one take playing at different positions. Seeking before a region still leaves it silent until its span, where it starts from the top.
What the engine does#
A region track's own events list is empty by design. play voice [region …] keeps its music in Module.timeline_contents and its placements in Track::regions; Track::events stays [], so a runtime without the region plan plays silence rather than an approximation. An analysis that read Track::events would be blind on a written song: the compiler's lints go through refine.rs::region_analysis_tracks(m, scope), whose RegionScope names the question (Bound for polyphony, headroom and cost; EventSet for "does this part sound, which samples does it name"), and asset walks (build_sampler_synths, intel.rs::referenced_samples) and SourceMap read timeline_contents. A silent written song is fixed by checking its regions are in the timeline list, never by adding an arrange.
A region is prepared into an ordinary track plan. It is the same structure a pattern track compiles to, carrying the region's content, its offset-adjusted origin and its admission window, and it is scheduled by the one note path the engine already has. Velocity and gain lanes, swing, nudge and push, glide, the sampler, MIDI out and trackParam therefore apply to a region unchanged, and there is one note path to keep correct rather than two. The receiver stays the track, so several regions of one part share its instrument, inserts and mixer strip.
Activation slots and event-order buffers are prepared with the module, including future regions and the densest alternative of a pattern choice. Stops and live takeovers return those buffers for reuse. A recompile prepares the new plans' capacity before replacing the old buffers, while existing admission and cursor rules continue to determine which notes play. Definitions used by sustaining voices remain owned on the control side until their snapshots finish, so a voice release does not destroy an old synth graph in the audio callback.
Admission is positional. An onset is admitted when it falls in [start, end), compared on the sample axis through the same rounding the onset itself used — not in beats, where an onset written exactly on the edge could fall a rounding error short. Because it is a position test rather than a cursor state, a seek, a transport loop and a hot swap all land the same set of onsets without a delivery ledger, and an onset outside the window is skipped rather than queued for later. That claim is a set of tests, not an argument: engine/tests/timeline_regions.rs seeks into a region, into multi-cycle content, before a region's start and into a held gate (which must not retrigger); loops a brace whose start is not zero; and hot-swaps a song that shrinks four regions to one and one that inserts a region between two running ones. Each was checked to FAIL with its fix reverted.
A running region survives an edit by its ID, not its index. The workbench hot-swaps after every Arrange gesture, and the new module numbers its plans freely, so an activation is carried across by the region's durable id; its receiver comes from the new plan too, which is what lets a bind move a group placement onto another track without interrupting it. A region the edit removed stops scheduling, and the gates it already admitted ring out — the same contract a removed track has had since scenes.
Both phases are absolute. entry puts content cycle 0 at the region's own (offset-adjusted) bar and transport puts it at song zero; neither uses the engine's "phase zero at activation" origin, which belongs to the live profile. A written placement's origin must not move because the transport was started somewhere else. An offset that would push cycle 0 before song zero wraps forward by whole passes for looping content — phase-identical — and clamps for repeat = "once", which cannot wrap, so it contributes silence before zero.
Lanes write their destination's base every block, evaluated analytically from the transport beat, with the owner the workbench already calls lane. A manual takeover still wins, and Restore returns the destination to the lane at the current musical position. Stopping the song clears the lane writers — stop must be silent for base writers too.
playback = "loop" arms the ordinary transport loop over [0, length), so a declared loop and a brace you drag are one mechanism. "once" simply runs out of regions and stays out.
songPos and sectionPos read off the transport. A written song has no walk to arm them, so the engine derives them from the transport beat each block: songPos ramps 0 → 1 over the declared length, and sectionPos ramps across the marker span the block is in — from the latest marker at or before it to the next one, or over the whole song when the module declares no markers. Both are positional, so a seek lands on the right value with nothing replayed, and each is published as a one-block ramp so a voice still interpolates inside the block. On a scene song they keep their old meaning: over the arrangement, and within the current scene.
The plan is versioned and validated. A plan version this build does not implement is a load failure with a message saying so, never a degraded flat loop. Preparation also builds the plan in the exact temporal core and reads back its occupancy, so an unrepresentable placement or an oversized plan is caught before it can reach the audio callback. volta-time resolves and validates; it does not own delivery — see Temporal core.
Offline renders and exports seat the timeline instead of starting tracks, and take their extent from the declared length. --solo and stem selection still narrow which receivers are audible.
In Studio#
On a written-timeline song the Map becomes Arrange: the same bar axis, rows and play-head, drawing sparse region containers instead of contiguous scene occupancy. A container shows its content's name, a non-zero offset, the notes it plays (a fold's window too) and loop ticks where the scheduler starts a pass. Automation lanes expand beneath the track whose voice reads their destination, and a curve is drawn as its own breakpoints against the destination's declared range — never resampled to the container. Right-click a track's row header for ⟲ history of …: the track's versions to hear and use (Save ideas, compare versions and record takes).
Selection is shared with Cycles, which states the selected region's base cycle, its phrase period, and — with the transport running — which pass is sounding and the phase inside it. A fold's phrase is its window (folded), read the same by every surface: Cycles, NOW, the roll's context, the harmony lane, linked notes and the Pool and Pattern playheads. A legacy scene song is untouched: without a written timeline the Map is the Map.
Converting a scene song in Studio. Under the Map is a convert to timeline… chip. It previews the song as a written timeline with each scene kept as a group (volta convert --timeline --groups, Import and export music files § Convert a scene song): the report, the diff, and an accepted-event comparison over the whole song. The comparison runs in the background and does not compare audio, and the preview says so. Apply conversion installs it as one Undo step, and the canvas becomes Arrange with a structure rail of one group per scene. A song the converter cannot carry (|> at, a songGraph, a meter change…) shows the blocker and writes nothing; when only the groups refuse it (a |> once part held across a seam), Convert without groups previews the flat conversion.
Dragging a clip's edge inside a scene band opens a popover with three spellings. Its third, Convert to a written timeline (keeps scenes as groups), opens the same preview. On Apply it replays the drag on the converted song: the right edge becomes the clip's length, the left edge a trim (at, length and offset, like the canvas's own trim). A clip the form places once has its written region rewritten. A clip placed more than once gets a this-pass-only editRegions override, because the drag changed one section. The replay is a second Undo step, so ⌘Z takes back the drag and a second ⌘Z restores the scene song byte for byte. When the drag has no region equivalent, the notice says why and the conversion stays. That happens when no clip of that track spans the bar (lengthening a part past its scene is a group edit) or when the compiler refuses the edit.
Cycles is one compact lane per track. Each lane header is two quiet rows: the track's name, then its patch (dimmed, with a chooser's live branch as · 2/3) beside three ghost toggles — play/stop, Solo and Mute. The toggles have no fill at rest and light only when they are on: solo in the theme's accent, mute in its alert colour, and a mute that comes from someone else's solo or a VCA as an alert outline. The coloured edge down the header's left side is the track's live meter: a dim seam at rest, lit from the bottom by the track's level (post-strip RMS, instant attack and a short release). Hover the name for the full name and patch; click it to open the track's controls. A single-pitch lane (most drums) draws its hits on the centre line. Click a note (in Cycles, or a glyph at the Map's Event tier) to link it: its atom and transformation chain light in Source, every note from the same written atom lights across the lanes, and a card offers a one-step transposition (Link a note to its source). A click on empty lane space seeks, as before; a Map glyph's double-click still opens its source.
The lanes react to what plays. Every hit flashes and decays over about a fifth of a second, and the play-head strikes that lane in the track's colour; row dots pulse with each track's level. Flashes run on the transport's clock, so nothing animates while nothing plays. Light themes drop the glows: the head is a plain line, a hit a small solid bar.
Every gesture writes the source, through the same guarded transaction and one Undo step as any other workbench edit — there is no second document. It is heard on commit, and so are its Undo and Redo, at once and adding no History version (as a Pattern-pane step's, a live scrub's, a linked note's and a Rack or command-bar edit's are), unless an unrun edit or Run came between:
| Gesture | What it edits |
|---|---|
| Drag the container | its at — the placement moves, the content moves with it |
| Drag the right edge | its length — whole passes (Shift: whole bars), nothing rescales; a pull that rounds back says so. The left edge trims (at, length, offset) |
| Alt-drag the container | its offset — slip: the container stands still and the material moves under it |
| Alt-drag the right edge | its rate — stretch: the same material fills the new span (written as an exact ratio where it is one) |
| ⌘/Ctrl-click inside it | a split at the pointer: two regions on one content, the right half's offset derived from the old map, and a shared continuity link when the region is clip |
| Drag across a curve region with the lane's ✏ chip on | the curve's points literal — the stroke is resampled on the Timeline's Snap grid (Off or Shift: a 32nd), fitted to the fewest breakpoints by the same simplifier a recorded take commits through, and written in the destination's own unit; rows outside the stroke are kept |
| Drag a marker's flag on the ruler | its row's bar in form.markers […] — whole bars, Shift for quarter bars; ←/→ nudge a selected marker |
| Drag a tempo point in the tempo lane | that tempoMap row's beat and bpm (a constant tempo = only moves up or down) |
| Click a meter label in the tempo lane | selects the meterMap change; its bar, beats and note value are edited by number |
Drag a repeat's ×8 badge on the structure rail | its count literal — whole passes; later concat parts move, and the notice counts the placements that moved |
| Drag a silence's trailing edge on the rail | its silence length, in the unit it was written in (4bars → 6bars) — whole bars, Shift for quarter bars |
| Drag a group bracket (or its start grip) on the rail | the placement's at literal, in its own unit |
Drop a track's name (its gutter head) on a bind chip | that one pair in the placement's bind = { … } record; the track must be one the song declares |
| Option/Alt-drag a clip that plays more than once | this pass only: an editRegions override keyed on the pass's id ("fenceT:0@48" = { at = 13bars }) — the container moves at, the right edge length, the left edge at, length and offset |
The inspector's numeric fields are equivalent to dragging and follow every drag and Undo; a region anchored in seconds declines the drag. Bars count from 1, as on the ruler, and a range names its last bar: region 16bars 8bars is bars 17–24, its START BAR reads 17 and writes 16bars.
A pass of a composed form edits what was written. A clip that concat, repeat or a group placed carries an @ id (fenceT:0@32, the second pass of repeat 8 bay after four bars of silence), and it is written only once. Dragging, trimming, resizing, slipping or splitting it rewrites that written region, brought back into its own coordinates, so every pass changes together and the dragged pass lands where it was dropped. One exception follows from concat and repeat going by extent: if the region is the one that ENDS its pass, moving it lengthens every pass by the same amount. The selection stays on the pass you dragged. Four refusals name what owns the value instead:
- a pass dragged before its parent starts names the
repeator group; - a field an
editRegionsoverride owns names the override; - a computed start says "computed — edit its source expression";
- ripple and consolidate on a pass name its written region.
To change one pass alone, Option/Alt-drag it (below).
The structure rail shows how the song is composed. On a song that composes its timeline, a rail between the bar ruler and the first track row draws the tree, outermost first. Each nesting depth gets a row, and parts at one depth that overlap in time get a row each:
- a
repeatis one container,bay ×8, with a tick at every pass; - a
silenceis a hatchedrest 4 bars; - a
concatis a join mark on each seam; - a group placement is a bracket,
post · post-b, drawn on the rail and again across the rows of its members; - a
bindis a chip,postT → answerT, on the bracket and on the clip it redirected; - a named timeline binding is a quiet container with its name.
A flat timeline = [a, b] has no rail. At the Form zoom tier, or on a panel too short for the full rail and three track rows, only the outermost depth draws. Click a node to select it and double-click it to open it in source; a click on the ×8 badge opens the count, and on a silence's length its literal.
The rail edits the literals it draws. A selected node's literals are fields in the inspector — a repeat's passes, a silence's bars, a group placement's start bar — with Reveal beside them, and ←/→ nudge it (a pass, or a bar; Shift a quarter bar). Escape clears it. One thing is selected at a time: a node click clears the region selection, and a region click clears the node. A drag draws its proposal (`repeat 8` → `repeat 9`) and writes nothing until release. A count, length or at that is not a literal refuses at the press ("repeat count is computed — edit its source"), and a node written in another module names that module.
Echoes look like copies. Passes 2–n of a repeat are drawn lighter than pass 1, and their names say pass 3 of 8. Before you drag a clip whose written region plays more than once, its hover says so: "written once in fenceT · plays 8 times through bay ×8". On the clip whose region ends the pass, the hover adds that a move or resize there also changes every pass's length, and every echo's hover ends "Option-drag (Alt) to move only this pass". An occurrence an editRegions override moved carries a pip that names the overridden fields; click the pip to open the override in source.
This pass only. Option/Alt on a clip that plays more than once changes that pass alone. The move, resize or trim is written as an editRegions override keyed on the pass's id, at the song's timeline = field. The first one wraps the field (timeline = form becomes timeline = editRegions form { "fenceT:0@48" = { at = 13bars } }), keeping every byte of what it wraps. Later ones join that record: a new key, a new field, or a field rewritten in place. The wrapper never goes inside a named binding, which may be placed elsewhere too. A field that another editRegions already overrides is refused at the press, in the compiler's words: "…at is already overridden — two editRegions edits of one field have no single answer. Keep one of them." On a clip that plays once, Alt keeps meaning slip and stretch.
Clear override is in the selected pass's inspector, one button per overridden field. It deletes that field, then the key when it was the key's last field, then the whole editRegions wrapper when it was the last key. So clearing everything the canvas wrote gives back the original source, byte for byte. An override is keyed on the pass's id, and an edit that moves the pass re-mints that id. A canvas edit therefore re-keys the overrides it shifts in the same undo step: a drag of the written region, a repeat count, a silence length or a group placement's at rewrites "fenceT:0@48" to "fenceT:0@52" with the compiler's own id rule, and leaves every key whose pass does not move byte-identical. An edit whose shift cannot be known without compiling (shortening the region that ends a stack, a track placed bare as a concat operand, a count that drops an overridden pass) is refused by name when it would move an overridden pass, and writes nothing.
The id rule (lower::occurrence_id, the only speller): the OUTERMOST combinator that shifts a placement names it, since each shift replaces the tag. Every repeat pass (the first included) and a concat's right operand tag the absolute start in beats (fenceT:0@48); a group placement tags its id, or g<at> when it has none; a stack, a named binding and a concat's left operand tag nothing. So a coordinate tag is on the song's beat axis, a group-tagged id survives moves inside the group, and an at override applies after minting: the key spells the un-overridden start.
Drawing automation. The ✏ chip under a lane's arm chip puts that lane in pencil mode: its curve regions become drawing surfaces and stop moving until the chip is turned off. One stroke is one Undo step, and nothing of the stroke is kept — only the breakpoints it fitted to. Before the pointer goes down, a curve whose points are expressions rather than numbers says so on the region ("This curve's points are computed — edit its source expression") and refuses the stroke, and a curve placed more than once says how many placements the edit reaches.
Double-click a region's phrase to open its content in the piano roll at the pass you pointed at, with the other tracks' notes at those bars drawn behind it as context (The Studio workspace § Working from a track).
Make a variation for this placement. Editing a phrase edits every placement of it, and the roll's header says so ("editing it changes 3 placements"). Beside that line, make variation for this occurrence — also Make variation in the selected region's inspector — copies the phrase under a new name (bassIdea2; the roll's chip has a field to rename it) and points only this region at the copy:
``volta norun an excerpt where…` elides the rest of the song bassIdea = !"<[c2 ~ c2 ~] [g2 ~ g2 ~]>" bassIdea2 = !"<[c2 ~ c2 ~] [g2 ~ g2 ~]>" bass = play bassVoice [region 0bars 4bars bassIdea, region 8bars 4bars bassIdea2 { id = "bass-b" }, …]
One undo step; the roll reopens on the copy. A pass that a `repeat` or a
group places is refused — "this pass is one of 3 passes of `repeat 3` —
convert the repeat to placements or edit the group" — because its written
region is shared by every pass. A phrase with one home shows a caption
instead of the button. In a scene song the same gesture forks one SECTION
([[reference/arrangement]] § Native arrangement authoring).
**The conductor lives on the ruler.** Markers are handles on both the Map and
Arrange: a drag shows `[8, "current"] → [12, "current"]` while it is held, and
once the song recompiles the notice counts the placements that re-resolved —
a curve with `attach = "marker"` re-snaps to the span it now starts in, and
the inspector names that marker. Under the seek strip a **tempo and meter
lane** draws the tempo over bars on a bpm axis with each `tempoMap` breakpoint
as a handle, and each meter change as a labelled tick (`7/8`). It opens by
default for a song with a `tempoMap` or `meterMap` and stays closed for a
constant one (the header already reads its bpm and meter) and at phone widths;
the **TEMPO** toggle in the gutter opens it. A drag never invents a `tempoMap`
— a constant tempo's one handle rewrites the constant. Anything the source does
not spell as a literal (an `accel`/`ritard` map, a marker list adopted from
another module, bar 1's implied 4/4) draws as visibly not a handle and says
where the change belongs.
**Several regions at once.** Shift-click extends the selection; the inspector
then shows what the selection has in common and edits all of it. **Split at the
playhead** cuts every selected region the playhead crosses in one transaction —
one Undo step, not one per region. A **ripple** move or delete shifts the later
regions on the same track by the same amount, and its second checkbox ripples
every track instead, so a bar removed from a song stays removed from the song
rather than only from one part.
**Consolidate** asks twice on purpose. The first press *says what would become
fixed* — which chooser, which random operator, which seeded placement stops
being able to change — and only the second writes the materialised range. A
gesture that turns a generator into literal notes should have to be confirmed
after reading what it costs.
**Reroll** writes a new `seed` to the selected placement, leaving where and how
long it plays exactly alone. That is the gesture the duplicate/reroll split
exists for: **Duplicate** keeps the music, **Reroll** changes it.
An **audio** region draws its own waveform, peak-columned from the bank, inside
the container — the clip's real shape at the clip's real length, rather than a
generic block.
**Dragging from Cycles into Arrange** creates a linked, entry-phase region: the
phrase you were looking at, placed where you dropped it, on the content you
were already editing.
At phone-like widths the detail panel stacks below the canvas or replaces it,
and track ownership and the transport stay visible either way. Every region,
lane, breakpoint and chip carries an accessible name, which is what the
workbench's own tests query — never a coordinate.
## Limits
| Bound | Value |
|---|---:|
| Placements in one song | 16,384 |
| Source passes a fold may walk | 256 |
| Group nesting depth | 4 |
| Later spans that may cover one `replace` region | 16 |
| Temporal-plan version this build emits and accepts | 1 |
[[reference/temporal-core]] has the exact core's own ceilings (contents,
events, query work). Storage is proportional to unique content, explicit
placements and curve points — a repeat costs a placement record, not a copy of
the music.
## What is not here yet
Everything below is deliberate, staged in [[unified-timeline]], and named here
so no one has to discover it by trying.
- **`warp = "beats"`** — a pitch-preserving stretch that follows the tempo map.
There is no real-time TSM to call: `engine/src/stretch.rs` pre-renders one
*constant* ratio on the control thread, which a tempo map defeats. The word
is refused by name and points at `"resample"`.
- **Streaming from disk.** An audio region reads a **resident** buffer: the
bank is loaded lazily in the background and the callback only reads an `Arc`,
so nothing decodes or allocates in `process` — but a two-hour take is two
hours of RAM, not a stream.
- ~~**Live exact seeking.**~~ Shipped: § Seeking. What is still not built is a
checkpoint with *recorded inputs* — the preroll re-renders the music, so a
performance that depended on live input or a hand on a knob prerolls without
them.
- **Lazy query nodes are BUILT but not on the delivery path.** Independent
periods **across** regions and tracks have always cost nothing: each keeps
its own cursor and `clock (p/q)` aligns by proof, so a three-beat ostinato
beside a four-beat loop never builds a twelve-beat pattern. *Inside* one
placed expression, `compiler/src/lower/contentplan.rs` now answers
"what happens in cycle *k*" without materialising the period — storage is
the base pass plus a handful of nodes, whatever the period — for the four
operators that multiply it:
| Bounded | Still materialising, and why |
|---|---|
| `<a b c>` alternation | `degradeBy` / `sometimesBy` / `undegradeBy` — the coin hashes an event's index in the **flattened** list, which a per-cycle query does not have |
| `every n f` (and `firstOf` / `lastOf`) | `shuffle` / `scramble` / `a?` / `[a\|b]` — the draw is unrolled against parse-order state |
| `fast a/b` | `mask` / `struct` / `sew` / `inv` — the positional rest law can hold a value set many cycles earlier: a fold, not a query |
| `slow a/b` | patterned combinator arguments (`fast !"<1 2>"`) — their windows are not cycle-aligned |
Each of the four is proved equal to the timing-version-2 materialisation by
**411 equivalence cases** (not re-counted at this stamp; `compiler/tests/content_plan.rs`), 200 of them
randomised over combinations from a printed seed, with nothing skipped.
What it is not yet is *installed*: installing it **buys nothing until the
IR is lazy too**. A plan is compositional only
across those four operators: the moment any other combinator wraps the
chain, or the lowerer has to hand the engine a `TimelineContent`, the plan
materialises and the stored period is exactly what it is today. The win —
storage proportional to the authored notes rather than to the LCM — needs
the IR to carry the plan and the engine to query it per cycle, which is an
IR shape, an engine scheduler and a swap-safety story of its own. Wiring the
lowerer onto `Plan::materialize` alone would move peak *lowering* memory and
change nothing a listener or a song file can see.
- **Group ports are a Volta deviation from the design sketch.** §3 of the
design says missing port bindings are errors. They are not: a Volta group's
receivers are its **own declared tracks**, so an unbound one simply plays on
itself and `bind` is a redirect. Only a name the group does not have, or one
bound twice, is an error.
- **Automation's own gaps** — discrete destinations, instrument construction
scalars, host parameter ownership — are listed in
[[reference/automation]] § "What is not here yet".
## Next steps
- [[reference/arrangement]] — the scripted walk, scenes, edges and transitions,
which a timeline song does not use.
- [[reference/automation]] — what a lane may aim at, curve segments, base
composition, and the Read / Touch / Latch / Write / Off recording modes.
- [[reference/macro-modulation]] — what a lane writes into, and who wins when a
hand and a lane disagree.
- [[reference/patterns]] — what a region places, and the timing rules it keeps.
- [[reference/temporal-core]] — the exact coordinates, queries and identity
laws underneath.