Skip to content

Sections & Songs

A timeline arranges one lane. A section arranges a slice of the whole piece: every track’s clips, every automation curve, and the picture on screen, all under one length. Sequence a few sections back to back with arrange() and you have a song — one value you can play, render, and keep editing while it runs.

use "std/instruments" { Sampler, Kit };
let drums = AudioTrack("drums");
drums.instrument(Sampler(Kit("CR-78")));
let verse = section(8);
verse[drums] << [bd bd sd bd];
let chorus = verse.clone();
chorus[drums] << [bd sd bd sd];
let song = arrange(#[verse, chorus]); // verse [0,8), chorus [8,16)
song.play();

Let’s take that apart.

section(length) makes an empty section length cycles long. Index it with a track to get that track’s lane, and assign with << exactly as you would send a pattern to the track directly:

use "std/instruments" { Sampler, Kit };
let drums = AudioTrack("drums");
drums.instrument(Sampler(Kit("CR-78")));
let bass = AudioTrack("bass");
bass.instrument(Sampler(Kit("CR-78")));
let verse = section(8);
verse[drums] << [bd bd sd bd];
verse[bass] << [c2 _ g1 _];

One section, two lanes, both eight cycles long. Because the length is the section’s and not the lane’s, the tracks can never drift out of alignment — the problem that one-timeline-per-track leaves you to solve by hand.

A lane also takes a positioned write, .at(start, duration) << clip, which places a clip inside the lane instead of covering the whole section. That is the same layer machinery a timeline has, and the rest of this page leans on it.

.clone() gives you a deep, independent copy — every lane, every curve. Edit the copy and the original is untouched, which is how you write a variation without retyping the parts that stay:

let chorus = verse.clone();
chorus[drums] << [bd sd bd sd]; // busier drums
// verse[drums] is still [bd bd sd bd]

arrange(#[...]) plays its elements back to back and returns one arrangement whose length is the sum:

let song = arrange(#[verse, chorus]); // 16 cycles: verse [0,8), chorus [8,16)

Repeat an element with elem * n — arrange(#[verse * 2, chorus]) is a 24-cycle song. Arrangements nest, so a chorus-plus-bridge group can itself be an element of a larger arrange().

What arrange() lays down goes into each lane’s base layer. That matters: anything you punch on top of the song afterwards sits above the structure and survives a later rewrite of it.

song.play() binds every lane — notes, automation, visuals — and starts the transport from the top. It plays once through and then goes silent; call it again to restart.

song.play();

Punch a fill over the song and it masks whatever is underneath, including across a section boundary:

song[drums].at(6, 4) << [cp cp]; // starts in the verse, reaches into the chorus
show(song);
section — 16 cycles, 2 lanes
drums layers: base, main
0..6 [bd bd sd bd] base
6..10 [cp cp] main
10..16 [bd sd bd sd] +2c base
bass layers: base
0..8 [C2 ~ G1 ~]
8..16 [C2 ~ G1 ~]

The claps run through the seam at cycle 8, and when they stop the chorus resumes at its own cycle 2 — the +2c. It was masked, not cut, so it kept its place. There is no need to split the write into two .at calls to cross a boundary.

Rewriting the structure underneath leaves the punch alone, because a bare song[drums] << clip replaces the base layer only:

song[drums] << [bd _ sd _]; // new groove everywhere; the claps at 6–10 stay

And .clear() lifts a mask to bring back exactly what it was hiding:

song[drums].at(6, 4).clear(); // the chorus's opening is intact again

Every lane on a section carries the full layer surface — .layer(name), .mute(), .unmute(), .layers(#[..]), .clear(), .entries() — described on the Arrangements page. Grouping punches onto a named layer means you can lift the whole group at once:

song[drums].layer("fills").at(7, 1) << [hh hh hh hh];
song[drums].layer("fills").at(15, 1) << [hh hh hh hh];
song[drums].layer("fills").mute(); // both gone
song[drums].layer("fills").unmute(); // both back

A track’s parameters are lanes too. Address one with .volume, .pan, or .get_effect("name").param("cutoff"), and assign a value or an automation curve:

use "std/instruments" { Sampler, Kit };
use "std/signals" { automation };
let drums = AudioTrack("drums");
drums.instrument(Sampler(Kit("CR-78")));
let verse = section(8);
verse[drums] << [bd bd sd bd];
let chorus = verse.clone();
chorus[drums] << [bd sd bd sd];
let song = arrange(#[verse, chorus]);
song[drums].volume << 0; // whole song, 0 dB
song[drums].volume.at(8, 8) << automation().at(0, -24).at(8, 0); // chorus swells in

A positioned curve masks the one underneath just like a clip does — and because a curve is continuous, “masks” needs to be precise about the edges. The base holds its level right up to the mask edge and steps there; it does not slide into the mask across the preceding cycles:

show(song);
section — 16 cycles, 1 lane
drums layers: base
0..8 [bd bd sd bd]
8..16 [bd sd bd sd]
drums .volume layers: base, main
0..8 0.00 flat base
8..16 -24.00 → 0.00 main

Cycles 0–7 sit flat at 0 dB, then cycle 8 drops to −24 dB and swells back. Clear the mask and the base curve comes back exactly as authored.

section[screen] is the visual lane. A bare write puts a visual up for the section’s whole span, so sequencing sections cuts the picture on section boundaries — a section without a screen write renders black.

use "std/visuals" as gfx;
let verse = section(8);
verse[screen] << gfx.Noise();
let chorus = verse.clone();
chorus[screen] << (gfx.Noise() >> gfx.Blur());
let song = arrange(#[verse, chorus]);
song[screen].layer("hits").at(14, 2) << gfx.Edge();
show(song);
section — 16 cycles
screen layers: base, hits
0..8 Noise base
8..14 Blur base
14..16 Edge hits

Open the window to watch it, exactly as for a static v >> screen route:

screen.show_window();
song.play();

show(song) renders the resolution of every lane — notes first, then automation, then screen — one row per audible interval, tagged with the layer that won it. It is the answer to “what will this actually sound like”, which the list of writes you made cannot give you once masks are involved.

Two things it deliberately does not show: a muted layer contributes no rows, so it stays findable in the layers: list instead; and an entry that is completely hidden by another is not an interval at all. .entries() is where those show up, flagged masked — it works on any lane, so song[drums].entries() and song[drums].volume.entries() both answer “what did I actually write here”.

Every lane edit lands live. Change a clip, punch a fill, write an automation curve, mute a layer, swap the visual — it is heard with no second play() and no rewind:

song.play();
// ...somewhere in the middle of the verse, execute just this line:
song[drums].volume.at(8, 8) << automation().at(0, -24).at(8, 0);

The transport keeps running and the swell is there when the playhead reaches cycle 8. Automation is positioned in absolute cycles, so it is bound the same way whether the song is playing or stopped.

“Live” does not mean the same thing on all three lanes, which matters when you are punching things in on the beat:

LaneTakes effectWhy
Notes (song[drums])next cycle boundarythe lane swaps its pattern once per cycle, so notes already sounding are never cut off mid-cycle
Automation (.volume)next audio blockthe merged curve re-binds and the engine picks it up on its next callback
Screen (song[screen])next framethe renderer re-selects the covering span every frame

So a note edit — including .mute() and .clear() on a note layer — is quantized to the cycle, while automation and visuals are effectively immediate. If a mute seems not to have taken, wait out the current cycle before assuming something is wrong.