Subsystem

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

Or the piece can follow another device's clock:

Not checked: the build does not run this.

composition.midi_input("Keyboard", clock_follow=True)

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

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

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

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.

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)

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

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)