Skip to content

sauloverissimo/midi2

midi2

🎹 The MIDI 2.0 core for platforms

midi2

C99, portable, zero-allocation, MIT. From 8-bit MCUs to 64-bit hosts.

License: MIT C99 Zero Alloc ESP Component Registry Sponsor


The library

midi2 is a small piece of infrastructure for boards that already have too much to do. Sound chips, displays, USB stacks, sensors, network. MIDI 2.0 arrives at the transport layer as 32-bit words; midi2 turns those words into structured events and stays out of the way the rest of the time.

The library implements 100% of the UMP format (M2-104-UM v1.1.2) and 100% of MIDI-CI (M2-101-UM v1.2). The application owns memory and decides which modules to link. The transport (USB, BLE, network, serial) lives somewhere else, by design.

Wrappers

midi2 is the C99 core. Higher-level wrappers depend on it and add language-specific ergonomics for each platform target.

The first published wrapper is midi2cpp: a C++17 Arduino-friendly wrapper with m2device, m2host, m2bridge and m2ci classes, distributed through the Arduino Library Manager, the PlatformIO Registry and the ESP Component Registry. It ships 20 platform recipes under midi2cpp/examples/, one per role (device, host, bridge), covering RP2040, RP2350, ESP32-S3, ESP32-P4, ESP32-C6, nRF52840 and SAMD21 boards via Pico SDK / TinyUSB native CMake / ESP-IDF / PlatformIO build paths.

Contents

Spec coverage

Spec Document Coverage
M2-104-UM v1.1.2 UMP Format and MIDI 2.0 Protocol 100% (all message types MT 0x0 through 0xF)
M2-101-UM v1.2 MIDI-CI 100% (all 31 messages, 4 categories, Appendix E responder)
M2-115-U v1.0.2 Bit Scaling and Resolution 100% (round-trip safe 7/14/16/32 bit)
M2-116-U v1.0 MIDI Clip File Partial (Delta Clockstamp, Start/End of Clip)

Quick start

#include "midi2.h"

uint32_t w[2];
midi2_msg_note_on(w, /*group*/ 0, /*channel*/ 0,
                  /*note*/ 60, /*velocity*/ 0xC000,
                  /*attr_type*/ 0, /*attr_data*/ 0);

uint8_t  note = midi2_msg_get_note(w);
uint16_t vel  = midi2_msg_get_velocity(w);

uint16_t vel16 = midi2_msg_scale_up_7to16(127);

With typed dispatch:

#include "midi2.h"

void on_note_on(uint8_t group, uint8_t channel, uint8_t note,
                uint16_t velocity, uint8_t attr_type,
                uint16_t attr_data, void *ctx) {
    /* application logic */
}

midi2_dispatch dp;
midi2_dispatch_init(&dp);
dp.on_note_on = on_note_on;
midi2_dispatch_feed(w, 2, &dp);

Modules

The amalgam (dist/midi2.h + dist/midi2.c) is the recommended path. For finer control, modules can be picked individually:

Module Files State Purpose
midi2_msg .h Stateless UMP construction, parsing, value scaling. All MTs. MT 0x2 / MT 0x4 protocol translation. USB MIDI 1.0 cable event to UMP.
midi2_dispatch .h .c Caller-allocated Typed UMP dispatch (49 callbacks). Optional MT 0x2 to MT 0x4 upscale.
midi2_proc .h .c Caller-allocated Group filtering and remap, SysEx7/8 reassembly, UMP Stream and SysEx8 fragmenting senders.
midi2_conv .h .c Caller-allocated MIDI 1.0 byte stream to UMP (Running Status, streaming SysEx, MT 0x2 to MT 0x4 translation).
midi2_ci_msg .h Stateless MIDI-CI message construction and parsing. All 32 messages.
midi2_ci_dispatch .h .c Caller-allocated Typed CI dispatch (33 callbacks).
midi2_ci .h .c Caller-allocated Convenience CI responder: Discovery, Profiles, PE (Subscribe/Notify), Process Inquiry, MUID collision detection, optional NAK-on-unknown. Appendix E complete.

midi2_msg alone is a header include with zero overhead.

Memory

Memory belongs to the application. midi2 does not call malloc, does not keep a hidden heap, and does not bake buffer sizes into headers. A 16 KB board declares small buffers. A desktop tool declares large ones. Same source, same API.

uint8_t sysex_buf[256];
midi2_proc_state proc;
midi2_proc_init(&proc, sysex_buf, sizeof(sysex_buf), NULL, 0);

uint8_t profiles[4][5];
midi2_ci_property props[2];
midi2_ci_state ci;
midi2_ci_init(&ci, seed, profiles, 4, props, 2);

Error handling

Functions that can fail return error codes. There is no global errno, no asserts that fire in release builds, no silent truncation.

int rc = midi2_ci_add_profile(&ci, profile_id);
if (rc == MIDI2_CI_ERR_FULL) {
    /* the application decides what to do */
}

Continuous integration

328 assertions across 8 suites compile clean with -Wall -Wextra -Wpedantic. CI runs 11 jobs on every push:

Target Type
gcc Linux x64 Compile and run
clang Linux x64 Compile and run
Apple clang macOS Compile and run
MSVC Windows Compile and run
gcc 32-bit Linux x86 Compile and run
gcc + ASan + UBSan Compile and run
ARM Cortex-M4 Cross-compile
AArch64 Cortex-A Cross-compile
RISC-V 64 Cross-compile
ESP32 (ESP-IDF component) Cross-compile
AVR ATmega328P Cross-compile (header-only)

Build and test locally

make test
make CC=clang test

Integration

midi2 ships in four shapes. Pick the one that matches your build flow.

Arduino IDE / arduino-cli

Install via Library Manager (search midi2, click Install) or manually drop the repo into ~/Arduino/libraries/midi2/. The library.properties declares the library; Arduino IDE compiles the modular src/*.c files automatically. Sketches use the umbrella header:

#include <midi2.h>

Two example sketches under examples/ (BasicUsage, CIDiscovery) appear in the IDE's File > Examples menu after install. Validated on Arduino UNO (AVR) and Teensy 4.1.

PlatformIO

platformio.ini:

lib_deps = sauloverissimo/midi2 @ ^0.4.0

Library Manager pulls the same src/ modular layout via library.json (srcDir = src).

ESP-IDF (Component Manager)

Published on the ESP Component Registry. Add to your project's main/idf_component.yml:

dependencies:
  idf: ">=5.0"
  sauloverissimo/midi2: ">=0.4.0"

idf.py reconfigure drops the component into managed_components/midi2/. The if(ESP_PLATFORM) gate in CMakeLists.txt routes ESP-IDF builds to idf_component_register with the modular src/midi2_*.c set, so the same source serves IDF, Arduino, PlatformIO, and native CMake without forks.

Single-header (stb-style)

Drop dist/midi2.h only. In exactly one .c file:

#define MIDI2_IMPLEMENTATION
#include "midi2.h"

Manual vendor (amalgam pair)

Download dist/midi2.h and dist/midi2.c from the release page. Drop both into the tree. Compile midi2.c alongside the project.

#include "midi2.h"

Works with Arduino, PlatformIO, ESP-IDF, CMake, Make, plain gcc.

Module by module

For finer control, copy individual files from src/:

midi2_msg.h          Always needed. Header-only.
  +- midi2_dispatch  Typed UMP callbacks.
  +- midi2_proc      SysEx reassembly, group filtering.
  +- midi2_conv      MIDI 1.0 byte stream to UMP.
  +- midi2_ci_msg    MIDI-CI message construction and parsing.
       +- midi2_ci_dispatch  Typed CI callbacks.
       +- midi2_ci           Convenience CI responder.

Architecture

midi2: infrastructure layer 2 of a 4-layer MIDI 2.0 stack:

midi2

What each layer owns:

Concern Layer
USB descriptors, endpoints, DMA Transport
UMP construction, parsing, scaling midi2
Typed dispatch (49 UMP + 33 CI callbacks) midi2
SysEx reassembly, group filtering midi2
CI construction and parsing midi2
MIDI 1.0 byte stream conversion midi2
CI convenience responder (optional) midi2
Arduino-style callbacks (onNoteOn) Platform
Profile behavior (GM2, MPE) Platform or App
PE JSON schema interpretation Platform or App
FreeRTOS tasks, event queues Platform
Musical application logic App

Design goals

  • Embedded first. The same source compiles for AVR ATmega328P, Cortex-M0, RISC-V 64, ESP32 Xtensa, and 64-bit desktops. The smallest target dictates the design.
  • Zero allocation. No malloc, no hidden heap, no per-call buffers. The application sizes everything at init.
  • Stateless where possible. midi2_msg keeps no state; the same word array round-trips through any number of helpers without coupling.
  • Three system headers. <stdint.h>, <stdbool.h>, <string.h>. That is the dependency list.
  • Errors are codes, not exceptions. Functions report what happened; the application decides.
  • Spec-complete on its own scope. 100% of UMP. 100% of MIDI-CI. Anything outside those documents lives elsewhere.

What this library is not

The boundary is drawn so the core stays portable. A few things deliberately do not belong here.

  • Not a transport. USB-MIDI 2.0, BLE MIDI, Network MIDI 2.0, Serial DIN: each has its own driver. midi2 reads and writes UMP words; the wire is somebody else's responsibility.
  • Not a JSON parser. Property Exchange headers are JSON, but parsing JSON in a zero-allocation embedded library is a poor fit. The application chooses a JSON parser at the platform layer.
  • Not a Profile implementation. GM2, MPE, and the M2-118 through M2-125 profiles describe device behavior. midi2 carries the negotiation messages; the behavior lives in the application.
  • Not the fastest UMP library. Speed was not a primary goal. Correctness, portability, and predictable memory were. On a Cortex-M4 at 168 MHz, midi2 dispatches faster than any USB transport can deliver words anyway.

Sponsor

You can sponsor midi2 at GitHub Sponsors. Sponsorship funds spec access, hardware for cross-platform validation, and continued maintenance.

About

midi2 is created and maintained by Saulo Veríssimo.

Specifications and trademarks

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.

License

MIT. Free for commercial and open-source use.

About

🎹 Portable MIDI 2.0 UMP library. C99, portable, zero-allocation, MIT. From 8-bit MCUs to 64-bit hosts.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages