subsequence.motifs

Motif and Phrase — immutable musical values.

A Motif is a short musical figure stored as a value: a frozen tuple of timed note events (with specification pitches — scale degrees, chord tones, drum names, or absolute MIDI) plus an optional stream of control gestures (CC sweeps, pitch bends, NRPN/RPN moves), and an explicit length in beats. A Phrase is a frozen sequence of Motifs whose segmentation is preserved.

Values are frozen dataclasses: immutable, deterministic to construct, hashable, printable, and safe to define at module level in a live-coded file. They carry no playback position — the engine owns position; values are placed onto patterns with p.motif() / p.phrase().

Pitch is resolved late: a stored Degree or ChordTone becomes a MIDI note only at placement, against the key/scale (and, where applicable, the chord) in effect at that event’s own beat. The same motif therefore sounds different under different harmony — by design.

The combination algebra:

  • a + b — sequential: a Phrase of the two (segmentation preserved).

  • a.then(b) / Motif.join([...]) — closed sequential concat (one longer Motif).

  • a & b / a.stack(b) — parallel merge (event union; length = max).

  • m * n — repetition: a Phrase of n segments (m * 1 is m).

  • m.slice(start, end) — a window; durations and ramps truncate at the cut.

Transforms are pure and return new values. Time transforms (reverse, rotate, stretch, slice) carry control gestures with them — a reversed rising sweep becomes a falling one; pitch- and note-scoped transforms (transpose, invert, pitched, accent, with_velocity, quantize) leave control gestures untouched.

Module Contents

class subsequence.motifs.Approach[source]

A half-step approach into a target pitch at the next chord boundary.

Resolves at placement, one semitone below its target (the leading-tone approach); a ChordTone target reads the NEXT chord through the harmony window, so the approach lands as the harmony arrives.

class subsequence.motifs.CC[source]

A MIDI CC signal — number, or a name resolved at placement via the pattern’s cc_name_map.

class subsequence.motifs.ChordTone(index_or_name: int | str, octave: int = 0)[source]

An index into the current chord’s tones — 1-based, resolved at placement.

Accepts an int (1 = root, 2 = third, …) or one of the names "root" / "third" / "fifth" / "seventh". octave shifts whole octaves.

Normalize a tone name to its 1-based index.

class subsequence.motifs.ControlEvent[source]

One timed control gesture inside a Motif: a discrete write or a shaped ramp.

A discrete write has end=None and span=0.0; a ramp interpolates startend over span beats through the easing shape. Pulse density (resolution=) is deliberately not stored here — beats and shapes are music; MIDI traffic density is set at the placement call.

class subsequence.motifs.Degree[source]

A scale degree — 1-based, resolved against key + scale at placement.

Degree 1 is the tonic; 8 is the tonic an octave up (steps may exceed the scale length and resolve into higher octaves). octave shifts whole octaves; chroma is a chromatic offset in semitones (+1 = sharpened).

class subsequence.motifs.Motif[source]

An immutable musical figure: timed note events + control gestures + a length in beats.

Construct via the classmethods (degrees(), notes(), hits(), steps(), euclidean(), the control-gesture constructors, or from_events()) rather than positionally. length is explicit — a trailing rest is meaningful.

accent(beat: float, amount: int = 20) Motif[source]

Add amount velocity to every note at the given beat position (0-based beats).

answer(to: int | Degree = 1) Motif[source]

Call → response: re-aim the tail to a stable degree.

The classic consequent move — the figure repeats but its last pitched note lands home (degree 1 by default; pass to=5 for a half-close, or a full Degree for register control). Everything else — rhythm, the other pitches, velocities, controls — is untouched.

Degree content only: absolute MIDI has no degrees to re-aim (build the call with motif([...])), and drums raise.

classmethod cc(control: int | str, values: List[int], beats: List[float], length: float | None = None, probabilities: Any = 1.0) Motif[source]

Discrete CC writes at beat positions — mirrors p.cc(); names resolve at placement.

classmethod cc_ramp(control: int | str, start: int, end: int, beat_start: float = 0.0, beat_end: float | None = None, shape: str | subsequence.easing.EasingFn = 'linear', length: float | None = None, probability: float = 1.0) Motif[source]

A CC value swept startend over a beat range — mirrors p.cc_ramp().

classmethod degrees(degrees: List[int | Degree | None], beats: List[float] | None = None, velocities: Any = _DEFAULT_VELOCITY, durations: Any = 1.0, probabilities: Any = 1.0, length: float | None = None) Motif[source]

A melody written as 1-based scale degrees, one per beat by default.

Elements are ints (1 = tonic, 8 = tonic an octave up), None for a rest (the beat slot still advances), or Degree for octave/ chromatic detail. Resolved against key + scale at placement. Durations default to a full beat (each note holds its slot).

describe() str[source]

A readable one-line summary: length, notes (pitch@beat), and control gestures.

classmethod empty() Motif[source]

The empty motif (zero events, zero length) — the identity for then.

classmethod euclidean(pulses: int, steps: int, pitch: int | str, length: float = 4.0, velocities: Any = _DEFAULT_VELOCITY, durations: Any = 0.1, probabilities: Any = 1.0) Motif[source]

A euclidean rhythm as a value: pulses spread evenly across steps over length beats.

classmethod from_events(events: Iterable[MotifEvent], length: float | None = None, controls: Iterable[ControlEvent] = ()) Motif[source]

Build a motif from explicit events (power use; length defaults to the next whole beat).

classmethod generate(rhythm: Any, length: float | None = None, scale: str | Sequence[int] | None = None, contour: str | None = None, end_on: int | Degree | None = None, cadence: str | None = None, pins: Dict[int, int | Degree] | None = None, max_pitches: int | None = None, velocities: Any = _DEFAULT_VELOCITY, durations: Any = 0.25, seed: int | None = None, rng: random.Random | None = None, state: Any | None = None, nir_strength: float = 0.5, pitch_diversity: float = 0.6, tessitura_strength: float = 0.6) Motif[source]

Generate a melodic motif — rhythm first, pitches walked, a value out.

The melody engine emitting a value: you give the rhythm (an onset list in beats, or another motif whose rhythm to borrow — cross-pattern rhythm reuse is shared values); the engine walks pitches over it through the soft scoring factors (NIR expectation, contour envelope, tessitura regression, diversity), honouring any pins.

The result emits scale degrees (resolved at placement against the composition key/scale), so a generated hook transposes, varies, and develops like a hand-written one. scale= constrains candidate choice only: a name or interval list masks which pitches the walk may use, spelled relative to its best-fit reference (major or minor) — bind it in a composition whose scale matches that family and resolution is exact. An explicit MIDI pitch pool (a list of note numbers) switches to absolute output (the sieve/atonal path).

Parameters:
  • rhythm – Onset beats ([0, 1, 1.5, 1.75, 2.5]) or a Motif (its onsets are borrowed).

  • length – Motif length in beats; defaults to the onsets rounded up to a whole 4-beat bar.

  • scale – A scale name, an interval list, or an explicit MIDI pitch pool. None = the plain seven degrees.

  • contour – Envelope shaping the line’s height over its span — "arch", "valley", "ascending", "descending".

  • end_on – Degree the line must end on — sugar for pins={-1: ...}.

  • cadence – A cadence name ("strong"/"soft"/"open"/ "fakeout") — the line closes on that cadence’s melodic degree (1 for the full closes and the fakeout, 5 for the open half). Sugar for end_on=; conflicts with it.

  • pins{position: degree} — 1-based note positions (-1 = the last, the Python idiom); the engine fills between.

  • max_pitches – Cap on distinct pitches (a tight pool is a hook); keeps the most central candidates.

  • durations (velocities /) – Scalar or per-note list (the parallel- list convention).

  • seed – Seed for the walk (required or warned — module-level nondeterminism breaks live reload).

  • rng – Explicit stream (overrides seed).

  • state – A MelodicState whose settings and history seed the walk. It is copied — building a value never mutates a module-level live object.

  • tessitura_strength (nir_strength / pitch_diversity /) – The walk’s dials when no state is given.

Example

hook = subsequence.Motif.generate(
        rhythm=[0, 1, 1.5, 1.75, 2.5], scale="minor_pentatonic",
        contour="arch", end_on=1, seed=7,
)
classmethod hits(pitch: int | str, beats: List[float], length: float | None = None, velocities: Any = _DEFAULT_VELOCITY, durations: Any = 0.1, probabilities: Any = 1.0) Motif[source]

One pitch (usually a drum name) at a list of beat positions — the hit() convention.

invert(pivot: int | None = None) Motif[source]

Mirror pitches around a pivot: MIDI content around a MIDI pivot, degree content around a degree pivot (default: the first note’s pitch). Drum motifs raise.

classmethod join(motifs: Iterable[Motif]) Motif[source]

Fold a list of motifs into one with then (empty list → Motif.empty()).

classmethod notes(notes: List[int | None], beats: List[float] | None = None, velocities: Any = _DEFAULT_VELOCITY, durations: Any = 1.0, probabilities: Any = 1.0, length: float | None = None) Motif[source]

A melody written as absolute MIDI note numbers (60 = middle C); None = rest.

classmethod nrpn(parameter: int | str, values: List[int], beats: List[float], fine: bool = False, null_reset: bool = True, length: float | None = None, probabilities: Any = 1.0) Motif[source]

Discrete NRPN parameter writes at beat positions — mirrors p.nrpn().

classmethod nrpn_ramp(parameter: int | str, start: int, end: int, beat_start: float = 0.0, beat_end: float | None = None, shape: str | subsequence.easing.EasingFn = 'linear', fine: bool = True, null_reset: bool = True, length: float | None = None, probability: float = 1.0) Motif[source]

An NRPN value swept over a beat range — mirrors p.nrpn_ramp().

onsets() List[float][source]

The note onset beats, in order — ready for rhythm-first generation.

classmethod osc(address: str, values: List[float], beats: List[float], length: float | None = None, probabilities: Any = 1.0) Motif[source]

Discrete OSC float sends at beat positions — mirrors p.osc().

classmethod osc_ramp(address: str, start: float, end: float, beat_start: float = 0.0, beat_end: float | None = None, shape: str | subsequence.easing.EasingFn = 'linear', length: float | None = None, probability: float = 1.0) Motif[source]

An OSC float swept over a beat range — mirrors p.osc_ramp().

classmethod pitch_bend(values: List[float], beats: List[float], length: float | None = None, probabilities: Any = 1.0) Motif[source]

Discrete pitch-bend writes (-1.0 to 1.0) at beat positions — mirrors p.pitch_bend().

classmethod pitch_bend_ramp(start: float, end: float, beat_start: float = 0.0, beat_end: float | None = None, shape: str | subsequence.easing.EasingFn = 'linear', length: float | None = None, probability: float = 1.0) Motif[source]

Pitch bend swept startend (-1.0 to 1.0) over a beat range — mirrors p.pitch_bend_ramp().

pitched(spec: PitchSpec) Motif[source]

Replace every pitch with one spec — a kick rhythm becomes a bass line.

"root" / "third" / "fifth" / "seventh" become chord tones; any other string is a drum name; ints are MIDI; Degree / ChordTone / Approach pass through.

classmethod preset(name: str, pitch: int | str | None = None, length: float = 4.0, velocities: Any = _DEFAULT_VELOCITY, durations: Any = 0.1, probabilities: Any = 1.0) Motif[source]

A named world-rhythm timeline as a value — Motif.preset("son_clave_3_2").

Looks a curated timeline up in the world-rhythm table (clave family, West-African bell patterns, tresillo/cinquillo, samba) and lays its onsets across length beats. Onset positions are exact pulse indices from Toussaint’s “The Geometry of Musical Rhythm”; each preset declares its own grid (16 for the clave/4-4 timelines, 12 for the bell patterns) and a default drum voice.

Parameters:
  • name – A preset name (KeyError-style ValueError lists them all).

  • pitch – The voice — a drum name or MIDI int; defaults to the preset’s General-MIDI voice ("claves", "cowbell", "side_stick", "low_conga"), so it sounds against the standard GM drum map without a pitch=.

  • length – Total beats the cycle spans (4 = one common-time bar).

  • probabilities (velocities / durations /) – The parallel-list params.

Returns:

A drum/pitched Motif of the timeline’s onsets.

Raises:

ValueError – If name is not a known preset.

Example

clave = subsequence.Motif.preset("son_clave_3_2")              # GM "claves"
bell  = subsequence.Motif.preset("bembe", pitch="cowbell")     # 12-pulse
quantize(grid: float) Motif[source]

Snap note onsets to the nearest multiple of grid beats (control gestures untouched).

reverse() Motif[source]

Mirror the figure in time; ramps swap direction (a rising sweep falls).

rhythm() Motif[source]

Strip pitches (and control gestures): a reusable rhythmic skeleton.

Timing, velocities, durations, and probabilities survive; re-pitch with pitched() before placement (placing a skeleton raises).

rotate(beats: float) Motif[source]

Shift every onset by beats, wrapping modulo the length (spans ride along).

classmethod rpn(parameter: int | str, values: List[int], beats: List[float], fine: bool = False, null_reset: bool = True, length: float | None = None, probabilities: Any = 1.0) Motif[source]

Discrete RPN parameter writes at beat positions — mirrors p.rpn().

classmethod rpn_ramp(parameter: int | str, start: int, end: int, beat_start: float = 0.0, beat_end: float | None = None, shape: str | subsequence.easing.EasingFn = 'linear', fine: bool = True, null_reset: bool = True, length: float | None = None, probability: float = 1.0) Motif[source]

An RPN value swept over a beat range — mirrors p.rpn_ramp().

slice(start: float, end: float) Motif[source]

A window onto the motif, on its own authority: events starting outside are dropped; durations and ramp spans truncate at the cut (a truncated ramp ends at its interpolated cut value). Beats shift so the window starts at 0.

stack(other: Motif | Phrase) Motif[source]

Parallel merge (the spelled form of &): event union, length = max.

No implicit tiling — a short gesture stacked under a long figure plays once. Phrase operands flatten first.

classmethod steps(steps: List[int], pitches: Any, velocities: Any = _DEFAULT_VELOCITY, durations: Any = 0.1, probabilities: Any = 1.0, step_duration: float = 0.25, length: float | None = None) Motif[source]

Grid placement — the sequence() convention: steps are 0-based grid indices (sixteenths by default), pitches a scalar or parallel list of MIDI ints or drum names.

stretch(factor: float) Motif[source]

Scale time by factor (2.0 = half-time feel): beats, durations, spans, and length.

then(other: Motif) Motif[source]

Closed sequential concat: glue other after this motif into ONE longer motif.

transpose(steps: int | None = None, semitones: int | None = None) Motif[source]

Transpose pitched content; the keyword names the unit.

steps= moves scale degrees diatonically (the sequencing move) and raises on absolute-MIDI or drum content; semitones= is the literal chromatic form for MIDI ints and degrees. Drum motifs raise on both — a transposed drum name is a different instrument, not a transposition.

vary(notes: int = 1, position: str = 'end', seed: int | None = None, rng: random.Random | None = None, keep_contour: bool = False) Motif[source]

Replace a few pitches, preserving the rhythm — the smallest variation.

Rhythm, velocities, durations, rests, and control gestures are untouched; only the chosen notes’ pitches move (by a small melodic nudge: scale steps for degrees, semitones for MIDI ints).

Parameters:
  • notes – How many pitched notes to vary (clamped to what exists).

  • position – Which notes — "end" (the tail, the default), "start", or "anywhere" (drawn from the stream).

  • seed – Seed for the variation. A standalone vary without a seed warns — module-level nondeterminism breaks live reload.

  • rng – An explicit random stream (overrides seed; used by recipe machinery).

  • keep_contour – When True, the variation preserves the line’s CSEG — every varied note keeps its rank relations with every other note, so the melodic shape is identical (the motif-identity guard). Where no nudge can preserve the contour, that note stays unchanged — shape wins over motion.

Example

answer = call.vary(notes=1, seed=4)     # same figure, new tail note
with_velocity(velocity: int | Tuple[int, int]) Motif[source]

Replace every note’s velocity (an int, or a (low, high) random range).

class subsequence.motifs.MotifEvent[source]

One timed note event inside a Motif.

pitch is a specification: an absolute MIDI int, a drum name string, a Degree, ChordTone, or Approach — or None for a pitch-stripped skeleton event (see Motif.rhythm()), which must be re-pitched via Motif.pitched() before placement. velocity is an int or a (low, high) random-range tuple.

class subsequence.motifs.NRPN[source]

An NRPN parameter — number, or a name resolved at placement via the pattern’s nrpn_name_map.

class subsequence.motifs.OSC[source]

An OSC address; values are sent as the single float argument.

class subsequence.motifs.Phrase(segments: Iterable[Motif], recipe: _PhraseRecipe | None = None)[source]

A sequence of Motifs with segmentation preserved.

Segmentation is the unit of editing — it is what development and per-region regeneration operate on. flatten() erases it into one long Motif. Length is the sum of segment lengths.

A phrase made by develop() carries its recipe, so reroll() can regenerate a region; transforms and hand edits return recipe-less phrases (their notes no longer come from the recipe, so there is nothing honest to regenerate from).

Coerce any iterable of Motifs.

describe() str[source]

A readable summary: total length and each segment on its own line.

classmethod develop(motif: Motif, bars: int = 8, plan: Sequence[str] | str | None = None, seed: int | None = None, beats_per_bar: float = 4.0) Phrase[source]

Grow a motif into a phrase by a plan — the phrase generator.

plan follows the standard form. The literal form is a list of unit labelsplan=["a", "a", "a", "b"], equivalently ["a"] * 3 + ["b"]: the first label is the given motif, each new label is a generated contrast unit (the source’s rhythm, freshly re-pitched), a repeated label is a restatement, and bars spreads evenly across the units. A bare string is a recipe name from the curated table — plan="call_response" (call, answer, call, varied answer) — reserved for plans whose semantics exceed a label skeleton. A letter string is not a plan: a sequence of labels is a sequence, so it is a list.

The result carries its recipe, so reroll() can regenerate a region later.

Parameters:
  • motif – The source unit (its length must be bars / len(units) bars — the plan’s units tile the phrase exactly).

  • bars – Phrase length in bars (must divide evenly by the unit count).

  • plan – A list of unit labels, or a recipe name.

  • seed – Seed for the generated units. Without one, develop() warns — module-level nondeterminism breaks live reload.

  • beats_per_bar – Bar size in beats (the value is context-free; 4 is the common-time default).

Example

call = subsequence.motif([5, 6, 5, 3, None, 1, 2, 3])
lead = subsequence.Phrase.develop(call, bars=8, plan="call_response", seed=11)
flatten() Motif[source]

Erase segmentation: one long Motif (the monoid homomorphism onto then).

invert(pivot: int | None = None) Phrase[source]

Mirror pitches in every segment around one pivot (see Motif.invert()).

pitched(spec: PitchSpec) Phrase[source]

Replace every pitch, segment-wise.

quantize(grid: float) Phrase[source]

Snap note onsets segment-wise.

replace(position: int, motif: Motif) Phrase[source]

Replace the segment at a 1-based position (musicians count from one).

reroll(bar: int | None = None, bars: Sequence[int] | None = None, seed: int | None = None) Phrase[source]

Regenerate only the named bars — rhythm and boundary pitches kept.

Within each named bar, the first and last pitched notes stay (the boundary pins) and the interior pitches re-roll from the recipe’s stream; onsets, durations, velocities, rests, drums, and control gestures are untouched. Segmentation and the recipe survive, so rerolls compose.

Only a phrase that carries a recipe can reroll — a hand-written or transformed phrase raises loudly (its notes no longer come from a generator, so regenerating them would invent music).

Parameters:
  • bar – A single 1-based bar to reroll.

  • bars – A list of 1-based bars (the paired plural spelling).

  • seed – Seed for the new pitches (salted per bar). Without one, reroll() warns.

Example

lead = lead.reroll(bar=7, seed=4)    # only bar 7; rhythm + boundaries kept
reverse() Phrase[source]

Reverse the whole timeline: segments reverse order AND each reverses internally.

rhythm() Phrase[source]

Strip pitches segment-wise: a phrase-shaped skeleton.

rotate(beats: float) Phrase[source]

Rotate the whole timeline modulo the total length, then re-segment at the original boundaries.

slice(start: float, end: float) Phrase[source]

A window; re-segments at the cut points (partial segments are sliced).

stack(other: Motif | Phrase) Motif[source]

The spelled form of & — flattens, then merges.

stretch(factor: float) Phrase[source]

Scale time in every segment (lengths scale with them).

transpose(steps: int | None = None, semitones: int | None = None) Phrase[source]

Transpose every segment (see Motif.transpose()).

with_velocity(velocity: int | Tuple[int, int]) Phrase[source]

Replace every note’s velocity, segment-wise.

property length: float[source]

Total length in beats (sum of segment lengths).

class subsequence.motifs.PitchBend[source]

The channel pitch-bend wheel; values are normalised -1.0 to 1.0.

class subsequence.motifs.RPN[source]

An RPN parameter — number, or one of the standard RPN names (resolved at placement).

subsequence.motifs.motif(degrees: List[int | Degree | None], beats: List[float] | None = None, velocities: Any = _DEFAULT_VELOCITY, durations: Any = 1.0, probabilities: Any = 1.0, length: float | None = None) Motif[source]

The lowercase shortcut: a melody as 1-based scale degrees.

subsequence.motif([5, 6, 5, 3]) is Motif.degrees([5, 6, 5, 3]) — relative pitch is the primary form. For absolute MIDI note numbers use Motif.notes([64, 65, 64, 60]); implausibly large ints here raise so a pasted MIDI list fails loud instead of squealing octaves up.

subsequence.motifs.period(antecedent: Motif | Phrase, cadence: str = 'strong', beats_per_bar: float = 4.0) Phrase[source]

The classical period, as a thin combinator — question, then answer.

Two halves: the antecedent with its tail re-aimed to the open half-close (degree 5 — the question), then the same material restated with its tail on the cadence’s close degree (the answer). The two halves differ exactly at their closes — the open/closed contrast is the period.

Deterministic: no notes are generated, only the two tail notes re-aim (so there is no seed). Vary the consequent yourself for a looser restatement: period(a).reroll(bar=7, seed=4).

Parameters:
  • antecedent – The first half — a Motif, or a Phrase whose segmentation is kept (only its last segment’s tail re-aims).

  • cadence – The consequent’s close — "strong" lands on 1 (theory aliases accepted).

  • beats_per_bar – Bar size in beats, recorded for reroll() windows.

Example

idea = subsequence.motif([3, 4, 5, 1, None, 6, 5, 4], length=8)
lead = subsequence.period(idea)        # 16 beats: half-close, then home
subsequence.motifs.sentence(motif: Motif, bars: int = 8, cadence: str = 'strong', seed: int | None = None, beats_per_bar: float = 4.0) Phrase[source]

The classical sentence, as a thin combinator — idea, idea, drive, close.

Four units: the basic idea stated twice (the presentation), a generated contrast unit (the continuation — the source’s rhythm, freshly re-pitched), and a second contrast unit whose tail lands on the cadence’s close degree (the cadential close). An 8-bar sentence from a 2-bar idea is the textbook proportion; a shorter idea tiles up to the unit size first.

The melodic side of a cadence only — pair it with the harmonic side (prog.cadence(), Progression.generate(cadence=), or request_cadence()) and the two arrive together.

Parameters:
  • motif – The basic idea (degree content — the close re-aims a degree).

  • bars – Sentence length (must divide evenly across the 4 units).

  • cadence – The close — "strong" lands on 1, "open" on 5, "soft"/"fakeout" on 1 (theory aliases accepted).

  • seed – Seed for the generated continuation units (seed-or-warn).

  • beats_per_bar – Bar size in beats (context-free; 4 is the default).

Example

idea = subsequence.motif([5, 6, 5, 3, None, 1, 2, 3])
verse_lead = subsequence.sentence(idea, bars=8, cadence="open", seed=11)