Skip to content

Commit 33aaa44

Browse files
author
Simon Holliday
committed
- New subsequence/cadences.py (formula table); Progression.cadence(), generate(cadence=), freeze(cadence=); Motif.generate(cadence=)
- request_cadence() / section_cadence() clock request-hooks; cadential edge labels on the graphs - sentence() / period() combinators; tests/test_cadences.py - Form payload + energy + boundaries - subsequence/forms.py (Section/Form); FormState rework (at_end, list-mode nav, ending) - form_freeze(), energy() + p.energy + min_energy=, on_section(), transition(); tests/test_forms.py - Key-resolution consistency (the three-intent model) - Layered effective key (_effective_key_scale), Section.scale/Form.key, relative section harmony re-keys per occurrence, pin_chord re-keying - 3 bug fixes (trigger keyless, transition-fill key, MelodicState freeze); tests/test_key_resolution.py - New subsequence/roles.py, chord_graphs/hooktheory_major.py; genre _PRESETS, Motif.preset + _WORLD_RHYTHMS, sieve/Toussaint kernels, Progression.elaborate(); tests/test_presets.py - subsequence/__init__.py (new exports), api-cheatsheet.md, README/README-AGENTS, scripts/generate_cheatsheet.py
1 parent 46fc408 commit 33aaa44

21 files changed

Lines changed: 3907 additions & 133 deletions

README.md

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,7 +897,10 @@ composition.form(my_form())
897897
| `progress` | `float` | `bar / bars` (0.0 → ~1.0) |
898898
| `first_bar` | `bool` | True on the first bar of the section |
899899
| `last_bar` | `bool` | True on the last bar of the section |
900+
| `ending` | `bool` | True on the last bar before a *different* section (repeats don't count) |
900901
| `next_section` | `str?` | Name of the upcoming section, or `None` at the end |
902+
| `energy` | `float` | The section's energy payload (0.5 unless a bound `Form` says otherwise) |
903+
| `key` | `str?` | The section's key override, or `None` (the composition key) |
901904

902905
`next_section` is pre-decided when the current section begins (graph mode picks probabilistically; list mode peeks the iterator). Use it for lead-ins:
903906

@@ -915,6 +918,121 @@ A performer or code can override the pre-decided next section with `composition.
915918

916919
For a `beats=4` pattern in 4/4, they're always equal. For a `beats=8` pattern, `p.cycle` is half `p.bar` (the pattern runs once every two bars). For a `beats=2` pattern, `p.cycle` is double `p.bar`. Use `p.bar` for composition-wide synchronisation (e.g. "fire on bar 8") and `p.cycle` for pattern-local variation (e.g. "every 4th rebuild of this pattern").
917920

921+
### Form values: Section and Form
922+
923+
A form can also be a **value** — a frozen `Form` of `Section`s, each carrying its payload (`bars`, `energy`, an optional `key` override). Values are inspectable and editable before binding:
924+
925+
```python
926+
S = subsequence.Section
927+
form = subsequence.Form([
928+
S("verse", bars=8, energy=0.55), S("chorus", bars=8, energy=0.9),
929+
S("outro", bars=4, energy=0.3),
930+
])
931+
932+
print(form) # listen on paper first
933+
form = form.replace(3, bars=8) # 1-based slots; field edits or whole Sections
934+
form = form.with_energy({"chorus": 0.95})
935+
936+
composition.form(form, at_end="stop")
937+
```
938+
939+
`at_end=` names what happens when a sequence form runs out: `"stop"` (the form finishes — the default), `"hold"` (the final section repeats until you navigate away), or `"loop"` (start over; `loop=True` is sugar for it).
940+
941+
**Form freeze.** A graph form can be frozen into an editable value — the walk is seeded, so the frozen path is exactly the path the live graph would have played:
942+
943+
```python
944+
composition.form({...}, start="intro") # the graph
945+
path = composition.form_freeze() # walk → editable Form (until a terminal section)
946+
path = path.replace(2, bars=24) # stretch the verse
947+
composition.form(path, at_end="stop") # rebind the edited value
948+
```
949+
950+
**Navigation works on every navigable form**: `composition.form_jump("chorus")` and `composition.form_next("chorus")` work on graph forms *and* sequence forms (the jump lands on the next occurrence of the name, wrapping). Only generator forms cannot be navigated.
951+
952+
**`Section.key`** and **`Section.scale`** re-anchor the section — see [How key resolution works](#how-key-resolution-works) below for the full model and the modulation story.
953+
954+
### How key resolution works
955+
956+
Whether the key moves a piece of content depends on **how you spelled it** — that is the contract. There are three intents:
957+
958+
| Intent | How you spell it | Resolves against | Does a key move it? |
959+
|--------|------------------|------------------|---------------------|
960+
| **Absolute** | note names (`"Am"`), MIDI pitches (`Motif.notes([60])`), `PitchSet`, frozen `freeze()` captures, drum names | nothing — the exact notes | **No** — ever |
961+
| **Key-relative** | scale degrees (`motif([1,5,6])`), romans (`"V"`, `[1,4,5]`), generated relative material | a key + scale | **Yes** |
962+
| **Chord-relative** | `ChordTone("third")`, `Approach(...)`, the `fit` dial | the sounding/next chord | No — it tracks the chord, whatever key that chord is in |
963+
964+
So `progression(["Am", "F"])` never moves (you named exact chords), `progression([1, 6])` follows the key (you wrote intervals), and `ChordTone("root")` plays the root of whatever chord is sounding regardless of key. The same applies everywhere — a motif, a chord part, a generated phrase — so you never have to remember per-feature exceptions.
965+
966+
**The key a relative thing resolves against is layered**, most specific wins:
967+
968+
```
969+
Section.key > form key (form(key=...) / Form(key=...)) > Composition(key=...)
970+
```
971+
972+
Key and scale/mode resolve independently, so a section can move the tonic, the mode, or both.
973+
974+
**Modulation — the truck-driver's key change.** Because a numbered progression is key-relative, the *same* progression bound to two sections in two keys plays in two keys — chords and melody move together:
975+
976+
```python
977+
S = subsequence.Section
978+
composition.form(subsequence.Form([
979+
S("chorus", 8),
980+
S("chorus_final", 8, key="D"), # up a tone for the last chorus
981+
]))
982+
983+
prog = subsequence.progression([1, 5, 6, 4]) # written once, in numbers
984+
composition.section_chords("chorus", prog) # plays in C (the composition key)
985+
composition.section_chords("chorus_final", prog) # the SAME value, now in D
986+
```
987+
988+
If you wanted the chords to *stay put* under a moving melody, you'd spell them absolute — `["C", "G", "Am", "F"]` — and they wouldn't budge. That choice is yours, made by how you write the chords.
989+
990+
Two boundaries worth knowing:
991+
992+
- **The live graph engine stays in the composition key.** `harmony(style="...")` generates chords in one key for the whole piece — it doesn't modulate per section (a stateful walk doesn't transpose mid-stream). To modulate generated harmony, write the section's changes (relative or absolute) with `section_chords`.
993+
- **A re-keyed section that runs out of written chords falls through to the live engine in the composition key.** If `Section("verse", bars=8, key="D")` has only 4 bars of `section_chords` bound, bars 5–8 hand off to composition-key harmony. Bind the full section length if you want it all in the section's key. (Rare, but documented so it's never a surprise.)
994+
995+
### Energy: the arranging dial
996+
997+
`composition.energy({...})` sets a per-section energy level — one plain dict, with `(start, end)` tuples interpolating across a section (the build gesture):
998+
999+
```python
1000+
composition.energy({"intro": 0.2, "verse": 0.55, "build": (0.3, 1.0), "drop": 0.95})
1001+
```
1002+
1003+
Patterns read `p.energy` (0.5 when nothing is configured) and gate themselves, or declare a threshold and let the gate be automatic:
1004+
1005+
```python
1006+
@composition.pattern(channel=10, beats=4, drum_note_map=DRUM_MAP)
1007+
def drums (p):
1008+
p.motif(KICK)
1009+
if p.energy >= 0.6:
1010+
p.euclidean("hh", pulses=int(5 + 6 * p.energy), velocity=(60, 90))
1011+
1012+
@composition.pattern(channel=10, beats=4, drum_note_map=DRUM_MAP, min_energy=0.8)
1013+
def perc (p):
1014+
p.bresenham("conga", 7) # silent until the energy reaches 0.8
1015+
```
1016+
1017+
The dict **overrides** any energy payload carried by bound `Section` values (the dict is the later, performance-level dial). `min_energy` composes with `mute()`/`unmute()` — a performer mute always wins.
1018+
1019+
### Section boundaries: transitions and the section event
1020+
1021+
`composition.transition()` declares boundary material in one line — an automatic fill before a section change, or a mute over the approach:
1022+
1023+
```python
1024+
composition.transition(before="*", fill=FILL, channel=10, beat=2.0) # any change: fill in the last bar
1025+
composition.transition(before="drop", mute=["pads"], beats=4) # pads drop out approaching the drop
1026+
```
1027+
1028+
`before` names the incoming section, or `"*"` for any *different* section (repeats don't fire it). Fills play in the final bar, starting at `beat`; their drum names resolve through the rule's `drum_note_map=` or, if omitted, a map borrowed from a registered pattern on the same channel. Mutes are bar-granular (`beats` rounds up to whole bars) and reopen at the boundary.
1029+
1030+
`composition.on_section(fn)` fires on every section change (one lookahead-beat early, in time to affect the new section's first patterns), receiving the new `SectionInfo` — or `None` when the form finishes:
1031+
1032+
```python
1033+
composition.on_section(lambda info: print(f"now: {info.name if info else 'end'}"))
1034+
```
1035+
9181036
#### Bar-cycle position - `p.bar_cycle(length)`
9191037

9201038
For bar-position logic, `p.bar_cycle(length)` replaces raw modulo arithmetic with readable musical vocabulary:
@@ -1503,6 +1621,112 @@ def keys (p):
15031621

15041622
**Fixed or breathing.** `comp.chords()` realises its phrase once, so it repeats identically. `p.progression()` realises on each rebuild, so omitting `seed=` lets the lengths breathe from cycle to cycle (still reproducible under the composition's own `seed=`); pass `seed=` for a fixed phrase. Velocity humanises per voice via the usual `velocity=(low, high)` convention.
15051623

1624+
### Presets
1625+
1626+
Curated starting points — recognizable material you reach for by name and then bend.
1627+
1628+
**Genre progressions.** `progression("name")` is a named, key-relative loop:
1629+
1630+
```python
1631+
verse = subsequence.progression("trance_epic") # i VI III VII — epic minor
1632+
chorus = subsequence.progression("doo_wop") # I vi IV V
1633+
blues = subsequence.progression("twelve_bar_blues")
1634+
```
1635+
1636+
About two dozen ship — `pop_axis`, `doo_wop`, `andalusian`, `mixolydian_vamp`, `ii_v_i`, `pachelbel`, `rhythm_changes_a`, and more (an unknown name lists them all). They're ordinary progressions: spice them, `.cadence()` them, bind them to sections — and because they're key-relative, they follow whatever key they're bound to.
1637+
1638+
**World rhythms.** `Motif.preset("name", pitch=...)` is a timeline at its exact pulse positions (from Toussaint's *Geometry of Musical Rhythm*):
1639+
1640+
```python
1641+
clave = subsequence.Motif.preset("son_clave_3_2", pitch="rim") # the 3-2 son clave
1642+
bell = subsequence.Motif.preset("bembe", pitch="bell") # the 12-pulse standard pattern
1643+
```
1644+
1645+
The clave family (son/rumba/bossa, 2-3 and 3-2), tresillo/cinquillo, the West-African bell timelines (shiko, gahu, soukous, bembé, fume-fume), and samba all ship. They're Motifs — transform, `&`-stack, and place them like any other.
1646+
1647+
**Role bundles.** `subsequence.roles` holds taste-default parameter bundles you splat and override (no role API — just data):
1648+
1649+
```python
1650+
comp.phrase_part(channel=2, part="bass", **subsequence.roles.BASS) # low, locked to chord tones
1651+
comp.phrase_part(channel=4, part="lead", **subsequence.roles.LEAD, root=78) # override anything
1652+
```
1653+
1654+
### Elaboration: `prog.elaborate(depth)`
1655+
1656+
`elaborate(depth)` approaches every chord by a backward cycle-of-fifths dominant chain (Steedman's jazz/blues grammar) — `depth` is how many fifth-steps back it reaches. Its flagship is the 12-bar blues elaborated more each chorus:
1657+
1658+
```python
1659+
blues = subsequence.progression("twelve_bar_blues").resolve("C")
1660+
chorus1 = blues # plain: C7 … F7 … G7 …
1661+
chorus2 = blues.elaborate(1) # a V7 before each chord (G7 C7 …)
1662+
chorus3 = blues.elaborate(2) # secondary ii-Vs (Dm7 G7 C7 …)
1663+
chorus4 = blues.elaborate(3, seed=4) # extended chains + tritone subs
1664+
```
1665+
1666+
`depth=0` is the bare progression; the result is concrete (resolve or bind under a key first), and each chord keeps its decorations on its subdivided slot.
1667+
1668+
### Sieves and rhythm measures
1669+
1670+
For the experimental composer: `sieve()` is Xenakis's integer-set kernel — residual classes that serve as custom scales, non-octave pitch pools, rhythm grids, or bar-selection masks alike:
1671+
1672+
```python
1673+
from subsequence import sieve, residual_class as rc
1674+
1675+
sieve([(12, 0), (12, 2), (12, 4), (12, 5), (12, 7), (12, 9), (12, 11)], hi=12) # the major scale
1676+
sieve([(5, 0), (7, 1)], lo=60, hi=96) # a non-octave pitch pool
1677+
((rc(2, 0) | rc(3, 0)) & ~rc(4, 1)).evaluate(hi=24) # the full union/intersection/complement algebra
1678+
```
1679+
1680+
And Toussaint's rhythm measures analyse a list of onset pulses: `rhythmic_evenness(onsets, grid)` (how close to maximally even — the Euclidean rhythms score ~1.0), `offbeatness(onsets, grid)` (onsets on intrinsically off-beat pulses), `syncopation(onsets, grid)` (weighted pull away from the strong beats).
1681+
1682+
### Cookbook
1683+
1684+
A few recipes that compose the primitives above — patterns, not features.
1685+
1686+
**Per-section tempo** — ease the BPM at each section boundary (`on_section` + the eased `target_bpm`):
1687+
1688+
```python
1689+
TEMPI = {"verse": 120, "chorus": 128, "bridge": 112}
1690+
1691+
def shift_tempo (info):
1692+
if info and info.name in TEMPI:
1693+
comp.target_bpm(TEMPI[info.name], bars=1, shape="ease_in_out") # glide over a bar
1694+
1695+
comp.on_section(shift_tempo)
1696+
```
1697+
1698+
**A single-note rhythmic lead** — one pitch, all groove, locked to the kick's rhythm (the *same value* the drums use):
1699+
1700+
```python
1701+
KICK = subsequence.Motif.preset("tresillo_16", pitch="kick")
1702+
1703+
@comp.pattern(channel=4, beats=4)
1704+
def stab (p, chord):
1705+
p.motif(KICK.pitched(subsequence.ChordTone("root")), root=48) # the kick rhythm, on the chord root
1706+
```
1707+
1708+
**"Something changes every 8 bars"** — a seeded chooser over a small move set, written on the bar counter:
1709+
1710+
```python
1711+
@comp.pattern(channel=3, bars=2)
1712+
def pads (p):
1713+
move = p.rng.choice(["play", "drop", "octave_up"]) # re-rolled per cycle on the seeded stream
1714+
if p.bar_cycle(8) == 7: # the 8th bar of every 8-bar block
1715+
move = "drop"
1716+
if move != "drop":
1717+
lift = 12 if move == "octave_up" else 0
1718+
p.chord(p.harmony.chord, root=60 + lift)
1719+
```
1720+
1721+
**A probability overlay** — thin a busy part by chance, re-rolled each cycle on the seeded stream:
1722+
1723+
```python
1724+
@comp.pattern(channel=10, beats=4, drum_note_map=DRUMS)
1725+
def hats (p):
1726+
p.euclidean("hh", pulses=11, velocity=(50, 80))
1727+
p.dropout(0.2) # 20% of hits silenced this cycle (seeded, reproducible)
1728+
```
1729+
15061730
## 5. Live Performance & Tools
15071731

15081732
### Seed and deterministic randomness

0 commit comments

Comments
 (0)