Skip to content

Generative — markov() & lsys()

markov() — Discrete Markov Chain

markov<T>(states: T[], matrix: number[][], seed?: number): MarkovChain<T>

Returns a MarkovChain whose .next(ctx) method deterministically steps the chain to ctx.cycle, producing the same state for the same cycle number regardless of where it is called. The randomness is seeded by seed (default 42).

Arguments

ArgumentTypeDescription
statesT[]List of values the chain can emit
matrixnumber[][]Transition probability matrix — matrix[i][j] is the probability of moving from state i to state j. Each row must sum to 1.
seednumber (optional)Random seed for reproducibility (default: 42)

Example — evolving chord progressions

const prog = markov(
['I,IV', 'IV,V', 'V,I'],
[
[0.5, 0.3, 0.2], // from I,IV
[0.3, 0.4, 0.3], // from IV,V
[0.6, 0.2, 0.2], // from V,I
],
)
function* song(ctx) {
const key = scales(8, `C3:major`)
const pad = strings({ attack: 0.8, gain: 0.4 })
yield chords(8, key, prog.next(ctx))
}

Each cycle prog.next(ctx) returns the chord string for that cycle, determined by the transition probabilities. Because the chain is replayed from cycle 0, the same cycle always yields the same result — safe to use with live swap().

Deterministic replay

markov() is stateless between cycles — calling .next(ctx) replays the chain from cycle 0 to ctx.cycle using the fixed seed. This means:

  • Create the chain once (outside or inside the generator — both work)
  • swap() does not break the sequence
  • The result for cycle N is always the same for a given seed and matrix

lsys() — L-System String Expansion

lsys(axiom: string, rules: Record<string, string>, generations: number): string

Expands an axiom string by applying rewrite rules for generations steps. Characters with no matching rule are kept as-is.

Arguments

ArgumentTypeDescription
axiomstringStarting string
rulesRecord<string, string>Rewrite rules — each key is replaced by its value
generationsnumberNumber of expansion steps

Example — rhythmic pattern from L-system

// x → x.x . → ..
const rhythm = lsys('x', { x: 'x.x', '.': '..' }, 3)
// gen 0: 'x'
// gen 1: 'x.x'
// gen 2: 'x.xx..x' (note: . not replaced first gen because it wasn't there)
// gen 3: 'x.xx..xx.xx..x.xx..x' — use as a drum seq string
function* song(ctx) {
const kk = kick({ freq: 55, gain: 0.7 })
yield cast(kk, 'drums', seq(4, rhythm), ctx)
}

lsys() is a pure function — call it once and reuse the result across cycles.

Melodic example

Use symbols that map to seq degree notation:

const melody = lsys('0', { '0': '0,2,4', '4': '4,5,4,2' }, 2)
// → '0,2,4,5,4,2,0,2,4'
function* song(ctx) {
const key = scales(4, 'C4:major')
const synth = vasynth({ wave: 'triangle', gain: 0.4 })
yield cast(synth, key, seq(4, melody), ctx)
}