Subsystem Search

subsequence.motifs

The API reference for Motif, Phrase, motif, sentence, period, Degree, ChordTone, Approach, MotifEvent, and ControlEvent.

Motif

class Motif(
    events: typing.Tuple[MotifEvent, ...],
    length: float,
    controls: typing.Tuple[ControlEvent, ...] = (),
)

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, and a trailing rest is meaningful: left to itself it runs to the next whole beat at or after the last slot the figure occupies, silent slots included, so a bar-long idea ending in silence is a bar long. A length that would leave an event past the end is refused rather than dropping it, while a note ringing past the end is fine - that is a tie over the barline.

Members: accent, answer, cc, cc_ramp, controls, degrees, describe, empty, euclidean, events, fit, from_events, generate, hits, invert, join, length, notes, nrpn, nrpn_ramp, onsets, osc, osc_ramp, pitch_bend, pitch_bend_ramp, pitched, preset, quantize, reverse, rhythm, rotate, rpn, rpn_ramp, slice, stack, steps, stretch, then, transpose, vary, with_velocity

Motif.events

Motif.events: typing.Tuple[MotifEvent, ...]

Motif.length

Motif.length: float

Motif.controls

Motif.controls: typing.Tuple[ControlEvent, ...] = ()

Motif.fit

property Motif.fit: typing.Optional[float]

The fit the motif's notes share, or None when they have none or differ.

Fit belongs to each note (MotifEvent.fit): generate gives the notes it makes 0.7, and notes written by hand have none. This reads it back for a whole motif - 0.7 for a generated one, None for a written one or a mix - and placement never reads it: each note plays its own. A motif-wide fit lent a generated line's dial to every note then() or & put beside it, and lost it wherever a motif was rebuilt (#3458).

Motif.empty

classmethod Motif.empty() -> Motif

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

Motif.from_events

classmethod Motif.from_events(
    events: typing.Iterable[MotifEvent],
    length: typing.Optional[float] = None,
    controls: typing.Iterable[ControlEvent] = (),
) -> Motif

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

Motif.degrees

classmethod Motif.degrees(
    degrees: typing.List[typing.Union[int, Degree, None]],
    beats: typing.Optional[typing.List[float]] = None,
    velocities: typing.Any = _DEFAULT_VELOCITY,
    durations: typing.Any = 1.0,
    probabilities: typing.Any = 1.0,
    length: typing.Optional[float] = None,
) -> Motif

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).

Motif.notes

classmethod Motif.notes(
    notes: typing.List[typing.Union[int, None]],
    beats: typing.Optional[typing.List[float]] = None,
    velocities: typing.Any = _DEFAULT_VELOCITY,
    durations: typing.Any = 1.0,
    probabilities: typing.Any = 1.0,
    length: typing.Optional[float] = None,
) -> Motif

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

Motif.hits

classmethod Motif.hits(
    pitch: typing.Union[int, str],
    beats: typing.List[float],
    length: typing.Optional[float] = None,
    velocities: typing.Any = _DEFAULT_VELOCITY,
    durations: typing.Any = 0.1,
    probabilities: typing.Any = 1.0,
) -> Motif

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

Motif.steps

classmethod Motif.steps(
    steps: typing.List[int],
    pitches: typing.Any,
    velocities: typing.Any = _DEFAULT_VELOCITY,
    durations: typing.Any = 0.1,
    probabilities: typing.Any = 1.0,
    step_duration: float = 0.25,
    length: typing.Optional[float] = None,
) -> Motif

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.

Motif.euclidean

classmethod Motif.euclidean(
    pulses: int,
    steps: int,
    pitch: typing.Union[int, str],
    length: float = 4.0,
    velocities: typing.Any = _DEFAULT_VELOCITY,
    durations: typing.Any = 0.1,
    probabilities: typing.Any = 1.0,
) -> Motif

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

Motif.preset

classmethod Motif.preset(
    name: str,
    pitch: typing.Optional[typing.Union[int, str]] = None,
    length: float = 4.0,
    velocities: typing.Any = _DEFAULT_VELOCITY,
    durations: typing.Any = 0.1,
    probabilities: typing.Any = 1.0,
) -> Motif

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

Returns

Raises

Example

clave = subsequence.Motif.preset("son_clave_3_2")              # GM "claves"
bell  = subsequence.Motif.preset("bembe", pitch="cowbell")     # 12-pulse

Motif.cc

classmethod Motif.cc(
    control: typing.Union[int, str],
    values: typing.List[int],
    beats: typing.List[float],
    length: typing.Optional[float] = None,
    probabilities: typing.Any = 1.0,
) -> Motif

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

Motif.cc_ramp

classmethod Motif.cc_ramp(
    control: typing.Union[int, str],
    start: int,
    end: int,
    beat_start: float = 0.0,
    beat_end: typing.Optional[float] = None,
    shape: typing.Union[subsequence.declarations.EasingCurve, subsequence.easing.EasingFn] = 'linear',
    length: typing.Optional[float] = None,
    probability: float = 1.0,
) -> Motif

A CC value swept start → end over a beat range - mirrors p.cc_ramp().

Motif.pitch_bend

classmethod Motif.pitch_bend(
    values: typing.List[float],
    beats: typing.List[float],
    length: typing.Optional[float] = None,
    probabilities: typing.Any = 1.0,
) -> Motif

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

Motif.pitch_bend_ramp

classmethod Motif.pitch_bend_ramp(
    start: float,
    end: float,
    beat_start: float = 0.0,
    beat_end: typing.Optional[float] = None,
    shape: typing.Union[subsequence.declarations.EasingCurve, subsequence.easing.EasingFn] = 'linear',
    length: typing.Optional[float] = None,
    probability: float = 1.0,
) -> Motif

Pitch bend swept start → end (-1.0 to 1.0) over a beat range - mirrors p.pitch_bend_ramp().

Motif.nrpn

classmethod Motif.nrpn(
    parameter: typing.Union[int, str],
    values: typing.List[int],
    beats: typing.List[float],
    fine: bool = False,
    null_reset: bool = True,
    length: typing.Optional[float] = None,
    probabilities: typing.Any = 1.0,
) -> Motif

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

Motif.nrpn_ramp

classmethod Motif.nrpn_ramp(
    parameter: typing.Union[int, str],
    start: int,
    end: int,
    beat_start: float = 0.0,
    beat_end: typing.Optional[float] = None,
    shape: typing.Union[subsequence.declarations.EasingCurve, subsequence.easing.EasingFn] = 'linear',
    fine: bool = True,
    null_reset: bool = True,
    length: typing.Optional[float] = None,
    probability: float = 1.0,
) -> Motif

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

Motif.rpn

classmethod Motif.rpn(
    parameter: typing.Union[int, subsequence.declarations.RpnParameter],
    values: typing.List[int],
    beats: typing.List[float],
    fine: bool = False,
    null_reset: bool = True,
    length: typing.Optional[float] = None,
    probabilities: typing.Any = 1.0,
) -> Motif

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

Motif.rpn_ramp

classmethod Motif.rpn_ramp(
    parameter: typing.Union[int, subsequence.declarations.RpnParameter],
    start: int,
    end: int,
    beat_start: float = 0.0,
    beat_end: typing.Optional[float] = None,
    shape: typing.Union[subsequence.declarations.EasingCurve, subsequence.easing.EasingFn] = 'linear',
    fine: bool = True,
    null_reset: bool = True,
    length: typing.Optional[float] = None,
    probability: float = 1.0,
) -> Motif

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

Motif.osc

classmethod Motif.osc(
    address: str,
    values: typing.List[float],
    beats: typing.List[float],
    length: typing.Optional[float] = None,
    probabilities: typing.Any = 1.0,
) -> Motif

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

Motif.osc_ramp

classmethod Motif.osc_ramp(
    address: str,
    start: float,
    end: float,
    beat_start: float = 0.0,
    beat_end: typing.Optional[float] = None,
    shape: typing.Union[subsequence.declarations.EasingCurve, subsequence.easing.EasingFn] = 'linear',
    length: typing.Optional[float] = None,
    probability: float = 1.0,
) -> Motif

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

Motif.then

Motif.then(other: Motif) -> Motif

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

Motif.join

classmethod Motif.join(motifs: typing.Iterable[Motif]) -> Motif

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

Motif.generate

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

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= sets which pitches the walk may use. A scale name is also the scale its degrees count in, so the line is exact in a composition using that scale, and under any other scale its degrees map step for step, as a typed motif's do: a "minor_pentatonic" line sounds its own notes in a minor pentatonic piece, and minor-scale degrees in a plain minor one. An interval list is a mask over major or minor instead ([0, 4, 7] plays C E G in C major), unless it is exactly one of the built-in scales. An explicit MIDI pitch pool (a list of note numbers) switches to absolute output (the sieve/atonal path).

Parameters

Example

hook = subsequence.Motif.generate(
        rhythm=[0, 1, 1.5, 1.75, 2.5], scale="minor_pentatonic",
        contour="arch", end_on=1, seed=7,
)

Motif.stack

Motif.stack(other: typing.Union[Motif, Phrase]) -> Motif

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.

Motif.slice

Motif.slice(start: float, end: float) -> Motif

A window onto the motif, on its own authority: events starting outside are dropped. Beats shift so the window starts at 0.

A note keeps its whole duration even where that runs past the end of the window. The sequencer already lets a note ring into the next cycle, and cutting it here made a two-beat note at beat 3 last one beat under a four-beat pattern and two under an eight-beat one - the same phrase, played differently for no musical reason (#3010).

A ramp that began before the window is resumed, not dropped. It comes back as the piece of itself that falls inside, carrying where in the whole curve that piece sits, so it plays its own stretch of the real sweep. An eight-beat cc_ramp(74, 0, 127) walked four beats at a time used to send the first half and then nothing, and the filter sat at 64 for the rest of the piece.

Motif.reverse

Motif.reverse() -> Motif

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

Onsets mirror around the downbeat, as p.reverse() does, so a figure on the grid stays on it and reversing twice gives it back: notes on beats 0, 1, 2 and 3 come back on 0, 3, 2 and 1. A ramp runs the other way over its mirrored span with its curve mirrored too, so an ease_in sweep comes back as an ease_out one, and a slice of a ramp plays the mirrored slice. A control write holds its value until the next one, so the steps of a stepped gesture come back in reverse order; a write on the length itself closes the gesture, and stays.

Motif.rotate

Motif.rotate(beats: float) -> Motif

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

Motif.stretch

Motif.stretch(factor: float) -> Motif

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

Motif.quantize

Motif.quantize(grid: float) -> Motif

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

An onset exactly midway between grid lines snaps LATER (round half up) - every midpoint moves the same way, the predictable behaviour for a musician. (Python's own round() is half-to-even, which made exact midpoints snap in alternating directions.)

An onset that snaps to the end of the motif moves to its start, since the end is the next downbeat: hits at 1, 2 and 3.8 in four beats, snapped to half beats, play at 0, 1 and 2.

Motif.accent

Motif.accent(beat: float, amount: int = 20) -> Motif

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

Motif.with_velocity

Motif.with_velocity(
    velocity: subsequence.declarations.VelocityValue,
) -> Motif

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

Motif.vary

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

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

Example

answer = call.vary(notes=1, seed=4)     # same figure, new tail note

Motif.answer

Motif.answer(to: typing.Union[int, Degree] = 1) -> Motif

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.

Motif.pitched

Motif.pitched(spec: PitchSpec) -> Motif

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.

Motif.rhythm

Motif.rhythm() -> Motif

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).

Motif.onsets

Motif.onsets() -> typing.List[float]

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

Motif.transpose

Motif.transpose(
    steps: typing.Optional[int] = None,
    semitones: typing.Optional[int] = None,
) -> Motif

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 - and a captured drum raises too, because its number still remembers which instrument it came from.

Motif.invert

Motif.invert(
    pivot: typing.Optional[typing.Union[int, Degree]] = None,
) -> Motif

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, captured ones included.

By default the first note is the pivot, and it stays exactly where it is. Degrees mirror in scale steps, so a note two steps above the pivot lands two steps below it, an octave higher lands an octave lower, and a line may go below the tonic. A note on the pivot's own step keeps its accidental; every other accidental flips, sharp to flat, so the mirror keeps to the key: in C major [^3b1, ^5, ^1] (E♭ G C) becomes E♭ C G. Inverting twice gives back the motif.

An int pivot is a MIDI note for MIDI content and a scale step for degree content; a Degree pivot also gives an octave and an accidental. A motif mixing MIDI notes and degrees raises, since one pivot cannot mirror both.

Motif.describe

Motif.describe() -> str

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

Phrase

class Phrase(
    segments: typing.Iterable[Motif],
    recipe: typing.Optional[_PhraseRecipe] = None,
)

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.

Members: describe, develop, flatten, invert, length, pitched, quantize, recipe, replace, reroll, reverse, rhythm, rotate, segments, slice, stack, stretch, transpose, with_velocity

Phrase.segments

Phrase.segments: typing.Tuple[Motif, ...]

Phrase.recipe

Phrase.recipe: typing.Optional[_PhraseRecipe]

Phrase.length

property Phrase.length: float

Total length in beats (sum of segment lengths).

Phrase.develop

classmethod Phrase.develop(
    motif: Motif,
    bars: int = 8,
    plan: typing.Optional[typing.Union[typing.Sequence[str], str]] = None,
    seed: typing.Optional[int] = None,
    beats_per_bar: float = 4.0,
) -> Phrase

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 labels - plan=["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

Example

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

Phrase.reroll

Phrase.reroll(
    bar: typing.Optional[int] = None,
    bars: typing.Optional[typing.Sequence[int]] = None,
    seed: typing.Optional[int] = None,
) -> Phrase

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 a fresh per-bar stream salted by seed= (an unseeded call draws a fresh salt, so each call genuinely differs); 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

Example

lead = lead.reroll(bar=7, seed=4)    # only bar 7; rhythm + boundaries kept

Phrase.flatten

Phrase.flatten() -> Motif

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

Phrase.stack

Phrase.stack(other: typing.Union[Motif, Phrase]) -> Motif

The spelled form of & - flattens, then merges.

Phrase.slice

Phrase.slice(start: float, end: float) -> Phrase

A window, re-segmented at whichever original boundaries fall inside it.

The window is taken from the whole timeline and then divided up, rather than each segment being sliced against its own bounds. The difference is what happens to something that crosses an internal boundary: a note ringing over it, or a ramp sweeping across it, used to lose everything past the edge of the segment it started in - which undid rotate's promise that a note may ring past its segment, and meant phrase.slice(0, phrase.length) was not the phrase (#3010).

Each event belongs to the segment its onset falls in, and goes there whole.

Phrase.replace

Phrase.replace(position: int, motif: Motif) -> Phrase

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

Phrase.reverse

Phrase.reverse() -> Phrase

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

Phrase.rotate

Phrase.rotate(beats: float) -> Phrase

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

Phrase.stretch

Phrase.stretch(factor: float) -> Phrase

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

Phrase.quantize

Phrase.quantize(grid: float) -> Phrase

Snap note onsets segment-wise.

Phrase.with_velocity

Phrase.with_velocity(
    velocity: subsequence.declarations.VelocityValue,
) -> Phrase

Replace every note's velocity, segment-wise.

Phrase.pitched

Phrase.pitched(spec: PitchSpec) -> Phrase

Replace every pitch, segment-wise.

Phrase.rhythm

Phrase.rhythm() -> Phrase

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

Phrase.transpose

Phrase.transpose(
    steps: typing.Optional[int] = None,
    semitones: typing.Optional[int] = None,
) -> Phrase

Transpose every segment (see Motif.transpose).

Phrase.invert

Phrase.invert(
    pivot: typing.Optional[typing.Union[int, Degree]] = None,
) -> Phrase

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

Phrase.describe

Phrase.describe() -> str

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

motif

motif(
    degrees: typing.List[typing.Union[int, Degree, None]],
    beats: typing.Optional[typing.List[float]] = None,
    velocities: typing.Any = _DEFAULT_VELOCITY,
    durations: typing.Any = 1.0,
    probabilities: typing.Any = 1.0,
    length: typing.Optional[float] = None,
) -> Motif

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.

None is a rest, and it holds its slot: motif([1, 3, 5, None]) is four beats long, not three, so a figure that ends in silence still loops on the bar it was written for.

sentence

sentence(
    motif: Motif,
    bars: int = 8,
    cadence: str = 'strong',
    seed: typing.Optional[int] = None,
    beats_per_bar: float = 4.0,
) -> Phrase

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

Example

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

period

period(
    antecedent: typing.Union[Motif, Phrase],
    cadence: str = 'strong',
    beats_per_bar: float = 4.0,
) -> Phrase

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

Example

idea = subsequence.motif([3, 4, 5, 1, None, 6, 5, 4], length=8)
lead = subsequence.period(idea)        # 16 beats: half-close, then home

Degree

class Degree(step: int, octave: int = 0, chroma: int = 0)

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).

A line can also go below the tonic: 0 is the step just under it and -1 the one under that, so transpose(steps=-1) and invert() work on a melody that starts on the tonic. A 0 typed into a melody is still refused, as the slip it usually is: motif([0, 1, 2]) raises.

Members: chroma, octave, step

Degree.step

Degree.step: int

Degree.octave

Degree.octave: int = 0

Degree.chroma

Degree.chroma: int = 0

ChordTone

class ChordTone(index_or_name: typing.Union[int, str], octave: int = 0)

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.

Normalise a tone name to its 1-based index.

Members: index, octave

ChordTone.index

ChordTone.index: int

ChordTone.octave

ChordTone.octave: int = 0

Approach

class Approach(target: typing.Union[int, Degree, ChordTone])

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.

Approach.target

Approach.target: typing.Union[int, Degree, ChordTone]

MotifEvent

class MotifEvent(
    beat: float,
    pitch: PitchSpec,
    velocity: subsequence.declarations.VelocityValue = _DEFAULT_VELOCITY,
    duration: float = 0.25,
    probability: float = 1.0,
    origin: typing.Optional[str] = None,
    fit: typing.Optional[float] = None,
)

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.

origin names the drum a pitch was resolved from. Only capture sets it - capture reads notes back as absolute MIDI, so "kick" arrives here as 36 - and it is what lets the pitch-moving methods go on refusing a drum they can no longer see.

fit is how likely the note is to move to a chord tone when it lands on a strong beat, 0.0 to 1.0. Motif.generate gives each note it makes a fit of 0.7; a note written by hand has none and plays exactly as written. The fit travels with the note through every transform, so a generated line keeps its fit however it is cut, joined or stacked, and never lends it to a written one.

Members: beat, duration, fit, origin, pitch, probability, velocity

MotifEvent.beat

MotifEvent.beat: float

MotifEvent.pitch

MotifEvent.pitch: PitchSpec

MotifEvent.velocity

MotifEvent.velocity: subsequence.declarations.VelocityValue = _DEFAULT_VELOCITY

MotifEvent.duration

MotifEvent.duration: float = 0.25

MotifEvent.probability

MotifEvent.probability: float = 1.0

MotifEvent.origin

MotifEvent.origin: typing.Optional[str] = None

MotifEvent.fit

MotifEvent.fit: typing.Optional[float] = None

ControlEvent

class ControlEvent(
    beat: float,
    signal: ControlSignal,
    start: float,
    end: typing.Optional[float] = None,
    span: float = 0.0,
    shape: typing.Union[subsequence.declarations.EasingCurve, subsequence.easing.EasingFn] = 'linear',
    probability: float = 1.0,
    shape_from: float = 0.0,
    shape_to: float = 1.0,
)

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 start → end over span beats through the easing shape.

shape_from and shape_to say which part of the whole gesture this is, as fractions of it: 0.0 and 1.0 for all of it. A ramp that a phrase's window cuts through keeps the whole ramp's start, end and shape, and plays only its own stretch of that curve, from shape_from to shape_to, rather than a fresh ramp between two sampled values. Pulse density (resolution=) is deliberately not stored here - beats and shapes are music; MIDI traffic density is set at the placement call.

Members: beat, end, is_partial, probability, shape, shape_from, shape_to, signal, span, start

ControlEvent.beat

ControlEvent.beat: float

ControlEvent.signal

ControlEvent.signal: ControlSignal

ControlEvent.start

ControlEvent.start: float

ControlEvent.end

ControlEvent.end: typing.Optional[float] = None

ControlEvent.span

ControlEvent.span: float = 0.0

ControlEvent.shape

ControlEvent.shape: typing.Union[subsequence.declarations.EasingCurve, subsequence.easing.EasingFn] = 'linear'

ControlEvent.probability

ControlEvent.probability: float = 1.0

ControlEvent.shape_from

ControlEvent.shape_from: float = 0.0

ControlEvent.shape_to

ControlEvent.shape_to: float = 1.0

ControlEvent.is_partial

property ControlEvent.is_partial: bool

True when this is a piece of a longer gesture rather than all of one.