Skip to content

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" }]);
}

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: x is 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; null is 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.flags is 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.

  • The model — the three timescales and how hot reload works.
  • Patterns — every constructor (r, grid, at, euclidean, generate, notes, …).
  • Nebelwand — the full 192-bar example, annotated.