Skip to content

Combinators

Pattern transforms are free functions, not methods (Ramda-style). A class that implements Pattern shouldn’t have to implement forty chaining methods to be first-class — so slow(2, p) reads instead of p.slow(2). Any pattern composes with any other.

import { stack, sequence, slow, fast, gain, chance, swing, humanize } from "@barline/core";

All of these are pure and deterministic. The ones that need randomness (chance, humanize) draw from ctx.rng(seed), so visualization and playback never disagree.

Play several patterns in parallel. The result’s length is the longest input.

stack(
euclidean(3, 16, { note: "F3" }),
euclidean(3, 16, { note: "Ab3" }),
euclidean(3, 16, { note: "C4" }),
); // an F-minor stab chord on one euclidean rhythm

Play patterns one after another, looping the concatenation.

const phraseA = notes(["F3", "Ab3", "C4", "Bb3"], beats(0.5));
const phraseB = notes(["F3", "Ab3", "C4", "Eb4"], beats(0.5));
sequence(phraseA, phraseB); // two 1-bar phrases, 2 bars per cycle

slow(factor, pattern) / fast(factor, pattern)

Section titled “slow(factor, pattern) / fast(factor, pattern)”

Stretch or compress time. slow(2, p) takes twice as long; fast(2, p) plays twice as fast (fast is exactly slow(1 / factor, …)).

const hymn = slow(2, sequence(phraseA, phraseB)); // half speed → 4 bars per cycle
hats.play(fast(2, crescRoll)); // double-time turnaround roll

Scale every velocity, clamped to [0, 1]. (Distinct from the play-handle .gain() modifier — gain() the combinator transforms the pattern.)

hats.play(gain(0.25, r`x*16`)); // sixteen hits, all at a quarter velocity

Drop events with probability 1 - probability, deterministically per event (seeded by offset). Thins a pattern without making it jitter between frames.

hats.play(chance(0.6, at([2, 6, 10, 14]))); // ~60% of the offbeat hats survive
perc.play(chance(0.4, scatter)); // sparse texture

Musical 16th-note swing. Every off-16th — an event on the second 16th of an 8th-note pair — is delayed by amount × 1/12 beats. amount = 1 lands exactly on the triplet position (classic MPC feel); downbeats, straight 8ths, and triplets are untouched. amount clamps to 0..1.

hats.play(swing(0.6, r`x x x x x x x x x x x x x x x x`));

It is window-consistent: the underlying pattern is queried on a back-widened window so a delayed event near the bar end is still reported correctly.

Per-event jitter, seeded per event so it is deterministic.

  • timing — a gaussian standard deviation in beats (Box–Muller, clamped to ±3σ). 0.010.03 is a subtle human feel on a 16th grid.
  • velocity — a uniform spread 0..1; each velocity moves by ±spread/2, clamped to [0, 1].
clap.play(humanize({ timing: 0.02, velocity: 0.15 }, backbeat()));