Skip to content

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.

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:

  1. Pure and deterministic. Same (window, ctx) → same events. Any randomness goes through ctx.rng(seed), never Math.random(). This is what keeps the visualization honest against playback.
  2. Cheap. query runs 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).

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

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.