Subsystem

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.

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

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)

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

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