Script Patterns
When streams and Markov chains aren’t enough — you need several state variables,
helper methods, or logic that doesn’t fit a single fn(prev, cycle) — you can
write the pattern as a class. Any class with a query(cycle) method becomes a
pattern: send an instance to a track and Resonon calls query() once per cycle,
turning whatever you return into notes. It’s like scripting your own sequencer
device.
Here’s a stepper that walks through a list of notes, one per cycle:
use "std/instruments" { SamplerMelodic, Kit };
class Stepper { let notes; let idx;
fn new(notes) { this.notes = notes; this.idx = 0; }
fn query(cycle) { let note = this.notes[this.idx % this.notes.length]; this.idx = this.idx + 1; return note; }}
let lead = AudioTrack("lead");lead.load_instrument(SamplerMelodic(Kit("keys")));lead >> master;lead << Stepper(#[C4, D4, E4, F4, G4]);
PLAY;Assigning the instance to the track is all it takes — Resonon spots the query()
method and drives it. Each cycle, Stepper returns the next note and bumps its
index.
How it works
Section titled “How it works”Each cycle, the track:
- Recomputes the state from cycle 0 — replays every past
query()to rebuild the current field values (just like streams and Markov chains) - Calls
query(cycle)with the current cycle number - Converts the return value into pattern events for that cycle
Because state is always rebuilt from the initial values, script patterns are seekable and deterministic — querying cycle 100 always produces the same result, no matter how you got there.
State persists across cycles
Section titled “State persists across cycles”Mutating a field inside query() carries forward. When Stepper runs
this.idx = this.idx + 1, the next cycle sees the bumped value:
| Cycle | this.idx before | query() returns | this.idx after |
|---|---|---|---|
| 0 | 0 | notes[0] = C4 | 1 |
| 1 | 1 | notes[1] = D4 | 2 |
| 2 | 2 | notes[2] = E4 | 3 |
| 3 | 3 | notes[3] = F4 | 4 |
| 4 | 4 | notes[4] = G4 | 5 |
| 5 | 5 | notes[0] = C4 | 6 |
Return types
Section titled “Return types”query() can return anything that converts to a pattern:
| Return value | Result |
|---|---|
Number (60) | MIDI note number, one note filling the cycle |
Note (C4) | A single note filling the cycle |
Array (#[C4, E4, G4]) | A chord — all notes sound together |
Pattern ([C4 E4 G4]) | A sub-pattern, distributed across the cycle |
[_] | A rest — silence for that cycle |
Helper methods
Section titled “Helper methods”A script pattern is a normal class, so query() can call helper methods. This
bounded random walk keeps its step list in a field (set up in new()) and
clamps the result to a register:
use "std/random" { choose };
class BoundedWalk { 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) { 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);Cycle-dependent logic
Section titled “Cycle-dependent logic”You don’t always need mutable state — the cycle argument alone is enough for
time-varying patterns. This progression just indexes its chord list by cycle:
use "std/instruments" { SamplerMelodic, Kit };
class ChordProgression { let chords;
fn new(chords) { this.chords = chords; }
fn query(cycle) { 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.load_instrument(SamplerMelodic(Kit("keys")));pad >> master;pad << prog;
PLAY;Inside query() the current tempo is also available as cps (cycles per second),
in case your logic needs to react to it.
What’s allowed inside query()
Section titled “What’s allowed inside query()”Script patterns run in a restricted evaluator for thread-safety and determinism:
- Supported:
let,if/else,match,return,for,loop, arithmetic, field and array access, calls to your own functions and helper methods, and the seededrand()/choose()fromstd/random. - Not supported:
PRINT/PUT, audio or MIDI operations, and native functions. Keepquery()to pure computation that returns a value.
Script pattern or stream?
Section titled “Script pattern or stream?”| Script pattern | Stream | |
|---|---|---|
| Defined as | A class with query(cycle) | stream(init, fn(prev, cycle)) |
| State | Any number of named fields | A single value (prev) |
| Extras | Helper methods, captured tables | Closure only |
| Reach for it when | Logic needs structure or several state variables | Walking one value forward |
See Also
Section titled “See Also”- Generative Patterns — streams and Markov chains
- Classes & References — class syntax and semantics
- Transforming Patterns — reshaping pattern output
Next Steps
Section titled “Next Steps”Patterns make the notes — now make them sound.