Subsystem

Live coding

By the end of this chapter, the piece plays while you edit its patterns, and every save is heard from the next bar without the music stopping. You can also type into the piece while it plays.

The build checks nothing in this chapter. Live coding needs a piece playing and a person saving a file, and a render has neither, so every block is marked.

Two files

Live coding keeps apart what runs once and what changes as the piece plays. The patterns move into a file of their own, patterns.py, which Subsequence runs again every time it is saved:

Not checked: the build does not run this.

import subsequence.constants.instruments.gm_drums as gm_drums
import subsequence.constants.midi_notes as notes
import subsequence.sequence_utils

swing = subsequence.Groove.swing(percent=57)
hook = subsequence.motif([5, 6, 5, 3, None, 1, 2, 3])
answered = subsequence.period(hook)
melody_state = composition.data["melody_state"]
temperature = composition.data["temperature"]
wind = composition.data["wind"]

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

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

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

piece.py keeps everything else, and runs once:

Not checked: the build does not run this.

import itertools

import subsequence
import subsequence.constants.instruments.gm_drums as gm_drums
import subsequence.constants.midi_notes as notes
import subsequence.easing

composition = subsequence.Composition(bpm=120, key="E", scale="minor", seed=1)
composition.harmony(style="aeolian_minor")
melody_state = subsequence.MelodicState(low=notes.E4, high=notes.E5, rest_probability=0.2)
composition.data["melody_state"] = melody_state

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

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.conductor.line("cutoff", start_val=20, end_val=127, duration_beats=32, start_beat=16)

# 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()
composition.data["temperature"] = temperature
composition.data["wind"] = wind

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)

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)

composition.watch("patterns.py")
composition.play()

Editing while it plays

Start the piece with python piece.py, as before, and open patterns.py in your editor. To play the hats on eighth notes, change the first line of hats to this and save:

Not checked: the build does not run this.

	p.hit_steps("hi_hat_closed", range(0, 16, 2))

range(0, 16, 2) counts from 0 up to 15 in twos.

Typing into the piece

composition.live lets you type Python into the piece while it plays. In piece.py, add this line before the watch line:

Not checked: the build does not run this.

composition.live()

Then, with the piece playing, open a second terminal in the same folder, with the same virtual environment active, and start the prompt:

Not checked: the build does not run this.

python -m subsequence.live_client

It says it has connected, shows where the piece has got to, and waits at a >>> prompt. A line is heard from the next bar, as a save is:

Not checked: the build does not run this.

>>> composition.mute("hats")
OK
>>> composition.unmute("hats")
OK