# Chapter 14 · Live Performance, External Data, and Sync In [Chapter 13](13-expression-hardware) 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. ```{testsetup} ch14 import subsequence from subsequence import Composition import subsequence.constants.instruments.gm_drums as gm_drums import subsequence.constants.midi_notes as notes from subsequence.easing import EasedValue from subsequence.sequence_utils import scale_clamp ``` (sec-ch14-hotswap)= ## 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._ ``` ```python # 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 ``` ```python # 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._ ``` ```python 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: ```text >>> 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._ ``` ```python 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") ``` ```{list-table} Three ways to swap patterns live :header-rows: 1 :widths: 26 74 * - Tool - Reach for it when * - `composition.watch(path)` - You're iterating on a whole piece in your editor — save to hear. The main live-coding workflow. * - `composition.live(port)` - You want a REPL for one-line tweaks, queries, and single-pattern swaps while the music plays. * - `composition.load_patterns(source)` - Patterns arrive as a string (web upload, queue, generated text) with no file backing. ``` ```{admonition} Reference :class: seealso {py:meth}`~subsequence.composition.Composition.watch`, {py:meth}`~subsequence.composition.Composition.live`, {py:meth}`~subsequence.composition.Composition.load_patterns` ``` (sec-ch14-control)= ## 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._ ``` ```python 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](10-form-sections) — `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](#sec-ch14-data)) standing in for the live keypresses: ```{testcode} ch14 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._ ``` ```python composition.hotkey("o", lambda: composition.tweak("bass", velocity=120)) # open up composition.hotkey("p", lambda: composition.clear_tweak("bass", "velocity")) ``` ```{list-table} The live-control verbs :header-rows: 1 :widths: 30 70 * - Verb - What it does * - `hotkeys()` / `hotkey(key, action)` - Enable the listener; bind a single key to a zero-argument action. * - `mute(name)` / `unmute(name)` - Silence or restore a running pattern by its function name; it keeps cycling so it stays in sync. * - `tweak(name, **kwargs)` - Override parameters a pattern reads via `p.param()`; persists until cleared. * - `clear_tweak(name, *params)` / `get_tweaks(name)` - Clear some/all overrides; inspect the current ones. * - `form_jump(name)` / `form_next(name)` - Steer the form now / at the next boundary ([§10.6](10-form-sections)). ``` ```{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](10-form-sections)) would otherwise open it. ``` ```{admonition} Reference :class: seealso {py:meth}`~subsequence.composition.Composition.hotkeys`, {py:meth}`~subsequence.composition.Composition.hotkey`, {py:meth}`~subsequence.composition.Composition.mute`, {py:meth}`~subsequence.composition.Composition.unmute`, {py:meth}`~subsequence.composition.Composition.tweak`, {py:meth}`~subsequence.composition.Composition.clear_tweak`, {py:meth}`~subsequence.composition.Composition.get_tweaks`, {py:meth}`~subsequence.pattern_builder.PatternBuilder.param` ``` (sec-ch14-midi-in)= ## 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._ ``` ```python 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._ ``` ```python 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._ ``` ```python 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._ ``` ```python 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](13-expression-hardware) covered the multi-device naming). ``` ```{admonition} Reference :class: seealso {py:meth}`~subsequence.composition.Composition.midi_input`, {py:meth}`~subsequence.composition.Composition.note_input`, {py:meth}`~subsequence.pattern_builder.PatternBuilder.held_notes`, {py:meth}`~subsequence.composition.Composition.cc_map`, {py:meth}`~subsequence.composition.Composition.cc_forward` ``` (sec-ch14-osc)= ## 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._ ``` ```python 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 ` sets the tempo, `/mute/` and `/unmute/` toggle a pattern, `/data/ ` 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._ ``` ```python 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](13-expression-hardware), 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: ```{testcode} ch14 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. ``` ```{admonition} Reference :class: seealso {py:meth}`~subsequence.composition.Composition.osc`, {py:meth}`~subsequence.composition.Composition.osc_map`, {py:meth}`~subsequence.pattern_midi.PatternMidiMixin.osc`, {py:meth}`~subsequence.pattern_midi.PatternMidiMixin.osc_ramp` ``` (sec-ch14-data)= ## 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: 1. **Schedule** a background task to fetch the value on a beat cycle, writing it into `composition.data`. 2. **Ease** each new reading into the previous one so the music glides rather than jumps. 3. **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: ```{testcode} ch14 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](13-expression-hardware). 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: ```{testcode} ch14 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/` 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._ ``` ```python composition.web_ui() # dashboard on localhost composition.display() composition.play() ``` ```{admonition} Reference :class: seealso {py:meth}`~subsequence.composition.Composition.schedule`, {py:class}`~subsequence.easing.EasedValue`, {py:func}`~subsequence.sequence_utils.scale_clamp`, {py:meth}`~subsequence.composition.Composition.web_ui` ``` (sec-ch14-link)= ## 14.6 Ableton Link: locking to the room The last input is *other instruments*. [Ableton Link](https://www.ableton.com/en/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._ ``` ```python 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, and `target_bpm()` (the smooth ramp) is ignored — the shared clock wins. - **The start is bar-aligned.** `play()` waits for the next `quantum` boundary 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](10-form-sections) 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. ``` ```{admonition} Reference :class: seealso {py:meth}`~subsequence.composition.Composition.link`, {py:meth}`~subsequence.composition.Composition.set_bpm`, {py:meth}`~subsequence.composition.Composition.target_bpm` ``` --- 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](00-setup) 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.