Skip to content

Streams & Markov Chains

The markov() function creates patterns where each cycle’s state depends on the previous state through a transition probability matrix. Markov chains are seekable and deterministic — querying the same cycle always gives the same result.

use "std/generative" { markov };
markov(states, transitions, initial)
ParameterTypeDescription
statesIntegerNumber of states in the chain
transitionsArray of arraysTransition probability matrix — row i defines the weights for leaving state i
initialIntegerStarting state index (0-based)

markov() outputs state indices (0, 1, 2, …). Use .map() to convert these to actual musical values.

Deterministic alternation between two states:

let alternating = markov(
2,
#[
#[0.0, 1.0], // State 0 always goes to state 1
#[1.0, 0.0] // State 1 always goes to state 0
],
0 // Start at state 0
);
let alt_notes = #[C4, G4];
let melody = alternating.map(fn(state) {
return alt_notes[state];
});

Each cycle, the chain:

  1. Hashes the cycle number to get a deterministic random value in [0.0, 1.0)
  2. Looks up the current state’s row in the transition matrix
  3. Walks the cumulative probabilities until the hash value is exceeded
  4. Transitions to that state

Because hash(cycle) is a pure function, the same cycle always produces the same transition. The chain is fully deterministic.

For the 2-state alternating example above, the first few cycles trace like this:

CycleCurrent stateRowHash selectsNext state
00[0.0, 1.0]state 1 (100%)1
11[1.0, 0.0]state 0 (100%)0
20[0.0, 1.0]state 1 (100%)1
31[1.0, 0.0]state 0 (100%)0

With probabilistic rows, different hash values land in different cumulative buckets, producing varied but repeatable sequences.

let three_state = markov(
3,
#[
#[0.5, 0.5, 0.0], // State 0: equal chance of 0 or 1
#[0.0, 0.5, 0.5], // State 1: equal chance of 1 or 2
#[0.5, 0.0, 0.5] // State 2: equal chance of 2 or 0
],
0
);
let melody_notes = #[C4, E4, G4];
let simple_melody = three_state.map(fn(state) {
return melody_notes[state];
});

The zero entries prevent certain jumps — state 0 can never go directly to state 2, and state 2 can never reach state 1. This creates a directed flow: 0 → 1 → 2 → 0, with occasional self-loops.

Rows do not need to sum to 1.0. Resonon normalizes them automatically, so you can use intuitive weights:

let weighted = markov(
3,
#[
#[1, 3, 1], // "3" means 3x more likely than "1"
#[1, 1, 3],
#[3, 1, 1]
],
0
);

The shape of the transition matrix determines the character of the resulting pattern. Here are common design patterns:

PatternMatrix shapeMusical effectUse case
StepwiseHigh weights on adjacent states, low elsewhereSmooth, conjunct motionMelodies, bass lines
Hub-and-spokeOne state reachable from all others, others only reachable from the hubReturns to a tonal centerChord progressions with a strong tonic
CyclicWeights form a loop (0→1→2→0)Predictable rotation with variationRepeating phrase structures
AbsorbingOne state has [0, ..., 0, 1] (self-loop only)Settles permanently on a final valueCadences, endings, resolution
ErgodicAll entries non-zeroAny state reachable from any otherFree exploration, ambient textures

Give adjacent states higher weights to create smooth melodic lines. The Scale Degree Walk example below uses this approach — neighbor states get ~0.27, next-neighbors ~0.13, and far states ~0.07.

Make one state the “hub” that most other states transition back to:

// Hub-and-spoke: state 0 (tonic) is the center
let hub = markov(
4,
#[
#[0, 2, 2, 1], // Tonic -> anywhere
#[4, 0, 1, 0], // State 1 -> strongly back to tonic
#[3, 0, 0, 2], // State 2 -> mostly back to tonic
#[5, 1, 0, 0] // State 3 -> very strongly back to tonic
],
0
);

Set diagonal entries (self-transitions) to zero or low values to prevent a state from repeating:

#[
#[0, 1, 1], // State 0 never repeats
#[1, 0, 1], // State 1 never repeats
#[1, 1, 0] // State 2 never repeats
]

Model a chord progression as a Markov chain (I, IV, V, vi, ii):

let progression = markov(
5,
#[
#[0.0, 0.3, 0.4, 0.3, 0.0], // I -> IV, V, vi
#[0.2, 0.0, 0.5, 0.0, 0.3], // IV -> I, V, ii
#[0.7, 0.0, 0.0, 0.3, 0.0], // V -> I, vi
#[0.0, 0.5, 0.0, 0.0, 0.5], // vi -> IV, ii
#[0.0, 0.2, 0.8, 0.0, 0.0] // ii -> IV, V
],
0
);
let chords = #[
[C4, E4, G4], // I
[F4, A4, C5], // IV
[G4, B4, D5], // V
[A4, C5, E5], // vi
[D4, F4, A4] // ii
];
let chord_pattern = progression.map(fn(state) {
return chords[state];
});

The matrix encodes common-practice voice leading tendencies: V resolves strongly to I (0.7), ii is a predominant that leads to V (0.8), and vi moves to subdominant territory (IV or ii). Zeros enforce constraints — I never jumps directly to ii, and V never returns to IV.

All standard pattern methods work on Markov chain output:

// Transpose up a fifth
chord_pattern.transpose(7)
// Double speed
simple_melody.fast(2)
// Layer two Markov patterns
let bass = alternating.map(fn(s) { return #[C4, G4][s]; }).transpose(-12);
simple_melody.map(fn(s) { return #[C4, E4, G4][s]; }).stack(bass)

A state that transitions only to itself acts as an absorbing state, useful for resolution:

let resolving = markov(
3,
#[
#[0.0, 0.7, 0.3], // Start: mostly to state 1
#[0.0, 0.3, 0.7], // Middle: mostly to state 2
#[0.0, 0.0, 1.0] // End: stays forever (absorbing)
],
0
);
let resolution_chords = #[
[D4, F4, A4], // ii
[G4, B4, D5], // V
[C4, E4, G4] // I (absorbing)
];
let resolution = resolving.map(fn(state) {
return resolution_chords[state];
});

Use a transition matrix that favors stepwise motion for natural-sounding melodies:

// Neighbors (~0.27), next-neighbors (~0.13), far states (~0.07)
let scale_chain = markov(
7,
#[
#[0.06, 0.27, 0.13, 0.07, 0.07, 0.13, 0.27],
#[0.27, 0.06, 0.27, 0.13, 0.07, 0.07, 0.13],
#[0.13, 0.27, 0.06, 0.27, 0.13, 0.07, 0.07],
#[0.07, 0.13, 0.27, 0.06, 0.27, 0.13, 0.07],
#[0.07, 0.07, 0.13, 0.27, 0.06, 0.27, 0.13],
#[0.13, 0.07, 0.07, 0.13, 0.27, 0.06, 0.27],
#[0.27, 0.13, 0.07, 0.07, 0.13, 0.27, 0.06]
],
0
);
let scale_notes = #[C4, D4, E4, F4, G4, A4, B4];
let scale_melody = scale_chain.map(fn(state) {
return scale_notes[state];
});

This matrix has a Toeplitz structure — each row is a rotation of the same weight pattern. This means every scale degree has the same relative transition probabilities to its neighbors, producing uniform melodic behavior regardless of starting position.

Map states to entire patterns instead of single notes:

let phrase_chain = markov(
3,
#[
#[0.2, 0.4, 0.4],
#[0.5, 0.2, 0.3],
#[0.4, 0.4, 0.2]
],
0
);
let phrases = #[
[C4 E4 G4 E4], // Arpeggio
[G4 F4 E4 D4], // Descending
[C4 D4 E4 F4] // Ascending
];
let phrase_melody = phrase_chain.map(fn(state) {
return phrases[state];
});

Stack a Markov melody with an Euclidean rhythm:

let rhythm = euclid(3, 8, [C2]);
let combined = phrase_melody.stack(rhythm);

The .describe() method shows the pattern type:

alternating.describe()
// => "Markov[2 states]"

When chained with methods, .describe() shows the full transformation chain:

chord_pattern.fast(2).transpose(7).describe()
// => "Transpose(Fast(Markov[5 states], 2), 7)"

Markov chains are deterministic. The same cycle always produces the same state, regardless of when you query it:

viz(chord_pattern, 1, 20); // Query cycle 20
viz(chord_pattern, 1, 20); // Identical result

This means you can seek freely without affecting the output. Methods like .slow(), .fast(), and tools like viz() all work correctly because seeking to any cycle reproduces the exact same state sequence.

Seekability has a cost: to determine the state at cycle N, the chain must replay all N transitions from cycle 0 (O(N) recomputation). Each transition is cheap (a hash and a cumulative-probability walk), and the scheduler checks deadlines between cycles, so extremely long seeks are interrupted gracefully rather than blocking.

Markov chainScript PatternStreamchoose_weighted()
State memoryYes — next value depends on current stateYes — multiple named fieldsYes — carries state between cyclesNo — stateless selection
Control mechanismTransition matrixClass with query() methodCallback with mutable stateWeight array
DeterministicYesYes (O(N) replay)Yes (with seeded rand())Yes
SeekableYes (O(N) replay)Yes (O(N) replay)Yes (O(N) replay)Yes
Best use caseProbabilistic sequences with directed flowComplex multi-field stateful logicAccumulating or evolving stateWeighted one-off picks

A hub-and-spoke matrix where the tonic (I) is the center of gravity:

let chords_track = MidiTrack(1);
let prog = markov(
4,
#[
#[0, 3, 2, 1], // I -> IV (strong), V, vi
#[4, 0, 2, 0], // IV -> I (strong), V
#[5, 0, 0, 1], // V -> I (very strong), vi
#[1, 3, 2, 0] // vi -> I, IV (strong), V
],
0
);
let chord_voicings = #[
[C4, E4, G4], // I
[F4, A4, C5], // IV
[G4, B4, D5], // V
[A4, C5, E5] // vi
];
let chord_seq = prog.map(fn(state) {
return chord_voicings[state];
});
chords_track << chord_seq;
PLAY;

A stepwise bass walk layered with an Euclidean kick pattern:

let bass_track = MidiTrack(2);
let bass_chain = markov(
5,
#[
#[0, 3, 1, 0, 1], // Favor stepwise up
#[2, 0, 3, 1, 0],
#[1, 2, 0, 3, 1],
#[0, 1, 2, 0, 3],
#[3, 0, 0, 2, 0] // Top wraps back to bottom
],
0
);
let bass_notes = #[C2, D2, E2, G2, A2];
let bass_line = bass_chain.map(fn(state) {
return bass_notes[state];
});
let kick = euclid(3, 8, [C1]);
let combined = bass_line.stack(kick);
bass_track << combined;
PLAY;

A fast melody chain and a slow pad chain on separate tracks:

let melody_track = MidiTrack(1);
let pad_track = MidiTrack(2);
// Fast melody — 7-state scale walk at double speed
let mel_chain = markov(
7,
#[
#[0, 3, 1, 0, 0, 1, 3],
#[3, 0, 3, 1, 0, 0, 1],
#[1, 3, 0, 3, 1, 0, 0],
#[0, 1, 3, 0, 3, 1, 0],
#[0, 0, 1, 3, 0, 3, 1],
#[1, 0, 0, 1, 3, 0, 3],
#[3, 1, 0, 0, 1, 3, 0]
],
0
);
let mel_notes = #[C5, D5, E5, F5, G5, A5, B5];
let mel = mel_chain.map(fn(state) {
return mel_notes[state];
}).fast(2);
// Slow pad — 3-state chord progression at half speed
let pad_chain = markov(
3,
#[
#[0, 2, 1],
#[1, 0, 2],
#[2, 1, 0]
],
0
);
let pad_chords = #[
[C4, E4, G4],
[F4, A4, C5],
[G4, B4, D5]
];
let pad = pad_chain.map(fn(state) {
return pad_chords[state];
}).slow(2);
melody_track << mel;
pad_track << pad;
PLAY;

The stream() function creates patterns where each cycle’s output depends on the previous cycle’s state. This is ideal for “walking” patterns like random melodic walks, evolving chords, and accumulating transformations.

use "std/generative" { stream };
stream(initial, fn(prev, cycle) { ... })
ParameterTypeDescription
initialNote, number, array, or patternThe starting value — returned on cycle 0, then passed as prev to each subsequent call
callbackFunction fn(prev, cycle)Receives the previous cycle’s output and the current cycle number; returns the next state

Each cycle, the stream:

  1. Cycle 0: returns the initial value directly — the callback is not called
  2. Cycle N (N > 0): calls callback(prev, N) where prev is the result of cycle N-1
  3. Converts the returned value to pattern output for that cycle

Because choose() and rand() use the cycle number as a deterministic seed, the same cycle always produces the same result. The stream is fully seekable and deterministic.

For a chromatic walk starting at C4 with prev.transpose(1):

CycleprevCallback returnsOutput
0C4 (initial)
1C4C4.transpose(1)C#4
2C#4C#4.transpose(1)D4
3D4D4.transpose(1)D#4
4D#4D#4.transpose(1)E4

The simplest stream returns its previous value unchanged:

let constant = stream(C4, fn(prev, cycle) {
return prev;
});

Use .transpose() on the previous value to walk up or down:

// Ascending chromatic
let chromatic_up = stream(C4, fn(prev, cycle) {
return prev.transpose(1);
});
// Descending by whole steps
let descending = stream(C5, fn(prev, cycle) {
return prev.transpose(-2);
});

Use choose() with the cycle as a seed for deterministic random steps:

let steps = #[-2, -1, 1, 2];
let random_walk = stream(E4, fn(prev, cycle) {
let step = choose(cycle, steps);
return prev.transpose(step);
});

Bias the walk in a direction with choose_weighted():

let up_steps = #[-1, 1, 2, 3];
let up_weights = #[1, 2, 2, 1];
let upward_walk = stream(C4, fn(prev, cycle) {
let step = choose_weighted(cycle, up_steps, up_weights);
return prev.transpose(step);
});

Use the cycle number to introduce periodic behavior:

let varied_walk = stream(G4, fn(prev, cycle) {
if (cycle % 2 == 0) {
return prev.transpose(12); // Jump up an octave
}
return prev.transpose(-1); // Otherwise step down
});

Keep notes within a range by checking bounds:

let lower = C4;
let upper = C5;
let bound_steps = #[-3, -1, 1, 3];
let bounded = stream(C4, fn(prev, cycle) {
let step = choose(cycle, bound_steps);
let next = prev.transpose(step);
if (next > upper) {
return prev.transpose(-3);
}
if (next < lower) {
return prev.transpose(3);
}
return next;
});

Use rand() for probability-based decisions:

let prob_walk = stream(F4, fn(prev, cycle) {
if (rand(cycle) < 0.7) {
return prev.transpose(1); // 70% chance up
}
return prev.transpose(-1); // 30% chance down
});

Streams accept different types as the initial value. Each type carries state differently:

State typeInitial exampleHow state evolvesMusical use
NoteC4.transpose(), arithmeticMelodic walks, bass lines
Number0Arithmetic, moduloIndex into arrays, counters
Array#[C4, E4, G4].transpose(), element replacementEvolving chords, voicings
Pattern[C4 E4 G4 E4].transpose(), pattern methodsShifting sequences

Use a number as state and map it to musical values:

let scale_degrees = #[C4, D4, E4, F4, G4, A4, B4];
let index_walk = stream(0, fn(prev, cycle) {
let step = choose(cycle, #[-1, 0, 1]);
return clamp(prev + step, 0, 6);
});
let scale_melody = index_walk.map(fn(idx) {
return scale_degrees[idx];
});

Pass an array to create an evolving chord:

let evolving_chord = stream(#[C4, E4, G4], fn(prev, cycle) {
return prev.transpose(2); // Shifts up a whole step each cycle
});

Pass a pattern as the initial state. The pattern transforms itself each cycle:

let evolving_pattern = stream([C4 E4 G4 E4], fn(prev, cycle) {
return prev.transpose(1); // Shifts up chromatically each cycle
});

Walk through scale degrees using an index as state and an array lookup:

let major_scale = #[C4, D4, E4, F4, G4, A4, B4, C5, D5, E5, F5, G5];
let scale_run = stream(0, fn(prev, cycle) {
let direction = choose(cycle, #[-1, 1, 1]); // Biased upward
let next = prev + direction;
// Wrap around within the scale
if (next >= 12) { return 0; }
if (next < 0) { return 11; }
return next;
});
let melody = scale_run.map(fn(idx) {
return major_scale[idx];
});

The state is just an integer index — the musical mapping happens in .map(). This separates the walk logic from the note selection, making both easier to tweak independently.

Common stream patterns and when to use them:

PatternTechniqueMusical effectUse case
Linear walkFixed .transpose()Steady ascending/descending motionScale runs, chromatic lines
Random walkchoose(cycle, steps)Exploratory, unpredictable melodyAmbient, generative leads
Bounded walkRange check + bounceContained exploration within registerBass lines, backing parts
ConvergentProbability biased toward targetGradually approaches a noteTension-to-resolution phrases
Periodic resetcycle % N logicRepeating phrases with variationRiff-based patterns
AccumulatingArray state with transformationsEvolving chords/voicingsHarmonic progression

Bias the walk toward a target note for tension-resolution phrasing:

let target = G4;
let converging = stream(C4, fn(prev, cycle) {
if (prev < target) {
// Below target: 80% chance up, 20% chance down
if (rand(cycle) < 0.8) {
return prev.transpose(1);
}
return prev.transpose(-1);
}
if (prev > target) {
// Above target: 80% chance down, 20% chance up
if (rand(cycle) < 0.8) {
return prev.transpose(-1);
}
return prev.transpose(1);
}
return prev; // At target, stay
});

Reset state periodically to create repeating phrases with variation:

let reset_walk = stream(C4, fn(prev, cycle) {
if (cycle % 8 == 0) {
return C4; // Reset every 8 cycles
}
let step = choose(cycle, #[-2, -1, 1, 2]);
return prev.transpose(step);
});

Use .map() to transform the notes a stream produces:

let descending = stream(C5, fn(prev, cycle) {
return prev.transpose(-2);
});
// Notes below E4 jump up an octave
let jumped = descending.map(fn(note) {
if (note < E4) {
return note + 12;
}
return note;
});

All standard pattern methods work on streams:

// Transpose the stream output
descending.transpose(12)
// Double speed
descending.fast(2)
// Layer two streams
let bass_steps = #[-1, 0, 1];
let bass_walk = stream(C3, fn(prev, cycle) {
return prev.transpose(choose(cycle, bass_steps));
});
descending.stack(bass_walk)

Layer multiple streams on the same track for independent melodic lines:

let melody_walk = stream(E5, fn(prev, cycle) {
let step = choose(cycle, #[-2, -1, 1, 2]);
return prev.transpose(step);
});
let bass_walk = stream(C3, fn(prev, cycle) {
let step = choose(cycle * 7, #[-1, 0, 1]);
return prev.transpose(step);
});
let layered = melody_walk.stack(bass_walk);

Alternate between streams cycle by cycle:

let ascending = stream(C4, fn(prev, cycle) {
return prev.transpose(2);
});
let descending = stream(C5, fn(prev, cycle) {
return prev.transpose(-2);
});
let alternating = ascending.cat(descending);

Combine a stream with any other pattern type:

let rhythm = euclid(3, 8, [C2]);
let walk = stream(G4, fn(prev, cycle) {
return prev.transpose(choose(cycle, #[-1, 1]));
});
let combined = walk.stack(rhythm);

The .describe() method shows the pattern type:

random_walk.describe()
// => "Stream"

When chained with methods, .describe() shows the full transformation chain:

random_walk.fast(2).transpose(7).describe()
// => "Transpose(Fast(Stream, 2), 7)"

Streams are seekable and deterministic. Querying the same cycle always produces the same result:

let seek_steps = #[-1, 0, 1];
let seekable = stream(A4, fn(prev, cycle) {
return prev.transpose(choose(cycle, seek_steps));
});
viz(seekable, 1, 10); // Query cycle 10
viz(seekable, 1, 10); // Identical result

This means you can seek freely without affecting the output. Methods like .slow(), .fast(), and tools like viz() all work correctly because seeking to any cycle reproduces the exact same state sequence.

Seekability has a cost: to determine the state at cycle N, the stream must replay all N callbacks from cycle 0 (O(N) recomputation). Each callback is cheap, and the scheduler checks deadlines between cycles, so extremely long seeks are interrupted gracefully rather than blocking.

StreamScript PatternMarkov chainchoose_weighted()
State memoryYes — single valueYes — multiple named fieldsYes — next value depends on current stateNo — stateless selection
Control mechanismCallback with prevClass with query() methodTransition matrixWeight array
DeterministicYes (with seeded rand())Yes (O(N) replay)YesYes
SeekableYes (O(N) replay)Yes (O(N) replay)Yes (O(N) replay)Yes
Best use caseAccumulating or evolving a single valueComplex multi-field stateful logicProbabilistic sequences with directed flowWeighted one-off picks

A bounded random walk mapped to a pentatonic scale:

let melody_track = MidiTrack(1);
let pentatonic = #[C4, D4, E4, G4, A4, C5, D5, E5];
let walk = stream(3, fn(prev, cycle) {
let step = choose(cycle, #[-2, -1, 1, 2]);
let next = prev + step;
if (next >= 8) { return prev - 1; }
if (next < 0) { return prev + 1; }
return next;
});
let mel = walk.map(fn(idx) {
return pentatonic[idx];
}).fast(2);
melody_track << mel;
PLAY;

An array-state stream that shifts chord voicings, layered with an Euclidean rhythm:

let chord_track = MidiTrack(1);
let rhythm_track = MidiTrack(2);
let voicing = stream(#[C4, E4, G4, B4], fn(prev, cycle) {
if (cycle % 4 == 0) {
return #[C4, E4, G4, B4]; // Reset every 4 cycles
}
let shift = choose(cycle, #[1, 2, 3]);
return prev.transpose(shift);
});
let kick = euclid(5, 8, [C2]);
chord_track << voicing;
rhythm_track << kick;
PLAY;

A fast melody stream and a slow bass stream on separate tracks:

let melody_track = MidiTrack(1);
let bass_track = MidiTrack(2);
// Fast melody — random walk at double speed
let mel_steps = #[-2, -1, 1, 2, 3];
let mel = stream(E5, fn(prev, cycle) {
let step = choose(cycle, mel_steps);
let next = prev.transpose(step);
if (next > B5) { return prev.transpose(-3); }
if (next < C4) { return prev.transpose(3); }
return next;
}).fast(2);
// Slow bass — stepwise walk at half speed
let bass = stream(C3, fn(prev, cycle) {
let step = choose(cycle * 7, #[-1, 0, 1]);
return prev.transpose(step);
}).slow(2);
melody_track << mel;
bass_track << bass;
PLAY;