Custom patterns
Pattern is a small interface, and that is the whole extension story. Implement
three members and your class is exactly as first-class as r, euclidean, or
notes — it composes with every combinator, plays into
any track, and renders in the piano roll.
The contract
Section titled “The contract”import type { Pattern, QueryContext, NoteEvent, TimeWindow, Beats } from "@barline/core";
interface Pattern { readonly id: string; readonly length: Beats; query(window: TimeWindow, ctx: QueryContext): NoteEvent[]; // optional: source?: { line } — call-site capture, set by leaf constructors}query returns the events that fall inside window ({ start, end } in
beats). The two rules:
- Pure and deterministic. Same
(window, ctx)→ same events. Any randomness goes throughctx.rng(seed), neverMath.random(). This is what keeps the visualization honest against playback. - Cheap.
queryruns at scheduling resolution (the hot path). No I/O, no heavy allocation.
QueryContext gives you ctx.rng(seed) (a seeded 0..1 draw) and ctx.flags
(the live flag values — read here for sub-beat control).
A real example: RatchetPattern
Section titled “A real example: RatchetPattern”This is the custom class from Nebelwand. It wraps any
source pattern and probabilistically explodes a hit into a fast roll. It reads
ctx.flags live at query time, so turning the drive knob makes rolls more
likely mid-bar, and it seeds ctx.rng per event so playback and the piano roll
agree on every roll.
import { beats } from "@barline/core";import type { Pattern, QueryContext, NoteEvent, TimeWindow, Beats } from "@barline/core";
let ratchetIds = 0;
class RatchetPattern implements Pattern { readonly id = `ratchet-${++ratchetIds}`; readonly length: Beats;
constructor( private readonly inner: Pattern, private readonly opts: { /** Base chance [0,1] that a hit becomes a roll. */ probability: number; /** Sub-hits per roll (default 3 — the classic triplet stutter). */ divisions?: number; /** Flag name whose live value adds up to +0.5 probability. */ boostFlag?: string; }, ) { this.length = inner.length; }
query(window: TimeWindow, ctx: QueryContext): NoteEvent[] { const divisions = this.opts.divisions ?? 3; const boost = this.opts.boostFlag ? Number(ctx.flags[this.opts.boostFlag] ?? 0) : 0; const probability = Math.min(1, this.opts.probability + boost * 0.5);
const out: NoteEvent[] = []; for (const event of this.inner.query(window, ctx)) { // Deterministic per-event coin flip: same window + offset → same roll. if (ctx.rng(`${this.id}:${event.offset}`) >= probability) { out.push(event); continue; } const sub = event.duration / divisions; for (let i = 0; i < divisions; i++) { out.push({ ...event, offset: beats(event.offset + i * sub), duration: beats(sub), velocity: Math.max(0.2, event.velocity * (1 - i * 0.18)), }); } } return out; }}Use it like any other pattern — it wraps a pattern, and combinators wrap it:
const offHats = at([2, 6, 10, 14]);
const ratchetHats = new RatchetPattern(offHats, { probability: 0.2, divisions: 3, boostFlag: "drive", // mod wheel up → more rolls, live, mid-bar});
const scatterRolls = new RatchetPattern(scatter, { probability: 0.35, divisions: 4 });
song.section("drop2", bars(16), (t) => { hats.play(t.flags.ratchet === true ? ratchetHats : offHats).gain(0.8);});Why this matters
Section titled “Why this matters”Because Pattern is just an interface, the whole language of music structure
is open to you. A generative sequencer, a Markov-chain melody, a port of a
TidalCycles idea, a pattern that reads a CSV at composition time — all of them
slot into the same engine and the same projections, with no plugin API to learn.
That’s the point of “it’s real TypeScript”: the library gives you primitives and
a runtime, not a DSL.