# Chapter 13 · Expression and Hardware Control In [Chapter 12](12-deep-generative) you pushed the note-generating engine as far as it goes — chaos, automata, scored melody. This chapter steps off the keyboard and onto the *rest* of the MIDI cable: the controller sweeps, pitch bends, deep parameter writes, patch changes, groove feel, multi-device routing, and non-12-TET tunings that turn a stream of notes into a performance your gear actually responds to. Everything here rides alongside the notes you already know how to place. A pattern is still a function on `p`; you just add a few more verbs — `p.cc(...)`, `p.pitch_bend(...)`, `p.nrpn(...)` — that emit the *non*-note MIDI your synth has always understood. Like notes, none of them sound until you `render()` (or `play()`); like notes, they're placed at a beat position inside the bar. ```{important} **This is the most hardware-specific chapter in the book.** CC numbers, NRPN parameters, bank-select layouts, pitch-bend ranges and tuning support all vary by instrument — Subsequence sends exactly what you ask for, but *what each message means* is defined by your synth's manual, not by Subsequence. We teach the verbs and the standard conventions; reach for your device's MIDI implementation chart for the numbers. ``` ```{testsetup} ch13 import subsequence import subsequence.constants.instruments.gm_drums as gm_drums from subsequence import Composition, Groove, bank_select from subsequence import Tuning composition = Composition(bpm=120, key="A", scale="minor") ``` (sec-ch13-cc)= ## 13.1 Continuous controllers: `cc` and `cc_ramp` A **continuous controller** (CC) message carries a value 0–127 on a numbered control — the same mod wheel, filter cutoff, and expression messages your controller already sends. Subsequence places them with two verbs: - **`p.cc(control, value, beat=0.0)`** — one CC message at a beat. - **`p.cc_ramp(control, start, end, beat_start=0.0, beat_end=None, ...)`** — a *swept* CC value, interpolated from `start` to `end` across a beat range. Here's a synth bass whose filter opens across the bar — the single most common expressive move in electronic music, written as one ramp. The first example shows the imports in full; later blocks rely on them: ```{testcode} ch13 import subsequence import subsequence.constants.instruments.gm_drums as gm_drums composition = subsequence.Composition(bpm=120, key="A", scale="minor") @composition.pattern(channel=2, beats=4) def bass(p): p.sequence(steps=[0, 4, 8, 12], pitches=[40, 40, 43, 38], velocities=100, durations=0.9) p.cc(71, 95, beat=0) # resonance up, once, at the top of the bar p.cc_ramp(74, 30, 120, beat_start=0, beat_end=4, shape="exponential") # cutoff sweep composition.render(bars=2, filename="filter-sweep.mid") ``` `74` is the General MIDI "brightness / filter cutoff" controller and `71` is "resonance" — but on *your* synth they're whatever its manual says. The `shape=` on the ramp is the same easing vocabulary the Conductor used in [Chapter 12](12-deep-generative): `"exponential"` holds low and then rushes open, which sounds far more like a hand on a filter knob than a flat `"linear"` climb. ```{list-table} cc_ramp arguments worth knowing :header-rows: 1 :widths: 24 76 * - Argument - What it does * - `control` - CC number 0–127 (or a name — see below). * - `start`, `end` - CC values 0–127 to sweep between. Out-of-range values are clamped. * - `beat_start`, `beat_end` - The beat window for the sweep. `beat_end` defaults to the pattern length. * - `shape` - Easing curve: `"linear"` (default), `"ease_in"`, `"ease_out"`, `"ease_in_out"`, `"exponential"`, `"logarithmic"`, `"s_curve"`. * - `resolution` - Pulses between messages (default `1` — one every ~21 ms at 120 BPM). Raise it (e.g. `2` or `4`) to thin MIDI traffic on a busy DIN link. ``` ### Naming your controllers: `cc_name_map=` Remembering that cutoff is `74` is exactly the chore the drum-note map saved you in [Chapter 1](01-step-grid). The cure is the same: hand the pattern decorator a **`cc_name_map=`** dictionary and then write the *name* instead of the number. ```{testcode} ch13 composition = subsequence.Composition(bpm=120, key="A", scale="minor") # One place to record what this synth's CCs mean — musical names, not magic numbers. JUNO = {"cutoff": 74, "resonance": 71, "lfo_rate": 76, "expression": 11} @composition.pattern(channel=2, beats=4, cc_name_map=JUNO) def bass(p): p.sequence(steps=[0, 4, 8, 12], pitches=[40, 40, 43, 38], velocities=100, durations=0.9) p.cc("resonance", 95, beat=0) p.cc_ramp("cutoff", 30, 120, beat_start=0, beat_end=4, shape="exponential") composition.render(bars=2, filename="filter-named.mid") ``` ```{tip} Keep a `cc_name_map` per instrument near the top of your script — it's a tiny patch sheet in code. When you swap synths you change *one dict*, not every `p.cc(...)` call. A bare number still works anywhere a name does, so you can mix the two while you're figuring out a new device. ``` ```{note} A CC value is *one number from 0 to 127* — it has no idea what a "filter" is. `p.cc(74, 100)` tells the synth "controller 74 is now 100"; whether that brightens the tone, opens a wah, or does nothing depends entirely on how the synth is patched. This is why the same script can sound transformed on a different instrument: the notes are identical, but the controllers land on different destinations. ``` ```{admonition} Reference :class: seealso {py:meth}`~subsequence.pattern_midi.PatternMidiMixin.cc`, {py:meth}`~subsequence.pattern_midi.PatternMidiMixin.cc_ramp` ``` (sec-ch13-bend)= ## 13.2 Pitch bend, portamento, and slide Pitch bend is its own MIDI message (not a CC) carrying a **14-bit** value, which Subsequence exposes to you as a tidy **−1.0 … +1.0** float: `0.0` is no bend, `+1.0` is the wheel pushed fully up, `−1.0` fully down. How many *semitones* that spans is the synth's **pitch-bend range** setting (the near-universal default is ±2 semitones, so `+0.5` is one semitone up). There are two raw verbs and three musical ones built on top: - **`p.pitch_bend(value, beat=0.0)`** — one bend message at a beat. - **`p.pitch_bend_ramp(start, end, beat_start, beat_end, ...)`** — a swept bend. - **`p.bend(note, amount, ...)`** — bend *one note* by its index. - **`p.portamento(time, ...)`** — glide between *every* consecutive note. - **`p.slide(notes=/steps=, ...)`** — glide into *selected* notes (the 303 move). ### Raw bend and bend ramp The two raw verbs are the controller equivalents of `p.cc` / `p.cc_ramp` — a value at a beat, or a swept value across a range — and they fire independently of the notes. Useful for a dub-style pitch dive on a sustained drone: ```{testcode} ch13 composition = subsequence.Composition(bpm=120, key="A", scale="minor") @composition.pattern(channel=2, beats=4) def riser(p): p.note(48, beat=0, duration=4.0, velocity=100) p.pitch_bend(0.0, beat=0) # start in tune p.pitch_bend_ramp(0.0, 0.5, beat_start=2, beat_end=4, shape="ease_in") # bend up over beats 3–4 composition.render(bars=2, filename="bend-ramp.mid") ``` ### Bending a single note: `p.bend` Raw bends don't know where your notes are. **`p.bend(note, amount)`** does: you name a note by index (`0` = first, `-1` = last) and Subsequence ramps the bend across that note's duration, then snaps back to centre at the next note's onset. `amount` is the same −1.0…+1.0 float. Because it reads each note's *final* duration, call it **after** any `legato()` / `detached()` / `duration()`: ```{testcode} ch13 composition = subsequence.Composition(bpm=120, key="A", scale="minor") @composition.pattern(channel=2, beats=4) def lead(p): p.sequence(steps=[0, 4, 8, 12], pitches=[40, 42, 40, 43], velocities=100, durations=0.9) p.legato(0.95) # finalise durations first… p.bend(note=-1, amount=0.5, shape="ease_in") # …then scoop the last note up a semitone composition.render(bars=2, filename="single-bend.mid") ``` ### Glide everything: `p.portamento` A monophonic synth in *portamento* (glide) mode slurs from one pitch to the next. **`p.portamento(time)`** writes that as pitch bend: in the tail of each note it bends toward the next note's pitch, resetting at the next onset. `time` is the *fraction* of each note used for the glide (default `0.15` — the last 15%): ```{testcode} ch13 composition = subsequence.Composition(bpm=120, key="A", scale="minor") @composition.pattern(channel=3, beats=4) def synth_lead(p): p.sequence(steps=[0, 4, 8, 12], pitches=[48, 50, 48, 53], velocities=90, durations=0.9) p.legato(0.95) p.portamento(time=0.15, shape="ease_in_out") # gentle slur across every transition composition.render(bars=2, filename="portamento.mid") ``` ```{important} **Portamento works through pitch bend, so it lives inside the bend range.** With the standard ±2 semitones, an interval *wider* than 2 semitones can't be reached by the wheel — `portamento()` simply skips those pairs rather than glide to the wrong note. If your synth's wheel is set wider, tell Subsequence: `bend_range=12` glides across leaps up to an octave. Pass `bend_range=None` to disable the check entirely (large intervals then clamp to a full ±1.0 sweep). Because pitch bend is per-channel, glide is most convincing on a **mono** instrument — on a polyphonic patch the bend tilts every sounding voice at once. ``` ### The 303 slide: `p.slide` Acid basslines don't glide *everywhere* — only into the notes flagged "slide," which also tie through (the note doesn't retrigger). **`p.slide`** is exactly that: name the destinations by note index (`notes=[1, 3]`) or by grid step (`steps=[4, 12]`), and only those transitions glide. With `extend=True` (the default) the preceding note stretches to meet the slide, the 303's signature legato: ```{testcode} ch13 composition = subsequence.Composition(bpm=120, key="A", scale="minor") @composition.pattern(channel=2, beats=4) def acid(p): p.sequence(steps=[0, 4, 8, 12], pitches=[40, 42, 40, 43], velocities=110, durations=0.9) p.legato(0.95) p.slide(steps=[8], time=0.2, shape="ease_in", bend_range=2) # slide only into the 3rd note composition.render(bars=2, filename="acid-slide.mid") ``` ```{list-table} Which pitch-bend verb? :header-rows: 1 :widths: 26 74 * - Verb - Reach for it when * - `p.pitch_bend` / `p.pitch_bend_ramp` - You want a bend at a beat, independent of the notes — a drone dive, a whammy-style sweep. * - `p.bend(note, amount)` - You want to scoop or fall *one specific note* — an expressive lead inflection. * - `p.portamento(time)` - You want a mono line to glide through *every* transition — a slurred synth lead. * - `p.slide(notes=/steps=)` - You want to glide into *selected* notes only — TB-303 acid, slide guitar. ``` ```{admonition} Reference :class: seealso {py:meth}`~subsequence.pattern_midi.PatternMidiMixin.pitch_bend`, {py:meth}`~subsequence.pattern_midi.PatternMidiMixin.pitch_bend_ramp`, {py:meth}`~subsequence.pattern_midi.PatternMidiMixin.bend`, {py:meth}`~subsequence.pattern_midi.PatternMidiMixin.portamento`, {py:meth}`~subsequence.pattern_midi.PatternMidiMixin.slide` ``` (sec-ch13-deep)= ## 13.3 NRPN/RPN, program change, bank select, and SysEx The 128 CC slots run out fast on a deep synth. The MIDI spec's answer is **parameter numbers** — a CC handshake that addresses thousands of parameters, optionally with 14-bit precision. There are two flavours, and Subsequence gives each its own verb: - **NRPN** (*Non-Registered* Parameter Number) — vendor-specific. Sequential, Korg, Roland, Elektron and others use NRPN for filter cutoff, envelope amounts, oscillator detune, and the like. The numbers are in *your synth's* manual. - **RPN** (*Registered* Parameter Number) — the small standardised set defined by the MIDI spec (pitch-bend range, master tuning, …), the same on every device. ```{testcode} ch13 composition = subsequence.Composition(bpm=120, key="A", scale="minor") # A vendor patch sheet: parameter names → NRPN numbers (from the synth's manual). TAKE5 = {"osc1_fine": 9, "filter_cutoff": 110, "filter_resonance": 111} @composition.pattern(channel=2, beats=4, nrpn_name_map=TAKE5) def synth(p): p.note(48, beat=0, duration=4.0, velocity=100) # RPN: set the synth's pitch-bend range to ±12 semitones (a standard name). p.rpn("pitch_bend_sensitivity", 12, beat=0) # NRPN by name: a fine (14-bit) oscillator detune. p.nrpn("osc1_fine", 700, beat=0, fine=True) # NRPN sweep: open a deep filter parameter across the whole bar (14-bit). p.nrpn_ramp("filter_cutoff", 0, 16383, beat_start=0, beat_end=4) composition.render(bars=2, filename="nrpn.mid") ``` A few things are doing quiet work here: - **`fine=`** chooses the value range. `fine=False` (the `nrpn`/`rpn` default) sends a single 7-bit value, `0–127` — enough for most parameters. `fine=True` sends the full **14-bit** value, `0–16383`, for parameters that need it (the `nrpn_ramp`/`rpn_ramp` default is already `fine=True`, since a sweep wants the resolution). - **RPN names resolve for free.** NRPN names need *your* `nrpn_name_map=` because the numbers are vendor-specific, but RPN names are standardised, so `"pitch_bend_sensitivity"`, `"channel_fine_tuning"`, `"channel_coarse_tuning"`, `"modulation_depth_range"` and friends work with no map at all. - **A parameter write is several CC messages**, co-scheduled at the same beat (parameter-select, then data-entry, then a defensive null reset). Subsequence emits them in the right order so the value lands on the right parameter — you just call `p.nrpn(...)`. ```{warning} **Don't issue plain `p.cc(6, …)` or `p.cc(38, …)` during a `nrpn_ramp`/`rpn_ramp` window.** A ramp selects the parameter once at the start and then streams bare data-entry messages (CC 6 / CC 38) for the rest of the sweep. A stray CC 6 or 38 on the same channel mid-ramp would be read as *more data* for the ramped parameter rather than as its own control. Keep other data-entry CCs off that channel until the ramp's `beat_end`. ``` ### Switching patches: `program_change` and `bank_select()` **`p.program_change(program, beat=0.0, bank_msb=None, bank_lsb=None)`** sends a Program Change — "switch to patch *N*" — on the pattern's channel. Program numbers follow GM (0 = Acoustic Grand, 48 = Strings, …). Modern synths hold far more than 128 patches, organised into **banks**; you reach a bank with the two bank-select bytes, sent automatically just before the program change when you pass them. The free function **`bank_select(bank)`** spares you the bit-twiddling: hand it a plain bank number and it returns the `(msb, lsb)` pair to forward on: ```{testcode} ch13 composition = subsequence.Composition(bpm=120, key="A", scale="minor") @composition.pattern(channel=4, beats=4) def strings(p): msb, lsb = bank_select(81) # bank 81 → (msb, lsb) bytes p.program_change(48, beat=0, bank_msb=msb, bank_lsb=lsb) # patch 48 in that bank p.note(60, beat=0, duration=4.0, velocity=80) composition.render(bars=2, filename="program-change.mid") ``` ```{tip} Put the patch change where the patch should change — usually beat 0 of the first bar of a section. Reading the form ([Chapter 10](10-form-sections)), you can swap sounds at a boundary: `if p.section and p.section.first_bar: p.program_change(...)`. The `on_section()` callback (also [Chapter 10](10-form-sections)) fires a beat early, which is the right moment to send a program change so the new patch is loaded *before* the downbeat. ``` ### Raw vendor messages: `p.sysex` When a parameter has no CC or NRPN — a patch dump, a global setting, a vendor-specific command — the escape hatch is **System Exclusive**. Pass the *inner* payload bytes (Subsequence adds the `F0` / `F7` framing); each byte is 0–127: ```{testcode} ch13 composition = subsequence.Composition(bpm=120, key="A", scale="minor") @composition.pattern(channel=1, beats=4) def setup(p): # "GM System On" — reset a GM-compatible device to its defaults at the downbeat. p.sysex([0x7E, 0x7F, 0x09, 0x01], beat=0) p.note(60, beat=0, duration=1.0, velocity=80) composition.render(bars=1, filename="sysex.mid") ``` ```{warning} SysEx is the rawest message there is — it's bytes straight to the device, with no musical meaning Subsequence can check. A wrong byte does nothing at best and mis-configures the synth at worst. Copy the exact byte sequence from your device's MIDI implementation chart, and test on one parameter before automating it. ``` ```{admonition} Reference :class: seealso {py:meth}`~subsequence.pattern_midi.PatternMidiMixin.rpn`, {py:meth}`~subsequence.pattern_midi.PatternMidiMixin.nrpn`, {py:meth}`~subsequence.pattern_midi.PatternMidiMixin.nrpn_ramp`, {py:meth}`~subsequence.pattern_midi.PatternMidiMixin.program_change`, {py:meth}`~subsequence.pattern_midi.PatternMidiMixin.sysex`, {py:func}`~subsequence.midi_utils.bank_select` ``` (sec-ch13-groove)= ## 13.4 Groove and `.agr` import A perfectly quantised beat is the giveaway of a machine. **Groove** is the cure: a small repeating template of per-step timing pushes (and optional velocity nudges) that you stamp onto a pattern *after* placing its notes. It's the same "feel" dial as the hardware sampler's swing, and the [Chapter 1](01-step-grid) distinction holds — groove and swing are **timing**, never pitch. A `Groove` is a value (like a `Motif` or a `Progression`). The quickest one to make is **`Groove.swing(percent)`**: `50` is straight, `~67` is triplet swing, `57` is the moderate Ableton-default shuffle. Apply it with **`p.groove(template, strength=1.0)`**: ```{testcode} ch13 composition = subsequence.Composition(bpm=120, key="A", scale="minor") SHUFFLE = subsequence.Groove.swing(percent=58) # a value you can reuse across 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) p.hit_steps("snare_1", [4, 12], velocity=90) p.hit_steps("hi_hat_closed", range(16), velocity=(55, 85)) p.groove(SHUFFLE) # full-strength shuffle p.randomize(timing=0.02, velocity=0.08) # then a touch of human jitter on top composition.render(bars=2, filename="shuffle.mid") ``` `strength=` blends the groove in: `0.0` leaves the timing untouched, `1.0` applies it fully, anything between dials it back (`p.groove(SHUFFLE, strength=0.5)` is half a shuffle). And as the example shows, groove pairs naturally with **`p.randomize()`** from the production toolkit — the groove gives a *repeating, intentional* pocket, `randomize` adds *uncorrelated* micro-jitter on top, the two layers that make programmed drums breathe. For a fully bespoke feel, build the `Groove` by hand — a per-slot list of timing offsets in beats (positive = late, negative = early), an optional matching velocity-scale list, and the grid the slots sit on: ```{testcode} ch13 # A hand-built MPC-style pocket: lay the 'e' and 'a' back a hair, duck their velocity. POCKET = subsequence.Groove( grid=0.25, # one slot per sixteenth offsets=[0.0, +0.02, 0.0, -0.01], # repeats every 4 sixteenths velocities=[1.0, 0.8, 0.95, 0.75], # accents on the beat, ghosts off it ) composition = subsequence.Composition(bpm=120, key="A", scale="minor") @composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP) def drums(p): p.hit_steps("hi_hat_closed", range(16), velocity=80) p.groove(POCKET) composition.render(bars=2, filename="pocket.mid") ``` ### Importing an Ableton groove: `Groove.from_agr` If you've collected `.agr` groove files (Ableton's groove-pool format, ripped from classic drum machines), **`Groove.from_agr(path)`** reads one straight into a `Groove` — timing offsets and velocity scaling lifted from the file, pre-scaled by its own Timing/Velocity amounts. Because it reads a file from disk, it can't run in our headless check, but the shape is exactly the swing example above with the constructor swapped: ```{note} _File-reading feature — point it at a real `.agr` and run it at your instrument; not executed here. The API is `Groove.from_agr(path) -> Groove`._ ``` ```python # Load a vintage shuffle ripped to an .agr file, then apply it like any Groove. mpc_swing = subsequence.Groove.from_agr("grooves/MPC-16-Swing-62.agr") @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=100) p.hit_steps("hi_hat_closed", range(16), velocity=80) p.groove(mpc_swing, strength=0.8) # 80% of the imported feel ``` ```{admonition} Reference :class: seealso {py:class}`~subsequence.groove.Groove`, {py:meth}`~subsequence.groove.Groove.swing`, {py:meth}`~subsequence.groove.Groove.from_agr`, {py:meth}`~subsequence.pattern_builder.PatternBuilder.groove`, {py:meth}`~subsequence.pattern_builder.PatternBuilder.randomize` ``` (sec-ch13-devices)= ## 13.5 Routing to several devices: `mirrors` and `layer` So far every composition has spoken to one MIDI port. A real rig has several — two synths, a drum machine, an effects unit. Subsequence addresses them by **device index**: the port you pass to `Composition(output_device=...)` is device `0` (the primary), and each call to **`composition.midi_output(device, name=...)`** registers another, returning its index (`1`, `2`, …). A friendly `name=` alias lets patterns say `device="synth_b"` instead of `device=2`. ```{testcode} ch13 # Three ports: the primary (device 0) plus two registered synths. composition = subsequence.Composition(bpm=120, key="A", scale="minor", output_device="Dummy MIDI") composition.midi_output("Dummy MIDI", name="synth_a") # returns 1 composition.midi_output("Dummy MIDI", name="synth_b") # returns 2 # Send this pattern to synth_a instead of the primary device. @composition.pattern(channel=1, beats=4, device="synth_a") def bass(p): p.sequence(steps=[0, 4, 8, 12], pitches=[40, 40, 43, 38], velocities=100) composition.render(bars=2, filename="multi-device.mid") ``` ```{note} The three outputs above all name the same mock port only because this is a headless check with one dummy device. On real hardware each `midi_output(...)` names a *different* physical port from `mido.get_output_names()` ([§0.5](00-setup)), and `device=` picks which one a pattern plays through. ``` ### Doubling a part across destinations: `mirrors=` **Mirroring** sends *every* event a pattern emits — notes, CCs, pitch bend, NRPN/RPN, program changes, SysEx — to one or more extra `(device, channel)` destinations as well as its own. It's how you stack two synths on one bassline for a fatter sound, or feed a part to both a synth and a recorder. Declare it with **`mirrors=`** on the decorator, a list of destination tuples: ```{testcode} ch13 composition = subsequence.Composition(bpm=120, key="A", scale="minor", output_device="Dummy MIDI") composition.midi_output("Dummy MIDI", name="synth_b") # device 1 # Bass plays on the primary device, channel 1, AND is doubled on device 1, channel 2. @composition.pattern(channel=1, beats=4, mirrors=[(1, 2)]) def bass(p): p.sequence(steps=[0, 4, 8, 12], pitches=[40, 40, 43, 38], velocities=100) p.cc(74, 90, beat=0) # the CC is mirrored too — both synths open their filter composition.render(bars=2, filename="mirrored-bass.mid") ``` Each mirror entry is `(device, channel)`, or `(device, channel, drum_note_map)` when the destination is a drum device that lays its sounds out differently — the named hits are re-resolved through *that* device's map, so `"kick_1"` lands on the right note number on each machine: ```{testcode} ch13 composition = subsequence.Composition(bpm=120, key="A", scale="minor", output_device="Dummy MIDI") composition.midi_output("Dummy MIDI", name="drum_machine") # device 1 # Drums on the primary GM kit, mirrored to a second machine with its own map. @composition.pattern(channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP, mirrors=[(1, 10, gm_drums.GM_DRUM_MAP)]) def drums(p): p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100) p.hit_steps("snare_1", [4, 12], velocity=90) composition.render(bars=2, filename="mirrored-drums.mid") ``` ```{warning} Each mirror is a *full copy* of the pattern's MIDI — every note and controller sent twice. That's the point (two synths in unison), but it also doubles the traffic on the bus, so don't mirror a dense CC ramp across five destinations on a slow DIN link. A mirror whose `(device, channel)` equals the pattern's *own* destination would double-fire on one port; Subsequence warns you if you do that by accident. (To toggle a mirror live mid-performance, use `composition.mirror(name, device, channel)` / `unmirror(...)`.) ``` ### Combining builders into one pattern: `composition.layer` `mirrors=` copies *one* builder to many destinations. **`composition.layer`** does the reverse — it merges *several* builder functions into a single pattern on one channel. It's the tool for composing a drum part out of reusable pieces (a `kick` function, a `hats` function) without each becoming its own scheduled pattern: ```{testcode} ch13 composition = subsequence.Composition(bpm=120, key="A", scale="minor") # Small, reusable builders — each does one job. def kick(p): p.hit_steps("kick_1", [0, 4, 8, 12], velocity=100) def hats(p): p.hit_steps("hi_hat_closed", range(16), velocity=(55, 85)) def perc(p): p.hit_steps("hand_clap", [4, 12], velocity=80) # Fold all three into one pattern on channel 10. composition.layer(kick, hats, perc, channel=10, beats=4, drum_note_map=gm_drums.GM_DRUM_MAP) composition.render(bars=2, filename="layered-drums.mid") ``` `layer` takes the same `device=`, `mirrors=`, `cc_name_map=` and `voice_leading=` keywords as `pattern` — and if any of the builders declares a `chord` parameter, the merged pattern follows the harmony just like a single chord-aware pattern would. ```{list-table} mirrors= versus layer :header-rows: 1 :widths: 24 76 * - Tool - Use it when * - **`mirrors=`** - You have *one* part and want it to sound on *several* destinations at once — unison stacking, doubling to a recorder. * - **`composition.layer`** - You have *several* builder functions and want them to be *one* pattern on one destination — assembling a part from reusable pieces. ``` ```{admonition} Reference :class: seealso {py:meth}`~subsequence.composition.Composition.midi_output`, {py:meth}`~subsequence.composition.Composition.mirror`, {py:meth}`~subsequence.composition.Composition.unmirror`, {py:meth}`~subsequence.composition.Composition.layer` ``` (sec-ch13-tuning)= ## 13.6 Microtonal tuning Subsequence speaks 12-tone equal temperament by default, but it can play **any** tuning on ordinary MIDI gear — no MPE, no special synth — by quietly injecting a pitch bend before each note to nudge it from the nearest 12-TET pitch to the exact microtonal frequency. You describe the tuning as a value, then apply it. A **`Tuning`** is a list of cent offsets from the unison. Build one four ways (`Tuning` lives in `subsequence`, so import it): ```{testcode} ch13 from subsequence import Tuning just = Tuning.from_ratios([9/8, 5/4, 4/3, 3/2, 5/3, 15/8, 2]) # 5-limit just intonation nine = Tuning.equal(19) # 19-tone equal temperament cents = Tuning.from_cents([100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200]) # explicit cents (= 12-TET) print(just.size, just.period_cents) ``` ```{testoutput} ch13 7 1200.0 ``` ```{list-table} Building a Tuning :header-rows: 1 :widths: 30 70 * - Factory - What it makes * - `Tuning.equal(n)` - `n`-tone equal temperament — `equal(12)` is standard, `equal(19)` / `equal(31)` are the popular microtonal EDOs. * - `Tuning.from_ratios([...])` - A tuning from frequency ratios — just intonation, harmonic scales. `2` is the octave. * - `Tuning.from_cents([...])` - A tuning from explicit cent values for degrees 1…N (the last is usually 1200). * - `Tuning.from_scl(path)` - Parse a Scala `.scl` file — the standard interchange format, with thousands of historical and world tunings freely available. ``` ### Tuning the whole piece: `composition.tuning` The usual way is **`composition.tuning(...)`** — one call sets a global tuning that Subsequence applies to every melodic pattern automatically. Pass exactly one source (`source=` for a `.scl` path, or `cents=` / `ratios=` / `equal=`). Drum patterns are excluded by default (their pitches are fixed GM note numbers, not scale degrees): ```{testcode} ch13 composition = subsequence.Composition(bpm=120, key="A", scale="minor") composition.tuning(equal=19, bend_range=2.0) # the whole track in 19-TET @composition.pattern(channel=2, beats=4) def melody(p): p.sequence(steps=[0, 3, 6, 9, 12], pitches=[60, 62, 64, 65, 67], velocities=90) @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) # untouched — drums opt out composition.render(bars=2, filename="tuning-global.mid") ``` ```{important} **`bend_range` must match your synth's pitch-bend setting.** Tuning rounds each note to the *nearest* 12-TET pitch and then bends it the rest of the way, so the correcting bend is never more than half a semitone — the standard ±2 semitones has ample room. The number you pass isn't there to grant more reach; it's there so the bend is *scaled correctly*. If your synth's wheel is set to ±12 but you leave the default `bend_range=2.0`, the same correction lands six times too far and the tuning goes sour. So: set the value to whatever your synth's wheel actually is (`bend_range=12` if you've set it to ±12). And because the correcting bend is per-channel, a **polyphonic** part needs a `channels=` pool so simultaneous notes can each carry their own bend (see below) — otherwise the last note's bend tilts the whole chord. ``` ### Tuning one part: `p.apply_tuning` For a single microtonal line over an otherwise 12-TET track, skip the global call and apply the tuning to just that pattern with **`p.apply_tuning(tuning, bend_range=2.0)`** — a post-build transform, like `groove` or `randomize`: ```{testcode} ch13 composition = subsequence.Composition(bpm=120, key="A", scale="minor") just = Tuning.from_ratios([9/8, 5/4, 4/3, 3/2, 5/3, 15/8, 2]) @composition.pattern(channel=2, beats=4) def lead(p): p.sequence(steps=[0, 4, 8, 12], pitches=[60, 64, 67, 72], velocities=90, durations=0.9) p.apply_tuning(just, bend_range=2.0) # this line only, in just intonation composition.render(bars=2, filename="tuning-part.mid") ``` For a polyphonic tuned part, hand a **`channels=`** pool — a list of MIDI channels Subsequence rotates simultaneous notes across, so each chord voice gets an independent bend. `composition.tuning(...)` takes the same `channels=` argument for a globally-tuned pad: ```{testcode} ch13 composition = subsequence.Composition(bpm=120, key="A", scale="minor") composition.harmony(style="aeolian_minor", cycle_beats=4) # Just intonation across the whole piece; spread chord voices over channels 3–6 # so each voice carries its own tuning bend. composition.tuning(ratios=[9/8, 5/4, 4/3, 3/2, 5/3, 15/8, 2], channels=[3, 4, 5, 6], bend_range=2.0) @composition.pattern(channel=3, beats=4, voice_leading=True) def pad(p, chord): p.chord(chord, root=52, velocity=65, sustain=True) composition.render(bars=2, filename="tuning-poly.mid") ``` ```{note} **Reading a Scala file (`.scl`).** The classic way to load a historical or exotic tuning is from a `.scl` file — meantone, Pythagorean, gamelan, hundreds more. It reads a file from disk, so it's not part of our headless check, but the call is a drop-in for the factories above: `comp.tuning("scales/meanquar.scl")`, or `Tuning.from_scl("scales/young.scl")` for a value. (For testing a tuning inline without a file, `Tuning.from_scl_string(text)` parses the same format from a string.) _Not executed here; the API is `Tuning.from_scl(path) -> Tuning`._ ``` ```{note} **Under the hood: tuning is just pitch bend, automated.** Everything in this chapter is the same family of move — a *control* message placed against the notes. Microtonal tuning is the most striking instance: Subsequence replaces each note's pitch with the nearest 12-TET note and prepends a pitch-bend event carrying the leftover cents, exactly the `pitch_bend` you met in [§13.2](#sec-ch13-bend) — only computed from a `Tuning` table instead of written by hand. Any pitch bends you *did* write (a `portamento`, a `slide`) are shifted to ride on top of the tuning offset rather than fight it. One mechanism — a controller event at a pulse — spans a dub siren, a 303 glide, a filter sweep, and a whole gamelan scale. ``` ```{admonition} Reference :class: seealso {py:class}`~subsequence.tuning.Tuning`, {py:meth}`~subsequence.tuning.Tuning.equal`, {py:meth}`~subsequence.tuning.Tuning.from_ratios`, {py:meth}`~subsequence.tuning.Tuning.from_cents`, {py:meth}`~subsequence.tuning.Tuning.from_scl`, {py:meth}`~subsequence.composition.Composition.tuning`, {py:meth}`~subsequence.pattern_builder.PatternBuilder.apply_tuning` ``` --- That's the full expressive surface: continuous controllers and pitch bend for gestures, NRPN/RPN/program-change/SysEx for the deep hardware, groove for feel, mirrors and layers for routing, and tunings for the notes between the notes. Your patterns no longer just place notes — they *perform* them on the gear in front of you. This closes the main guide. In [Chapter 14](14-live-and-data) we take it on stage — hot-swapping code as it plays, live control by hotkey and MIDI input, OSC, external-data sonification, and Ableton Link — and from there the **appendices** open the power-user path: the Direct Pattern API, the analysis and set-theory toolkit, the full MIDI-routing reference, and the API quick reference and glossary you'll keep coming back to.