USB MIDI 2.0 for classic Arduino boards. On the stock core, with nothing else to install.
C99 transport over PluggableUSB: no LUFA, no TinyUSB, no extra USB stack. Zero UMP dependency, MIT. AVR and classic SAMD.
midi2duino turns a classic Arduino board into a USB MIDI 2.0 device using only the core it already ships with. Through PluggableUSB (the same extension the Arduino ecosystem already uses to add USB functions), it plugs a dual alternate-setting MIDIStreaming interface (alt 0 = USB-MIDI 1.0, alt 1 = USB-MIDI 2.0 / UMP) and its Group Terminal Block into the core's own USB, then pumps whole Universal MIDI Packets between the bulk endpoints and two word rings.
No LUFA, no TinyUSB, nothing to install beyond the board core. Your sketch sees only 32-bit UMP words in and out.
midi2duino is a transport. It carries UMP; it does not interpret it. It has no opinion about what the messages mean, answers no MIDI-CI, and owns no device identity. Pair it with a UMP library or your own code:
your sketch notes, controllers, device logic
UMP library parse / build UMP, MIDI-CI, Stream discovery
midi2duino <- you are here: UMP words <-> USB wire
Arduino core (PluggableUSB) USB device stack
AVR / SAMD hardware
The reference pairing is the midi2 C99 core, which builds and parses UMP and runs MIDI-CI on top of this transport.
The node self-registers at startup, so a sketch needs no USB setup:
#include <midi2duino.h>
void setup() {}
void loop() {
midi2duino_task(); // pump the endpoints
uint32_t w;
while (midi2duino_read(&w)) { // handle inbound UMP
midi2duino_write_word(w); // (this echoes it back)
}
}Open the device in a MIDI 2.0 host (Linux with a 6.5+ kernel exposes a /dev/snd/ump* node; Windows via MIDI Services; the MIDI 2.0 Workbench on any platform). See examples/Echo.
| Function | Purpose |
|---|---|
midi2duino_task() |
pump both endpoints once per loop() |
midi2duino_read(&word) |
pop one received UMP word, false if empty |
midi2duino_write(words, n) |
queue a whole UMP message atomically |
midi2duino_write_word(word) |
queue one word (one-word message types only) |
midi2duino_configured() |
true once the host has configured the device |
Multi-word messages are never split across a USB packet: the pump reads the message type's word count and only flushes a message when it fits whole. When the RX ring fills, the OUT bank is left unread and the hardware NAKs, which is the correct USB flow control, so the host simply retries.
midi2duino always speaks UMP. The descriptors advertise both alternate settings so the device enumerates correctly everywhere, and a MIDI 2.0 host selects alt 1 and receives the full protocol. The stock Arduino cores do not surface the host's alternate-setting choice to PluggableUSB, so alt 0 (legacy MIDI 1.0) is not a separate runtime path here. Target a MIDI 2.0 host.
- AVR (ATmega32U4): Arduino Leonardo, Pro Micro, Micro. Uses
arduino:avr. - Classic SAMD (SAMD21): Seeed XIAO SAMD21, MKR, Zero. Uses
Seeeduino:samdorarduino:samd.
Boards whose core routes USB through TinyUSB (RP2040, ESP32, Adafruit SAMD/nRF52) already have a MIDI 2.0 path through TinyUSB; use midi2cpp there instead.
Add the library to the Arduino IDE with Sketch > Include Library > Add .ZIP Library (download this repository as a ZIP), or clone it into your libraries/ folder. The Echo sketch then appears under File > Examples > midi2duino.
Build and upload as usual, or from the command line:
arduino-cli compile --upload -p /dev/ttyACM0 --fqbn arduino:avr:leonardo examples/EchoDefaults build a working device. Override before building with a -D flag if needed.
| Macro | Default | Meaning |
|---|---|---|
MIDI2DUINO_RING_WORDS |
64 |
depth of each UMP word ring |
MIDI2DUINO_EP_SIZE |
64 |
bulk packet size (full speed) |
Under PluggableUSB the core owns the device descriptor: the USB VID, PID, and manufacturer / product strings come from the board's build properties, not from this library. To brand the device, set them with a boards.local.txt next to the core's boards.txt, or inline with arduino-cli:
--build-property build.vid=0x1209 --build-property build.pid=0x0001 \
--build-property 'build.usb_product="My MIDI 2.0 Device"'
Without an override the device enumerates with the board's stock Arduino VID/PID. The UMP Endpoint Name and the MIDI-CI identity live in the UMP layer above this transport, not here.
- Not a USB stack. The Arduino core is, and stays untouched.
- Not a UMP parser or builder. That is the layer above; midi2duino moves opaque words.
- Not a MIDI-CI or Property Exchange implementation. Those are application concerns.
- Not a host. Device side only.
- midi2 — the portable C99 UMP + MIDI-CI core. The natural layer above this transport.
- midi2cpp — a C++17 wrapper over midi2 for TinyUSB platforms.
You can sponsor midi2duino at GitHub Sponsors. Sponsorship funds boards for cross-platform validation, spec access, and continued maintenance.
midi2duino is created and maintained by Saulo Veríssimo. It is the classic-Arduino-core member of the midi2 family, validated on hardware (ATmega32U4 and SAMD21).
The MIDI 2.0 specifications referenced here are copyright of the MIDI Association and available at https://midi.org/midi-2-0.
"MIDI" is a registered trademark of the MIDI Manufacturers Association (now MIDI Association). "MIDI 2.0", "MIDI-CI", and "UMP" are terms defined by the MIDI Association in the public specifications.
MIT. Free for commercial and open-source use.