Skip to content

Generative Patterns

So far your patterns have been written out note by note. Generative patterns flip that around: you describe a process, and the pattern produces its own notes as it plays. Random choices, walking melodies, chord progressions that wander and resolve — all from a few lines of rules.

The catch with most generative tools is that they’re unrepeatable: hit play twice and you get two different takes. Resonon’s generators are different. They’re all deterministic and seekable — driven by the cycle number rather than live randomness, so the same cycle always produces the same result. You can loop, seek, or restart and hear the exact same performance every time. It’s like a generative MIDI plugin that never forgets what it played.

This chapter builds up in three steps: the randomness primitives that make choices, streams that walk a value forward cycle by cycle, and Markov chains that move between states by probability.

The randomness functions live in std/random. The key ones take a seed and are fully deterministic — same seed, same answer, every time:

use "std/random" { rand, choose, choose_weighted };
PRINT rand(0); // a float in [0.0, 1.0), fixed for seed 0
PRINT rand(1); // a different float, fixed for seed 1
let notes = #[C4, E4, G4, B4];
PRINT choose(0, notes); // picks one element, deterministically
PRINT choose(2, notes); // a different seed, a different (fixed) pick

choose_weighted biases the pick — pass a parallel array of weights, and higher weights are chosen more often:

use "std/random" { choose_weighted };
let chords = #[[C4 E4 G4], [F4 A4 C5], [G4 B4 D5]];
let weights = #[3, 1, 1]; // C major three times as likely
PRINT choose_weighted(0, chords, weights);

On their own these just return values. Their real power shows up when you feed them the cycle number as the seed inside a stream or script pattern — then every cycle makes a reproducible “random” choice. That’s the next section.

A stream walks a value forward: each cycle’s output is computed from the previous cycle’s. You give stream() an initial value and a function fn(prev, cycle) that returns the next one. It lives in std/generative.

Streams work with plain MIDI numbers (60 = C4, 64 = E4, 67 = G4), so you can do arithmetic on them directly. The simplest walk just climbs by a semitone each cycle:

use "std/generative" { stream };
let chromatic = stream(60, fn(prev, cycle) {
return prev + 1;
});
PRINT chromatic.query(0); // 60
PRINT chromatic.query(1); // 61
PRINT chromatic.query(4); // 64

.query(n) asks the stream for the value at cycle n — handy for checking what a stream does before you play it. Now feed choose the cycle number and you have a random walk — a melody that steps around unpredictably, but identically on every playback:

use "std/instruments" { SamplerMelodic, Kit };
use "std/generative" { stream };
use "std/random" { choose };
let steps = #[-2, -1, 1, 2];
let walk = stream(64, fn(prev, cycle) {
return prev + choose(cycle, steps);
});
let lead = AudioTrack("lead");
lead.load_instrument(SamplerMelodic(Kit("keys")));
lead >> master;
lead << walk;
PLAY;

To keep a walk inside a register, check the bounds and bounce it back:

use "std/generative" { stream };
use "std/random" { choose };
let bound_steps = #[-3, -1, 1, 3];
let bounded = stream(60, fn(prev, cycle) {
let next = prev + choose(cycle, bound_steps);
if (next > 72) { return prev - 3; } // too high — turn around
if (next < 60) { return prev + 3; } // too low — turn around
return next;
});
PRINT bounded.query(0);
PRINT bounded.query(8);

rand gives you probability-based decisions — here, a 70% chance to step up:

use "std/generative" { stream };
use "std/random" { rand };
let prob_walk = stream(65, fn(prev, cycle) {
if (rand(cycle) < 0.7) { return prev + 1; }
return prev - 1;
});
PRINT prob_walk.query(3);

The initial value can also be an array, which lets you evolve a whole chord:

use "std/generative" { stream };
// A chord that drifts up a whole step each cycle
let voicing = stream(#[60, 64, 67], fn(prev, cycle) {
let result = #[];
for note in prev { result.push(note + 2); }
return result;
});
PRINT voicing.query(2);

A Markov chain moves between numbered states by probability. You give markov() the number of states, a transition matrix, and a starting state. Each cycle it looks at the current state’s row, rolls the (cycle-seeded) dice, and jumps to the next state.

Read the matrix row by row: row i says “when in state i, here are the relative chances of going to each state.” This 2-state chain just alternates — state 0 always goes to 1, state 1 always back to 0:

use "std/generative" { markov };
let alternating = markov(
2,
#[
#[0.0, 1.0], // from state 0 -> always state 1
#[1.0, 0.0] // from state 1 -> always state 0
],
0 // start in state 0
);
PRINT alternating.query(0); // 0
PRINT alternating.query(1); // 1
PRINT alternating.query(2); // 0

markov() outputs state indices (0, 1, 2…), not notes. To turn states into music, map them through a note array. The cleanest way is a small script pattern — a class with a query() method that asks the chain for a state, then looks up the note:

use "std/instruments" { SamplerMelodic, Kit };
use "std/generative" { markov };
class Mapped {
let chain;
let notes;
fn new(chain, notes) {
this.chain = chain;
this.notes = notes;
}
fn query(cycle) {
return this.notes[this.chain.query(cycle)];
}
}
let three_state = markov(
3,
#[
#[0.5, 0.5, 0.0],
#[0.0, 0.5, 0.5],
#[0.5, 0.0, 0.5]
],
0
);
let lead = AudioTrack("lead");
lead.load_instrument(SamplerMelodic(Kit("keys")));
lead >> master;
lead << Mapped(three_state, #[C4, E4, G4]);
PLAY;

The zero entries enforce constraints: state 0 can never jump straight to state 2, and state 2 can never reach state 1. That creates a directed flow with occasional self-loops.

Rows are normalized automatically, so you can use intuitive whole-number weights instead of fiddling with probabilities:

use "std/generative" { markov };
let weighted = markov(
3,
#[
#[1, 3, 1], // "3" means three times as likely as "1"
#[1, 1, 3],
#[3, 1, 1]
],
0
);
PRINT weighted.query(0);
PRINT weighted.query(4);

A state whose row points only to itself is absorbing: once reached, the chain never leaves. That’s a natural fit for a cadence — wander through tension, then resolve and stay home:

use "std/generative" { markov };
let resolving = markov(
3,
#[
#[0.0, 0.7, 0.3], // start: mostly move toward state 1
#[0.0, 0.3, 0.7], // middle: mostly move toward state 2
#[0.0, 0.0, 1.0] // end: stay forever (absorbing)
],
0
);
PRINT resolving.query(2);
PRINT resolving.query(10); // settled on state 2

Every generator here is built on the cycle number, so querying the same cycle always returns the same value — no matter when or how often you ask:

use "std/generative" { markov };
let chain = markov(3, #[#[0.5,0.5,0.0],#[0.0,0.5,0.5],#[0.5,0.0,0.5]], 0);
PRINT chain.query(20);
PRINT chain.query(20); // identical

This is what makes loops, restarts, and seeking behave: there’s no hidden live state to drift. The cost is that finding the value at cycle N replays all N steps from the start (it’s O(N)). Each step is cheap, and the scheduler stays responsive, so for normal musical ranges — hundreds to a few thousand cycles — you’ll never notice.

RandomnessStreamMarkov chain
What it isSeeded one-off picksA value walked forward each cycleProbabilistic moves between states
StateNoneOne value (prev)Current state index
You provideA seedfn(prev, cycle)A transition matrix
Reach for it whenMaking a single choiceEvolving one value over timeDirected, rule-based progressions

For anything more elaborate — multiple state variables, helper methods, custom logic — graduate to a full script pattern.

For full control over generation, write patterns as code.