Performing the piece
By the end of this chapter, the piece plays live under your hands: the hats drop out and come back, a tom fill fires from a key, the drop comes when you choose, and the second snare thickens while it plays. A rehearsal makes the same moves at set bars, so each one can be heard before it is played.
The piece so far
Everything A lead driven by the weather built, without its render, since the tuning study left the piece as it was. The euclidean_snare pattern is left out, because this chapter rewrites it.
import itertools
import subsequence
import subsequence.constants.instruments.gm_drums as gm_drums
import subsequence.constants.midi_notes as notes
import subsequence.sequence_utils
import subsequence.easing
composition = subsequence.Composition(bpm=120, key="E", scale="minor", seed=1)
swing = subsequence.Groove.swing(percent=57)
composition.harmony(style="aeolian_minor")
melody_state = subsequence.MelodicState(low=notes.E4, high=notes.E5, rest_probability=0.2)
hook = subsequence.motif([5, 6, 5, 3, None, 1, 2, 3])
answered = subsequence.period(hook)
composition.form(subsequence.Form([
subsequence.Section("intro", 4, energy=0.2),
subsequence.Section("build", 8, energy=0.5),
subsequence.Section("drop", 8, energy=1.0),
subsequence.Section("breakdown", 4, energy=0.3),
]), loop=True)
composition.transition(before="drop", mute=["drums", "hats"])
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP, min_energy=0.5)
def fill (p):
if p.section.last_bar:
p.sequence(steps=[12, 13, 14, 15], pitches=["high_tom", "high_mid_tom", "low_tom", "low_floor_tom"], velocities=[100, 90, 90, 110])
p.groove(swing)
@composition.pattern(channel=10, beats=0.75, reschedule_lookahead=0.75, drum_note_map=gm_drums.GM_DRUM_MAP, min_energy=0.5)
def pedal_hats (p):
p.hit_steps("hi_hat_pedal", [0], velocity=80)
@composition.pattern(channel=4, beats=4, min_energy=0.3)
def melody (p, chord):
p.melody(melody_state, spacing=0.5, chord_tones=chord.tones(notes.E4))
@composition.pattern(channel=5, bars=4, min_energy=1.0)
def hook_line (p):
p.phrase(answered, root=notes.E5)
composition.conductor.line("swell", start_val=40, end_val=90, duration_beats=32, start_beat=16)
composition.conductor.lfo("breath", shape="sine", cycle_beats=16, min_val=60, max_val=100)
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP, min_energy=0.2)
def hats (p):
p.hit_steps("hi_hat_closed", range(16))
p.velocity_shape(low=40, high=int(p.signal("breath")))
p.groove(swing)
def tempo (section):
if section is not None and section.name == "build":
composition.target_bpm(126, bars=8)
elif section is not None and section.name == "breakdown":
composition.target_bpm(120, bars=4)
composition.on_section(tempo)
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP, min_energy=0.5)
def drums (p):
p.hit_steps("kick", [0, 4, 8, 12], velocity=110)
p.hit_steps("snare", [4, 12], velocity=100)
p.ghost_fill("snare", density=0.5, velocity=(20, 40), bias="before")
steps = [note.position // 6 for note in p.placed()]
p.data["drum_syncopation"] = subsequence.sequence_utils.syncopation(steps, p.grid)
p.groove(swing)
composition.conductor.line("cutoff", start_val=20, end_val=127, duration_beats=32, start_beat=16)
@composition.pattern(channel=3, beats=4, voice_leading=True)
def pad (p, chord):
p.chord(chord, root=notes.E3, sustain=True, velocity=int(p.signal("swell")))
p.cc_ramp(74, int(p.signal("cutoff")), int(p.conductor.get("cutoff", (p.bar + 1) * p.bar_beats)))
@composition.pattern(channel=2, beats=4, min_energy=0.5)
def bass (p, chord):
busy = p.data.get("drum_syncopation", 0.0) > 0.5
if busy:
p.arpeggio(chord, root=notes.E1, count=1, spacing=1)
else:
p.arpeggio(chord, root=notes.E1, count=4, spacing=0.25)
p.groove(swing)
if not busy:
p.slide(notes=[3, 7, 11, 15], time=0.5, bend_range=12)
# Weather data by Open-Meteo.com, CC BY 4.0: central London, 15 September 2026.
recorded = {
"time": ["2026-09-15T03:00", "2026-09-15T06:00", "2026-09-15T09:00", "2026-09-15T12:00", "2026-09-15T15:00", "2026-09-15T18:00", "2026-09-15T21:00"],
"temperature_2m": [18.1, 17.8, 17.7, 19.8, 22.3, 21.7, 18.4],
"wind_speed_10m": [13.0, 14.4, 15.5, 16.2, 13.7, 13.0, 15.8],
}
readings = itertools.cycle(zip(recorded["temperature_2m"], recorded["wind_speed_10m"]))
temperature = subsequence.easing.EasedValue()
wind = subsequence.easing.EasedValue()
async def read_weather ():
temperature_now, wind_now = next(readings)
temperature.update(temperature_now)
wind.update(wind_now)
composition.schedule(read_weather, cycle_beats=16, reschedule_lookahead=2, wait_for_initial=True)
@composition.pattern(channel=6, beats=4, min_energy=1.0)
def lead (p, chord):
progress = (p.bar % 4) / 4
p.arpeggio(chord, root=int(notes.E4 + 2 * (temperature.get(progress) - 18)), count=3, spacing=0.5)
p.cc(74, int(wind.get(progress) * 6))
A pattern you can reach into
Some of what a performance changes, a pattern leaves open itself. The second snare, rewritten so that how many hits it spreads across the bar can be changed while it plays:
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP, min_energy=1.0)
def euclidean_snare (p):
p.euclidean("snare_2", pulses=p.param("pulses", 5), velocity=60).rotate(2).groove(swing)
In piece.py, replace euclidean_snare with this.
p.paramreads a value by its name, herepulses, and gives 5 until something changes it. The rest of the line is as it was.
The moves
Each move is a function of its own, so a key can make it, and so can a rehearsal:
def hats_out ():
composition.mute("hats")
def hats_in ():
composition.unmute("hats")
def tom_fill (p):
p.sequence(steps=[8, 10, 12, 14], pitches=["high_tom", "high_mid_tom", "low_tom", "low_floor_tom"], velocities=[90, 90, 100, 110])
def fire_fill ():
composition.trigger(tom_fill, channel=10, bars=1, drum_note_map=gm_drums.GM_DRUM_MAP, quantize=4)
def drop_now ():
composition.form_jump("drop")
def more_snare ():
composition.tweak("euclidean_snare", pulses=7)
def drop_again ():
composition.form_next("drop")
composition.mutesilences a pattern by its name, andcomposition.unmutebrings it back. A muted pattern keeps counting its cycles, so it comes back in step.composition.triggerplays a pattern once.tom_fillis a bar long, on MIDI channel 10, andquantize=4starts it on the next bar line.composition.form_jumpgoes to a section at once, andcomposition.form_nextchooses the section that follows the one playing, when that one ends. A jump is a change of section like any other, so the function given tocomposition.on_sectionhears it, and a jump to the breakdown eases the tempo back to 120.composition.tweakgives a pattern'sp.parama new value, which it keeps until it is changed again: the second snare spreads seven hits instead of five.
Each change is heard from the next bar, when the patterns next build.
Keys, and a display
composition.display(grid=True)
composition.hotkeys()
composition.hotkey("m", hats_out)
composition.hotkey("u", hats_in)
composition.hotkey("f", fire_fill)
composition.hotkey("d", drop_now)
composition.hotkey("s", more_snare)
composition.hotkey("r", drop_again)
composition.displaykeeps a status line at the foot of the terminal while the piece plays: the tempo, the key, the bar, the section and the chord.grid=Truedraws every pattern's steps above it, updated each bar.composition.hotkeyslistens to the keyboard while the piece plays, andcomposition.hotkeygives a key a move. A key acts when it is pressed, with no Enter, and?lists every key and its move.- Neither the display nor the keys do anything in a render, so the build runs these lines but cannot check what they do. The rehearsal below checks the moves themselves.
Playing it
In piece.py, put the moves and the keys after the other patterns, and end with this in place of the render:
Not checked: the build does not run this.
composition.play()
Press m as the build begins and u two bars later, f for a fill, d when the drop should come, and s to thicken the snare in it. Ctrl+C stops the piece.
A rehearsal
A rehearsal writes the performance down, so every move can be heard before it is played. It makes the moves at set bars:
bars = itertools.count(1)
async def rehearse ():
bar = next(bars)
if bar == 5:
hats_out()
elif bar == 7:
hats_in()
elif bar == 9:
fire_fill()
elif bar == 10:
drop_now()
elif bar == 12:
more_snare()
elif bar == 18:
drop_again()
composition.schedule(rehearse, cycle_beats=4, reschedule_lookahead=2, wait_for_initial=True)
composition.render(bars=28, filename="performing-the-piece.mid")
itertools.count(1)counts 1, 2, 3 and on, giving the next number each timenextasks.rehearseruns two beats before every bar, and once before the piece starts, sobaris the number of the bar about to begin.- Leave the rehearsal out of
piece.pywhen you play the piece yourself, or it makes its moves as well as yours.
What the rehearsal did
The hats stop after bar 4 and come back in bar 7:
MIDI channel 10
4 4.792 note F#2 (42) velocity 58 length 0.083
7 1.000 note F#2 (42) velocity 40 length 0.083
The fill plays across the second half of bar 9:
MIDI channel 10
9 3.000 note D3 (50) velocity 90 length 0.083
9 3.500 note C3 (48) velocity 90 length 0.083
9 4.000 note A2 (45) velocity 100 length 0.083
9 4.500 note F2 (41) velocity 110 length 0.083
The lead plays only in the drop, so it shows where the drop falls. The jump comes two beats before bar 10, and the drop counts the rest of bar 9 as its first bar, so the lead starts at bar 10 and the drop plays seven full bars. The breakdown follows at bar 17, and because of drop_again, the drop comes again at bar 21 instead of the intro:
MIDI channel 6
10 1.000 note D4 (62) velocity 100 length 0.500
16 4.500 note D#5 (75) velocity 100 length 0.500
21 1.000 note C5 (72) velocity 100 length 0.500
The second snare, E2 on MIDI channel 10, spreads five hits across bar 11 and seven across bar 12, after the tweak:
MIDI channel 10
11 1.500 note E2 (40) velocity 60 length 0.083
11 2.292 note E2 (40) velocity 60 length 0.083
11 3.000 note E2 (40) velocity 60 length 0.083
11 3.792 note E2 (40) velocity 60 length 0.083
11 4.500 note E2 (40) velocity 60 length 0.083
12 1.000 note E2 (40) velocity 60 length 0.083
12 1.500 note E2 (40) velocity 60 length 0.083
12 2.000 note E2 (40) velocity 60 length 0.083
12 2.500 note E2 (40) velocity 60 length 0.083
12 3.292 note E2 (40) velocity 60 length 0.083
12 3.792 note E2 (40) velocity 60 length 0.083
12 4.292 note E2 (40) velocity 60 length 0.083