Appendix C · MIDI Routing and Troubleshooting Reference¶
This appendix is the single place to get MIDI out of Subsequence and into anything that makes sound — a hardware synth, a DAW, a software instrument, or several at once — and to diagnose it when no sound comes out. Chapter 0 gets you to your first loop; this is the depth it points to.
Subsequence makes no sound of its own. It sends MIDI to a named port, and the thing on the other end makes the sound. So almost every “it doesn’t work” turns out to be a routing question: is the port name right, is anything listening, and is it on the channel you sent to? The five sections below take those in order, ending with a symptom-to-fix table.
Note
The runnable blocks here use a mocked MIDI backend (the same one that validates
every example in this guide), so the port is the stand-in name "Dummy MIDI" and
mido.get_output_names() returns exactly one entry. On your machine that list is
your real gear. Blocks that show real hardware names (an IAC bus, a Minilogue) are
marked illustrative — they’re verified for API shape but name your ports, so
they aren’t executed here.
C.1 How Subsequence chooses an output¶
Every Composition owns one primary output, called device 0. You either name
it, or let Subsequence discover it.
You name it with output_device= on the constructor — the exact port name from
§C.2:
>>> composition = Composition(bpm=120, output_device="Dummy MIDI")
Leave output_device out and Subsequence auto-discovers, following three rules
that depend only on how many ports it can see:
Ports visible |
What happens |
|---|---|
Exactly one |
That port is selected automatically — no prompt, no argument needed. This is why the very first loop in Chapter 0 just works when one synth is plugged in. |
Several |
Subsequence prints a numbered menu and waits for you to type a number. After
you pick, it prints the |
None |
It logs an error and opens no port. Nothing sounds; the run does not crash. |
Important
The prompt is interactive — it calls input() and blocks for a keystroke. That
is fine when you run a script in a terminal, but it will hang a non-interactive
process (a cron job, a CI run, a background launcher) with no obvious error. In any
unattended context, always pass output_device= explicitly so discovery never
has to ask.
Auto-discovery is convenient for one-synth setups and for trying things quickly. For anything you’ll run more than once — and for every multi-device rig — name the port. It documents your intent and removes the prompt.
C.2 Listing and matching ports¶
To name a port you first need its exact spelling. Ask mido — the MIDI library
Subsequence sits on — what your operating system is currently exposing:
>>> import mido
>>> mido.get_output_names()
['Dummy MIDI']
On your machine that list is your real ports, for example
['IAC Driver Bus 1', 'Minilogue']. Copy the one you want verbatim into
output_device=.
Warning
Matching is strict and exact. The string you pass must equal an entry in
mido.get_output_names() character for character. Subsequence does not match
on substrings or guess from a partial name — output_device="Minilogue" will not
find a port actually called "Minilogue:Minilogue MIDI 1 24:0". This verbatim-match
rule applies everywhere a device name is accepted —
Composition(output_device=…), composition.midi_output(…), and
composition.midi_input(…) — but the consequence of a miss differs by direction.
A wrong output name fails quietly: the port simply doesn’t open (the error is
logged, not raised) and you get silence. A wrong input name is louder — it
raises (see Input ports below). Either way the cure is the same: re-list and paste
the exact string.
A throwaway one-liner from the shell is the fastest way to read the list before you write any code:
python -c "import mido; print(mido.get_output_names())"
Tip
If a name has tricky punctuation or trailing numbers (common on Linux — see §C.3), don’t retype it. Print the list, copy the exact string, and paste it. One stray space or wrong digit is the difference between sound and silence.
Input ports¶
Listing input ports — for live keyboards, external clock, or CC control — uses the matching call, and the same exact-name rule holds:
>>> mido.get_input_names()
['Dummy MIDI']
Inputs differ in one way worth knowing: a named input that isn’t present raises an error rather than falling back to another port. That is deliberate — an input drives clock-follow and live note capture, so silently listening to the wrong device would desynchronise or mis-record a take. (Live input is covered in Chapter 14; this appendix is about getting MIDI out.)
C.3 Virtual ports, per operating system¶
A hardware instrument shows up as a port the moment you plug it in — there is
nothing to install. A software instrument (a DAW track, a plug-in host, a
sampler) lives inside the computer, so you need a virtual MIDI port: an
operating-system cable that carries MIDI from Subsequence to the other app.
Subsequence does not create one itself; you make it once at the OS level, and then
it appears in mido.get_output_names() like any other port.
OS |
Create a virtual port |
Then it appears as… |
|---|---|---|
macOS |
Audio MIDI Setup → Window ▸ Show MIDI Studio → double-click IAC Driver → tick “Device is online”. (Add more buses with + if you want several.) |
|
Windows |
Install loopMIDI and click + to create a named port. Name it something you’ll recognise. |
The name you typed in loopMIDI (e.g. |
Linux |
Load the ALSA virtual-MIDI module once: |
An ALSA name like |
Once the port exists, point your software at it: in the DAW, set a track’s MIDI input to that virtual port, arm the track, and aim its instrument at it. Subsequence sends to the port; the DAW receives and plays the sound.
Note
A virtual port is a one-way named pipe — there’s nothing to “connect” beyond
naming the same port on both ends. If you see the IAC bus or loopMIDI port in
mido.get_output_names() but hear nothing, the gap is almost always on the
receiving side: the DAW track isn’t armed, or its input is set to a different
port. See the table in §C.5.
Linux: ALSA names drift, and the audio group¶
Two Linux specifics catch people, both worth pinning down here.
Names include client and port IDs, and they move. An ALSA port name carries
trailing :client:port digits — for example
"Scarlett 2i4 USB:Scarlett 2i4 USB MIDI 1 16:0". Those numbers are assigned in
connection order, so they can change between reboots, or when you unplug and
replug a device, or when a virtual port is recreated. A name that worked yesterday
can stop matching today.
Tip
On Linux, treat the port name as volatile: re-run
python -c "import mido; print(mido.get_output_names())" whenever a
previously-working output_device= suddenly fails, and paste the fresh name. The
musical part of the name ("Scarlett 2i4 USB…") is stable; the trailing digits are
what drift.
Your user must be in the audio group. The ALSA backend needs access to
/dev/snd/seq. If opening a port fails with open /dev/snd/seq failed: Permission denied, add yourself to the group once:
sudo usermod -a -G audio $USER
Then log out and back in (or run newgrp audio to start a single shell with the
group already active). This is a one-time setup, separate from the build-tools
note in Chapter 0.
C.4 Multiple devices and latency compensation¶
A real rig has more than one destination. Subsequence addresses them by device
index: the port from Composition(output_device=...) is device 0 (the
primary), and each call to composition.midi_output(device, name=, latency_ms=)
registers another, returning its index — 1, 2, and so on.
>>> comp = Composition(bpm=120, output_device="Dummy MIDI") # device 0
>>> comp.midi_output("Dummy MIDI", name="sampler") # device 1
1
The name= alias is optional but worth setting: it lets a pattern say
device="sampler" instead of device=1. (Omit it and the raw port name becomes
the alias.) A pattern with no device= always routes to device 0, so
single-device compositions need no changes at all.
# Illustrative — names YOUR ports. API shape verified.
import subsequence
comp = subsequence.Composition(bpm=120, output_device="MOTU Express") # device 0
comp.midi_output("Roland Integra", name="strings") # device 1
comp.midi_output("Elektron Analog Four", name="bass") # device 2
@comp.pattern(channel=1, beats=4, device="strings") # plays on the Integra
def pad(p):
p.note(60, beat=0, duration=4)
@comp.pattern(channel=1, beats=4) # no device= → device 0
def lead(p):
p.note(72, beat=0)
The same device= keyword exists on composition.layer(...) and on
composition.trigger(...), and the mirrors=[(device, channel), ...] keyword fans a
single pattern out to several destinations at once. Multi-device routing — the
device=/mirrors= decorator keywords, per-destination drum maps, and
composition.layer — is taught with full musical examples in
§13.5. This appendix covers the plumbing underneath:
registering the ports, and aligning them in time.
Latency compensation¶
When you mix instruments that respond at different speeds — a hardware synth fires almost instantly, while a software sampler might sound ~20 ms late because of its audio buffer — notes scheduled on the same beat don’t sound together. You tell Subsequence each device’s physical latency in milliseconds and it lines everything up. Because you can’t send a note into the past, it aligns the rig by delaying the faster devices to meet the slowest one.
Set latency_ms= on the constructor for the primary device, and on each
midi_output() call for the rest:
>>> comp = Composition(bpm=120, output_device="Dummy MIDI", latency_ms=0)
>>> comp.midi_output("Dummy MIDI", name="sampler", latency_ms=22)
1
Here the sampler (device 1) is the slowest at 22 ms, so every event bound for
device 0 is held back 22 ms; all of them then sound at the same instant. The figure
is a non-negative number you measure once for your rig — a negative value is
meaningless (a device can’t sound before it’s triggered) and raises a ValueError.
Behaviour |
Why it matters |
|---|---|
It’s per device, so mirroring just works. |
Mirror a part from a fast synth to a slow sampler and each copy is compensated for its own destination — they still land together. |
The recorded |
The file captures the musical (logical) timing. Compensation is specific to your physical setup; re-playing the file through the same rig applies it again. |
The MIDI clock is never delayed. |
Only note and expression events shift, so external gear slaved to the clock stays locked to the beat. |
Watch the whole-rig latency. |
The slowest device sets how late everything responds to live input. Above ~30 ms Subsequence logs a warning, because tight live playing starts to suffer. |
Warning
latency_ms is for physical output latency you measured, not a creative
delay. The slowest device defines the alignment point and every faster device is
held back to meet it, so an inflated figure makes the whole rig sluggish to live
input. If you want a musical delay, use a CC or your instrument’s own effects —
not the compensation figure.
C.5 Common problems and fixes¶
Most routing failures fall into a handful of patterns. Find the symptom, apply the fix.
Symptom |
Likely cause and fix |
|---|---|
It prints a numbered list and waits for input. |
Several ports were found and auto-discovery is prompting (§C.1).
Type the number, or set |
The script hangs with no output at all. |
The interactive prompt is blocking in a non-interactive context (CI, cron, a
launcher). Pass |
Logged “device not found”, then silence. |
The name doesn’t match |
It worked yesterday, now “not found” (Linux). |
ALSA |
No errors, but silence (hardware). |
MIDI is flowing but the synth isn’t listening on that channel. Confirm the
instrument’s MIDI-receive channel matches your |
No errors, but silence (DAW / software). |
The virtual port exists but nothing receives it. In the DAW, set the track’s MIDI input to that exact port, arm the track, and point its instrument at it (§C.3). |
|
Your user isn’t in the |
Wrong drum sounds on a second machine. |
A plain |
Two devices don’t sound together. |
Different physical latencies. Measure each and set |
|
A negative |
A |
The alias doesn’t match a registered device, so it falls back to device 0 (a
warning is logged). Check the |
Tip
The fastest sanity check is “is anything listening at all?” Render to a file
first (composition.render(filename=...)) to prove your code makes the notes you
expect — that needs no hardware. If the file is right but you hear nothing live,
the problem is downstream of Subsequence: the port name, the channel, or the
receiving instrument. Splitting “did my code work?” from “did my routing work?”
this way isolates the fault in seconds.
You can now route Subsequence to any rig and diagnose it when it goes quiet. For the musical side of multi-device work — doubling parts, per-device drum maps, and assembling layered patterns — see §13.5; for live input, clock, and OSC, see Chapter 14; and for the exact signatures of every routing method, Appendix D.