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 output_device="..." line that would skip the prompt next time.

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 SetupWindow ▸ Show MIDI Studio → double-click IAC Driver → tick “Device is online”. (Add more buses with + if you want several.)

IAC Driver Bus 1 (one entry per bus you enable).

Windows

Install loopMIDI and click + to create a named port. Name it something you’ll recognise.

The name you typed in loopMIDI (e.g. loopMIDI Port).

Linux

Load the ALSA virtual-MIDI module once: sudo modprobe snd-virmidi. Or wire a port in your JACK / patchbay setup.

An ALSA name like Virtual Raw MIDI 1-0:VirMIDI 1-0 24:0.

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.

Things to know about latency compensation

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 .mid is left uncompensated.

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 output_device= to skip the prompt for good.

The script hangs with no output at all.

The interactive prompt is blocking in a non-interactive context (CI, cron, a launcher). Pass output_device= explicitly so discovery never asks.

Logged “device not found”, then silence.

The name doesn’t match mido.get_output_names() exactly (§C.2). Re-list and paste the string verbatim — no substrings, no partial names.

It worked yesterday, now “not found” (Linux).

ALSA :client:port digits drifted across a reboot or replug (§C.3). Re-list the ports and update output_device= with the current name.

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 channel= (drums = channel 10), and that local control / the right multitimbral part is set up.

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

open /dev/snd/seq failed: Permission denied (Linux).

Your user isn’t in the audio group. sudo usermod -a -G audio $USER, then log out and back in (§C.3).

Wrong drum sounds on a second machine.

A plain (device, channel) mirror copies the raw note number, which may hit a different voice. Add a per-destination drum map — (device, channel, drum_note_map) — so names re-resolve per device (see §13.5).

Two devices don’t sound together.

Different physical latencies. Measure each and set latency_ms= so the faster devices are delayed to meet the slowest (§C.4).

ValueError: latency_ms must be non-negative.

A negative latency_ms was passed. Latency is a physical figure ≥ 0; remove the minus sign.

A device="name" pattern plays on the wrong synth.

The alias doesn’t match a registered device, so it falls back to device 0 (a warning is logged). Check the name= you gave the matching midi_output(...).

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.