subsequence.pattern_algorithmic

Mixin class providing algorithmic and generative pattern-building methods.

This module is not intended to be used directly. PatternAlgorithmicMixin is inherited by PatternBuilder in pattern_builder.py.

Module Contents

class subsequence.pattern_algorithmic.PatternAlgorithmicMixin[source]

Algorithmic and generative note-placement methods for PatternBuilder.

All methods here operate on self._pattern (a Pattern instance) and self.rng (a random.Random instance), both of which are set by PatternBuilder.__init__.

branch(pitches: List[int | str], depth: int = 2, path: int = 0, mutation: float = 0.0, velocity: int | Tuple[int, int] = subsequence.constants.velocity.DEFAULT_GENERATIVE_VELOCITY, duration: float = 0.2, spacing: float = 0.25, seed: int | None = None, rng: random.Random | None = None) subsequence.pattern_builder.PatternBuilder[source]

Generate a melodic variation by navigating a fractal tree of transforms.

The pitches sequence is the “trunk”. At each branch level, two musical transforms are assigned deterministically (derived from the original sequence), and the path index selects left or right at each level. After depth levels the result is a variation that is always structurally related to the input pitches.

Use path=p.cycle to step through all 2 ** depth variations in order; the index wraps automatically.

Transforms (assigned deterministically per level):

  • Retrograde — reverse the sequence.

  • Invert — mirror each pitch around the first note.

  • Transpose — shift all pitches by the interval between the first two notes.

  • Rotate — shift the starting position by one step.

  • Scale intervals — multiply intervals from the first note by 0.5 (compress) or 2.0 (expand), rounded to the nearest semitone.

An optional mutation layer randomly substitutes individual notes with other input pitches on top of the deterministic branching.

Parameters:
  • pitches – Original pitch sequence. All variations are derived from this.

  • depth – Branching levels. 2 ** depth unique variations are available before the path wraps.

  • path – Which variation to play (0-based). path=p.cycle advances automatically. Values wrap modulo 2 ** depth.

  • mutation – Probability that any step is replaced by a random input pitch after branching (0.0 = none, 1.0 = fully random).

  • velocity – MIDI velocity. An (low, high) tuple randomises per step.

  • duration – Note duration in beats.

  • spacing – Beat interval between steps.

Example

# Cycle through 8 variations (depth=3) of a 4-note motif
p.branch([60, 64, 67, 72], depth=3, path=p.cycle,
         velocity=85, spacing=0.5)
p.snap_to_scale("C", "minor")
bresenham(pitch: int | str, pulses: int, velocity: int | Tuple[int, int] = subsequence.constants.velocity.DEFAULT_VELOCITY, duration: float = 0.1, probability: float = 1.0, no_overlap: bool = False, seed: int | None = None, rng: random.Random | None = None) subsequence.pattern_builder.PatternBuilder[source]

Generate a rhythm using the Bresenham line algorithm.

This is an alternative to Euclidean rhythms that often results in slightly different (but still mathematically even) distributions.

Parameters:
  • pitch – MIDI note or drum name.

  • pulses – Total number of notes to place.

  • velocity – MIDI velocity, or a (low, high) tuple for a fresh random draw per hit.

  • duration – Note duration.

  • probability – Chance (0.0–1.0) that each pulse plays — 1.0 places them all, lower thins the rhythm.

  • seed – Fix the thinning for this call (an int); omit to use the pattern’s RNG. rng= is the advanced form.

  • no_overlap – If True, skip steps where a note of the same pitch already exists. Useful for layering ghost notes around hand-placed anchors.

bresenham_poly(parts: Dict[int | str, float], velocity: int | Dict[int | str, int] = subsequence.constants.velocity.DEFAULT_VELOCITY, duration: float = 0.1, grid: int | None = None, probability: float = 1.0, no_overlap: bool = False, seed: int | None = None, rng: random.Random | None = None) subsequence.pattern_builder.PatternBuilder[source]

Distribute multiple drum voices across the pattern using weighted Bresenham.

Each step is assigned to exactly one voice - voices never overlap, producing interlocking rhythmic patterns. Density weights control how frequently each voice fires. If the weights sum to less than 1.0, the remainder becomes evenly-distributed rests (silent steps).

Because notes are placed via self.note(), all post-placement transforms (groove, randomize, velocity_shape, rotate, etc.) work normally.

Parameters:
  • parts – Mapping of pitch (MIDI note or drum name) to density weight. Higher weight means more hits per bar. Weights in the range (0, 1] are typical; a weight of 0.5 targets roughly one hit every two steps.

  • velocity – Either a single MIDI velocity applied to all voices, or a dict mapping each pitch to its own velocity. Pitches absent from the dict fall back to the default velocity (100).

  • duration – Note duration in beats (default 0.1).

  • grid – Number of steps to divide the pattern into. Defaults to the pattern’s default_grid.

  • probability – Chance (0.0–1.0) that each hit plays — 1.0 places them all, lower thins.

  • seed – Fix the thinning for this call (an int); omit to use the pattern’s RNG.

  • no_overlap – If True, skip steps where a note of the same pitch already exists. Useful for layering ghost notes around hand-placed anchors.

  • rng – Advanced determinism form — a random.Random (wins over seed=).

Example

p.bresenham_poly(
        parts={"kick_1": 0.25, "snare_1": 0.125, "hi_hat_closed": 0.5},
        velocity={"kick_1": 100, "snare_1": 90, "hi_hat_closed": 70},
)
Layering with hand-placed hits:
# Algorithmic base - interlocking texture, no overlaps within this layer
p.bresenham_poly(
        parts={"hi_hat_closed": 0.5, "snare_2": 0.1},
        velocity={"hi_hat_closed": 65, "snare_2": 40},
)
# Hand-placed anchors on top - these CAN overlap the algorithmic layer
p.hit_steps("kick_1", [0, 8], velocity=110)
p.hit_steps("snare_1", [4, 12], velocity=100)
Stable vs shifting patterns:

Because the algorithm redistributes all positions when weights change, a single voice with a continuously ramping density will shift positions every bar. This is great for background texture (hats, shakers) but can sound jarring for prominent, distinctive sounds (claps, cowbells).

For stable patterns - use bresenham() with integer pulses. Positions stay fixed until the pulse count steps up:

pulses = max(1, round(density * 16))
p.bresenham("hand_clap", pulses=pulses, velocity=95)

For shifting texture - use bresenham_poly() with continuous density. Positions evolve every bar:

p.bresenham_poly(parts={"hi_hat_closed": density}, velocity=70)

To stabilise a solo voice - pair it with a second voice. More voices in a single call means less positional shift per voice:

p.bresenham_poly(
        parts={"hand_clap": 0.12, "snare_2": 0.08},
        velocity={"hand_clap": 95, "snare_2": 40},
)
static build_ghost_bias(grid: int, bias: str) List[float][source]

Build probability weights for ghost notes or other generative functions.

Generates a list of probability weights (values between 0.0 and 1.0) spanning a given grid size. These curves shape probability over a beat, assigning higher or lower chances of an event occurring based on the rhythmic position within the beat (downbeat, offbeat, syncopated 16th note, etc).

This is a public escape hatch: call it yourself, manipulate the returned list, then pass the result as bias= to ghost_fill(). This lets you pin specific steps, boost a weak position, or combine two named curves.

Parameters:
  • grid – The total number of steps in the sequence (usually 16 or 32).

  • bias

    The probability distribution shape to generate:

    • "uniform" - 1.0 everywhere.

    • "offbeat" - 1.0 on 8th note off-beats (&), 0.3 on 16ths (e/a), 0.05 on downbeats.

    • "sixteenths" - 1.0 on 16th notes (e/a), 0.3 on 8th off-beats (&), 0.05 on downbeats.

    • "before" - 1.0 preceding a beat, 0.25 on other 16ths, 0.05 on beats.

    • "after" - 1.0 following a beat, 0.25 on other 16ths, 0.05 on beats.

    • "downbeat" - 1.0 on downbeats, 0.15 on 8th off-beats, 0.05 on other 16ths.

    • "upbeat" - 1.0 on 8th note off-beats only, 0.05 everywhere else.

    • "e_and_a" - 1.0 on all non-downbeat 16th positions, 0.05 on downbeats.

Returns:

A List[float] of length grid where each value is a probability multiplier from 0.0 to 1.0. The list is a plain Python list — modify it freely before passing to ghost_fill(bias=...).

Example

# Start from a named curve, then zero out beat 3 (step 8) entirely
# and give the step before the snare (step 11) maximum weight.
weights = p.build_ghost_bias(16, "sixteenths")
weights[8] = 0.0   # silence around beat 3
weights[11] = 1.0  # boost the "and" before beat 4
p.ghost_fill("snare_1", density=0.25, velocity=(25, 45),
             bias=weights, no_overlap=True)
cellular_1d(pitch: int | str, rule: int = 30, generation: int | None = None, velocity: int | Tuple[int, int] = subsequence.constants.velocity.DEFAULT_CA_VELOCITY, duration: float = 0.1, no_overlap: bool = False, probability: float = 1.0, seed: int | None = None, rng: random.Random | None = None) subsequence.pattern_builder.PatternBuilder[source]

Generate an evolving rhythm using a 1D cellular automaton.

Uses an elementary CA (1D binary cellular automaton) to produce rhythmic patterns that change organically each bar. The CA state evolves by one generation per cycle, creating patterns that are deterministic yet surprising - structured chaos.

Rule 30 is the default: it produces quasi-random patterns with hidden self-similarity. Rule 90 produces fractal patterns. Rule 110 is Turing-complete.

Parameters:
  • pitch – MIDI note number or drum name.

  • rule – Wolfram rule number (0–255). Default 30.

  • generation – CA generation to render. Defaults to self.cycle so the pattern evolves each bar automatically.

  • velocity – MIDI velocity, or a (low, high) tuple for a fresh random draw per hit.

  • duration – Note duration in beats.

  • no_overlap – If True, skip where same pitch already exists.

  • probability – Chance (0.0–1.0) that each hit plays — 1.0 places them all, lower thins.

  • seed – Fix the thinning for this call (an int); omit to use the pattern’s RNG.

  • rng – Advanced determinism form — a random.Random (wins over seed=).

Example

p.hit_steps("kick_1", [0, 8], velocity=100)
p.cellular_1d("kick_1", rule=30, velocity=40, no_overlap=True)
cellular_2d(pitches: List[int | str], rule: str = 'B368/S245', generation: int | None = None, velocity: int | Tuple[int, int] | List[int] = subsequence.constants.velocity.DEFAULT_CA_VELOCITY, duration: float = 0.1, no_overlap: bool = False, probability: float = 1.0, initial_state: str | List[List[int]] = 'center', density: float = 0.5, seed: int | None = None, rng: random.Random | None = None) subsequence.pattern_builder.PatternBuilder[source]

Generate polyphonic patterns using a 2D Life-like cellular automaton.

Evolves a 2D grid where rows map to pitches or instruments and columns map to time steps. Live cells in the final generation become note onsets, producing patterns with spatial structure that evolves each bar.

The default rule B368/S245 (Morley/”Move”) produces chaotic, active patterns. B3/S23 is Conway’s Life; B36/S23 is HighLife.

Parameters:
  • pitches – MIDI note numbers or drum names, one per row. Row 0 maps to the first pitch.

  • rule – Birth/Survival notation, e.g. "B3/S23" for Conway’s Life, "B368/S245" for Morley.

  • generation – CA generation to render. Defaults to self.cycle so the grid evolves each bar automatically.

  • velocity – Single MIDI velocity for all rows, or a list with one value per row.

  • duration – Note duration in beats.

  • no_overlap – If True, skip notes where same pitch already exists.

  • probability – Chance (0.0–1.0) that each live cell plays — 1.0 places them all, lower thins.

  • initial_state – The generation-0 grid. "center" (default) lights a single cell at the centre; "random" fills cells with probability density (seed it with seed for a reproducible fill); or pass an explicit list[list[int]] (rows × cols) for a custom start.

  • density – Fill probability for initial_state="random" (0.0–1.0).

  • seed – RNG seed (an int) for the "random" initial grid, so it reproduces. Ignored for "center" or an explicit grid (a warning is emitted if passed there).

  • rng – Random generator for the probability thinning. Defaults to self.rng.

Example

pitches = [36, 38, 42, 46]  # kick, snare, hihat, open hihat
p.cellular_2d(pitches, rule="B3/S23", initial_state="random", seed=7, density=0.3)
de_bruijn(pitches: List[int | str], window: int = 2, spacing: float | None = None, velocity: int | Tuple[int, int] = subsequence.constants.velocity.DEFAULT_GENERATIVE_VELOCITY, duration: float = 0.2, seed: int | None = None, rng: random.Random | None = None) subsequence.pattern_builder.PatternBuilder[source]

Generate a melody that exhaustively traverses all pitch subsequences.

A de Bruijn sequence B(k, n) over an alphabet of size k with window n contains every possible subsequence of length n exactly once (cyclically). Mapping each symbol to a pitch produces a melody that systematically explores all possible n-gram transitions - every permutation of window consecutive pitches appears exactly once.

With spacing=None (default) the full sequence is auto-fitted into the bar, matching the behaviour of lsystem(). With a fixed spacing the sequence is truncated to fill the available beats.

Parameters:
  • pitches – List of MIDI note numbers or note strings. The alphabet size k is len(pitches).

  • window – Subsequence length n. The output has len(pitches) ** window notes. Keep small (2–4) for practical bar lengths.

  • spacing – Time between notes in beats. None auto-fits the sequence into the bar; a float uses fixed spacing and truncates.

  • velocity – MIDI velocity. An (low, high) tuple randomises per note.

  • duration – Note duration in beats.

Example

# All 2-note combinations of a pentatonic scale
p.de_bruijn([60, 62, 64, 67, 69], window=2, velocity=(60, 100))
euclidean(pitch: int | str, pulses: int, velocity: int | Tuple[int, int] = subsequence.constants.velocity.DEFAULT_VELOCITY, duration: float = 0.1, probability: float = 1.0, no_overlap: bool = False, seed: int | None = None, rng: random.Random | None = None) subsequence.pattern_builder.PatternBuilder[source]

Generate a Euclidean rhythm.

This distributes a fixed number of ‘pulses’ as evenly as possible across the pattern. This produces many of the world’s most common musical rhythms.

Parameters:
  • pitch – MIDI note or drum name.

  • pulses – Total number of notes to place.

  • velocity – MIDI velocity, or a (low, high) tuple for a fresh random draw per hit.

  • duration – Note duration.

  • probability – Chance (0.0–1.0) that each pulse plays — 1.0 places them all, lower thins the rhythm.

  • seed – Fix the thinning for this call (an int); omit to use the pattern’s RNG. rng= is the advanced form.

  • no_overlap – If True, skip steps where a note of the same pitch already exists. Useful for layering ghost notes around hand-placed anchors.

Example

# A classic 3-against-16 rhythm
p.euclidean("kick", pulses=3)
evolve(pitches: List[int | str], length: int | None = None, drift: float = 0.0, velocity: int | Tuple[int, int] = subsequence.constants.velocity.DEFAULT_GENERATIVE_VELOCITY, duration: float = 0.2, spacing: float = 0.25, seed: int | None = None, rng: random.Random | None = None) subsequence.pattern_builder.PatternBuilder[source]

Loop a pitch sequence that gradually mutates each cycle.

On cycle 0, the sequence is locked to the initial pitches (truncated to length if provided). Each subsequent cycle, every step has a drift probability of being replaced by a randomly-chosen value from the pool. When drift=0.0 the loop never changes; when drift=1.0 every step is redrawn every cycle.

State is stored in p.data under a key derived from the pitch content, so the buffer persists across pattern rebuilds. The buffer is reset whenever cycle == 0 so restarts produce deterministic output.

Combine with p.snap_to_scale() to keep drifted pitches in key:

p.evolve([60, 64, 67, 72], length=8, drift=0.12)
p.snap_to_scale("C", "minor")
Parameters:
  • pitches – Initial pitch pool. The initial buffer is built from the first length values (cycling if shorter than length). Mutation also draws replacements from this pool.

  • length – Number of steps in the loop. Defaults to len(pitches).

  • drift – Per-step mutation probability each cycle (0.0–1.0). 0.0 = locked loop, 1.0 = fully random each cycle.

  • velocity – MIDI velocity. An (low, high) tuple randomises per step.

  • duration – Note duration in beats.

  • spacing – Beat interval between steps.

Example

# 8-step loop that slowly diverges from its seed
p.evolve([60, 62, 64, 65, 67, 69], length=8, drift=0.1,
         velocity=(70, 100), spacing=0.5)
p.snap_to_scale("C", "dorian")
fibonacci(pitch: int | str, count: int, velocity: int | Tuple[int, int] = subsequence.constants.velocity.DEFAULT_GENERATIVE_VELOCITY, duration: float = 0.2, seed: int | None = None, rng: random.Random | None = None) subsequence.pattern_builder.PatternBuilder[source]

Place notes at golden-ratio-spaced beat positions (Fibonacci spiral timing).

Uses the golden angle method - position_i = (i × φ) mod bar_length - to distribute count events across the bar. The result is sorted into ascending time order. Unlike a Euclidean rhythm (maximally even spacing on a fixed grid), Fibonacci timing is irrational and places events off-grid in a way that sounds organic and avoids metronomic repetition.

Parameters:
  • pitch – MIDI note number or drum name.

  • count – Number of notes to place.

  • velocity – MIDI velocity. An (low, high) tuple randomises per note.

  • duration – Note duration in beats.

Example

# 11 hi-hat hits with golden-ratio spacing
p.fibonacci("hi_hat_closed", count=11, velocity=(60, 90))
ghost_fill(pitch: int | str, density: float = 0.3, velocity: int | Tuple[int, int] | Sequence[int | float] | Callable[[int], int | float] = subsequence.constants.velocity.GHOST_FILL_VELOCITY, bias: str | List[float] = 'uniform', no_overlap: bool = True, grid: int | None = None, duration: float = 0.1, seed: int | None = None, rng: random.Random | None = None) subsequence.pattern_builder.PatternBuilder[source]

Fill the pattern with probability-biased ghost notes.

A single method for generating musically-aware ghost note layers. Combines density control, velocity randomisation, and rhythmic bias to produce the micro-detail layering heard in dense electronic music production.

Parameters:
  • pitch – MIDI note number or drum name.

  • density – Overall density (0.0–1.0). How many available steps receive ghost notes. 0.3 = roughly 30% of steps at peak bias.

  • velocity – Single velocity, (low, high) tuple for random range, a list/sequence of values (indexed by step), or a callable that takes the step index i and returns a velocity. Allows dynamic values like Perlin noise curves.

  • bias

    Probability distribution shape:

    • "uniform" - equal probability everywhere

    • "offbeat" - prefer 8th-note off-beats (&)

    • "sixteenths" - prefer 16th-note subdivisions (e/a)

    • "before" - cluster just before beat positions

    • "after" - cluster just after beat positions

    • "downbeat" - reinforce the beat (inverse of offbeat)

    • "upbeat" - strictly 8th-note off-beats only

    • "e_and_a" - all non-downbeat 16th positions

    • Or: a list of floats (one per grid step) for a custom field. Use build_ghost_bias() to generate a named curve and then modify specific steps before passing it here.

  • no_overlap – If True (default), skip where same pitch already exists. Essential for layering ghosts around hand-placed anchors.

  • grid – Grid resolution. Defaults to the pattern’s default grid.

  • duration – Note duration in beats (default 0.1).

  • rng

    Random generator. Defaults to self.rng.

    Tip — freeze placement each cycle: Pass rng=random.Random(seed) to create a fresh RNG instance on every rebuild. Because the seed resets, the same steps are chosen on every cycle — ghost positions are locked in place while velocity variation (from a (low, high) tuple) can still vary each cycle, because it consumes separate RNG draws. Using the default self.rng advances state across rebuilds so placement differs every cycle.

Example

import random

p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
p.hit_steps("snare_1", [4, 12], velocity=95)

# Different ghost placement each cycle (default)
p.ghost_fill("kick_1", density=0.2, velocity=(30, 45),
             bias="sixteenths", no_overlap=True)

# Same ghost steps every cycle — placement frozen, velocity still varies
p.ghost_fill("snare_1", density=0.15, velocity=(25, 40),
             bias="before", rng=random.Random(42))
lorenz(pitches: List[int | str], spacing: float = 0.25, velocity: int | Tuple[int, int] = subsequence.constants.velocity.DEFAULT_GENERATIVE_VELOCITY, duration: float = 0.2, dt: float = 0.01, sigma: float = 10.0, rho: float = 28.0, beta: float = 8.0 / 3.0, x0: float = 0.1, y0: float = 0.0, z0: float = 0.0, mapping: Callable[[float, float, float], Tuple[int | str, int, float] | None] | None = None) subsequence.pattern_builder.PatternBuilder[source]

Generate a note sequence driven by the Lorenz strange attractor.

Integrates the Lorenz system to produce a trajectory of (x, y, z) points, each normalised to [0, 1]. The three axes provide correlated but independent modulation sources: by default x drives pitch selection, y drives velocity, and z drives duration.

The Lorenz attractor is deterministic but extremely sensitive to initial conditions: changing x0 by even 0.001 produces a divergent trajectory over time. This makes it ideal for cycle-by-cycle variation - pass x0=p.cycle * 0.001 to generate a unique but slowly evolving phrase each bar.

A custom mapping callable can override the default x/y/z → pitch/vel/dur assignment, or return None for a rest.

Parameters:
  • pitches – Pitch pool. The x-axis selects an index: int(x * len(pitches)) % len(pitches).

  • spacing – Time between notes in beats. Default 0.25 (16th note).

  • velocity – Fixed velocity or (low, high) tuple. Overridden by mapping.

  • duration – Maximum note duration. z is scaled to [0.05, duration]. Overridden by mapping.

  • dt – Integration time step. Default 0.01.

  • sigma – Lorenz parameters. Defaults produce the classic butterfly attractor (chaotic regime).

  • rho – Lorenz parameters. Defaults produce the classic butterfly attractor (chaotic regime).

  • beta – Lorenz parameters. Defaults produce the classic butterfly attractor (chaotic regime).

  • x0 – Initial conditions. Use x0=p.cycle * small_delta for slowly evolving variation.

  • y0 – Initial conditions. Use x0=p.cycle * small_delta for slowly evolving variation.

  • z0 – Initial conditions. Use x0=p.cycle * small_delta for slowly evolving variation.

  • mapping – Optional callable (x, y, z) -> (pitch, velocity, duration) or None for rest.

Example

scale = [60, 62, 64, 65, 67, 69, 71, 72]
p.lorenz(scale, spacing=0.25, velocity=(50, 110), x0=p.cycle * 0.002)
lsystem(pitch_map: Dict[str, int | str], axiom: str, rules: Dict[str, str | List[Tuple[str, float]]], generations: int = 3, spacing: float | None = None, velocity: int | Tuple[int, int] = subsequence.constants.velocity.DEFAULT_GENERATIVE_VELOCITY, duration: float = 0.2, seed: int | None = None, rng: random.Random | None = None) subsequence.pattern_builder.PatternBuilder[source]

Generate a note sequence using L-system string rewriting.

Expands axiom by applying rules for generations iterations, then walks the resulting string placing a note for each character found in pitch_map. Unmapped characters are silent rests - they advance time but produce no note.

The defining musical property is self-similarity: patterns repeat at different time scales. The Fibonacci rule (A AB, B A) places hits at golden-ratio spacings. Koch and dragon curve rules produce fractal melodic contours.

With spacing=None (default) the entire expanded string is fitted into the bar: each generation makes notes twice as dense while preserving the overall shape. With a fixed spacing the string is truncated to fit and the density stays constant.

Parameters:
  • pitch_map – Maps single characters to MIDI notes or drum names. Characters absent from the map produce rests.

  • axiom – Starting string (e.g. "A").

  • rules – Production rules. Deterministic: {"A": "AB"}. Stochastic: {"A": [("AB", 3), ("BA", 1)]}.

  • generations – Rewriting iterations. String length grows exponentially - keep this to 3–8 for practical use.

  • spacing – Time between symbols in beats. None (default) auto-fits the full expanded string into the bar. A float uses fixed spacing and truncates excess symbols.

  • velocity – MIDI velocity. An (low, high) tuple randomises per note.

  • duration – Note duration in beats.

Example

# Fibonacci kick rhythm - self-similar hit spacing
p.lsystem(
    pitch_map={"A": "kick_1"},
    axiom="A",
    rules={"A": "AB", "B": "A"},
    generations=6,
    velocity=80,
)

# Fractal melody over scale notes
p.lsystem(
    pitch_map={"F": 60, "G": 62, "+": 64, "-": 67},
    axiom="F",
    rules={"F": "F+G", "G": "-F"},
    generations=4,
    spacing=0.25,
    velocity=(70, 100),
)
markov(transitions: Dict[str, List[Tuple[str, int]]], pitch_map: Dict[str, int], velocity: int | Tuple[int, int] = subsequence.constants.velocity.DEFAULT_VELOCITY, duration: float = 0.1, spacing: float = 0.25, start: str | None = None, seed: int | None = None, rng: random.Random | None = None) subsequence.pattern_builder.PatternBuilder[source]

Generate a sequence by walking a first-order Markov chain.

Builds a WeightedGraph from transitions and walks it, placing one note per spacing beats. The probability of each next state depends only on the current one - use this to generate basslines, melodies, or rhythm motifs that have stylistic coherence without being perfectly repetitive.

The transition dict uses the same (target, weight) pair format as Composition.form(), so the idiom is already familiar.

Parameters:
  • transitions – Mapping of state name to a list of (next_state, weight) tuples. Higher weight means higher probability of that transition.

  • pitch_map – Mapping of state name to absolute MIDI note number. States absent from this dict are walked but produce no note.

  • velocity – MIDI velocity for all placed notes (default 100), or a (low, high) tuple for a fresh random draw per step.

  • duration – Note duration in beats (default 0.1).

  • spacing – Time between note onsets in beats (default 0.25 = 16th note).

  • start – Name of the starting state. Defaults to the first key in transitions when not provided.

Raises:

ValueError – If transitions or pitch_map is empty.

Example

# Walking bassline: root anchors, 3rd and 5th passing tones
p.markov(
    transitions={
        "root": [("3rd", 3), ("5th", 2), ("root", 1)],
        "3rd":  [("5th", 3), ("root", 2)],
        "5th":  [("root", 3), ("3rd", 1)],
    },
    pitch_map={"root": 52, "3rd": 56, "5th": 59},
    velocity=80,
    spacing=0.5,
)
melody(state: subsequence.melodic_state.MelodicState, spacing: float = 0.25, velocity: int | Tuple[int, int] = subsequence.constants.velocity.DEFAULT_CHORD_VELOCITY, duration: float = 0.2, chord_tones: List[int] | None = None, seed: int | None = None, rng: random.Random | None = None) subsequence.pattern_builder.PatternBuilder[source]

Generate a melodic line by querying a persistent MelodicState.

Places one note (or rest) per spacing beats for the full pattern length. Pitch selection is guided by the NIR cognitive model inside state: after a large leap the model expects a direction reversal; after a small step it expects continuation. Chord tones, range gravity, and a pitch-diversity penalty further shape the output.

Because state lives outside the pattern builder and persists across bar rebuilds, melodic continuity is maintained automatically - no manual history management is required.

Parameters:
  • state – Persistent MelodicState instance created once at module level.

  • spacing – Time between note onsets in beats (default 0.25 = 16th note).

  • velocity – MIDI velocity. An int applies a fixed level; a (low, high) tuple draws uniformly from that range each spacing.

  • duration – Note duration in beats (default 0.2 - slightly shorter than a 16th note, giving a crisp attack).

  • chord_tones – Optional list of MIDI note numbers that are chord tones this bar (e.g. from chord.tones(root)). Chord-tone pitch classes receive a chord_weight bonus inside state.

Example

melody_state = subsequence.MelodicState(
    key="A", mode="aeolian",
    low=60, high=84,
    nir_strength=0.6,
    chord_weight=0.4,
)

@composition.pattern(channel=4, beats=4)
def lead (p, chord):
    tones = chord.tones(72) if chord else None
    p.melody(melody_state, spacing=0.5, velocity=(70, 100), chord_tones=tones)
ratchet(subdivisions: int = 2, pitch: int | str | None = None, probability: float = 1.0, velocity_start: float = 1.0, velocity_end: float = 1.0, shape: str | Callable[[float], float] = 'linear', gate: float = 0.5, steps: List[int] | None = None, grid: int | None = None, seed: int | None = None, rng: random.Random | None = None) subsequence.pattern_builder.PatternBuilder[source]

Subdivide existing notes into rapid repeated hits (rolls/ratchets).

A post-placement transform: takes notes already in the pattern and replaces each one with subdivisions evenly-spaced sub-hits within the original note’s duration window. The velocity of each sub-hit is interpolated from velocity_start to velocity_end (as multipliers on the original velocity) using the shape easing curve, so crescendo rolls, decrescendo buzzes, and flat repeats are all one parameter apart.

Call ratchet() after note-placement methods (euclidean, hit_steps, arpeggio, etc.) and before swing or groove so that the subdivisions sit inside the original note slot and swing displacement still affects the parent position.

Parameters:
  • subdivisions – Number of sub-hits replacing each note (default 2). If the note’s duration is shorter than subdivisions pulses, subdivisions are clamped to note.duration so they never stack on the same pulse.

  • pitch – Only ratchet notes matching this pitch (MIDI number or drum name). None (default) ratchets all notes regardless of pitch — useful for melodic patterns such as arpeggios.

  • probability – Chance (0.0–1.0) that each note gets ratcheted. Notes that fail the check are left completely unchanged. Default 1.0 (every note is ratcheted).

  • velocity_start – Velocity multiplier for the first sub-hit (0.0–2.0). Default 1.0 (same as the original).

  • velocity_end – Velocity multiplier for the last sub-hit (0.0–2.0). Default 1.0. Set velocity_start=0.3, velocity_end=1.0 for a crescendo roll; 1.0, 0.2 for a decrescendo buzz.

  • shape – Easing curve applied to the velocity interpolation across sub-hits. Accepts any name from subsequence.easing (e.g. "ease_in", "ease_out", "s_curve") or a custom callable f(t) t for t ∈ [0, 1]. Default "linear".

  • gate – Sub-note duration as a fraction of each subdivision slot (0.0–1.0). 1.0 = legato (sub-hits touch), 0.5 = staccato (half the slot). Default 0.5.

  • steps – Grid positions to ratchet (e.g. [0, 4, 12]). Notes are classified to grid zones the same way thin() works — swing- shifted notes remain in their original zone. None (default) applies ratchet to all eligible notes.

  • grid – Grid resolution used for steps zone classification. Defaults to the pattern’s default_grid.

  • rng – Random number generator. Defaults to self.rng.

Examples

# Subdivide every hi-hat into a triplet roll
p.euclidean("hh_closed", 8).ratchet(3, pitch="hh_closed")

# Crescendo roll into a snare
p.hit_steps("snare", [12]).ratchet(4, velocity_start=0.3,
                                      velocity_end=1.0,
                                      shape="ease_in")

# Probabilistic 2× ratchet on hi-hats only
p.euclidean("hh_closed", 12).ratchet(2, pitch="hh_closed",
                                        probability=0.4, gate=0.3)

# Ratchet only steps 0 and 8 (downbeats)
p.euclidean("kick_1", 6).ratchet(2, pitch="kick_1", steps=[0, 8])
reaction_diffusion(pitch: int | str, threshold: float = 0.5, velocity: int | Tuple[int, int] = subsequence.constants.velocity.DEFAULT_GENERATIVE_VELOCITY, duration: float = 0.1, feed_rate: float = 0.055, kill_rate: float = 0.062, steps: int = 1000, no_overlap: bool = False, probability: float = 1.0, seed: int | None = None, rng: random.Random | None = None) subsequence.pattern_builder.PatternBuilder[source]

Generate a rhythm from a 1D Gray-Scott reaction-diffusion simulation.

Simulates the Gray-Scott model on a ring of _default_grid cells, then thresholds the final V-concentration to produce a binary hit pattern. Cells where concentration exceeds threshold become note events.

Unlike cellular automata - where rules are discrete and the state is binary - reaction-diffusion evolves a continuous concentration field governed by diffusion rates and chemical reactions. The resulting spatial patterns (spots, stripes, travelling waves) have an organic, biological character that maps naturally to rhythm.

The feed_rate and kill_rate parameters control pattern type: typical values that produce spots (useful rhythms) range from 0.020–0.062 for feed and 0.045–0.069 for kill. The defaults (F=0.055, k=0.062) produce a stable spotted pattern.

Parameters:
  • pitch – MIDI note number or drum name.

  • threshold – V-concentration threshold for note placement (0.0–1.0). Lower values produce denser patterns.

  • velocity – MIDI velocity. An (low, high) tuple randomises per step.

  • duration – Note duration in beats.

  • feed_rate – Rate of U replenishment. Default 0.055.

  • kill_rate – Rate of V removal. Default 0.062.

  • steps – Number of simulation iterations. More = more developed pattern. Default 1000.

  • no_overlap – Skip steps where pitch is already sounding.

  • probability – Chance (0.0–1.0) that each active step plays — 1.0 places them all, lower thins.

  • seed – Fix the thinning for this call (an int); omit to use the pattern’s RNG.

  • rng – Advanced determinism form — a random.Random (wins over seed=).

Example

# Organic kick pattern from reaction-diffusion
p.reaction_diffusion("kick_1", threshold=0.4, feed_rate=0.037, kill_rate=0.060)
self_avoiding_walk(pitches: List[int | str], spacing: float = 0.25, velocity: int | Tuple[int, int] = subsequence.constants.velocity.DEFAULT_GENERATIVE_VELOCITY, duration: float = 0.2, seed: int | None = None, rng: random.Random | None = None) subsequence.pattern_builder.PatternBuilder[source]

Generate a melody using a self-avoiding random walk.

A self-avoiding walk moves ±1 step through a pitch index space, tracking visited positions and refusing to revisit them. When the walk is trapped (all neighbours visited), the visited set resets and the walk continues from the current position - creating natural phrase boundaries.

Compared to random_walk(), the self-avoiding variant guarantees pitch diversity within each phrase: no pitch repeats until the walk resets. The contiguous step motion (never skipping pitches) gives melodies a smooth, step-wise quality with occasional direction reversals.

Parameters:
  • pitches – Ordered list of MIDI note numbers or note strings. The walk moves through indices [0, len(pitches) - 1], mapping each to the corresponding pitch.

  • spacing – Time between notes in beats. Default 0.25 (16th note).

  • velocity – MIDI velocity. An (low, high) tuple randomises per note.

  • duration – Note duration in beats.

  • rng – Random number generator. Defaults to self.rng.

Example

scale = subsequence.scale_notes("C", "ionian", low=60, high=72)
p.self_avoiding_walk(scale, spacing=0.25, velocity=(60, 100))
thin(pitch: int | str | None = None, strategy: str | List[float] = 'strength', amount: float = 0.5, grid: int | None = None, seed: int | None = None, rng: random.Random | None = None) subsequence.pattern_builder.PatternBuilder[source]

Remove notes from the pattern based on their rhythmic position.

This is the musical inverse of ghost_fill(). Where ghost_fill uses bias weights to decide where to add ghost notes, thin uses the same position vocabulary to decide where to remove notes. A high strategy weight on a position means that position is dropped first.

The strategy names match those in build_ghost_bias() and carry the same rhythmic meaning:

  • "sixteenths" - removes 16th-note subdivisions (e/a), keeps beats and &.

  • "offbeat" - removes the & position, straightens the groove.

  • "e_and_a" - removes all non-downbeat positions, keeps only beats.

  • "downbeat" - removes beat positions (floating/displaced effect).

  • "upbeat" - removes only the & position.

  • "uniform" - removes from all positions equally (per-instrument dropout).

  • "strength" - progressive thinning: weakest positions (e/a) drop first, strongest (downbeat) last. Useful for Perlin-driven density control.

When pitch is given, only notes of that instrument are affected - useful for drum layers. When pitch is None (the default), all notes regardless of pitch are candidates. This makes thin a rhythm-aware generalisation of dropout(), and is ideal for tonal patterns such as arpeggios where each step carries a different pitch.

Position classification is zone-based: each grid step owns the pulse range [N * step_pulses, (N + 1) * step_pulses), so notes shifted by swing or groove are still classified correctly regardless of call order.

Parameters:
  • pitch – Drum name or MIDI note number to target, or None to thin all notes regardless of pitch. Defaults to None.

  • strategy – Named strategy string or a list of per-step drop-priority floats (0.0 = never drop, 1.0 = highest drop priority). Must have length equal to grid when a list is provided.

  • amount – Overall thinning depth (0.0 = remove nothing, 1.0 = remove all qualifying). Effective drop probability = priority * amount. Drive this with a Perlin field or section progress for smooth, organic thinning over time.

  • grid – Step grid size. Defaults to the pattern’s default_grid.

  • rng – Random number generator. Defaults to self.rng.

Example:

# Thin 16th ghost notes from the kick, keep anchors and off-beats
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
p.ghost_fill("kick_1", density=0.3, velocity=(25, 40), bias="sixteenths")
p.thin("kick_1", "sixteenths", amount=0.8)

# Perlin-driven progressive thinning of hi-hats
sparseness = perlin_1d(p.cycle * 0.07, seed=42)
p.thin("hi_hat_closed", "strength", amount=sparseness)

# Thin an arpeggio (all pitches) - no pitch loop needed
p.thin(strategy="strength", amount=sparseness)
thue_morse(pitch: int | str, velocity: int | Tuple[int, int] = subsequence.constants.velocity.DEFAULT_VELOCITY, duration: float = 0.1, pitch_b: int | str | None = None, velocity_b: int | Tuple[int, int] | None = None, no_overlap: bool = False, probability: float = 1.0, seed: int | None = None, rng: random.Random | None = None) subsequence.pattern_builder.PatternBuilder[source]

Place notes using the Thue-Morse aperiodic binary sequence.

The Thue-Morse sequence (0 1 1 0 1 0 0 1 1 0 0 1 0 1 1 0 …) is perfectly balanced, overlap-free, and self-similar but never periodic. It produces rhythmic patterns that feel structured yet never settle into a simple repeating loop - a quality distinct from Euclidean rhythms (evenly spaced) and cellular automata (rule-driven evolution).

In single-pitch mode (default), notes are placed at positions where the sequence is 1. In two-pitch mode (pitch_b given), pitch is placed at 0-positions and pitch_b at 1-positions - useful for alternating two drums or two chord tones.

Parameters:
  • pitch – Pitch (MIDI note number or drum name) placed at the sequence’s 0 positions (the hits in single-pitch mode).

  • velocity – MIDI velocity for pitch, or a (low, high) tuple for a fresh random draw per hit.

  • duration – Note duration in beats.

  • pitch_b – Optional second pitch placed at sequence-1 positions. When set, all steps produce a note (no rests).

  • velocity_b – Velocity for pitch_b (int or (low, high) tuple). Defaults to velocity.

  • no_overlap – Skip steps where pitch is already sounding.

  • probability – Chance (0.0–1.0) that each active step plays — 1.0 places them all, lower thins.

  • seed – Fix the thinning for this call (an int); omit to use the pattern’s RNG.

  • rng – Advanced determinism form — a random.Random (wins over seed=).

Example

# Single-pitch Thue-Morse kick
p.thue_morse("kick_1", velocity=100)

# Two-pitch mode: alternate kick and snare
p.thue_morse("kick_1", pitch_b="snare_1", velocity=100)