Skip to content

Write a Pattern as a Class

Write a script pattern when a stream or a Markov chain is not enough — when you need several state variables, helper methods, or logic that does not fit one fn(prev, cycle). The contract (what query() may return and do) is in the script pattern reference.

Script patternStream
Defined asA class that implements Patternstream(init, fn(prev, cycle))
StateAny number of named fieldsA single value (prev)
ExtrasHelper methods, tables in fieldsClosure only
Use it forLogic with structure or several state variablesWalking one value forward

Declare the class implements Pattern, give it a query(cycle) method that declares what it returns, -> Note | Number | Rest | Array | Pattern?, and send an instance to a track. Fields you change in query() carry over to the next cycle:

use "std/instruments" { SamplerMelodic, Kit };
class Stepper implements Pattern {
let notes;
let idx;
fn new(notes) {
this.notes = notes;
this.idx = 0;
}
fn query(cycle) -> Note | Number | Rest | Array | Pattern? {
let note = this.notes[this.idx % this.notes.length()];
this.idx = this.idx + 1;
return note;
}
}
let lead = AudioTrack("lead");
lead.set_instrument(SamplerMelodic(Kit("keys")));
lead << Stepper(#[C4, D4, E4, F4, G4]);
PLAY;
Cyclethis.idx beforequery() returnsthis.idx after
00notes[0] = C41
11notes[1] = D42
…………
55notes[0] = C46

A script pattern is an ordinary class, so query() can call your own methods. Build tables once in new(), store them in a field, and read the field in query(). This walk clamps itself to a register:

use "std/random" { choose };
class BoundedWalk implements Pattern {
let pos;
let lower;
let upper;
let steps;
fn new(start, lower, upper) {
this.pos = start;
this.lower = lower;
this.upper = upper;
this.steps = #[-2, -1, 1, 2];
}
fn clamp(val) {
if val < this.lower { return this.lower; }
if val > this.upper { return this.upper; }
return val;
}
fn query(cycle) -> Note | Number | Rest | Array | Pattern? {
this.pos = this.clamp(this.pos + choose(cycle, this.steps));
return this.pos;
}
}
let walk = BoundedWalk(60, 48, 72);
print(walk.query(0));
print(walk.query(1));

You don’t always need state. Index a list with cycle to play a chord progression:

use "std/instruments" { SamplerMelodic, Kit };
class ChordProgression implements Pattern {
let chords;
fn new(chords) {
this.chords = chords;
}
fn query(cycle) -> Note | Number | Rest | Array | Pattern? {
return this.chords[cycle % this.chords.length()];
}
}
let prog = ChordProgression(#[
#[C4, E4, G4],
#[D4, F4, A4],
#[G4, B4, D5],
#[C4, E4, G4],
]);
let pad = AudioTrack("pad");
pad.set_instrument(SamplerMelodic(Kit("keys")));
pad << prog;
PLAY;

An array return plays as a chord. Return a pattern such as [C4 E4 G4] to spread notes across the cycle, or [_] for a silent cycle.

A class that implements Pattern has every combinator, and every function that takes a pattern takes it. Each captures the instance’s fields as they are at that moment:

use "std/instruments" { SamplerMelodic, Kit };
class Rise implements Pattern {
let base;
fn new(base) {
this.base = base;
}
fn query(cycle) -> Note | Number | Rest | Array | Pattern? {
return this.base + cycle % 5;
}
}
let lead = AudioTrack("lead");
lead.set_instrument(SamplerMelodic(Kit("keys")));
lead << Rise(60).fast(2).transpose(12);
PLAY;