MIDI#
The patcher has 45 MIDI objects. They fall into five groups:
Input objects receive MIDI from a hardware port:
.midiin,.notein,.ctlinand the rest of the*infamily.Senders turn numbers into MIDI messages:
.noteon,.bendout,.xctlout,.rpnoutand more..midioutsends the result to a hardware port.Codecs take a byte stream apart and put it back together:
.midiparse/.midiformat,.sxformat, and the MPE trio.Note handling objects keep a patch’s notes in order:
.makenote,.stripnote,.flush,.sustain,.poly,.borax,.offerand.midiflush.Port lookup:
.midiinfolists the ports a machine has.
Only the objects that hold a hardware port depend on the platform. Everything else works everywhere, also in a build without MIDI device support. This page explains how MIDI moves into and out of a patch, which objects exist where, and how the groups fit together. MIDI has the full reference for every object.
Platform availability#
MIDI device I/O uses RtMidi and is controlled by the YSE_ENABLE_MIDI_DEVICE
CMake option.
Platform |
Default |
Notes |
|---|---|---|
Windows |
|
|
Linux (desktop) |
|
|
macOS |
|
The MIDI device backend is not supported on macOS. |
Android |
|
RtMidi is not built for Android. |
You can switch the option off on Windows and Linux with
-DYSE_ENABLE_MIDI_DEVICE=OFF. That build does not need RtMidi at all.
When the option is off, the 18 objects that hold a port are not registered. The other 27 MIDI objects are always registered.
Availability |
Objects |
|---|---|
Only with |
|
Every build, every platform |
|
The senders used to be Windows-only. Since 3.0 they are registered
everywhere, because they only build bytes and open nothing (issue #746). A
patch that formats MIDI and sends it somewhere other than a port (to .seq,
to a file, or to your own code through a .s) now loads on every platform.
What happens to an object that is not registered:
CreateObject(".notein")returnsnullptrand logs an error (see Building and editing graphs).ParseJSONskips the object and every cord to or from it, logs it, and loads the rest of the patch (see Patch file format). The patch still plays, but without MIDI input or output.
To check this ahead of time, use the C API’s
yse_patcher_get_metadata_json(). Each object in it has a
"requires_midi_device" field, which is true for exactly the 18 objects
above. A patch editor can use it to mark those objects as not portable. The
flag is the same in every build, so metadata taken on Windows also tells you
what will be missing on Android.
In C++, the port-listing functions YSE::System().getNumMidiInDevices(),
getMidiInDeviceName() and their output counterparts are only declared when
the option is on. Guard such code with #if YSE_ENABLE_MIDI_DEVICE.
Two things to know about numbers#
Channels are not numbered the same way everywhere#
Objects |
Channel |
Program |
|---|---|---|
Input objects, |
1-16 |
1-128 |
The single-message senders ( |
0-15 |
0-127 |
The input objects and the codecs count the way hardware displays it, from 1.
The single-message senders take their channel as a creation
argument that is added to the status byte, so .noteon 0 sends on the
channel hardware calls 1. .programchange sends the program number as
given, 0-127. .midiformat takes 1-128 and subtracts one.
Two spellings of a MIDI message#
A MIDI message travels between objects as a list, in one of two spellings:
- Numeric (
144 60 100) Each byte is written as its decimal number.
.midiformat,.sxformat,.mpeconfig,.mpeformat,.xmidiinand.seqsend this spelling. It is the only one that.midiparse,.mpeparseand.seqread.- Binary
The characters of the list are the bytes. The single-message senders (
.noteon,.noteoff,.controlchange,.polypressure,.channelpressure,.programchange,.bendout, the.x*outfamily,.rpnoutand.nrpnout) send this spelling.
.midiout and .midiflush read both. They tell them apart by the first
character: a MIDI message starts with a status byte of 128 or more, which is
never a decimal digit. Any other object only understands the numeric
spelling. To record or decode what you build, use .midiformat (or
.mpeformat, .sxformat) rather than the single-message senders.
MIDI input#
How input reaches a patch#
MIDI arrives on RtMidi’s own thread, one per open port. The patch runs on the
audio thread. The two never touch directly. An engine-wide hub
(MIDI::inHub) sits between them:
The first input object that asks for a port opens it. This happens on the control thread, when the object joins its patcher. Later objects on the same port share the open device, and each one gets a queue of its own.
When a message arrives, RtMidi’s thread copies it into the queue of every object listening to that port. The copy is wait-free and allocates nothing. A message longer than 8 bytes (a SysEx dump) is split into 8-byte chunks, in order, and queued whole or not at all.
At the start of every block the patcher renders, each input object takes up to 64 events (512 bytes) off its queue and sends what it found out of its outlets. What that triggers is rendered in the same block.
So a MIDI message reaches the patch at the start of the next block after it arrived. Latency is at most one block, plus whatever the driver adds. A long SysEx dump is the exception: it is spread over several blocks, 512 bytes at a time, so a DX7 bank (4104 bytes) takes nine.
The input objects have no inlets. The patcher finds them anyway: they ask to be polled once per block. This means that input only arrives while the patcher renders. A patcher that is not attached to a sound or an insert (see Patcher) does not drain its queues, and they fill up.
When the last object on a port is deleted, the port is closed.
Limits#
Limit |
Value |
|---|---|
Input ports open at once ( |
8 (ports 0-7) |
Input objects listening to one port |
8 |
Events one object can hold between two blocks |
1024 (each up to 8 bytes) |
Longest message an object can receive |
8192 bytes |
Events one object handles per block |
64 (512 bytes) |
These are fixed so that RtMidi’s thread can walk the table without a lock.
An object whose
portis 8 or higher, or the ninth object on one port, gets no subscription. The refusal is logged. The object is still valid, but it never receives anything.RtMidi hands over a SysEx dump as one message, so the queue has to take all of it at once. It is sized for 8192 bytes, twice a DX7 32-voice bank. A longer message is dropped whole and logged.
When a message does not fit in the space left in an object’s queue, it is dropped whole too. A dump is never cut short, so a 240 that reaches a patch is always followed by the rest of its message and its 247. Each drop is counted, and a warning is logged once per episode, not once per message. A full queue normally means the audio thread has stalled or the patcher is not rendering.
Ports that do not open#
The port argument is an index into
YSE::System().getMidiInDeviceName(...), and 0 by default. If the port
does not open (nothing is plugged in, or the backend allows one client per
port and the host already has it open), the object stays valid and simply
receives nothing. The failure is logged. This way a patch loads the same on a
machine without a controller.
Changing the port argument of a live object rebuilds the object, because
the subscription is taken when the object joins its patcher.
Finding a port: .midiinfo#
Every MIDI object addresses a port by a bare index, and indexes differ between
machines. .midiinfo tells a patch what each index is. A bang sends:
the number of ports out of the right outlet,
then, for each port in order, its index (middle outlet) followed by its name (left outlet).
Input and output ports are numbered separately. The direction argument
(input, the default, or output) selects the set, and the messages
input and output switch it. A patch can match a name and send the
index it found to the object that should open it.
The port list is read once, when the object joins its patcher, because
listing ports allocates and talks to the operating system. A device plugged
in later appears after a refresh message. The scan runs on a background
thread, and the whole list is sent again once it is done, at the start of a
later block. At most 32 ports are listed, and names are cut at 63
characters.
The input objects#
All of them take port as the first argument. Most take channel as the
second one (1-16, or 0 for all channels). A channel argument
does not remove the channel outlet. The outlet stays and reports the
filtered channel, so the outlet numbering never depends on the arguments and
saved cords stay valid.
Object |
Receives |
Outlets, left to right |
|---|---|---|
|
every byte, undecoded |
byte (one int per byte) |
|
every message, framed |
one numeric list per whole message |
|
note-on and note-off |
pitch, velocity, channel |
|
notes with release velocity |
pitch, velocity, release velocity, channel |
|
control change |
value, controller, channel |
|
14-bit control change (controllers 0-31 + 32-63) |
value 0-16383, controller, channel |
|
pitch bend, coarse byte |
bend 0-127 (64 = centre), channel |
|
pitch bend, 14 bits |
bend 0-16383 (8192 = centre), channel |
|
pitch bend, as two bytes |
MSB, LSB, channel |
|
program change |
program 1-128, channel |
|
channel aftertouch |
pressure, channel |
|
polyphonic key pressure |
pitch, pressure, channel |
|
system real-time |
status byte (248 clock, 250 start, 251 continue, 252 stop, 255 reset) |
|
system exclusive only |
byte (one int per byte, from 240 to 247) |
|
registered / non-registered parameter writes |
value 0-16383, parameter number, channel |
Behaviour they share:
Outlets fire right to left. The channel goes out first and the leftmost value last. Whatever the leftmost outlet triggers already has the other values.
A note-off is velocity 0.
.noteinreports both spellings of a release (a note-off message, or a note-on with velocity 0) as the pitch with velocity 0. Test for it once, with.sel 0on the velocity..xnoteinalso reports the release velocity that a note-off message carries.Active sensing (254) is filtered out by the backend. Timing clock and SysEx are passed on.
.ctlin’s optional third argument filters by controller number. Its default is -1 (all), because controller 0 is a real controller (Bank Select).
Decoding and encoding a stream#
.midiparse and .midiformat are exact inverses. .midiparse takes
raw bytes (one int at a time, as .midiin sends them, or a numeric list)
and sends each complete message out of the outlet for its type:
Pin |
|
Contents |
|---|---|---|
0 |
note |
|
1 |
poly |
|
2 |
control |
|
3 |
program |
1-128 |
4 |
aftertouch |
0-127 |
5 |
bend |
0-127, coarse byte only |
6 |
channel |
1-16. On |
7 |
raw |
everything else, as a byte list: SysEx, song position, time code, real-time bytes |
Wire outlet n of .midiparse to inlet n of .midiformat and the
stream passes through. In between, a patch can change the part it cares
about. .midiformat sends one whole message per numeric list, on the
channel its channel inlet holds (1 by default).
.midiparse handles the parts of MIDI that make a byte search go wrong:
Running status. A chord is often sent as one status byte followed by several pitch/velocity pairs. Each pair decodes as a note.
Interleaving. A real-time byte (a clock) may arrive in the middle of another message. It goes out of the raw outlet at once, and the interrupted message continues.
Long messages. A SysEx dump longer than 128 bytes leaves the raw outlet in consecutive chunks.
Two readings are lossy: pitch bend keeps only the coarse byte, and
a note-off message comes back from .midiformat as a note-on with velocity
0. For full-resolution bend use .xbendin / .xbendout.
.xmidiin does the framing part of this on a port: it sends every whole
message as one numeric list, with running status expanded.
Sending MIDI#
The senders#
Each single-message sender builds one kind of message. The left inlet is hot and sends; the other inlets store a value for the next message.
Object |
Inlets |
Arguments |
|---|---|---|
|
pitch, velocity |
channel (0-15) |
|
pitch, velocity, release velocity |
channel |
|
value |
channel, controller |
|
value 0-16383, controller 0-31 |
channel |
|
pitch, pressure |
channel |
|
pressure |
channel |
|
program 0-127 |
channel |
|
bend 0-127 (64 = centre) |
channel |
|
bend 0-16383 (8192 = centre) |
channel |
|
MSB, LSB |
channel |
|
value 0-16383, parameter 0-16383 |
channel |
Values out of range are clamped, not refused. A patch that scales a controller past 127 plays at the top of the range instead of falling silent.
.midiformat does the work of .noteon, .controlchange,
.polypressure, .channelpressure, .programchange and .bendout
in one object, on one channel setting. It sends a release as a note-on with
velocity 0; use .noteoff or .xnoteout for a real note-off message.
.midiout#
.midiout <port> sends every list it receives to a hardware output port,
as one MIDI message, at the length it has. It reads both spellings (see
above). A numeric list with a value outside 0-255, or longer than 256 bytes,
is dropped whole. It also understands the messages allnotesoff,
reset, omni on, omni off, poly on, poly off,
local control on and local control off.
The port opens on the first message, not when the object is created, so a patch loads on a machine with different devices. Opening a port can block, and the first message may well arrive on the audio thread, so the open runs on a background thread (issue #759). That first message is dropped, and so is anything else that arrives before the port is open, usually just the next few milliseconds. If the very first message matters, send something harmless first, such as a controller the device ignores.
The send is not made on the thread that delivered the message either
(issue #949). .midiout puts the bytes on a queue, and a dedicated MIDI
sender thread passes them to the device, usually within a millisecond.
Messages sent from one thread keep their order. If the queue is ever full,
the message is dropped and counted, not sent late.
Extended precision#
Plain MIDI values have 7 bits. The x objects use the extra bits that the
protocol has room for:
Pitch bend is really 14 bits.
.bendin/.bendoutuse only the coarse byte (0-127). Over a two-semitone range that is a step of about three cents, which is audible on a slow bend..xbendin/.xbendoutuse all 14 bits (0-16383)..xbendin2/.xbendout2keep the two bytes apart, so a value passes through unchanged.Controllers 0-31 each have a fine partner 32 higher.
.xctlin/.xctlouttreat the pair as one 14-bit value..xctloutsends the coarse message first, as the MIDI specification requires..xctlinsends a value when the coarse byte arrives (with the fine byte as 0) and again when the fine byte follows, so devices that only send the coarse byte still work.Release velocity: a note-off message has a velocity byte of its own.
.xnoteinreports it and.xnoteoutsends it.
RPN and NRPN#
Registered and non-registered parameter numbers reach parameters that no controller number covers. One write is four control changes: two select a 14-bit parameter number (controllers 101/100 for RPN, 99/98 for NRPN), and two carry a 14-bit value (controllers 6 and 38).
.rpnout/.nrpnoutsend all four for every value, and each goes out as its own list, because.midioutsends every list as a single message. The selection is sent again with every value, because anything else on the cable may have changed it. The closing RPN Null is not appended. Send parameter 16383 yourself if you want it..rpnin/.nrpninremember the selected parameter per channel and report each write as value, parameter, channel. Both kinds move the same selection, so each object only reports its own kind..rpnintreats parameter 16383 (the RPN Null) as “nothing selected”.
Registered numbers mean the same on every device: 0 is pitch-bend range, 1 fine tuning, 2 coarse tuning. Non-registered numbers are defined by the manufacturer.
System exclusive#
.sysexin <port> sends every byte of every SysEx message, from 240 to 247,
and nothing else. It is the receiving half of a patch dump: request a patch
with .sxformat, and the answer arrives here without notes or clocks mixed
in. A real-time byte in the middle of a dump is dropped (.rtin gets it).
Any other status byte ends the dump, because hardware that is interrupted
simply stops sending. The object stores nothing, so collect the dump
downstream and watch for the 247. A dump of up to 8192 bytes arrives whole;
see Limits above for longer ones.
.sxformat builds a SysEx message from a template in its arguments, one
token per byte:
Token |
Meaning |
|---|---|
|
a constant byte |
|
the value held by that inlet. |
|
emits nothing and marks where the checksum region starts |
|
the Roland checksum of the region: the byte that makes its total a
multiple of 128. Without |
For example, a Roland-style parameter change whose last data byte comes from the left inlet:
.sxformat 240 65 16 66 18 sumstart 64 0 4 $i1 sum 247
A number, a list (which fills $i1, $i2 … from left to right) or a bang
on the left inlet sends the whole message as one numeric list. A negative
value leaves its byte out, so one template can cover a short and a long form.
A value above 127 is clamped, because a byte with the top bit set would end
the message early at the device. Templates are limited to 256 tokens.
MPE#
MIDI Polyphonic Expression gives every sounding note its own channel, so pitch bend, channel pressure and controller 74 (“slide”) work per note. A zone has a master channel (1 for the lower zone, 16 for the upper zone) and a number of member channels, one note each.
.mpeconfig <zone>A number on the inlet (0-15, default 15) sends the MPE Configuration Message: three control changes, each its own list, on the master channel. 0 turns the zone off. Only the two zones that MPE 1.0 defines are supported (
0lower,1upper)..mpeformatBuilds note, bend, pressure and slide messages for one member channel. Inlets: note (
pitch velocity), bend (0-16383, 14 bits, because MPE devices use a 48-semitone range), pressure, slide, and a cold channel inlet (1-16). There is one channel inlet, not one inlet per member channel..mpeparse <zone> <members>The inverse. Outlets: note, bend, pressure, slide, channel, and role (2 master, 1 member, 0 not in this zone). The role goes out first. A Configuration Message on the zone’s master channel updates the member count as it passes, and is not reported. Program changes, other controllers and SysEx are not decoded here; wire a
.midiparseto the same source for those.
All three are codecs and work on every platform.
Handling notes#
The senders do not remember anything. A patch that sends a note-on has to
send the note-off itself, and a note-off that never arrives is a hanging
note. These objects keep track of notes. Apart from .midiflush they work
on pitch/velocity pairs, before a channel is chosen, and they send the same
kind of pairs.
Object |
What it does |
|---|---|
|
Plays a note: sends the pair now, and the same pitch with velocity 0
after the duration (ms). Up to 64 notes per object. |
|
Passes note-ons and drops every pair with velocity 0. Use it when only attacks matter, so a key does not trigger twice. |
|
Passes pairs through and remembers which pitches are sounding. A bang releases them all. |
|
A sustain pedal. While the pedal is down (right inlet non-zero), it holds note-offs back and sends them when the pedal lifts. |
|
Gives each note a voice number (outlets: voice, pitch, velocity,
overflow), so a |
|
Only measures: note serial number, voice, how many notes are sounding, durations and time between attacks. It sends no notes. |
|
Stores a pair and returns the second value once, when the first is asked for. A transposer uses it to find the pitch it sounded when the note-off for the original pitch arrives. |
|
Works on a MIDI byte stream instead of pairs. Passes everything through, remembers which notes on which channels are sounding, and sends a note-off for each on a bang. |
All pair objects share a few rules:
Velocity is sent before pitch. Outlets fire right to left, so the velocity reaches a sender’s cold inlet before the pitch reaches its hot one. Wire outlet 0 to the pitch inlet and outlet 1 to the velocity inlet.
A list on the left inlet is
pitch velocity, which is what.midiparse’s note outlet sends. So.midiparse→.stripnoteis a single cord.Velocity 0 is a release, everywhere.
A typical order is: note source → .sustain → .flush → a sender
(.noteon, .midiformat) → .midiflush → .midiout. .flush
sees what is really sounding, pedal included, and .midiflush sees what
reached the wire. .flush and .midiflush are complements, not
alternatives: one works before the channel is decided, the other after.
.makenote waits on the patcher’s deferred-message scheduler (see
Time, scheduling and clocks), so its timing has a resolution of one block, and a paused
engine holds its pending releases. Besides its own limit of 64 notes, its
pending releases count toward the patcher-wide limit of 128 pending messages
that it shares with .delay, .pipe, .qlist, .mtr and .seq.
When its own 64 are taken, a new note is refused whole, attack included. When
the patcher-wide budget is full, the note is released at once rather than
never.
Held notes on teardown#
Since 3.0, a patch that goes away does not leave notes hanging (issue #758). When a patcher is cleared or destroyed, or a single object is deleted, the patcher first lets every object stop, and only then removes the cords. The objects that hold notes release them in that step:
.makenotereleases every pending note (itsstop)..flushand.midiflushsend their note-offs (a bang)..sustainsends the note-offs it was holding back..polyreleases every voice (itsstop).
Because the cords are still there, the releases travel down the patch and
reach .midiout like any other message. .borax, .stripnote and
.offer have nothing to release. A process that is killed outright can
release nothing, so a .midiflush before .midiout is still the safety
net during normal use.
Examples#
A patch played from a keyboard on input port 0:
YSE::patcher patch;
patch.create(2);
YSE::pHandle* keys = patch.CreateObject(".notein", "0"); // port 0, all channels
YSE::pHandle* mtof = patch.CreateObject(".mtof");
YSE::pHandle* osc = patch.CreateObject("~sine");
YSE::pHandle* dac = patch.CreateObject("~dac");
patch.Connect(keys, 0, mtof, 0); // pitch
patch.Connect(mtof, 0, osc, 0); // frequency
patch.Connect(osc, 0, dac, 0);
patch.Connect(osc, 0, dac, 1);
// keys is nullptr in a build without YSE_ENABLE_MIDI_DEVICE.
A generative line sent to hardware, with a safety net:
YSE::pHandle* note = patch.CreateObject(".makenote", "100 250"); // velocity, ms
YSE::pHandle* on = patch.CreateObject(".noteon", "0"); // channel 1
YSE::pHandle* guard = patch.CreateObject(".midiflush");
YSE::pHandle* out = patch.CreateObject(".midiout", "0"); // output port 0
patch.Connect(note, 0, on, 0); // pitch (hot)
patch.Connect(note, 1, on, 1); // velocity (cold, arrives first)
patch.Connect(on, 0, guard, 0);
patch.Connect(guard, 0, out, 0);
// Anything that sends pitches into note's left inlet plays; a bang on
// guard releases whatever is still sounding.
See also#
MIDI: the reference for every MIDI object.
Messages and dispatch: hot and cold inlets and the order in which outlets fire.
Time, scheduling and clocks: the deferred-message scheduler and its limits.
File input and output: reading and writing Standard MIDI Files with
.seq.