Playing with other gear
By the end of this chapter, the piece keeps time with other gear and apps, takes a knob and a held chord from a keyboard, plays its bass on two instruments at once, and names its sounds in a file it shares with Subsample.
Most of this needs gear plugged in, which the build does not have, so those blocks are marked. The build runs the rest: a pattern that plays what the keyboard holds, the bass doubled, and the shared names.
The piece so far
Everything Performing the piece built, without its rehearsal and its render. The bass pattern is left out, because this chapter rewrites it. If you split the piece for live coding, patterns go in patterns.py and everything else in piece.py.
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)))
# 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))
@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)
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.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)
In time with other gear
Other gear can follow the piece's tempo:
Not checked: the build does not run this.
composition.clock_output()
composition.clock_outputsends MIDI clock from the piece's MIDI output: a Start message when the piece starts, 24 clock messages every beat, and a Stop message when it stops. A drum machine or synthesiser set to follow MIDI clock then plays at the piece's tempo, and follows it as it rises into the drop.
Or the piece can follow another device's clock:
Not checked: the build does not run this.
composition.midi_input("Keyboard", clock_follow=True)
composition.midi_inputopens a MIDI input port by its name, or any part of its name, such as"Keyboard". If no port matches, Subsequence stops and lists the ports it can see.clock_follow=Truemakes the piece take its tempo and its start from the clock arriving on that port, and send no clock of its own. The other device's Stop pauses the piece where it is and Continue carries on from there, while Start takes it back to its first bar. Only Ctrl+C ends the script.
Ableton Link keeps apps in time over a local network: Ableton Live, other apps with Link built in, and other copies of Subsequence. It needs Subsequence's link extra, so run the install line from Install and connect again, with [link] straight after subsequence. Then:
Not checked: the build does not run this.
composition.link()
composition.linkjoins the Link session on the network. The piece takes the session's tempo, and starts on the next bar line the apps share.
A knob and a held chord
A keyboard can play into the piece: a knob sets a value, and a chord held down gives a pattern its notes. Open the keyboard's port, if the clock above has not already, and listen to both:
Not checked: the build does not run this.
composition.midi_input("Keyboard")
composition.cc_map(74, "keys_velocity", min_val=40, max_val=127)
composition.note_input()
composition.cc_mapturns the knob that sends CC 74 into a value incomposition.datacalledkeys_velocity, from 40 with the knob all the way down to 127 all the way up.composition.note_inputkeeps track of the notes held down on the keyboard.
A pattern that plays them, on MIDI channel 7:
@composition.pattern(channel=7, beats=4)
def keys (p):
p.arpeggio(p.held_notes(), spacing=0.25, velocity=int(p.data.get("keys_velocity", 90)))
p.held_notesgives the notes held down as the pattern builds, and the arpeggio runs through them in sixteenth notes. With nothing held, it plays nothing.p.data.get("keys_velocity", 90)reads the knob, and gives 90 until the knob moves.- A render has no keyboard, so the build runs this pattern and it plays nothing, which is all a render can check.
One part on two instruments
The bass, rewritten to double itself on a second synthesiser:
@composition.pattern(channel=2, beats=4, min_energy=0.5, mirrors=[(0, 9)])
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)
In piece.py, give bass this first line. The rest of the pattern is as it was.
mirrors=[(0, 9)]sends everything the bass plays, its slides included, to MIDI channel 9 as well. The0is the MIDI output, counted from 0: the first, whichCompositionopens.composition.mirroradds a copy while the piece plays, andcomposition.unmirrortakes one away.
An instrument on a MIDI port of its own, such as a sampler, is another output:
Not checked: the build does not run this.
composition.midi_output("Sampler", name="sampler", latency_ms=20)
composition.midi_outputopens another MIDI output port by its name, or part of it, and gives back its number, 1 for the first one added.name="sampler"lets a pattern choose it withdevice="sampler"in its first line.latency_ms=20says the sampler sounds 20 milliseconds late, so Subsequence sends everything for the other outputs 20 milliseconds later, and they all sound together.
Names shared with Subsample
A definitions file gives names to MIDI note numbers, CC numbers, MIDI channels and program numbers, so a pattern can use a name where it would use a number. Subsample reads the same file, so a sound renumbered in it is renumbered in both, and Subsample's own page on it shows how its MIDI map reads it. A kit of field recordings in Subsample, played in the drop:
import pathlib
pathlib.Path("project.yaml").write_text("""notes:
rain: 36
thunder: 49
channels:
field_kit: 11
cc:
release: 72
""")
definitions = subsequence.load_definitions("project.yaml")
@composition.pattern(channel=definitions.channels["field_kit"], beats=4, drum_note_map=definitions.notes, cc_name_map=definitions.cc, min_energy=1.0)
def field_kit (p):
p.hit_steps("rain", [0, 6, 10], velocity=90)
p.hit_steps("thunder", [12], velocity=110)
p.cc("release", 100)
composition.render(bars=24, filename="playing-with-other-gear.mid")
project.yamlis written by the script, so everything the example needs is on the page. In a real project it is a file of its own, besidepiece.py, and Subsample is pointed at the same file.- Each section maps names to numbers:
notesto MIDI notes,channelsto MIDI channels counted from 1, andccto controller numbers.programsnames program changes the same way. A name is lowercase letters, digits and underscores. subsequence.load_definitionsreads the file.definitions.notesserves as a drum map, so"rain"plays note 36, anddefinitions.ccletsp.cctake a name.- With Subsample on its own MIDI port, add
device="sampler"tofield_kit's first line.
What the render shows
The bass on MIDI channel 2 and its copy on MIDI channel 9 play the same, slide and all:
MIDI channel 2
6 1.000 note G1 (31) velocity 100 length 0.250
6 1.292 note B1 (35) velocity 100 length 0.250
6 1.500 note D2 (38) velocity 100 length 0.292
6 1.625 pitch bend 0, ramping to 3413 at 6 1.792 in 5 changes
6 1.792 note G2 (43) velocity 100 length 0.250
6 1.792 pitch bend 0
MIDI channel 9
6 1.000 note G1 (31) velocity 100 length 0.250
6 1.292 note B1 (35) velocity 100 length 0.250
6 1.500 note D2 (38) velocity 100 length 0.292
6 1.625 pitch bend 0, ramping to 3413 at 6 1.792 in 5 changes
6 1.792 note G2 (43) velocity 100 length 0.250
6 1.792 pitch bend 0
In the drop, the field kit plays on MIDI channel 11, its names turned into note and controller numbers:
MIDI channel 11
13 1.000 note C2 (36) velocity 90 length 0.083
13 1.000 CC 72 value 100
13 2.500 note C2 (36) velocity 90 length 0.083
13 3.500 note C2 (36) velocity 90 length 0.083
13 4.000 note C#3 (49) velocity 110 length 0.083
Messages from Substation and Subsample
Substation and Subsample can each send OSC, Open Sound Control, messages over the network when they hear something. Substation sends /radio/state when a radio channel opens or closes, and Subsample sends /sample/captured when it has analysed a new recording. The piece can listen, and fire its tom fill whenever a radio transmission starts:
Not checked: the build does not run this.
composition.osc()
def radio_state (address, *values):
if values[2] == 1:
fire_fill()
composition.osc_map("/radio/state", radio_state)
composition.osclistens for OSC messages on port 9000, and sends messages about the piece, such as its bar and its chord, to port 9001 on the same computer. Send Substation's messages to port 9000.composition.osc_mapcalls a function of your own for every message to one address, and gives it the address and the message's values.*valuesgathers the values together: in/radio/state, the third is 1 when the radio channel opens.composition.osc()listens only to programs on the same computer. To hear Substation or Subsample running on another computer, usecomposition.osc(receive_host="0.0.0.0"), which listens on every network the computer is on. A message can change the piece, so do that only on a network you trust.