Chapter 8 · Motifs: Naming and Reusing Material¶
In Chapter 7 you learned to write harmony down as a value — a progression you build once, inspect, shape, and place. This chapter does the same for melody and rhythm: you name a short figure as a Motif, transform it with a small algebra, and play it under the moving chord.
A motif is the smallest unit of musical material — a four-note hook, a clave, a bass cell. In Subsequence it’s a value, exactly like a progression: you store it once at the top of your file, and every transform hands you back a new motif instead of changing the one you wrote. That single rule — build once, transform into copies — is what lets a whole part grow from one idea without ever losing the original. It’s the same separation you already know: describe the material first, decide how it sounds second.
8.1 What a Motif is (an immutable value)¶
A Motif is a frozen figure: a handful of timed notes and a length in beats.
It is not a pattern, and it makes no sound by itself — just as a progression is not
a chord part until something places it. You build a motif, look at it, reshape it,
and only then hand it to a pattern to be played.
The defining property is that a motif is immutable. Once built, it never
changes. Every method that “edits” a motif — transpose it, reverse it, vary its
last note — leaves the original exactly as it was and returns a brand-new motif
with the change applied. This is the same contract a Progression follows
(§7.3): transforms return new values, the original is sacred.
Important
A motif is a value you store, not a pattern that runs. You typically build your motifs once, at module level (the top of your file), give them clear names, and reuse them across patterns — precisely the module-level reusable values habit from §2.4. A pattern function still re-runs every cycle, but the motifs it places are fixed; what changes cycle to cycle is the chord they land on.
Like a progression, a motif prints itself, so you can check what you wrote
before rendering a note. print(m) calls its .describe():
import subsequence
from subsequence import motif, Motif
import subsequence.constants.instruments.gm_drums as gm_drums
import subsequence.constants.midi_notes as notes
hook = motif([5, 6, 5, 3]) # a four-note figure in scale degrees
print(hook.describe())
Motif 4 beats [^5@0, ^6@1, ^5@2, ^3@3]
Read the printout as “a four-beat figure: degree 5 at beat 0, degree 6 at beat 1,
degree 5 at beat 2, degree 3 at beat 3.” The ^5 is Subsequence’s compact label
for scale degree 5 — the figure stores degrees, not fixed pitches, so it’s a
specification resolved late, the same idea you met for a single pitch in
§5.5 and for a whole chord in §6.5.
We’ll see exactly what those degrees resolve to in §8.5.
8.2 Building one: motif(), degrees, notes, steps¶
There are three everyday ways to write a figure down, depending on whether you’re
thinking in scale degrees, fixed pitches, or a drum grid. Each is a factory that
returns a Motif.
Scale degrees — motif([...]). The lowercase motif(...) is the shortcut you
just used: a melody as 1-based scale degrees, one per beat by default. Degree
1 is the tonic, 8 is the tonic an octave up. This is the relative-first
form — write the shape, and it transposes to any key for free.
hook = motif([5, 6, 5, 3]) # one degree per beat
print(hook.describe())
Motif 4 beats [^5@0, ^6@1, ^5@2, ^3@3]
motif([5, 6, 5, 3]) is exactly Motif.degrees([5, 6, 5, 3]) — the lowercase
name is just the shortcut, the same way progression([...]) shortcuts the
Progression factory. A None in the list is a rest (the beat slot still
advances), and you can pin the timing yourself with a beats= list and the lengths
with durations=:
# A syncopated figure: a rest in the second slot, the rest on chosen beats.
synced = Motif.degrees([5, None, 6, 1], beats=[0, 1, 1.5, 3])
print(synced.describe())
Motif 4 beats [^5@0, ^6@1.5, ^1@3]
Absolute pitches — Motif.notes([...]). When you want fixed MIDI note
numbers rather than key-relative degrees — a sampled riff, a melody you already
know in concrete pitches — use Motif.notes(...). Here 60 is middle C, and again
None is a rest:
riff = Motif.notes([64, 65, 64, 60]) # E F E C, exact pitches
print(riff.describe())
Motif 4 beats [64@0, 65@1, 64@2, 60@3]
Important
Degrees follow the key; absolute notes don’t. A motif([5, 6, 5, 3]) stores
degrees, so it plays “the 5th, 6th, 5th, 3rd of whatever key the composition is
in” — change the key and it transposes itself. Motif.notes([64, 65, 64, 60])
stores MIDI numbers, so it always plays E–F–E–C regardless of key. Reach for
degrees when you want the figure to live inside the harmony, and for notes when you
mean those exact pitches. (Degrees above the octave are fine — degree 9 is the
2nd a register up — but paste a MIDI list like [64, 65] into motif() by mistake
and it raises: a value that large can only be a MIDI number, not a scale degree, so
the slip fails loudly instead of squealing octaves up.)
A drum/grid figure — Motif.steps(...). For percussion you think in grid
steps, exactly like p.hit_steps from §1.4. Motif.steps(...)
takes a list of 0-based step indices (sixteenths by default) and the drum to place
on them — a rhythmic cell you can name and reuse:
clave_cell = Motif.steps([0, 3, 6, 10, 12], pitches="side_stick")
print(clave_cell.describe())
Motif 4 beats [side_stick@0, side_stick@0.75, side_stick@1.5, side_stick@2.5, side_stick@3]
The step indices became beat positions (step 3 of a sixteenth grid is beat 0.75),
and the drum name rides along untouched — drum names resolve through the pattern’s
drum map at placement, just as they do in a hand-written beat.
Factory |
You write |
Reach for it when |
|---|---|---|
|
1-based scale degrees |
You want a melody that lives in the key and transposes for free (the default). |
|
Absolute MIDI numbers |
You mean those exact pitches, key or no key. |
|
0-based grid steps + a drum |
You’re writing a rhythmic/percussion cell, |
8.3 Transforms return NEW values¶
Here is the heart of working with motifs, and the one idea to hold onto: every transform returns a new motif and leaves the original untouched. You never modify a motif in place; you derive variations from it. A whole melody’s worth of material comes from one stored figure plus a few of these calls.
Watch the original survive a transform:
hook = motif([5, 6, 5, 3])
higher = hook.transpose(steps=2) # everything up two scale steps
print("derived: ", higher.describe())
print("original:", hook.describe()) # ← unchanged
derived: Motif 4 beats [^7@0, ^8@1, ^7@2, ^5@3]
original: Motif 4 beats [^5@0, ^6@1, ^5@2, ^3@3]
hook.transpose(steps=2) handed back a new figure a third higher; hook itself
is byte-for-byte what you wrote. That’s immutability doing its job — higher and
hook are two independent values, both available, neither aliasing the other.
These are the five transforms you’ll reach for first. Each takes a motif and returns a fresh one:
transpose(...) — move the pitches. The keyword names the unit:
transpose(steps=2) moves diatonically (along the scale, the sequencing move),
while transpose(semitones=12) moves chromatically (by half-steps, e.g. up an
octave). You must pass exactly one.
hook = motif([5, 6, 5, 3])
print(hook.transpose(steps=2).describe()) # diatonic: degrees shift along the scale
Motif 4 beats [^7@0, ^8@1, ^7@2, ^5@3]
invert(...) — mirror the contour. Flip the figure upside-down around a pivot
(by default, the first note’s pitch): where the original rose, the inversion falls.
This is the classic motivic-development move that keeps the rhythm and the
intervals but turns the shape over.
print(hook.invert().describe()) # mirror around the first note (degree 5)
Motif 4 beats [^5@0, ^4@1, ^5@2, ^7@3]
reverse() — mirror in time. Play the figure backwards: the last note becomes
the first. The pitches are unchanged; their order is flipped.
print(hook.reverse().describe())
Motif 4 beats [^3@0, ^5@1, ^6@2, ^5@3]
vary(...) — the smallest change. Keep the rhythm, nudge a note or two by a
small melodic step. By default it varies the tail (the last note) — the loosest
possible restatement, the way a player never repeats a lick quite identically.
Because it makes a random choice, pass a seed= so the variation is the same on
every run (the repeatability habit from §4.6):
print(hook.vary(notes=1, seed=4).describe()) # same figure, new last note
Motif 4 beats [^5@0, ^6@1, ^5@2, ^2@3]
answer(...) — turn a call into a response. Re-aim the last note to a stable
degree so the figure sounds like it’s coming home. By default it lands on degree
1 (the tonic — a full close); pass to=5 for a half-close that hangs open. This
is the melodic counterpart to a cadence, and it pairs perfectly with the +
operator below to write a question-and-answer pair.
print(hook.answer().describe()) # tail re-aimed to the tonic (degree 1)
print(hook.answer(to=5).describe()) # tail re-aimed to degree 5 (half-close)
Motif 4 beats [^5@0, ^6@1, ^5@2, ^1@3]
Motif 4 beats [^5@0, ^6@1, ^5@2, ^5@3]
Transform |
What it returns (a NEW motif) |
|---|---|
|
The figure moved — diatonically ( |
|
The contour mirrored around a pivot (default: the first note). |
|
The figure played backwards in time. |
|
The same rhythm with a few pitches nudged — the loosest restatement. |
|
The tail re-aimed to a stable degree — call becomes response. |
Tip
These chain, like the progression transforms. Each call returns a motif, so the
next call hangs right off it: hook.transpose(steps=2).reverse().vary(notes=1, seed=1) reads left-to-right as a recipe — raise it, flip it in time, then nudge a
note. It’s the same “learn one verb, predict the rest” fluency you met for
generators in §4.5 and for harmony in
§7.4. And because the source motif is never mutated, you can
spin off as many independent variations from it as you like.
Note
Why some transforms refuse some content. transpose(steps=), invert(), and
answer() all work on scale degrees — they need a degree to move along the scale
or re-aim home. Call them on a drum figure (a Motif.steps(...) of "side_stick")
and they raise, on purpose: a “transposed” drum name would be a different
instrument, not a transposition, and a drum has no tonic to answer to. For
rhythmic material reach for reverse() and the timing transforms — rotate
(shift in time, wrapping), stretch (scale the tempo feel), quantize (snap
onsets to a grid) — which only touch when a note sounds, never its pitch.
(transpose(semitones=) is the chromatic move, but it needs a pitch to shift: it
works on absolute-MIDI melodies and degrees, and still raises on drum names.)
Reference
8.4 Combining: then / stack / slice and the + / * operators¶
A motif is small by design. You build longer and richer material by combining motifs — gluing them end to end, layering them, or windowing out a fragment. Three methods and two operators cover it, and they split cleanly into “sequential” (one after another) and “parallel” (at the same time).
then(other) — glue end to end into ONE motif. a.then(b) plays a, then b
right after it, fused into a single longer motif. Use it when you want one
seamless figure built from two cells:
call = motif([5, 6, 5, 3])
response = call.answer() # same figure, landing on the tonic
phrase = call.then(response) # one 8-beat figure: call, then response
print(phrase.describe())
Motif 8 beats [^5@0, ^6@1, ^5@2, ^3@3, ^5@4, ^6@5, ^5@6, ^1@7]
There’s the whole figure in one motif: the call at beats 0–3, then the response
shifted to beats 4–7, its tail now on degree 1 (the tonic). Eight beats long, no
seam — then fused the two cells into a single value.
+ — sequential, but keeping the seam. The + operator also puts one motif
after another, but it produces a Phrase — a sequence of motifs that
remembers the boundary between them, rather than fusing them. (A Phrase is the
subject of Chapter 9; for now, think “a motif that knows its
segments.”)
pair = call + response # a two-segment Phrase, not one fused Motif
print(pair.describe())
Phrase 8 beats, 2 segments
1. Motif 4 beats [^5@0, ^6@1, ^5@2, ^3@3]
2. Motif 4 beats [^5@0, ^6@1, ^5@2, ^1@3]
Important
then fuses; + keeps the seam. Both place b after a. a.then(b) returns
one Motif with the boundary erased — useful when you want a single indivisible
figure. a + b returns a Phrase that still knows where a ends and b begins —
useful when you’ll later develop or reroll the parts independently (Chapter 9).
Same musical result on playback; different handle on the structure.
* — repetition. Multiply a motif to repeat it. Like +, it produces a
Phrase (a chain of identical segments), so the repeats stay individually
addressable:
vamp = call * 2 # the call, twice — a 2-segment Phrase
print("length:", vamp.length, "beats")
length: 8.0 beats
stack(other) — layer two motifs at once. Where then and + are
sequential, stack is parallel: it merges two motifs so they sound
together, returning one motif containing both. This is how you put a
counter-line under a melody, or two percussion voices in one cell:
melody = motif([5, 6, 5, 3])
counter = motif([3, 4, 3, 1]) # a line a third below
both = melody.stack(counter) # the two played at the same time
print(both.describe())
Motif 4 beats [^3@0, ^5@0, ^4@1, ^6@1, ^3@2, ^5@2, ^1@3, ^3@3]
The two figures now share every beat — degree 3 and degree 5 at beat 0, and so
on. stack is also spelled with the & operator (melody & counter), exactly as
then’s sequential cousin + mirrors it. The length is the longer of the two —
a short gesture stacked under a long figure plays once and stops, no looping.
slice(start, end) — window out a fragment. Take just part of a motif, with
the beats shifted so the window starts at 0. Reach for it to grab the first cell of
a longer idea, or to develop only the tail:
opening = call.slice(0, 2) # just the first two beats
print(opening.describe())
Motif 2 beats [^5@0, ^6@1]
Operation |
Result |
What it does |
|---|---|---|
|
|
Sequential: glue |
|
|
Sequential: |
|
|
Repeat |
|
|
Parallel: |
|
|
A window onto |
8.5 Placing under harmony: p.motif, root, fit¶
A motif is still just a value — nothing has sounded yet. To play it, hand it to a
pattern with p.motif(m, beat=...). This is the seam between the material
(the stored value) and the performance (the running pattern), exactly like
p.progression for harmony.
Here is the running arrangement carried forward: the GM drum loop, plus a lead that plays our stored hook under the live chord engine from Chapter 6. The motif is built once at module level; the pattern just places it each cycle.
import subsequence
from subsequence import motif
import subsequence.constants.instruments.gm_drums as gm_drums
import subsequence.constants.midi_notes as notes
HOOK = motif([5, 6, 5, 3, 1]) # the named material, built once
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.harmony(style="aeolian_minor", cycle_beats=4)
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
p.hit_steps("snare_1", [4, 12], velocity=95)
p.hit_steps("hi_hat_closed", range(16), velocity=70)
@composition.pattern(channel=4, beats=4)
def lead(p):
p.motif(HOOK, beat=0, root=notes.A4) # place the figure, near A4
composition.render(bars=4, filename="motif-lead.mid")
Two arguments steer the placement:
beat=is where in the bar the figure starts.beat=0is the downbeat;beat=2would drop it onto beat 3.root=is the register anchor for scale-degree resolution. The figure’s degrees resolve against the composition’s key and scale, voiced near this MIDI note —notes.A4puts the melody up in a lead register,notes.A2down in the bass. It’s the same “give me this, around here” idea aschord.root_note(near)in §6.2: the degrees decide which notes, you decide where.
Note
This is “a pitch resolved late,” one more time. HOOK stores degrees 5 6 5 3 1 — abstract until placed. At render, p.motif(HOOK, root=notes.A4) resolves them
against A minor near A4, producing concrete MIDI notes. Change the composition’s
key= and the same HOOK plays a transposed melody, with no edit to the motif —
the structural-vs-sonic distinction from §5.5 and
§6.5, now for a whole figure instead of one note.
Snapping melody to the chord: the fit dial¶
By default, a hand-written degree plays exactly the degree you wrote — it follows
the key, but it does not bend toward the current chord. Sometimes that’s
what you want; sometimes you’d rather the melody lean into the harmony, landing on
chord tones on the strong beats. The fit= dial does that, with a probability
from 0.0 to 1.0:
import subsequence
from subsequence import motif
import subsequence.constants.midi_notes as notes
HOOK = motif([5, 6, 5, 3, 1])
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.harmony(style="aeolian_minor", cycle_beats=4)
@composition.pattern(channel=4, beats=4)
def lead(p):
# On strong beats, snap to the nearest chord tone — the melody hugs the chord.
p.motif(HOOK, beat=0, root=notes.A4, fit=1.0)
composition.render(bars=4, filename="motif-fit.mid")
With fit=1.0, every degree that lands on a strong beat snaps to the nearest tone
of the chord sounding under it; at fit=0.5 it snaps about half the time, for a
looser feel; at fit=0.0 (the default for hand-written motifs) it never snaps and
your degrees play verbatim.
Important
Typed degrees are sacred — fit is opt-in. A degree you wrote by hand defaults
to fit=0.0: it plays exactly as written, following the key but never silently
rewritten by the chord. You opt in to chord-snapping with fit= at the placement
call — it’s a deliberate dial, not a default. (Motifs that Subsequence generates
for you carry a gentle fit=0.7, since they have no authored intent to protect.
Chord-relative content always tracks the chord regardless of fit, because asking
for “the third of the chord” is already a harmony reading.)
Tip
fit needs a chord to snap to. It does nothing without harmony — call
composition.harmony(...) (a style, or a bound progression) first, exactly as in
Chapters 6 and 7. With no chord context, fit= is quietly inert and your degrees
play as written.
Reference
8.6 Capturing placed notes back: p.capture¶
Sometimes you want to go the other way: place a figure, let Subsequence resolve all
its degrees and chord-snapping into concrete notes, and then read those notes back
out as a new motif — to layer them an octave down, hand-edit them, or freeze them.
p.capture(beat, span) does exactly that: it reads the notes placed so far in
the current pattern and returns them as a Motif.
Here the lead places the hook, then captures the resolved notes and doubles them an octave lower as a second voice:
import subsequence
from subsequence import motif
import subsequence.constants.midi_notes as notes
HOOK = motif([5, 6, 5, 3, 1])
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.harmony(style="aeolian_minor", cycle_beats=4)
@composition.pattern(channel=4, beats=4)
def doubled_lead(p):
p.motif(HOOK, beat=0, root=notes.A4) # place the figure up high
echo = p.capture(beat=0, span=4) # read those notes back out
p.motif(echo, beat=0, root=notes.A3) # play them again, an octave down
composition.render(bars=4, filename="motif-capture.mid")
p.capture(beat=0, span=4) grabs the four-beat window from the start of the bar.
What comes back is an absolute-MIDI motif — the degrees have already resolved to
concrete pitches against the chord, so the captured figure is fixed notes, not
degrees.
Important
Capture is absolute and lossy — by design. The degrees, chord tones, and
fit-snapping that produced the placed notes have already resolved by the time you
capture, so the returned motif holds plain MIDI numbers (like Motif.notes([...])),
not the relative specs you started from. Timing is snapped to the MIDI grid, and
control gestures (CC, pitch-bend) aren’t captured. The intended round trip is
place → capture → hand-edit → re-place, not a way to recover your original
degree spec. If you want the relative figure, keep the value you built; if you want
the sounded result as material, capture it.
Note
Why capture at all, if it’s lossy? Because the sounded result is sometimes
exactly the material you want. A generated or chord-fitted melody resolves to
specific notes you couldn’t easily have written by hand; capturing freezes that
happy accident into a value you can transpose, reverse, or stack like any other
motif. It’s the melodic echo of composition.freeze(...) for harmony in
§7.6: let the system resolve something live, then keep the result.
Reference
8.7 Presets & world rhythms¶
You don’t have to write every rhythmic cell from scratch. Motif.preset(name)
hands you a famous world rhythm — claves, bell patterns, tresillo — as a ready-made
motif. These are the timelines that anchor Afro-Cuban and West-African music, placed
at their exact canonical pulse positions:
clave = Motif.preset("son_clave_3_2") # the 3-2 son clave
print(clave.describe())
Motif 4 beats [claves@0, claves@0.75, claves@1.5, claves@2.5, claves@3]
Each preset comes with a sensible default General-MIDI voice (the son clave uses
"claves"), so it sounds correct against the standard drum map with no pitch=.
Override the voice — and play it on a different drum — by passing pitch=:
bembe = Motif.preset("bembe", pitch="cowbell") # the 12-pulse bembé bell
print(bembe.describe())
Motif 4 beats [cowbell@0, cowbell@0.666667, cowbell@1.33333, cowbell@1.66667, cowbell@2.33333, cowbell@3, cowbell@3.66667]
Notice the bembé’s onsets sit on a 12-pulse cycle (the beats land on thirds:
0.667, 1.333, …), where the clave is on the familiar 16-pulse sixteenth grid —
each preset declares its own grid, so the geometry is authentic.
Because a preset is just a Motif, every transform and combiner from this chapter
applies. Here the running drum loop gets a son-clave side-stick layered on top —
the preset placed straight into a pattern with p.motif:
import subsequence
from subsequence import Motif
import subsequence.constants.instruments.gm_drums as gm_drums
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
CLAVE = Motif.preset("son_clave_2_3", pitch="side_stick") # 2-3 son clave
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
p.hit_steps("snare_1", [4, 12], velocity=95)
p.hit_steps("hi_hat_closed", range(16), velocity=70)
p.motif(CLAVE, beat=0) # lay the clave timeline over the beat
composition.render(bars=4, filename="motif-clave.mid")
The available presets, grouped by family:
Family |
Preset names |
|---|---|
Cuban clave (16-pulse) |
|
Tresillo / cinquillo (the 3-3-2 family) |
|
West-African / Cuban 4-4 bell (16-pulse) |
|
12-pulse bell timelines |
|
Tip
A preset is a rhythm with a default voice — so it’s the percussion sibling of a
degree melody. Want the clave’s timing but a different sound? Pass pitch=. Want
its timing on a pitched part — a bassline that grooves to the clave? .pitched(...)
re-aims every onset at one pitch spec (a chord tone like "root", or a MIDI note),
turning the rhythm skeleton into a melodic line you can place under the harmony. An
unknown preset name raises a ValueError listing every valid one, so a typo fails
loudly.
You can now treat melody and rhythm the way Chapter 7 taught you
to treat harmony: name a figure as an immutable Motif, transform it into a family
of variations that never touch the original, combine those sequentially or in
parallel, and place them under the moving chord with p.motif — snapping to the
harmony with fit, or capturing the resolved result back out with p.capture. In
Chapter 9 we let these motifs grow: stringing them into phrases
and developing them with a plan, so a whole melodic part unfolds from a single
stored idea.