Chapter 14 · Live Performance, External Data, and Sync¶
In Chapter 13 you taught the running track to speak to your gear — continuous controllers, bend, NRPN, program changes, groove, tuning; now we make the track something you can play, react to, and feed from the world outside. This is the capstone, and it closes the loop the whole book has been building toward: explore a piece by writing and seeding it (Parts I–VI), then perform it — swapping patterns without stopping the clock, steering the form by hand, taking in a player’s held chord or an external clock, pulling live data into musical parameters, and locking to other instruments over Ableton Link.
Everything here sits on top of the deterministic, seeded composition you already know how to write. The drums, bass, harmony, and motif/form track from the earlier Parts keep running exactly as seeded; the live layer is an acknowledged overlay. That separation is the chapter’s one big idea: the seed reproduces the skeleton; the performance is what you add live. When you render headlessly there is no keyboard, no network, no clock partner — so the live inputs are simply empty and the seeded output is unchanged.
A note on what runs and what doesn’t. Most of this chapter is genuinely live — it needs a second terminal, a MIDI keyboard, a network peer, or your hands on the keys — so those examples are shown as reference blocks marked live feature and are not executed under our doctest harness. But the explore-to-perform spine that matters most — pulling external data on a schedule, easing it into parameters, and reading it back in a pattern — runs perfectly headless, so those examples are executed and validated, the same as every chapter before.
14.1 Hot-swapping patterns without stopping the clock¶
The first move of live performance is editing the music while it plays. You already write patterns as functions; the live tools let you redefine those functions — or load a whole new file of them — and have the running composition adopt the change on the next rebuild cycle, with the clock never pausing. The running pattern keeps its cycle count, its seeded RNG state, and its place in the bar; only the logic changes.
There are three ways in, from heaviest to lightest, and all three are live by nature (they need a second terminal, a file on disk, or a network sender), so the snippets below are shown for reference rather than executed here.
Watch a file: composition.watch(path)¶
The workhorse for working out a piece is composition.watch(path) — point it
at a Python file, and every time you save that file Subsequence re-runs it and
hot-swaps the patterns in place. The recommended shape is a two-file split: a
wrapper that does the one-time setup (device, harmony, form, tempo) and then
watches a live file that holds only your @composition.pattern definitions.
Note
Live feature — run it at your instrument; not executed here.
# live_init.py — the wrapper, runs once.
import subsequence
composition = subsequence.Composition(bpm=120, key="A", scale="minor",
output_device="IAC Driver Bus 1")
composition.harmony(style="aeolian_minor", cycle_beats=4)
composition.form([("verse", 8), ("chorus", 8)], loop=True)
composition.watch("live_patterns.py") # reload this file on every save
composition.display(grid=True)
composition.play() # Ctrl-C to stop
# live_patterns.py — edit and save this while it plays.
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
p.hit_steps("hi_hat_closed", range(16), velocity=70)
Save live_patterns.py and the next bar carries your edit. Delete a pattern’s
function from the file and Subsequence unregisters it automatically — its notes
stop and it leaves the running set. A SyntaxError or a runtime error during a
reload is logged and skipped: your previous patterns keep playing, so a typo
mid-set never drops the music.
Tip
If the two-file split feels heavy for solo work, a single file can watch itself.
Put the one-time setup inside if __name__ == "__main__": and call
composition.watch(__file__) there; Subsequence detects the self-watch and skips
the double-load. Running python my_session.py does the setup once, and each save
reloads only the pattern definitions below the guard.
Type one line at a time: composition.live(port)¶
For quick nudges — change the tempo, mute a part, query what’s playing, redefine a
single pattern — composition.live(port) starts a tiny TCP eval server you
connect to from another terminal. You type Python at a REPL and it runs inside the
playing composition.
Note
Live feature — run it at your instrument; not executed here.
composition.live() # start the eval server on localhost:5555
composition.display()
composition.play()
Then, in a second terminal, python -m subsequence.live_client connects you:
>>> composition.set_bpm(132)
>>> composition.mute("hats")
>>> composition.live_info() # what's playing right now
>>> @composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
... def drums(p):
... p.hit_steps("kick_1", [0, 8], velocity=120)
Warning
The eval server executes arbitrary Python in your process — it is not a
sandbox. It binds to localhost and is opt-in, but anything that can reach the
port on your machine gains full code execution. The same caution applies to
composition.watch() (it execs the watched file) and load_patterns() below.
Don’t enable these on a shared host, never expose the port to a network, and only
run code you trust.
Load patterns from a string: composition.load_patterns(source)¶
When the patterns arrive as text rather than from a file on disk — a web upload
from a trusted collaborator, a message-queue consumer, a one-shot session load —
composition.load_patterns(source, source_label=...) does exactly what a
watch() save does, but the source is an in-memory string. It compiles, execs,
activates new patterns, and unregisters any running pattern the source omits (the
source is treated as the full new truth).
Note
Live feature — run it at your instrument; not executed here.
composition.load_patterns("""
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100)
""", source_label="uploaded_beat")
Tool |
Reach for it when |
|---|---|
|
You’re iterating on a whole piece in your editor — save to hear. The main live-coding workflow. |
|
You want a REPL for one-line tweaks, queries, and single-pattern swaps while the music plays. |
|
Patterns arrive as a string (web upload, queue, generated text) with no file backing. |
Reference
14.2 Live control: hotkeys, tweaks, mutes, and form jumps¶
Hot-swapping is for changing the code. The lighter, faster moves of a live set —
mute this, drop to the breakdown, push the filter — want to be on a single
keypress. composition.hotkey(key, action) binds a one-character key to a
zero-argument callable that fires during playback. Turn the listener on first with
composition.hotkeys() (the ? key is reserved — it lists your bindings).
Note
Live feature — hotkeys read the live keyboard; not executed here.
composition.hotkeys() # enable the listener
# Each action is a zero-argument callable — usually a lambda.
composition.hotkey("c", lambda: composition.form_jump("chorus")) # jump now
composition.hotkey("b", lambda: composition.form_next("bridge")) # at the boundary
composition.hotkey("h", lambda: composition.mute("hats"), quantize=4)
composition.hotkey("H", lambda: composition.unmute("hats"), quantize=4)
composition.hotkey("1", lambda: composition.data.update({"mode": "chill"}))
composition.play()
Important
Most live actions want quantize=0 (the default). A form jump, a
composition.data write, or a tweak() is naturally heard at the next pattern
rebuild cycle, which gives you automatic musical quantization for free — the
change lands on a clean boundary without you timing it. Reserve quantize=N for
actions that need an explicit bar guarantee, like a mute() you want to fall
exactly on a 4-bar phrase line (quantize=4 above). A jump is heard on the next
rebuild, never mid-note: already-queued MIDI is left to finish.
The form-navigation verbs form_jump and form_next are the ones from
§10.6 — form_jump(name) cuts immediately (next rebuild),
form_next(name) queues a section for the natural boundary. Wiring them to keys is
the whole point of building a navigable or graph form: you steer the arrangement
by hand.
Tweaking a parameter live: tweak and p.param¶
Redefining a whole pattern to change one number is overkill. The lighter path is a
tweakable parameter: in the pattern body, read a value through
p.param(name, default) instead of hard-coding it; then override it live with
composition.tweak(name, **kwargs). The override persists across rebuilds
until you change or clear it, and takes effect on the next cycle.
This does run headless — tweak and param operate on the running-pattern set,
which render() populates just like play() — so here it is as an executed
example, with a scheduled “director” (we meet schedule properly in
§14.5) standing in for the live keypresses:
import subsequence
import subsequence.constants.midi_notes as notes
composition = subsequence.Composition(bpm=120, key="C")
@composition.pattern(channel=2, beats=4)
def bass(p):
# Read the live-tweakable values, each with a sensible default.
pitches = p.param("pitches", [notes.C2, notes.E2, notes.G2, notes.C3])
velocity = p.param("velocity", 90)
p.sequence(steps=[0, 4, 8, 12], pitches=pitches, velocities=velocity)
# A director firing each bar — stands in for the live keypresses you'd bind.
def director(p):
if p.cycle == 2:
composition.tweak("bass", pitches=[notes.A1, notes.C2, notes.E2, notes.A2])
if p.cycle == 4:
composition.clear_tweak("bass", "pitches") # revert to the default
composition.schedule(director, cycle_beats=4)
composition.render(bars=8, filename="tweak-bass.mid")
composition.get_tweaks(name) returns a copy of the current overrides for a
pattern, and composition.clear_tweak(name) with no parameter names removes them
all (with names, just those). In a live set you’d bind these to keys:
Note
Live feature — the hotkey binding reads the live keyboard; not executed here.
composition.hotkey("o", lambda: composition.tweak("bass", velocity=120)) # open up
composition.hotkey("p", lambda: composition.clear_tweak("bass", "velocity"))
Verb |
What it does |
|---|---|
|
Enable the listener; bind a single key to a zero-argument action. |
|
Silence or restore a running pattern by its function name; it keeps cycling so it stays in sync. |
|
Override parameters a pattern reads via |
|
Clear some/all overrides; inspect the current ones. |
|
Steer the form now / at the next boundary (§10.6). |
Warning
mute, unmute, tweak, clear_tweak, and get_tweaks all take a pattern’s
function name as a string ("bass" is the function def bass) and raise a
ValueError if no running pattern has that name. A performer mute always wins over
automatic gating — a part you muted by hand stays muted even if the form’s energy
(§10.3) would otherwise open it.
Reference
hotkeys(),
hotkey(),
mute(),
unmute(),
tweak(),
clear_tweak(),
get_tweaks(),
param()
14.3 Live MIDI input and arpeggiation¶
So far the music has flowed one way — Subsequence out to your instrument. Live
performance often wants the reverse too: a MIDI keyboard, clock source, or
controller feeding into the running composition. The door is
composition.midi_input(device, clock_follow=False), which opens an input
port. It does two jobs.
Following an external clock. Pass clock_follow=True and Subsequence slaves
its clock to incoming MIDI ticks and respects transport — it waits for a Start,
advances one pulse per tick (24 per beat), and halts on Stop. This is how you sync
the running track to a DAW or hardware sequencer that owns the tempo.
Note
Live feature — needs a hardware/DAW clock source; not executed here.
composition.midi_input("Your Device:Port", clock_follow=True)
composition.play() # waits for an incoming MIDI Start before the first note
Warning
Only one input device may have clock_follow=True (a second raises
ValueError). While following external clock, set_bpm() has no effect — the
clock source is authoritative — and MIDI clock output is automatically disabled
to prevent a feedback loop.
Taking in held notes for arpeggiation. The headline live-input feature is the
held-note arpeggiator. After opening an input port, call
composition.note_input(channel, latch=...) and Subsequence tracks the set of
keys you’re holding. Any pattern reads that set with p.held_notes() — sorted
ascending, empty when nothing is down — and the natural thing to do with it is hand
it straight to p.arpeggio(). The composition still authors the rhythm and
motion; your hands supply the pitch set.
Note
Live feature — needs a MIDI keyboard; not executed here.
composition.midi_input("Arturia KeyStep") # open the input port
composition.note_input(channel=1, release_ms=30)
@composition.pattern(channel=6, beats=4)
def arp(p):
p.arpeggio(p.held_notes(), direction="up", spacing=0.25) # rests when silent
Hold a chord and the arp runs over it; lift your hands and it rests. Two knobs keep
it musical: release_ms (default 30) keeps a just-released note counting as
held for a few milliseconds, so the momentary all-keys-up gap as you change hand
position doesn’t drop the arp to silence; and latch=True holds the chord
after you lift off until you play a new one, like a hardware arp’s latch.
Important
This is a performance layer over the deterministic skeleton. The seed still
reproduces the rhythm and structure exactly; the held notes are an acknowledged
live overlay. Because p.held_notes() returns an empty list when no note_input()
source exists and when rendering headlessly, a pattern written this way renders to
silence on that part and plays live over your chords — the same code, no branching.
note_input() requires a midi_input() first, and one note-input source is
supported.
Mapping a knob to a parameter: cc_map and cc_forward¶
Where note_input takes in notes, composition.cc_map(cc, data_key) takes
in a CC and writes its scaled value into composition.data for patterns to read
at rebuild time. A hardware knob becomes a live generative parameter with no
callback code — here a CC opens up the arp’s velocity while your held chord still
supplies its pitch set:
Note
Live feature — needs a MIDI controller; not executed here.
composition.midi_input("Arturia KeyStep")
composition.note_input(channel=1) # held notes feed the arp
composition.cc_map(74, "filter_cutoff") # CC 74 → 0.0–1.0 in data
composition.cc_map(7, "volume", min_val=0, max_val=127)
@composition.pattern(channel=6, beats=4)
def arp(p):
cutoff = p.data.get("filter_cutoff", 0.5) # same dict as composition.data
p.arpeggio(p.held_notes(), spacing=0.25,
velocity=int(60 + 60 * cutoff)) # knob shapes the dynamics
When you instead need the controller to reach your synth immediately — a mod
wheel to pitch bend, a pedal to expression — bypass the pattern cycle with
composition.cc_forward(cc, output). It routes the incoming CC straight to the
output: "cc" forwards it unchanged, "cc:N" remaps the number, "pitchwheel"
scales it to a bend, or a callable gives you full control.
Note
Live feature — needs a MIDI controller; not executed here.
composition.cc_forward(1, "pitchwheel", output_channel=1) # mod wheel → bend, ~1–5 ms
composition.cc_forward(1, "cc:74") # also reroute to CC 74
Tip
cc_map is for generative parameters read once per rebuild (a knob shaping note
choice or density); cc_forward is for expressive signals that must track your
hand in real time. They’re independent — register both for one CC and the value
drives a pattern and reaches the synth at once. Both take input_device= to
listen to one of several controllers (§13.5 covered the
multi-device naming).
Reference
midi_input(),
note_input(),
held_notes(),
cc_map(),
cc_forward()
14.4 OSC: control and broadcast over the network¶
MIDI isn’t the only way in. Open Sound Control (OSC) carries named, typed
messages over UDP — the lingua franca of TouchOSC layouts, Max/MSP and Pure Data
patches, Processing sketches, and modular environments. Enable a bi-directional OSC
server with composition.osc(receive_port, send_port) and Subsequence listens
for control messages and broadcasts its own state.
Note
Live feature — the OSC server binds real network ports; not executed here.
composition.osc(receive_port=9000, send_port=9001, send_host="127.0.0.1")
composition.play()
Out of the box the server understands a few incoming addresses — /bpm <int>
sets the tempo, /mute/<name> and /unmute/<name> toggle a pattern,
/data/<key> <value> writes shared data (preserving the existing numeric type) —
and broadcasts /bar, /chord, /section, and /bpm at each bar so a
controller surface can mirror what’s playing. For anything else, register your own
handler with composition.osc_map(address, handler) (call it after osc()):
Note
Live feature — needs an OSC sender; not executed here.
composition.osc()
def on_intensity(address, value):
composition.data["intensity"] = float(value)
composition.osc_map("/intensity", on_intensity) # a fader → composition.data
Warning
The OSC listener defaults to receive_host="0.0.0.0" — all interfaces — so any
machine on your LAN can change your tempo, mute patterns, and write data. On an
untrusted network restrict it to loopback: composition.osc(receive_host="127.0.0.1").
OSC also goes the other way as musical output: p.osc(address, *args) sends
a message at a beat position, and p.osc_ramp(address, start, end) sweeps a
float over a beat range — the OSC twins of p.cc and p.cc_ramp from
§13.1, for automating a remote mixer fader, a reverb
send, or a Max patch in time with the music. These emit calls are safe to render
(with no server configured the events are simply dropped), so unlike the
server-side examples above this one is executed:
composition = subsequence.Composition(bpm=120, key="C")
@composition.pattern(channel=1, beats=4)
def pad_with_fx(p):
p.note(notes.C4, beat=0, velocity=80, duration=4)
p.osc("/fx/chorus/enable", 1, beat=0.0) # flip a switch
p.osc_ramp("/fx/reverb/wet", 0.0, 0.8, beat_start=0, beat_end=4,
shape="ease_in") # ease a fader up
composition.render(bars=2, filename="osc-emit.mid")
Note
OSC events bypass MIDI entirely — they’re never recorded into a rendered .mid
file and aren’t mirrored to other MIDI devices. The block above renders cleanly
because the emit calls no-op without a server; point a real composition.osc() at
a listener and the same pattern drives it live.
Reference
14.5 External-data sonification: schedule, ease, sound¶
Here is where the chapter’s runnable heart is, and the most musically interesting input of all: the world. Weather, the position of the ISS, a stock ticker, a sensor on your desk — any value you can fetch in Python can steer the music. The pattern is always the same three steps, and all of it runs headless:
Schedule a background task to fetch the value on a beat cycle, writing it into
composition.data.Ease each new reading into the previous one so the music glides rather than jumps.
Read the smoothed value in a pattern and map it to a musical parameter.
Step 1 — fetch on a schedule¶
composition.schedule(fn, cycle_beats, wait_for_initial=...) registers a
function to run every cycle_beats beats. Synchronous functions run in a thread
pool so a slow network call never stalls the MIDI clock. Declare a first parameter
named p and the function receives a small context whose p.cycle counts the
calls (0-indexed). Set wait_for_initial=True and the first fetch runs before
playback starts, so composition.data is populated when patterns first build.
Important
schedule() must be called before play() (or render()) — scheduled tasks
register at startup. It raises RuntimeError if you call it on a running
composition. In the examples below we simulate the external reading with a small
table so the chapter stays self-contained and reproducible; in real use the body
would be a requests.get(...).json() call or a sensor read.
Step 2 — ease the jumps away with EasedValue¶
External readings arrive as discrete snapshots, often minutes apart. Jumping the
music instantly to each new number sounds jarring. subsequence.easing.EasedValue
remembers the previous value and interpolates smoothly toward the new one: create
one per field at module level, call .update(value) in the scheduled task, and
.get(progress) in the pattern — no manual previous/current bookkeeping.
The bridge from a raw reading to a musical range is scale_clamp(value, in_min, in_max, out_min, out_max) from subsequence.sequence_utils — it maps an input
range to an output range and clamps, so an out-of-range reading can never push a
velocity past 127 or a count negative.
Step 3 — read it in a pattern¶
The pattern computes its progress through one fetch cycle from p.cycle, asks
the EasedValue for the smoothed reading, and maps it. Here is the whole loop —
executed and validated — sonifying a stand-in temperature into the energy of a drum
part:
import subsequence
import subsequence.constants.instruments.gm_drums as gm_drums
from subsequence.easing import EasedValue
from subsequence.sequence_utils import scale_clamp
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
# One EasedValue per smoothed field, created at module level.
warmth = EasedValue(initial=0.4)
FETCH_BEATS = 16 # fetch once every 4 bars
BARS_PER_FETCH = FETCH_BEATS // 4
def fetch_weather(p):
# Stand-in for a real call, e.g. requests.get(API_URL).json()["temp_c"].
sample_c = [6.0, 12.0, 9.0, 15.0][p.cycle % 4] # degrees Celsius
warmth.update(scale_clamp(sample_c, 0.0, 20.0, 0.0, 1.0)) # → 0.0–1.0, clamped
composition.schedule(fetch_weather, cycle_beats=FETCH_BEATS, wait_for_initial=True)
@composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
progress = (p.cycle % BARS_PER_FETCH) / BARS_PER_FETCH # 0 → 1 over the fetch cycle
level = warmth.get(progress) # eased, no jumps
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=int(70 + 40 * level))
if level >= 0.5: # busier when it's warm
p.hit_steps("hi_hat_closed", range(16), velocity=int(50 + 40 * level))
composition.render(bars=16, filename="weather-drums.mid")
The .get(progress) call eases with a Hermite smoothstep by default; pass a shape
name ("ease_in", "s_curve", …) for a different curve, exactly as the ramps in
Chapter 13. And EasedValue exposes the direction of
the last change as .delta — positive when the value rose, negative when it
fell — which is constant across one fetch cycle and perfect for branching:
composition = subsequence.Composition(bpm=120, key="A", scale="minor")
composition.harmony(style="aeolian_minor", cycle_beats=4)
brightness = EasedValue(initial=0.5)
FETCH_BEATS = 16
BARS_PER_FETCH = FETCH_BEATS // 4
def fetch(p):
sample = [30, 80, 55, 95][p.cycle % 4] # some 0–100 reading
brightness.update(scale_clamp(sample, 0, 100, 0.0, 1.0))
composition.schedule(fetch, cycle_beats=FETCH_BEATS, wait_for_initial=True)
@composition.pattern(channel=6, beats=4, voice_leading=True)
def arp(p, chord):
progress = (p.cycle % BARS_PER_FETCH) / BARS_PER_FETCH
level = brightness.get(progress)
direction = "up" if brightness.delta >= 0 else "down" # rising data climbs
count = 3 + int(round(2 * level)) # 3..5 voices
p.arpeggio(chord, root=notes.C4, count=count, spacing=0.25,
velocity=int(60 + 40 * level), direction=direction)
composition.render(bars=16, filename="data-arp.mid")
Important
Match the read cycle to the fetch cycle. The pattern rebuilds every bar but the
data refreshes every FETCH_BEATS beats, so progress must run 0→1 across exactly
one fetch window — (p.cycle % BARS_PER_FETCH) / BARS_PER_FETCH. Get the divisor
wrong and the ease either finishes early (then holds flat) or never completes. Tying
both to one FETCH_BEATS constant keeps them honest.
Note
The same composition.data dict is the single meeting point for every live
input in this chapter: schedule() writes to it, cc_map() writes to it, OSC’s
/data/<key> writes to it, and a hotkey can write to it — and every pattern reads
it as p.data. One dict, many sources, read once per rebuild. That is why the
sonification recipe and the knob-mapping recipe are really the same recipe.
A live web dashboard: composition.web_ui¶
For a richer view than the terminal grid, composition.web_ui() starts a
WebSocket server that broadcasts live state — signals, active patterns, timing,
notes — to a browser dashboard. It’s read-only and binds to localhost by default.
The dashboard is a beta feature, and it currently loads its frontend from a
CDN, so it needs an internet connection.
Note
Live feature — serves a browser dashboard; not executed here.
composition.web_ui() # dashboard on localhost
composition.display()
composition.play()
Reference
14.6 Ableton Link: locking to the room¶
The last input is other instruments. Ableton Link
is the wireless standard for tempo and beat-phase sync — Ableton Live, Reason, a
shelf of iOS synths, and other Subsequence instances all lock to one shared clock
over the LAN with no configuration. composition.link(quantum) joins the
session.
Link needs the optional aalink package (pip install subsequence[link]) and a
live network session, so it’s illustrative here — but the call is a single line:
Note
Live feature — requires the link extra and a Link session; not executed here.
import subsequence
comp = subsequence.Composition(bpm=120, key="A", scale="minor")
comp.link(quantum=4.0) # join the session; 4.0 = one bar in 4/4
@comp.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP)
def drums(p):
p.hit_steps("kick_1", [0, 4, 8, 12], velocity=110)
comp.play() # waits for the next bar boundary, then enters in phase
Link synchronises three things — tempo, beat phase, and transport start/stop — but not notes: each participant still generates its own music; the pulses just stay aligned. Two behaviours are worth holding onto:
The network tempo is authoritative. If a peer changes the tempo, Subsequence follows on the next pulse. While Link is active,
set_bpm()proposes a new tempo to the session rather than setting it locally, andtarget_bpm()(the smooth ramp) is ignored — the shared clock wins.The start is bar-aligned.
play()waits for the nextquantumboundary before the first note, so you always enter an existing session cleanly on the downbeat.
Tip
Without Link, composition.target_bpm(bpm, bars, shape) smoothly ramps the tempo
over a number of bars — an accelerando into a chorus, a ritardando at the close —
using the same easing shapes as every other ramp. set_bpm(bpm) is the instant
jump. Reach for these when you own the clock; reach for link() when you’re
joining a room that already has one.
Note
Under the hood: every live input is the same shape. A held chord, a mapped CC, an OSC fader, a scheduled API poll, a Link tempo — each is an external fact read once per rebuild, exactly like the beat clock, the chord, and the section you’ve read all book long (§10.6 gathered those three). The seeded composition is the specification; the live layer answers “what’s true right now?” fresh each cycle and the patterns arrange themselves around the answer. Hot-swapping changes the spec without stopping the clock; hotkeys, data, and Link change the answer. Nothing here is a new idea — it’s the book’s one idea, context resolved late, now reaching all the way out to the world and back.
Reference
That completes the explore-to-perform loop and the main guide. You can now write a seeded, generative track (Parts I–VI), then perform it: hot-swap patterns from a watched file, a REPL, or a string; mute, tweak, and steer the form from single keys; take in a player’s held chord, an external clock, mapped knobs, and OSC; pull live data on a background schedule and ease it into musical parameters; and lock the whole thing to Ableton Link. From a four-on-the-floor loop in Chapter 0 to a living, reactive instrument — that was the journey. The appendices that follow are reference and reach: the Direct Pattern API for power users, the analysis and set-theory toolkit, a MIDI routing and troubleshooting reference, the API quick reference, and a glossary. Keep them by your side; the music is yours now.