C++17, callback-first, static-by-default, depends on midi2, MIT. From DIY to professional products.
midi2cpp is the layer where a sketch meets the protocol. Plug a board into the laptop, write five lines of C++, flash, and the device appears on the bus as a USB MIDI 2.0 endpoint with full Capability Inquiry, Property Exchange, and 32-bit resolution.
Underneath, midi2 (the portable C99 core) handles parsing, dispatch, and reassembly. midi2cpp adds the C++ ergonomics: callbacks, board glue, ready-made USB descriptors. The board does the talking; the sketch tells it what to say.
- midi2cpp
midi2cpp draws a clear line between what the library declares and what stays the caller's job:
- Exactly one declared dependency: midi2. The C99 core lives in its own repo, is versioned independently, and is resolved by whichever package manager fits the host build:
- Arduino Library Manager:
depends=midi2 (>=0.3.4)inlibrary.properties. - PlatformIO Registry:
dependencies."sauloverissimo/midi2": "^0.3.4"inlibrary.json. - ESP-IDF Component Manager:
idf_component.ymldeclaration plusmidi2inREQUIRES. - CMake:
find_package(midi2 0.3.4 CONFIG)first, falls back toFetchContent_Declare(midi2 GIT_TAG v0.3.4).
- Arduino Library Manager:
- No submodules.
git cloneis the install. No--recurse-submodules, no half-initialised state. - No transport library pulled in. TinyUSB, Teensy native USB, PIO-USB, libDaisy USBMidi: all caller-supplied. The library does not include
<Arduino.h>,pico/time.h,esp_timer.h, or any USB header. - No clock or RNG dependency. Caller injects
millis/time_us_64/esp_timer_get_timeandrandom/get_rand_32/esp_randomthrough public hooks. Unset hooks degrade silently, no missing-symbol link errors.
git clone https://github.com/sauloverissimo/midi2cpp.git
cmake -B build && cmake --build build && ctest --test-dir buildThree commands. The first cmake configure pulls midi2 once via FetchContent (or picks it up from a system install via find_package); subsequent rebuilds and tests run offline. Anywhere C++17 + CMake reach: Linux, macOS, Windows, embedded cross-toolchains for ARM/RISC-V/Xtensa.
midi2cpp is platform-agnostic: it parses, dispatches, and assembles UMP, and it leaves USB transport, clock, and entropy to the caller. The sketch wires four hooks to its platform's USB MIDI driver; the library does the rest.
#include <midi2cpp.h>
using namespace midi2;
m2device midi;
m2ci ci(midi);
// 1. Outbound UMP. Forward to the platform's USB MIDI write API.
void plat_write(const uint32_t* words, size_t count) {
// tud_midi_n_stream_write(0, (uint8_t*)words, count * 4); // TinyUSB
// usbMIDI.sendUMP(words, count); // Teensy native
// ...
}
// 2. Monotonic millisecond clock. Used by the JR Heartbeat.
uint32_t plat_now() { return millis(); }
// 3. Entropy source for MUID. Caller picks (esp_random / get_rand_32 / etc).
uint32_t plat_rng() { return random(); }
void setup() {
Serial.begin(115200);
midi.setWriteFn(plat_write);
midi.setNowFn(plat_now);
midi.setMounted(true);
midi.setAltSetting(1); // 1 = MIDI 2.0 stream
midi.begin();
midi.enableJRHeartbeat(500);
ci.setRngFn(plat_rng);
static const uint8_t mfrId[3] = {0x7D, 0x00, 0x00}; // educational prefix
ci.begin(mfrId, /*family*/ 0x0001, /*model*/ 0x0001, /*version*/ 0x00010000);
ci.addPropertyStatic("DeviceInfo",
"{\"manufacturer\":\"midi2cpp\",\"model\":\"hello\"}");
midi.onNoteOn([](uint8_t /*g*/, uint8_t ch, uint8_t n, uint16_t v,
uint8_t /*at*/, uint16_t /*ad*/) {
Serial.print("NoteOn ch="); Serial.print(ch);
Serial.print(" note="); Serial.print(n);
Serial.print(" vel="); Serial.println(v);
});
}
void loop() {
// 4. Inbound UMP. Pump RX from the platform's USB MIDI callback into the lib.
uint32_t in[16];
size_t n = /* read up to 16 UMP words from your USB driver */ 0;
if (n) midi.feedRx(in, n);
midi.task(); // dispatches reassembled SysEx, fires heartbeat
}The four hooks (setWriteFn, feedRx, setNowFn, setMounted + setAltSetting) are the entire platform contract. When an injection point is left unset the corresponding feature degrades safely (no transport, frozen MUID, no heartbeat).
- USB MIDI 2.0 device, host, or both, depending on the board.
- 49 typed UMP callbacks: notes, CCs, RPN/NRPN, per-note expression, Flex Data, Stream messages.
- MIDI-CI out of the box: Discovery, Profile negotiation, Property Exchange (with Subscribe/Notify), Process Inquiry.
- Static-by-default. The hot path is allocation-free; init-time
newonly insidem2bridgefor the per-slot tables. Fits a Cortex-M0+. - Pay-as-you-go: only the modules called by the sketch end up in the binary.
| Class | Role | Status |
|---|---|---|
m2device |
USB MIDI 2.0 device, board enumerates as a MIDI peripheral on a host (DAW, OS) | available |
m2host |
USB MIDI 2.0 host, board exposes a USB-A port and reads attached MIDI devices | available |
m2bridge |
Host + device, both ports active; route, group-rewrite, dynamic FB names, MIDI 1.0 alt 0 uplift | available |
Same callback API across the three. m2bridge composes m2device + m2ci + m2host and adds a multi-slot Stream Discovery responder, raw UMP forward with per-slot group window rewrite, and an internal ByteStreamConverter per slot for MIDI 1.0 alt 0 upstream devices. Reference platform glue at examples/esp32-p4-devkit-bridge2-midi2; the older examples/esp32-p4-devkit-bridge-midi2 and examples/adafruit-feather-rp2040-bridge-midi2 keep the same role with the slot table + responder carried inline, until they migrate to m2bridge.
Validated on real hardware against forks and PRs maintained internally while the upstream merges are pending. midi2cpp is one of several integrations of the underlying midi2 C99 core; concrete recipes for boards that use midi2cpp ship under examples/, one per role (device, host, bridge). The Status column names the override each test required.
| Board | MCU | Device | Host | Bridge | Transport | Status |
|---|---|---|---|---|---|---|
| ESP32-S3 DevKitC-1 | ESP32-S3 | ✅ | - | - | TinyUSB PR #3571, recipe in esp32-s3-devkitc-usb-midi2 |
|
| Arduino Nano ESP32 | ESP32-S3 | ✅ | - | - | TinyUSB PR #3571, recipe in arduino-nano-esp32-midi2 |
|
| Waveshare ESP32-P4-WIFI6-DEV-KIT | ESP32-P4 | ✅ | ✅ | ✅ | TinyUSB experiment/midi-coexistence branch (alt-walk bcdMSC defer for MIDI 1.0 + 2.0 host coexistence) on top of PR #3571 + mandatory LP_SYS.usb_ctrl PHY swap on the device side, recipes in esp32-p4-devkit-usb-midi2 (device, INT PHY, OTG_FS), esp32-p4-devkit-host-midi2 (host, UTMI PHY, OTG_HS) and esp32-p4-devkit-bridge-midi2 (dual-stack bridge, inline glue) and esp32-p4-devkit-bridge2-midi2 (same role on top of m2bridge, PID 0x4095) |
|
| LilyGo T-Display S3 | ESP32-S3 | ✅ | - | - | TinyUSB PR #3571, recipe in t-display-s3-midi2 (Tier A receiver with on-board ST7789 piano roll). Hardware validated 2026-05-01: enumerates cafe:4094 as TDisplayS3, Group 1 (Main) visible to ALSA, NoteOn/Off lights piano keys live |
|
| T-Display S3 AMOLED | ESP32-S3 | ✅ | ✅ | - | - | TinyUSB PR #3571 |
| Teensy 4.1 | i.MX RT1062 | ✅ | ✅ | - | - | direct consumer of midi2 (Teensy core fork, native USB MIDI 2.0 with AS0 + AS1) |
| Raspberry Pi Pico | RP2040 | ✅ | - | - | TinyUSB PR #3571, recipe in examples/rp2040-midi2 |
|
| Waveshare RP2040 Pi Zero | RP2040 | ✅ | - | - | TinyUSB PR #3571, recipe in examples/waveshare-rp2040-midi2 |
|
| Adafruit Feather RP2040 USB Host | RP2040 | ✅ | ✅ | ✅ | TinyUSB PR #3571 + Pico-PIO-USB 675543b (handshake delay fix), recipes in adafruit-feather-rp2040-host-midi2 and adafruit-feather-rp2040-bridge-midi2 |
|
| RP2040 Pro Micro (Tenstar Robot) | RP2040 | ✅ | - | - | TinyUSB PR #3571, deterministic UMP emitter for Windows MIDI Services testing in rp2040-promicro-ump-test-bench |
|
| Waveshare RP2350-USB-A | RP2350 | ✅ | ✅ | ✅ | TinyUSB PR #3571 + Pico-PIO-USB 675543b + R13 hardware mod for host mode (desolder the 1.5 kΩ pull-up on USB-A D+), recipes in waveshare-rp2350-usb-a-midi2 (device) and waveshare-rp2350-usb-a-bridge-midi2 (bridge) |
|
| SparkFun Pro Micro RP2350 | RP2350 | ✅ | - | - | TinyUSB PR #3571, recipe in sparkfun-promicro-rp2350-midi2 |
|
| Raspberry Pi Pico 2 | RP2350 | ✅ | ✅ | - | - | TinyUSB PR #3571 |
| ESP32-C6-DevKitC-1 | ESP32-C6 | - | - | BLE-MIDI 1.0 + ESP-NOW | esp32-c6-devkitc-multi-midi2 (Tier B wireless, no PID consumed). |
|
| nRF52840 Pro Micro (Nice!Nano class) | nRF52840 | ✅ | - | - | TinyUSB native CMake build (PR #3571 fork via FetchContent + hw/bsp/family_support.cmake, BSP feather_nrf52840_express upstream), recipe in nrf52840-promicro-midi2 (Tier B). Hardware validated 2026-04-30 on Nice!Nano: enumerates cafe:40F1, Group 1 (Main) visible to ALSA, Per-Note PB vibrato + chromatic walk + RPN/NRPN streaming live |
|
| Seeed XIAO SAMD21 | SAMD21 | ✅ | - | - | TinyUSB native CMake build (PR #3571 fork via FetchContent + hw/bsp/family_support.cmake, BSP seeeduino_xiao upstream), recipe in xiao-samd21-midi2 (Tier C). Hardware validated 2026-04-30: enumerates cafe:40F0, Group 1 (Main) visible to ALSA, chromatic walk + CC sweep streaming live |
|
| T-PicoC3 | RP2040 + ESP32-C3 | ✅ | - | - | - | TinyUSB PR #3571 |
Three override sources cover everything: TinyUSB PR #3571 (the bulk of the matrix, USB MIDI 2.0 device + host driver), Pico-PIO-USB at SHA 675543b (PR #186 "reduce handshake delay", required for MIDI 2.0 host enumeration over PIO-USB; predates the next tagged release), and a local Teensy core fork (native USB MIDI 2.0 with AS0 + AS1 alt settings). Each will retire from the Status column as it merges into its respective upstream.
20 recipes ship under examples/, grouped by build path:
| Build system | Count | Recipes |
|---|---|---|
| Pico SDK | 8 | rp2040-midi2, waveshare-rp2040-midi2, sparkfun-promicro-rp2350-midi2, waveshare-rp2350-usb-a-midi2, waveshare-rp2350-usb-a-bridge-midi2, adafruit-feather-rp2040-host-midi2, adafruit-feather-rp2040-bridge-midi2, rp2040-promicro-ump-test-bench |
| ESP-IDF | 7 | arduino-nano-esp32-midi2, esp32-s3-devkitc-usb-midi2, esp32-p4-devkit-usb-midi2, esp32-p4-devkit-host-midi2, esp32-p4-devkit-bridge-midi2, esp32-p4-devkit-bridge2-midi2, t-display-s3-midi2 |
| PlatformIO + ESP32_Host_MIDI | 3 | esp32-c6-devkitc-multi-midi2, esp32-s3-devkitc-host-midi2, t-display-s3-shield-host-midi2 |
| TinyUSB native CMake | 2 | xiao-samd21-midi2, nrf52840-promicro-midi2 |
By role: 10 device, 4 host, 4 bridge, 1 multi-transport (BLE + ESP-NOW, no USB PID), 1 deterministic UMP test bench.
- Xiao Renesas RA4M1 (TinyUSB device, board on the bench)
Listed on the Arduino Library Manager. The IDE install path: search the manager, click Install. The dependency on midi2 is resolved automatically.
Manual install (mirror, or while the manager index is propagating):
git clone https://github.com/sauloverissimo/midi2cpp.git ~/Arduino/libraries/midi2cppThen install midi2 via Library Manager (search midi2, click Install).
Published on the PlatformIO Registry:
lib_deps = sauloverissimo/midi2cpp @ ^0.3.1Or pin by git tag:
lib_deps =
https://github.com/sauloverissimo/midi2cpp.git#v0.3.1Either way, midi2 is resolved transitively via the manifest declaration in library.json.
Published on the ESP Component Registry. Two install paths, depending on whether midi2cpp comes from the registry or lives inside the project tree:
Via the Component Manager (recommended):
# main/idf_component.yml
dependencies:
idf: ">=5.0"
sauloverissimo/midi2cpp: ">=0.3.1"midi2 is pulled transitively through midi2cpp's manifest. idf.py reconfigure drops both into managed_components/.
As a local component (vendoring pattern, useful when iterating on the wrapper):
# from your IDF project root
git clone https://github.com/sauloverissimo/midi2cpp.git components/midi2cppThen declare midi2 in main/idf_component.yml:
dependencies:
idf: ">=5.0"
sauloverissimo/midi2: ">=0.3.4"main/CMakeLists.txt lists midi2cpp in its idf_component_register(... REQUIRES midi2cpp ...) block. The seven ESP-IDF recipes under examples/ ship working templates for device, host and bridge roles.
include(FetchContent)
FetchContent_Declare(
midi2cpp
GIT_REPOSITORY https://github.com/sauloverissimo/midi2cpp.git
GIT_TAG v0.3.1
)
FetchContent_MakeAvailable(midi2cpp)midi2cpp cascades the dependency on midi2 to the parent project: find_package(midi2 0.3.4 CONFIG) is tried first, falling back to FetchContent_Declare(midi2 GIT_TAG v0.3.4) if no install is found.
git submodule add https://github.com/sauloverissimo/midi2cpp.git external/midi2cppDownload the midi2cpp and midi2 repositories side by side. Add midi2/dist/ and midi2cpp/src/ to includes. Compile midi2/dist/midi2.c, midi2cpp/src/midi2_device.cpp, midi2cpp/src/midi2_ci.cpp, and the host/bridge .cpp files you need alongside the project. No package manager required at build time, but the two repos must travel together.
m2device midi;
midi.begin();
// Inbound: Arduino-style, just the channel, note, velocity.
midi.onNoteOn ([](uint8_t ch, uint8_t note, uint16_t vel) { /* ... */ });
midi.onNoteOff([](uint8_t ch, uint8_t note, uint16_t vel) { /* ... */ });
midi.onCC ([](uint8_t ch, uint8_t idx, uint32_t val) { /* ... */ });
midi.onPitchBend([](uint8_t ch, uint32_t val) { /* ... */ });
// Outbound: same shape, no group prefix, 32-bit values.
midi.noteOn (0, 60, 0xC000);
midi.noteOff(0, 60);
midi.cc (0, 7, 0x80000000);
midi.pitchBend(0, 0x80000000);
midi.task();Async, callback-first, copy-paste-ready. Same shape as MIDI 1.0 Arduino libraries, with MIDI 2.0 resolution underneath.
Need full spec fidelity? Every callback and sender has a verbose form that exposes Group, MIDI 2.0 attribute type/data, and other Multi-Group Endpoint controls; see midi2_device.h. The simple and verbose forms share storage; the latest setter wins.
midi2cpp: platform layer of a 4-layer MIDI 2.0 stack:
The sketch touches the top. The rest is invisible until needed.
The boundary is drawn so the wrapper stays focused. A few things deliberately do not belong here.
- Not a low-level UMP parser. That is
midi2. midi2cpp wraps it and adds C++ ergonomics; if a project wants zero-overhead C with no callbacks, linkingmidi2directly is the right move. - Not a synthesizer. UMP arrives, callbacks fire, the sketch decides what to play. Sound generation is application territory.
- Not a desktop library. It targets MCU boards. It compiles on desktop for tests, but the API and memory model assume embedded constraints.
- Not opinionated about transport. TinyUSB, native USB (Teensy), PIO-USB (RP2350), STM32 HAL (Daisy), BLE: midi2cpp does not bring any of them with it. The sketch wires whichever transport its platform already ships.
You can sponsor midi2cpp at GitHub Sponsors. Sponsorship funds boards for cross-platform validation, spec access, and continued maintenance.
midi2cpp is created and maintained by Saulo VerÃssimo. It is the C++17 sibling of midi2, originally extracted from USBMIDI2 work in arduino-esp32-uac and validated across the boards listed above.
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, in any context.

