subsequence.motifs ================== .. py:module:: subsequence.motifs .. autoapi-nested-parse:: Motif and Phrase — immutable musical values. A :class:`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 :class:`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 :class:`Degree` or :class:`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 --------------- .. py:class:: Approach 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. .. py:class:: CC A MIDI CC signal — number, or a name resolved at placement via the pattern's ``cc_name_map``. .. py:class:: ChordTone(index_or_name: 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. Normalize a tone name to its 1-based index. .. py:class:: ControlEvent 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``. Pulse density (``resolution=``) is deliberately not stored here — beats and shapes are music; MIDI traffic density is set at the placement call. .. py:class:: Degree 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). .. py:class:: Motif An immutable musical figure: timed note events + control gestures + a length in beats. Construct via the classmethods (:meth:`degrees`, :meth:`notes`, :meth:`hits`, :meth:`steps`, :meth:`euclidean`, the control-gesture constructors, or :meth:`from_events`) rather than positionally. ``length`` is explicit — a trailing rest is meaningful. .. py:method:: accent(beat: float, amount: int = 20) -> Motif Add *amount* velocity to every note at the given beat position (0-based beats). .. py:method:: answer(to: 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. .. py:method:: cc(control: Union[int, str], values: List[int], beats: List[float], length: Optional[float] = None, probabilities: Any = 1.0) -> Motif :classmethod: Discrete CC writes at beat positions — mirrors ``p.cc()``; names resolve at placement. .. py:method:: cc_ramp(control: Union[int, str], start: int, end: int, beat_start: float = 0.0, beat_end: Optional[float] = None, shape: Union[str, subsequence.easing.EasingFn] = 'linear', length: Optional[float] = None, probability: float = 1.0) -> Motif :classmethod: A CC value swept ``start`` → ``end`` over a beat range — mirrors ``p.cc_ramp()``. .. py:method:: degrees(degrees: List[Union[int, Degree, None]], beats: Optional[List[float]] = None, velocities: Any = _DEFAULT_VELOCITY, durations: Any = 1.0, probabilities: Any = 1.0, length: Optional[float] = None) -> Motif :classmethod: 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 :class:`Degree` for octave/ chromatic detail. Resolved against key + scale at placement. Durations default to a full beat (each note holds its slot). .. py:method:: describe() -> str A readable one-line summary: length, notes (pitch@beat), and control gestures. .. py:method:: empty() -> Motif :classmethod: The empty motif (zero events, zero length) — the identity for ``then``. .. py:method:: euclidean(pulses: int, steps: int, pitch: Union[int, str], length: float = 4.0, velocities: Any = _DEFAULT_VELOCITY, durations: Any = 0.1, probabilities: Any = 1.0) -> Motif :classmethod: A euclidean rhythm as a value: *pulses* spread evenly across *steps* over *length* beats. .. py:method:: from_events(events: Iterable[MotifEvent], length: Optional[float] = None, controls: Iterable[ControlEvent] = ()) -> Motif :classmethod: Build a motif from explicit events (power use; length defaults to the next whole beat). .. py:method:: generate(rhythm: Any, length: Optional[float] = None, scale: Optional[Union[str, Sequence[int]]] = None, contour: Optional[str] = None, end_on: Optional[Union[int, Degree]] = None, cadence: Optional[str] = None, pins: Optional[Dict[int, Union[int, Degree]]] = None, max_pitches: Optional[int] = None, velocities: Any = _DEFAULT_VELOCITY, durations: Any = 0.25, seed: Optional[int] = None, rng: Optional[random.Random] = None, state: Optional[Any] = None, nir_strength: float = 0.5, pitch_diversity: float = 0.6, tessitura_strength: float = 0.6) -> Motif :classmethod: 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). :param rhythm: Onset beats (``[0, 1, 1.5, 1.75, 2.5]``) or a Motif (its onsets are borrowed). :param length: Motif length in beats; defaults to the onsets rounded up to a whole 4-beat bar. :param scale: A scale name, an interval list, or an explicit MIDI pitch pool. ``None`` = the plain seven degrees. :param contour: Envelope shaping the line's height over its span — ``"arch"``, ``"valley"``, ``"ascending"``, ``"descending"``. :param end_on: Degree the line must end on — sugar for ``pins={-1: ...}``. :param 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. :param pins: ``{position: degree}`` — 1-based note positions (``-1`` = the last, the Python idiom); the engine fills between. :param max_pitches: Cap on distinct pitches (a tight pool is a hook); keeps the most central candidates. :param velocities / durations: Scalar or per-note list (the parallel- list convention). :param seed: Seed for the walk (required or warned — module-level nondeterminism breaks live reload). :param rng: Explicit stream (overrides ``seed``). :param state: A ``MelodicState`` whose settings and history seed the walk. It is **copied** — building a value never mutates a module-level live object. :param nir_strength / pitch_diversity / tessitura_strength: The walk's dials when no ``state`` is given. .. rubric:: Example .. code-block:: python hook = subsequence.Motif.generate( rhythm=[0, 1, 1.5, 1.75, 2.5], scale="minor_pentatonic", contour="arch", end_on=1, seed=7, ) .. py:method:: hits(pitch: Union[int, str], beats: List[float], length: Optional[float] = None, velocities: Any = _DEFAULT_VELOCITY, durations: Any = 0.1, probabilities: Any = 1.0) -> Motif :classmethod: One pitch (usually a drum name) at a list of beat positions — the ``hit()`` convention. .. py:method:: invert(pivot: Optional[int] = 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. .. py:method:: join(motifs: Iterable[Motif]) -> Motif :classmethod: Fold a list of motifs into one with ``then`` (empty list → ``Motif.empty()``). .. py:method:: notes(notes: List[Union[int, None]], beats: Optional[List[float]] = None, velocities: Any = _DEFAULT_VELOCITY, durations: Any = 1.0, probabilities: Any = 1.0, length: Optional[float] = None) -> Motif :classmethod: A melody written as absolute MIDI note numbers (60 = middle C); ``None`` = rest. .. py:method:: nrpn(parameter: Union[int, str], values: List[int], beats: List[float], fine: bool = False, null_reset: bool = True, length: Optional[float] = None, probabilities: Any = 1.0) -> Motif :classmethod: Discrete NRPN parameter writes at beat positions — mirrors ``p.nrpn()``. .. py:method:: nrpn_ramp(parameter: Union[int, str], start: int, end: int, beat_start: float = 0.0, beat_end: Optional[float] = None, shape: Union[str, subsequence.easing.EasingFn] = 'linear', fine: bool = True, null_reset: bool = True, length: Optional[float] = None, probability: float = 1.0) -> Motif :classmethod: An NRPN value swept over a beat range — mirrors ``p.nrpn_ramp()``. .. py:method:: onsets() -> List[float] The note onset beats, in order — ready for rhythm-first generation. .. py:method:: osc(address: str, values: List[float], beats: List[float], length: Optional[float] = None, probabilities: Any = 1.0) -> Motif :classmethod: Discrete OSC float sends at beat positions — mirrors ``p.osc()``. .. py:method:: osc_ramp(address: str, start: float, end: float, beat_start: float = 0.0, beat_end: Optional[float] = None, shape: Union[str, subsequence.easing.EasingFn] = 'linear', length: Optional[float] = None, probability: float = 1.0) -> Motif :classmethod: An OSC float swept over a beat range — mirrors ``p.osc_ramp()``. .. py:method:: pitch_bend(values: List[float], beats: List[float], length: Optional[float] = None, probabilities: Any = 1.0) -> Motif :classmethod: Discrete pitch-bend writes (-1.0 to 1.0) at beat positions — mirrors ``p.pitch_bend()``. .. py:method:: pitch_bend_ramp(start: float, end: float, beat_start: float = 0.0, beat_end: Optional[float] = None, shape: Union[str, subsequence.easing.EasingFn] = 'linear', length: Optional[float] = None, probability: float = 1.0) -> Motif :classmethod: Pitch bend swept ``start`` → ``end`` (-1.0 to 1.0) over a beat range — mirrors ``p.pitch_bend_ramp()``. .. py:method:: 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. .. py:method:: preset(name: str, pitch: Optional[Union[int, str]] = None, length: float = 4.0, velocities: Any = _DEFAULT_VELOCITY, durations: Any = 0.1, probabilities: Any = 1.0) -> Motif :classmethod: 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. :param name: A preset name (``KeyError``-style ValueError lists them all). :param 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=``. :param length: Total beats the cycle spans (4 = one common-time bar). :param velocities / durations / probabilities: The parallel-list params. :returns: A drum/pitched :class:`Motif` of the timeline's onsets. :raises ValueError: If *name* is not a known preset. .. rubric:: Example .. code-block:: python clave = subsequence.Motif.preset("son_clave_3_2") # GM "claves" bell = subsequence.Motif.preset("bembe", pitch="cowbell") # 12-pulse .. py:method:: quantize(grid: float) -> Motif Snap note onsets to the nearest multiple of *grid* beats (control gestures untouched). .. py:method:: reverse() -> Motif Mirror the figure in time; ramps swap direction (a rising sweep falls). .. py:method:: rhythm() -> Motif Strip pitches (and control gestures): a reusable rhythmic skeleton. Timing, velocities, durations, and probabilities survive; re-pitch with :meth:`pitched` before placement (placing a skeleton raises). .. py:method:: rotate(beats: float) -> Motif Shift every onset by *beats*, wrapping modulo the length (spans ride along). .. py:method:: rpn(parameter: Union[int, str], values: List[int], beats: List[float], fine: bool = False, null_reset: bool = True, length: Optional[float] = None, probabilities: Any = 1.0) -> Motif :classmethod: Discrete RPN parameter writes at beat positions — mirrors ``p.rpn()``. .. py:method:: rpn_ramp(parameter: Union[int, str], start: int, end: int, beat_start: float = 0.0, beat_end: Optional[float] = None, shape: Union[str, subsequence.easing.EasingFn] = 'linear', fine: bool = True, null_reset: bool = True, length: Optional[float] = None, probability: float = 1.0) -> Motif :classmethod: An RPN value swept over a beat range — mirrors ``p.rpn_ramp()``. .. py:method:: slice(start: float, end: float) -> Motif 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. .. py:method:: stack(other: 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. .. py:method:: steps(steps: List[int], pitches: Any, velocities: Any = _DEFAULT_VELOCITY, durations: Any = 0.1, probabilities: Any = 1.0, step_duration: float = 0.25, length: Optional[float] = None) -> Motif :classmethod: 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. .. py:method:: stretch(factor: float) -> Motif Scale time by *factor* (2.0 = half-time feel): beats, durations, spans, and length. .. py:method:: then(other: Motif) -> Motif Closed sequential concat: glue *other* after this motif into ONE longer motif. .. py:method:: transpose(steps: Optional[int] = None, semitones: 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. .. py:method:: vary(notes: int = 1, position: str = 'end', seed: Optional[int] = None, rng: 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). :param notes: How many pitched notes to vary (clamped to what exists). :param position: Which notes — ``"end"`` (the tail, the default), ``"start"``, or ``"anywhere"`` (drawn from the stream). :param seed: Seed for the variation. A standalone vary without a seed warns — module-level nondeterminism breaks live reload. :param rng: An explicit random stream (overrides ``seed``; used by recipe machinery). :param 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. .. rubric:: Example .. code-block:: python answer = call.vary(notes=1, seed=4) # same figure, new tail note .. py:method:: with_velocity(velocity: Union[int, Tuple[int, int]]) -> Motif Replace every note's velocity (an int, or a ``(low, high)`` random range). .. py:class:: MotifEvent One timed note event inside a Motif. ``pitch`` is a specification: an absolute MIDI int, a drum name string, a :class:`Degree`, :class:`ChordTone`, or :class:`Approach` — or None for a pitch-stripped skeleton event (see :meth:`Motif.rhythm`), which must be re-pitched via :meth:`Motif.pitched` before placement. ``velocity`` is an int or a ``(low, high)`` random-range tuple. .. py:class:: NRPN An NRPN parameter — number, or a name resolved at placement via the pattern's ``nrpn_name_map``. .. py:class:: OSC An OSC address; values are sent as the single float argument. .. py:class:: Phrase(segments: Iterable[Motif], recipe: 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 :meth:`develop` carries its recipe, so :meth:`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. .. py:method:: describe() -> str A readable summary: total length and each segment on its own line. .. py:method:: develop(motif: Motif, bars: int = 8, plan: Optional[Union[Sequence[str], str]] = None, seed: Optional[int] = None, beats_per_bar: float = 4.0) -> Phrase :classmethod: 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 :meth:`reroll` can regenerate a region later. :param motif: The source unit (its length must be ``bars / len(units)`` bars — the plan's units tile the phrase exactly). :param bars: Phrase length in bars (must divide evenly by the unit count). :param plan: A list of unit labels, or a recipe name. :param seed: Seed for the generated units. Without one, develop() warns — module-level nondeterminism breaks live reload. :param beats_per_bar: Bar size in beats (the value is context-free; 4 is the common-time default). .. rubric:: Example .. code-block:: python call = subsequence.motif([5, 6, 5, 3, None, 1, 2, 3]) lead = subsequence.Phrase.develop(call, bars=8, plan="call_response", seed=11) .. py:method:: flatten() -> Motif Erase segmentation: one long Motif (the monoid homomorphism onto ``then``). .. py:method:: invert(pivot: Optional[int] = None) -> Phrase Mirror pitches in every segment around one pivot (see :meth:`Motif.invert`). .. py:method:: pitched(spec: PitchSpec) -> Phrase Replace every pitch, segment-wise. .. py:method:: quantize(grid: float) -> Phrase Snap note onsets segment-wise. .. py:method:: replace(position: int, motif: Motif) -> Phrase Replace the segment at a 1-based position (musicians count from one). .. py:method:: reroll(bar: Optional[int] = None, bars: Optional[Sequence[int]] = None, seed: 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 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). :param bar: A single 1-based bar to reroll. :param bars: A list of 1-based bars (the paired plural spelling). :param seed: Seed for the new pitches (salted per bar). Without one, reroll() warns. .. rubric:: Example .. code-block:: python lead = lead.reroll(bar=7, seed=4) # only bar 7; rhythm + boundaries kept .. py:method:: reverse() -> Phrase Reverse the whole timeline: segments reverse order AND each reverses internally. .. py:method:: rhythm() -> Phrase Strip pitches segment-wise: a phrase-shaped skeleton. .. py:method:: rotate(beats: float) -> Phrase Rotate the whole timeline modulo the total length, then re-segment at the original boundaries. .. py:method:: slice(start: float, end: float) -> Phrase A window; re-segments at the cut points (partial segments are sliced). .. py:method:: stack(other: Union[Motif, Phrase]) -> Motif The spelled form of ``&`` — flattens, then merges. .. py:method:: stretch(factor: float) -> Phrase Scale time in every segment (lengths scale with them). .. py:method:: transpose(steps: Optional[int] = None, semitones: Optional[int] = None) -> Phrase Transpose every segment (see :meth:`Motif.transpose`). .. py:method:: with_velocity(velocity: Union[int, Tuple[int, int]]) -> Phrase Replace every note's velocity, segment-wise. .. py:property:: length :type: float Total length in beats (sum of segment lengths). .. py:class:: PitchBend The channel pitch-bend wheel; values are normalised -1.0 to 1.0. .. py:class:: RPN An RPN parameter — number, or one of the standard RPN names (resolved at placement). .. py:function:: motif(degrees: List[Union[int, Degree, None]], beats: Optional[List[float]] = None, velocities: Any = _DEFAULT_VELOCITY, durations: Any = 1.0, probabilities: Any = 1.0, length: 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. .. py:function:: period(antecedent: 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)``. :param antecedent: The first half — a Motif, or a Phrase whose segmentation is kept (only its last segment's tail re-aims). :param cadence: The consequent's close — ``"strong"`` lands on 1 (theory aliases accepted). :param beats_per_bar: Bar size in beats, recorded for ``reroll()`` windows. .. rubric:: Example .. code-block:: python idea = subsequence.motif([3, 4, 5, 1, None, 6, 5, 4], length=8) lead = subsequence.period(idea) # 16 beats: half-close, then home .. py:function:: sentence(motif: Motif, bars: int = 8, cadence: str = 'strong', seed: 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. :param motif: The basic idea (degree content — the close re-aims a degree). :param bars: Sentence length (must divide evenly across the 4 units). :param cadence: The close — ``"strong"`` lands on 1, ``"open"`` on 5, ``"soft"``/``"fakeout"`` on 1 (theory aliases accepted). :param seed: Seed for the generated continuation units (seed-or-warn). :param beats_per_bar: Bar size in beats (context-free; 4 is the default). .. rubric:: Example .. code-block:: python idea = subsequence.motif([5, 6, 5, 3, None, 1, 2, 3]) verse_lead = subsequence.sentence(idea, bars=8, cadence="open", seed=11)