Subsystem Search

Capturing a take

By the end of this chapter, the piece is a MIDI file to open in a DAW: a take rendered with a new melody and steady drums, and a way to record a take as you play it live.

The piece so far

Everything Playing with other gear built, without its render.

import itertools
import pathlib

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)

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

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

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)

A new take of one part

The composition's seed=1 makes every render the same take. Each pattern draws its random choices from a seed of its own, made from the composition's seed and the pattern's name, so one part can change while every other part keeps what it plays. A new melody:

composition.reroll("melody")
reroll('melody') -> effective seed 2994986849 (nonce 1)

A steady part

A part can be locked, so that it plays the same in every bar:

composition.lock("drums")
composition.reroll("drums")
reroll('drums') refused: 'drums' is locked - call unlock('drums') first

Rendering the take

composition.render(bars=24, filename="capturing-a-take.mid")

The drums play the same three ghost notes in every bar:

MIDI channel 10
    5  1.792  note D2 (38)  velocity 34  length 0.083
    5  3.792  note D2 (38)  velocity 26  length 0.083
    5  4.792  note D2 (38)  velocity 26  length 0.083
    6  1.792  note D2 (38)  velocity 34  length 0.083
    6  3.792  note D2 (38)  velocity 26  length 0.083
    6  4.792  note D2 (38)  velocity 26  length 0.083
    7  1.792  note D2 (38)  velocity 34  length 0.083
    7  3.792  note D2 (38)  velocity 26  length 0.083
    7  4.792  note D2 (38)  velocity 26  length 0.083

The bass listens to the drums, as it has since Patterns that listen to each other. The locked drums are as busy in every bar, so the bass plays its roots all the way through, where it used to arpeggio and slide in the quieter bars:

MIDI channel 2
    6  1.000  note G1 (31)  velocity 100  length 1.000
    6  2.000  note G1 (31)  velocity 100  length 1.000
    6  3.000  note G1 (31)  velocity 100  length 1.000
    6  4.000  note G1 (31)  velocity 100  length 1.000

Recording a take as you play it

A render makes a take as fast as the computer can, with no instrument connected. To capture a take as you play it, with your keys, your fills and your knob, record it. In piece.py, give Composition two more arguments:

Example not checked: it is not run when this site is built.

composition = subsequence.Composition(bpm=120, key="E", scale="minor", seed=1, record=True, record_filename="take.mid")

Into a DAW

Open the file in a DAW, or drag it into an empty project. The file starts with the piece's tempo, its rise into the drop and its fall into the breakdown, and its time signature:

tempo 120 bpm from   1  1.000
tempo 120.01 bpm from   4  4.042, ramping to 126 bpm at  12  4.000 in 768 changes
tempo 125.98 bpm from  20  4.042, ramping to 120 bpm at  24  4.000 in 384 changes
time signature 4/4 from   1  1.000

Going further