Tracks, sections, arrangement
Every song is a default-exported function that receives a Song. Three calls
form the skeleton: you build tracks, declare sections, and lay out the
arrangement.
import type { Song } from "@barline/runtime";
export default function (song: Song) { // ... song.track(...) / song.section(...) / song.arrange(...)}Tracks
Section titled “Tracks”song.track(options) registers a named destination patterns play into. It’s
code-owned data: an instrument output, an optional static effect chain, and
optional bus sends.
const kick = song.track({ name: "kick", output: { kind: "synth", synth: { id: "kick" } }, effects: [fx.distortion({ drive: 0.35 }), fx.limiter({ threshold: -2 })],});
const hats = song.track({ name: "hats", output: { kind: "synth", synth: { id: "hat", params: { volume: -12 } } }, sends: { echo: 0.12 },});The output.kind is one of:
synth— a built-in or package-provided synth, referenced byidwith optionalparams(see Types for the deep synthskick909,acid303,stab).sampler— named sample slots:{ kind: "sampler", samples: { kick: "/samples/kick.wav" } }. Named keys map chromatically from C3 in declaration order.soundfont— a user-uploaded SoundFont (.sf2/.sf3) played live:{ kind: "soundfont", font: "<upload-id>", program: 0 }.fontis the id of an uploaded font (served from/api/uploads/<id>);programis a GM program number0–127(bank 0, GM melodic), one program per track.midi— Web MIDI out (“pro mode”: drive Ableton or any DAW), with achanneland optionalport.
Sections
Section titled “Sections”song.section(name, length, body) registers a section. The body runs at every
bar boundary, so per-bar logic and flag reads just work. Track methods are
legal only inside a body — they write into the current bar’s capture buffer:
track.play(pattern)→ aPlayHandleyou can chain.gain(v)and.through(...fx)on.track.automate(param, curve)→ attach an automation curve.track.clip(options)→ schedule an audio clip (one-shot or pre-warped loop).track.at(offset)→ offset subsequentplay()within the section, e.g.clap.at(bars(8)).play(p)makes the clap exist only from bar 8 onward.
The body receives a SectionContext (t by convention):
song.section("drop1", bars(16), (t) => { const drive = Number(t.flags.drive ?? 0); // the flag snapshot const bar = Math.floor(t.position() / 4); // position() is in beats kick.play(straightKick); if (bar === 15) hats.play(fast(2, crescRoll)).gain(0.5); // a turnaround fill});t.position() is the position within the section (in beats); t.flags is the
bar-boundary flag snapshot; t.bpm is the tempo. t.inherit("other") re-runs
another section’s body first, then you layer on top:
song.section("build1", bars(16), (t) => { t.inherit("intro"); // everything intro does, then: hats.play(at([2, 6, 10, 14])).gain(0.7); rumble.play(rumbleA).gain(0.5);});Arrangement
Section titled “Arrangement”song.arrange(entries) is the play order. repeat plays a section more than
once. Reorder it live and the clock applies the change at the next bar.
song.arrange([ { section: "intro" }, // 16 bars { section: "build1" }, // 16 bars { section: "drop1", repeat: 2 }, // 32 bars { section: "breakdown" }, // 16 bars { section: "drop2", repeat: 2 }, // 32 bars { section: "outro" }, // 16 bars]);Sections are 1-bar-and-up chunks of structure; the arrangement is a flat list of them. That’s the whole timeline — and the Arrange view is a direct projection of this list plus the dry-run of each section.