Your first song
A barline song is a default-exported function that receives a Song. You build
your tracks, declare sections, and arrange them. Here is a complete, playable
intro — every line is explained below.
import { r, at, notes, beats, bars, ramp, sine, fx } from "@barline/core";import type { Song } from "@barline/runtime";
export default function (song: Song) { // --- tracks: instrument + static effect chain, all code-owned data --- const kick = song.track({ name: "kick", output: { kind: "synth", synth: { id: "kick" } }, effects: [ fx.distortion({ drive: 0.35, wet: 0.6 }), // hard-techno kicks aren't clean fx.limiter({ threshold: -2 }), ], });
const ride = song.track({ name: "ride", output: { kind: "synth", synth: { id: "metal", params: { volume: -18 } } }, });
const perc = song.track({ name: "perc", output: { kind: "synth", synth: { id: "metal", params: { volume: -16, note: 86 } } }, });
// --- a section: a body re-run at EVERY bar boundary --- // INTRO — energy 2/10. A filtered heartbeat and air. Patience. song.section("intro", bars(16), (t) => { kick.play(r`x . . . x . . . x . . . x . . .`).gain(0.9); kick.automate("gain", ramp(0.7, 1, bars(16))); // fade the kick up over the section ride.play(at([0, 8])).gain(0.3); ride.automate("pan", sine(0, 0.5, bars(4))); // slow stereo drift if (Math.floor(t.position() / 4) >= 8) { perc.play(notes(["C3", null, null, "C3"], beats(0.5))).gain(0.5); } });
// --- arrangement: the order the clock plays sections in --- song.arrange([{ section: "intro" }]);}Line by line
Section titled “Line by line”The tracks. song.track({ … }) registers a named destination. Its
output chooses the instrument — here three built-in synth ids (kick,
metal). params carries synth-specific values (volume in dB, a note
offset). The effects array is a static chain declared once: pure fx.*
data the runtime turns into audio nodes. (Full synth + effect reference:
Effects, Types.)
The section. song.section(name, length, body) registers a section. The
body is plain TypeScript that runs at every bar boundary — that is why
per-bar branching like if (barOf(t) >= 8) and live flag reads just work.
Inside the body:
kick.play(pattern)schedules a pattern for this bar. It returns a handle so you can chain.gain(0.9)(scale the velocities) or.through(fx.delay())(route this one play through extra effects).r`x . . . x . . . x . . . x . . .`is the tagged-template pattern syntax:xis a hit,.a rest, 16 steps = one bar of 16ths. This is four-on-the-floor.at([0, 8])places hits at step indices 0 and 8 of a 16-step bar.notes(["C3", null, null, "C3"], beats(0.5))plays a melodic step grid;nullis a rest, the second arg is the step length.kick.automate("gain", ramp(0.7, 1, bars(16)))runs a curve over a parameter — here the kick fades up across the whole 16 bars.t.position()returns the position within the section in beats.t.flagsis the flag snapshot;t.inherit("other")re-applies another section first.
The arrangement. song.arrange([...]) lists the sections in play order.
{ section: "drop1", repeat: 2 } plays a section twice. Reorder it live and the
clock picks up the new order at the next bar.
Capture-and-commit (why a typo never stops the music)
Section titled “Capture-and-commit (why a typo never stops the music)”play() and automate() do not make sound directly. The bar clock runs your
section body into a fresh capture buffer, and only hands the result to the
scheduler if the body completes without throwing. A body that throws costs
exactly one bar — the runtime replays that section’s last good capture — never
the whole set. This is the “a live set never stops” guarantee. More in
The model.