Generative Music
Flow ships a broad palette of generative primitives — from classic Euclidean rhythms and weighted random choice to Markov chains, L-systems, cellular automata, chaos maps, Tidal-style pattern combinators, and chord-aware Markov improvisation. Every stochastic primitive is deterministic when seeded, and unseeded calls route through a per-render PRNG registry so two consecutive renders of the same source produce byte-identical output.
This page is a tour of the algorithmic surface. For deterministic transforms (transpose, invert, retrograde, etc.) see Pattern Transforms. For the standard-library module index see Standard Library.
At a Glance
| Surface | Import | Highlights |
|---|---|---|
Euclidean, vary, humanize, random choice | use "@std" + use "@composition" | Always-on basics — no extra import |
| Tidal-style combinators | use "@patterns" | 13 combinators on Sequence (every, fast, slow, jux, sometimes, …) |
| Markov / L-system / cellular / chaos | use "@generative" | Algorithmic generators returning Sequence or Array[Double] |
jam chord-aware improv + style packs | use "@improv" | Style-pack-driven Markov, composer-editable rule packs |
PRNG and Determinism
All stochastic primitives in @patterns, @generative, and @improv thread their randomness through Runtime/PrngRegistry, keyed by (source location, generator name). The registry is reseeded at every renderSong / writeWav boundary using a stable, platform-independent FNV-1a seed derivation. That gives composers two contracts:
- Two consecutive runs at the same git SHA produce byte-identical output. This is the project-wide “two-run cmp-clean” determinism contract.
- Explicit
seedarguments still work the way you’d expect. Passing a seed bypasses the registry and constructs a localRandom(seed)directly.
One important caveat: the chaos primitives (lorenz, logistic) are forward-Euler chaotic systems. Their chained floating-point arithmetic amplifies platform-specific FPU and Math.* quirks beyond ~50 iterations. Same-platform determinism is preserved; cross-platform reproducibility is not guaranteed for chaos outputs. Markov, L-system, and cellular automata all use integer arithmetic and stay cross-platform deterministic.
Euclidean Rhythms
Distribute k hits evenly across n steps using the Bjorklund algorithm:
use "@std"
Sequence e38 = (euclidean 3 8 C4) Note: X..X..X. — Cuban tresillo
Sequence e58 = (euclidean 5 8 E4) Note: X.XX.XX. — Cinquillo / West African bellOpen in playgroundSignatures
| Signature | Notes |
|---|---|
(euclidean Int hits, Int steps, Note pitch) -> Sequence | Plain pattern |
(euclidean Int, Int, Note, Double swing) -> Sequence | On-beat / off-beat velocity accent |
(euclidean Int, Int, Note, Double swing, Double humanize, Int seed) -> Sequence | Seeded uniform velocity jitter |
Common Euclidean patterns map to global rhythmic traditions:
hits / steps | Feel |
|---|---|
| 3/8 | Cuban tresillo |
| 5/8 | West African bell pattern |
| 3/4 | Simple triplet |
| 7/16 | Afro-Cuban bembé |
Random Choice in Note Streams
Pick a note at random from a set. This is syntax, not a function — it works only inside | ... | note streams. See Note Streams for the surrounding context.
Uniform Random — (? ...)
timesig 4/4 {
Sequence random = | (? C4 E4 G4) (? C4 E4 G4) (? C4 E4 G4) (? C4 E4 G4) |
}Open in playgroundWeighted Random — (? a:50 b:30 c:20)
Weights are relative ratios, not percentages: :50 :30 :20 is identical to :5 :3 :2.
Seeded Random — (?? ...)
(??) is a second, separately seeded RNG. Use (??set N) to seed it and (??reset) to reset to the initial state.
(??set 42)
timesig 4/4 {
Sequence seeded = | (?? C4 E4 G4) (?? D4 F4 A4) (?? E4 G4 B4) (?? C4 E4 G4) |
}Open in playgroundRests (_) are valid options anywhere a pitch is.
Humanize
| Function | Signature | Notes |
|---|---|---|
humanize | (Sequence, Double) -> Sequence | Uniform velocity jitter, non-deterministic shared RNG (frozen by design) |
humanizeGaussian | (Sequence, Double, Int seed) -> Sequence | Box-Muller normal-distribution jitter, seeded, recurses into voice blocks |
Sequence mel = | C4q D4q E4q F4q |
Sequence loose = (humanize mel 0.3) Note: uniform jitter
Sequence tight = (humanizeGaussian mel 0.15 42) Note: tighter normal curve, reproducibleOpen in playgroundhumanizeGaussian is the preferred surface for anything that needs to round-trip identically: it accepts a seed, clamps velocity to [0.05, 1.0], and walks into Phase 28 voice blocks (| {voice ...} {voice ...} |) so polyphonic passages stay coherent.
Sequence Mutation — vary
vary applies random mutations to a sequence. You can pick which dimension to mutate (pitch, rhythm, rest, velocity), pass a seed for reproducibility, and constrain pitch mutations to a key.
Sequence s = | C4 D4 E4 F4 G4 |
Sequence v1 = s -> vary(0.3) Note: random mutation type
Sequence v2 = (vary s 0.5 "pitch")
Sequence v3 = (vary s 0.5 "rhythm")
Sequence v4 = (vary s 0.5 "rest")
Sequence v5 = (vary s 0.5 "velocity")
Sequence v6 = (vary s 0.5 42) Note: seeded random type
Sequence v7 = (vary s 0.5 "pitch" 42) Note: seeded + type
Sequence v8 = (vary s 0.5 "pitch" "Cmajor") Note: diatonic
Sequence v9 = (vary s 0.5 "pitch" "Cmajor" 42) Note: diatonic + seedOpen in playgroundOverloads
| Signature | Description |
|---|---|
(Sequence, Double) -> Sequence | Random mutation type |
(Sequence, Double, String) -> Sequence | Specific type |
(Sequence, Double, Int) -> Sequence | Seeded random type |
(Sequence, Double, String, Int) -> Sequence | Seeded, specific type |
(Sequence, Double, String, String) -> Sequence | Diatonic (type, key) |
(Sequence, Double, String, String, Int) -> Sequence | Diatonic, seeded |
Mutation types: "pitch", "rhythm", "rest", "velocity".
Tidal-Style Pattern Combinators
use "@patterns"Open in playgroundThirteen combinators that operate on Sequence values. They borrow their semantics from TidalCycles, with one Flow-native twist: the cycle unit is bars, not beats. Transform-argument combinators are lambda-required — you pass (fn Sequence s => (fast s 2.0)), not a partially-applied function name.
Every combinator is charitable on degenerate input: zero / negative factors, NaN offsets, empty sequences, probabilities outside [0, 1] all return the input unchanged with a one-shot stderr advisory. They never throw.
Deterministic combinators
| Combinator | Signature | What it does |
|---|---|---|
every | (Int n, Function cb, Sequence seq) -> Sequence | Apply cb to bar i whenever i % n == 0 |
fast | (Sequence seq, Double factor) -> Sequence | Shorten each note by factor (2.0 = halve durations) |
slow | (Sequence seq, Double factor) -> Sequence | Lengthen each note by factor |
chunk | (Int n, Function cb, Sequence seq) -> Sequence | Apply cb to one 1/Nth chunk per call; rotates which chunk on successive invocations |
phase | (Double offset, Sequence seq) -> Sequence | Rotate bar order by round(offset × seq.Bars.Count) |
rev | (Sequence seq) -> Sequence | Reverse bar order (within-bar note order preserved — compare to retrograde) |
iter | (Int n, Sequence seq) -> Sequence | Rotate note list by totalNotes / n positions |
palindrome | (Sequence seq) -> Sequence | [A B C] → [A B C C B A] |
jux | (Function cb, Sequence seq) -> Sequence | Layer original with cb(seq) as a voice block (mono mix today; L/R stereo placement planned) |
superimpose | (Function cb, Sequence seq) -> Sequence | Mono voice-block overlay; functionally identical to jux in current builds |
Stochastic combinators (PRNG-routed)
| Combinator | Signature | What it does |
|---|---|---|
sometimes | (Double prob, Function cb, Sequence seq) -> Sequence | Apply cb to each bar with probability prob |
sometimes | (Function cb, Sequence seq) -> Sequence | Convenience overload at prob = 0.5 |
degrade | (Sequence seq) -> Sequence | Drop each bar with fixed 50% probability (Tidal compat) |
sparseSeq | (Double prob, Sequence seq) -> Sequence | Drop each bar with composer-controlled probability |
Composing combinators
use "@std"
use "@patterns"
tempo 120 {
timesig 4/4 {
Sequence base = | C4 D4 E4 F4 |
Sequence pat = base
-> (every 4 (fn Sequence s => (fast s 2.0)))
-> (sometimes 0.3 (fn Sequence s => (rev s)))
-> (jux (fn Sequence s => (transpose s 7st)))
}
}Open in playgroundSee examples/generative/tidal_combinators.flow for a runnable tour.
Markov Chains
use "@generative"Open in playgroundA Markov chain models note-to-note transition probabilities from a training corpus. Flow ships both a one-shot form and a train-once / generate-many split so you can reuse a trained model.
One-shot
Sequence corpus = | C4 D4 E4 F4 G4 A4 G4 F4 E4 D4 C4 |
Sequence gen = (markov corpus 2 16) Note: unseeded — PRNG-routed
Sequence gen2 = (markov corpus 2 16 42) Note: explicit seedOpen in playground| Signature | Notes |
|---|---|
(markov Sequence corpus, Int order, Int length) -> Sequence | Unseeded; PRNG via the registry |
(markov Sequence corpus, Int order, Int length, Int seed) -> Sequence | Explicit seed |
Train + generate split
MarkovModel model = (markovTrain corpus 2)
Sequence run1 = (markovGenerate model 16)
Sequence run2 = (markovGenerate model 16 42)Open in playground| Signature | Notes |
|---|---|
(markovTrain Sequence corpus, Int order) -> MarkovModel | Defaults to features=#pitch |
(markovTrain ..., Symbol features) -> MarkovModel | features=#pitch (default) or use named-arg form for tuple features |
(markovGenerate MarkovModel model, Int length) -> Sequence | Unseeded |
(markovGenerate MarkovModel model, Int length, Int seed) -> Sequence | Explicit seed |
(markovEqual MarkovModel a, MarkovModel b) -> Bool | Structural compare. (eq m1 m2) is reference identity — independently trained models compare unequal. |
Feature extraction
By default, each state in the chain is a raw MIDI pitch. Use the named-arg features= form to capture richer state:
MarkovModel pitchOnly = (markovTrain corpus 2)
MarkovModel withDur = (markovTrain corpus 2 features=<<#pitch, #duration>>)Open in playgroundThe tuple form encodes both pitch and quarter-note duration into a single state int. This gives higher fidelity at the cost of a sparser transitions table.
Charitable interpretation
- Order is clamped to
[1, 3]. Order 5 → order 3 + advisory. - Empty corpus or non-positive length → empty sequence + advisory.
- First
ordernotes are alphabet-seeded so the cold start is deterministic.
See examples/generative/markov_jazz.flow for a runnable jazz-corpus walkthrough.
L-Systems (Lindenmayer)
Pure deterministic Symbol rewriting. Useful for fractal-like melodic structures.
use "@generative"
Dict<Symbol, Tuple> rules = (dict
#A <<#A #B>>
#B <<#A>>)
Array[Symbol] expanded = (lsystem #A rules 5)
Note: bridge to musical Sequence
Sequence mel = (lsystemToSequence expanded
(fn Symbol s => (if (eq s #A) C4 E4)))Open in playground| Signature | Notes |
|---|---|
(lsystem Symbol axiom, Dict rules, Int iterations) -> Array[Symbol] | One-shot |
(lsystemModel Symbol axiom, Dict rules) -> LsystemModel | Train |
(lsystemGenerate LsystemModel, Int iterations) -> Array[Symbol] | Generate |
(lsystemToSequence Array[Symbol], Function mapper) -> Sequence | Map symbols to notes |
(lsystemEqual LsystemModel a, LsystemModel b) -> Bool | Structural compare |
Iteration count is clamped to [0, 20] as a DoS guard — at iteration 20 the alphabet has already grown past 10^6 symbols, well beyond any musical use. Terminal symbols (symbols that aren’t rule keys) pass through unchanged each iteration — standard Lindenmayer semantics.
Cellular Automata
use "@generative"
Note: 1D Wolfram-style — Rule 30, width 16, 8 steps
Sequence rule30 = (cellular 30 16 8 0)
Note: 1D with explicit seed pattern
Array[Bool] initial = (list false false false true true false false false)
Sequence custom = (cellularSeeded 30 8 8 0 initial)
Note: 2D Conway's Game of Life — 8 wide, 8 tall, 16 steps, seed 1
Array[Sequence] life = (life 8 8 16 1)Open in playground| Signature | Notes |
|---|---|
(cellular Int rule, Int width, Int steps, Int seed) -> Sequence | 1D elementary CA, Wolfram-canonical single-1-center initial; seed is accepted but ignored |
(cellularSeeded Int rule, Int width, Int steps, Int seed, Array[Bool] initial) -> Sequence | Explicit initial pattern |
(life Int width, Int height, Int steps, Int seed) -> Array[Sequence] | 2D Conway with wrap-around; seeded fill at 30% density |
Per-dimension cap of 1024 (DoS guard). Rule values outside [0, 255] wrap via (rule & 0xFF) with an advisory. The 1D grid maps to one bar per step: live cells become C4 notes, dead cells become rests. The 2D life grid returns one Sequence per row, with higher row indices mapped to lower pitches (so visually the “top” of the grid corresponds to the top of a piano roll).
Chaos Maps
use "@generative"
Array[Double] traj = (lorenz 10.0 28.0 2.667 256 42)
Array[Double] series = (logistic 3.9 256 42)
Note: bridge raw values into a Sequence
Sequence quantized1 = (quantizeToScale traj "Cmajor")
Sequence quantized2 = (quantizeToScale series (list C4 D4 E4 G4 A4))Open in playground| Signature | Notes |
|---|---|
(lorenz Double σ, Double ρ, Double β, Int length, Int seed) -> Array[Double] | Forward-Euler integration; returns the x-axis trajectory |
(logistic Double r, Int length, Int seed) -> Array[Double] | x_{n+1} = r × x_n × (1 - x_n), values in [0, 1] |
(quantizeToScale Array[Double], String scaleName) -> Sequence | Normalize to [0, 1], snap to the named scale, emit quarter notes |
(quantizeToScale Array[Double], Array[Note] scaleNotes) -> Sequence | Same, with an explicit note set |
Important determinism caveat: as noted at the top of this page, chaos outputs are same-platform deterministic only. Don’t pin cross-platform fixtures against chaos primitives. Bad params (Lorenz σ ≤ 0, logistic r outside [0, 4]) fall back to canonical butterfly / clamp with a one-shot advisory; lengths above 100,000 are clamped.
Improv — jam
use "@improv"Open in playgroundjam generates a chord-aware Markov melody over a sequence of chords. Chord tones land on strong beats, scale tones on weak beats, chromatic passing tones via per-style weighted roulette.
use "@std"
use "@improv"
key Cmajor {
Sequence chords = | Cmaj7 Am7 Dm7 G7 |
Note: only `over` is required
Sequence solo1 = (jam chords)
Note: name args anywhere
Sequence solo2 = (jam over=chords style=#blues length=16 seed=7)
}Open in playgroundSignature
jam(Sequence over,
Symbol style = #jazz,
Int length = 8,
String key = (active key),
Int seed = (PrngRegistry-routed),
Int order = 2) -> SequenceOpen in playgroundOnly over is required. The key= override pushes a synthetic musical-context frame for the jam, then pops — useful for chromatic pivot bars that break the surrounding key. The order argument is clamped to [1, 3] just like markov.
Style packs
Style packs are musical content, not engine internals — they live in composer-editable .flow files. Flow ships three baselines:
| Style | Pack | Character |
|---|---|---|
#jazz | flow-lang/improv/styles/jazz.flow | Bebop-leaning weighting, more scale + chromatic-passing motion |
#blues | flow-lang/improv/styles/blues.flow | Pentatonic-leaning, blues-scale chromatics |
#classical | flow-lang/improv/styles/classical.flow | Heavier chord-tone bias, less chromatic |
User packs live at ~/.config/flow/styles/*.flow and override shipped packs on Symbol-name collision (last-write-wins, with a one-shot stderr advisory when an override happens). Run (listStyles) from any Flow script to audit what’s registered in the current process.
Style registry surface
| Function | Signature | Notes |
|---|---|---|
registerStyle | (Symbol name, Dict pack) -> Void | Register or replace a style pack |
listStyles | () -> Array[Symbol] | All currently registered style names, insertion order |
A minimal pack looks like:
use "@improv"
(registerStyle #mystyle
(dict
#beat_weights (dict
#strong (dict #chord_tone 0.70 #scale_tone 0.20 #chromatic_passing 0.10)
#weak (dict #chord_tone 0.30 #scale_tone 0.50 #chromatic_passing 0.20))
#interval_transitions (dict
#step_up 0.30 #step_down 0.30
#leap_up 0.10 #leap_down 0.15
#chromatic 0.10 #repeat 0.05)
#rhythmic_template <<#eighth #eighth #eighth #eighth #eighth #eighth #eighth #eighth>>
#articulation_distribution (dict
#downbeat #legato
#offbeat #accent
#syncopated #marcato)))Open in playgroundThe full Dict-shape contract — every required field and its semantics — is documented at flow-lang/improv/styles/README.md.
Charitable behaviour
Unknown style → falls back to #jazz + advisory. Empty over or length <= 0 → empty Sequence + advisory. Style + key incompatibility (e.g. #blues over a chromatic key) is a soft advisory, not an error — Flow keeps producing music.
Polyrhythms
Overlay two sequences with different time signatures. polyrhythm figures out the cycle length (LCM of time signatures) and returns a mixed buffer.
use "@std"
use "@audio"
tempo 120 {
timesig 3/4 {
Sequence waltz = | A3 E4 E4 |
timesig 4/4 {
Sequence quarters = | C4 C4 C4 C4 |
Buffer poly = (polyrhythm waltz quarters)
(writeWav "polyrhythm.wav" poly)
}
}
}Open in playground| Signature | Notes |
|---|---|
(polyrhythm Sequence, Sequence) -> Buffer | Auto-align via LCM of time signatures |
(polyrhythm Sequence, Sequence, Int) -> Buffer | Explicit beat count override |
Microtonal / Tuning
Cent offsets and named tunings are part of the generative toolkit when you’re exploring non-12-TET soundworlds. Briefly:
- Cent offsets in note streams:
| C4 C4+50c C4-25c | - Named-tuning pragmas (file-scoped, last-wins):
enable justIntonation;,enable pythagorean;,enable equalTemperament; - Scala
.sclloader:Tuning t = (loadScala "examples/scala/22-shree.scl"); 2-arg form(loadScala "x.scl" "x.kbm")overrides the keyboard mapping. tuning <expr> { ... }musical-context block: three composer surfaces — identifier-bound (tuning partch { ... }), inline call (tuning (loadScala "x.scl") { ... }), string-literal sugar (tuning "x.scl" { ... }).
See examples/scala/intro.flow for a runnable tutorial chapter and Musical Context for context-block semantics.
Combining Techniques
Generative primitives compose cleanly:
use "@std"
use "@audio"
use "@patterns"
use "@generative"
use "@improv"
tempo 120 {
timesig 4/4 {
key Cmajor {
Note: a Euclidean hi-hat
Sequence hat = (euclidean 5 8 C5)
Note: chord progression for jam
Sequence chords = | Cmaj7 Am7 Dm7 G7 |
Sequence lead = (jam over=chords style=#jazz length=8 seed=7)
Note: roll dice on the lead each cycle
Sequence shaped = lead
-> (sometimes 0.3 (fn Sequence s => (rev s)))
-> (jux (fn Sequence s => (transpose s 7st)))
section groove {
Sequence a = hat
Sequence b = shaped
}
Song song = [groove*4]
Buffer buf = (renderSong song "piano")
(writeWav "generative.wav" buf)
}
}
}Open in playgroundSee Also
- Note Streams —
(? ...)/(?? ...)and full note-stream syntax - Pattern Transforms — deterministic transforms (
transpose,invert,retrograde, …) - Chords and Harmony — scales used by
quantizeToScaleand the diatonicvaryoverloads - Musical Context —
tuning,key,swing,tempoblocks - Standard Library — module index, including
@patterns,@generative,@improv - Visualization —
visualize,prettyBuffer,bufferHexfor sanity-checking generative output - Runnable examples:
examples/generative/markov_jazz.flow,examples/generative/tidal_combinators.flow,examples/sections/parameterized.flow,examples/scala/intro.flow