Flow

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

SurfaceImportHighlights
Euclidean, vary, humanize, random choiceuse "@std" + use "@composition"Always-on basics — no extra import
Tidal-style combinatorsuse "@patterns"13 combinators on Sequence (every, fast, slow, jux, sometimes, …)
Markov / L-system / cellular / chaosuse "@generative"Algorithmic generators returning Sequence or Array[Double]
jam chord-aware improv + style packsuse "@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 seed arguments still work the way you’d expect. Passing a seed bypasses the registry and constructs a local Random(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 bell
Open in playground

Signatures

SignatureNotes
(euclidean Int hits, Int steps, Note pitch) -> SequencePlain pattern
(euclidean Int, Int, Note, Double swing) -> SequenceOn-beat / off-beat velocity accent
(euclidean Int, Int, Note, Double swing, Double humanize, Int seed) -> SequenceSeeded uniform velocity jitter

Common Euclidean patterns map to global rhythmic traditions:

hits / stepsFeel
3/8Cuban tresillo
5/8West African bell pattern
3/4Simple triplet
7/16Afro-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 playground

Weighted 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 playground

Rests (_) are valid options anywhere a pitch is.

Humanize

FunctionSignatureNotes
humanize(Sequence, Double) -> SequenceUniform velocity jitter, non-deterministic shared RNG (frozen by design)
humanizeGaussian(Sequence, Double, Int seed) -> SequenceBox-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, reproducible
Open in playground

humanizeGaussian 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 + seed
Open in playground

Overloads

SignatureDescription
(Sequence, Double) -> SequenceRandom mutation type
(Sequence, Double, String) -> SequenceSpecific type
(Sequence, Double, Int) -> SequenceSeeded random type
(Sequence, Double, String, Int) -> SequenceSeeded, specific type
(Sequence, Double, String, String) -> SequenceDiatonic (type, key)
(Sequence, Double, String, String, Int) -> SequenceDiatonic, seeded

Mutation types: "pitch", "rhythm", "rest", "velocity".

Tidal-Style Pattern Combinators

use "@patterns"
Open in playground

Thirteen 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

CombinatorSignatureWhat it does
every(Int n, Function cb, Sequence seq) -> SequenceApply cb to bar i whenever i % n == 0
fast(Sequence seq, Double factor) -> SequenceShorten each note by factor (2.0 = halve durations)
slow(Sequence seq, Double factor) -> SequenceLengthen each note by factor
chunk(Int n, Function cb, Sequence seq) -> SequenceApply cb to one 1/Nth chunk per call; rotates which chunk on successive invocations
phase(Double offset, Sequence seq) -> SequenceRotate bar order by round(offset × seq.Bars.Count)
rev(Sequence seq) -> SequenceReverse bar order (within-bar note order preserved — compare to retrograde)
iter(Int n, Sequence seq) -> SequenceRotate note list by totalNotes / n positions
palindrome(Sequence seq) -> Sequence[A B C] → [A B C C B A]
jux(Function cb, Sequence seq) -> SequenceLayer original with cb(seq) as a voice block (mono mix today; L/R stereo placement planned)
superimpose(Function cb, Sequence seq) -> SequenceMono voice-block overlay; functionally identical to jux in current builds

Stochastic combinators (PRNG-routed)

CombinatorSignatureWhat it does
sometimes(Double prob, Function cb, Sequence seq) -> SequenceApply cb to each bar with probability prob
sometimes(Function cb, Sequence seq) -> SequenceConvenience overload at prob = 0.5
degrade(Sequence seq) -> SequenceDrop each bar with fixed 50% probability (Tidal compat)
sparseSeq(Double prob, Sequence seq) -> SequenceDrop 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 playground

See examples/generative/tidal_combinators.flow for a runnable tour.

Markov Chains

use "@generative"
Open in playground

A 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 seed
Open in playground
SignatureNotes
(markov Sequence corpus, Int order, Int length) -> SequenceUnseeded; PRNG via the registry
(markov Sequence corpus, Int order, Int length, Int seed) -> SequenceExplicit seed

Train + generate split

MarkovModel model = (markovTrain corpus 2)
Sequence run1 = (markovGenerate model 16)
Sequence run2 = (markovGenerate model 16 42)
Open in playground
SignatureNotes
(markovTrain Sequence corpus, Int order) -> MarkovModelDefaults to features=#pitch
(markovTrain ..., Symbol features) -> MarkovModelfeatures=#pitch (default) or use named-arg form for tuple features
(markovGenerate MarkovModel model, Int length) -> SequenceUnseeded
(markovGenerate MarkovModel model, Int length, Int seed) -> SequenceExplicit seed
(markovEqual MarkovModel a, MarkovModel b) -> BoolStructural 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 playground

The 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 order notes 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
SignatureNotes
(lsystem Symbol axiom, Dict rules, Int iterations) -> Array[Symbol]One-shot
(lsystemModel Symbol axiom, Dict rules) -> LsystemModelTrain
(lsystemGenerate LsystemModel, Int iterations) -> Array[Symbol]Generate
(lsystemToSequence Array[Symbol], Function mapper) -> SequenceMap symbols to notes
(lsystemEqual LsystemModel a, LsystemModel b) -> BoolStructural 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
SignatureNotes
(cellular Int rule, Int width, Int steps, Int seed) -> Sequence1D 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) -> SequenceExplicit 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
SignatureNotes
(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) -> SequenceNormalize to [0, 1], snap to the named scale, emit quarter notes
(quantizeToScale Array[Double], Array[Note] scaleNotes) -> SequenceSame, 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 playground

jam 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 playground

Signature

jam(Sequence over,
    Symbol style = #jazz,
    Int length = 8,
    String key = (active key),
    Int seed = (PrngRegistry-routed),
    Int order = 2) -> Sequence
Open in playground

Only 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:

StylePackCharacter
#jazzflow-lang/improv/styles/jazz.flowBebop-leaning weighting, more scale + chromatic-passing motion
#bluesflow-lang/improv/styles/blues.flowPentatonic-leaning, blues-scale chromatics
#classicalflow-lang/improv/styles/classical.flowHeavier 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

FunctionSignatureNotes
registerStyle(Symbol name, Dict pack) -> VoidRegister 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 playground

The 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
SignatureNotes
(polyrhythm Sequence, Sequence) -> BufferAuto-align via LCM of time signatures
(polyrhythm Sequence, Sequence, Int) -> BufferExplicit 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 .scl loader: 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 playground

See Also

  • Note Streams(? ...) / (?? ...) and full note-stream syntax
  • Pattern Transforms — deterministic transforms (transpose, invert, retrograde, …)
  • Chords and Harmony — scales used by quantizeToScale and the diatonic vary overloads
  • Musical Contexttuning, key, swing, tempo blocks
  • Standard Library — module index, including @patterns, @generative, @improv
  • Visualizationvisualize, prettyBuffer, bufferHex for sanity-checking generative output
  • Runnable examples: examples/generative/markov_jazz.flow, examples/generative/tidal_combinators.flow, examples/sections/parameterized.flow, examples/scala/intro.flow