Skip to content

sauloverissimo/ESP32_Host_MIDI

Repository files navigation

ESP32_Host_MIDI

The universal MIDI host hub for ESP32. One clean event API for USB, BLE, WiFi, Ethernet, ESP-NOW, OSC, and DIN-5.

ESP32_Host_MIDI

Arduino library, multi-transport, MIDI 1.0 and 2.0, MIT. Eight transports, one event queue.

License: MIT CI Release Platform MIDI Sponsor

Language: 🇧🇷 Português (Brasil)


Overview

ESP32_Host_MIDI turns an ESP32 into a multi-protocol MIDI hub. Connect a USB MIDI keyboard over USB Host, receive notes from an iPhone over BLE, bridge a DAW over WiFi with RTP-MIDI (Apple MIDI), talk to Max/MSP over OSC, reach vintage DIN-5 gear over serial, and link several ESP32 boards over ESP-NOW. Every transport delivers into a single MIDIHandler event queue and shares one send API.

#include <ESP32_Host_MIDI.h>
#include <USBConnection.h>

USBConnection usbHost;

void setup() {
    Serial.begin(115200);
    midiHandler.addTransport(&usbHost);
    usbHost.begin();
    midiHandler.begin();
}

void loop() {
    midiHandler.task();
    for (const auto& ev : midiHandler.getQueue()) {
        char buf[8];
        Serial.printf("%-12s %-4s ch=%d vel=%d\n",
            MIDIHandler::statusName(ev.statusCode),
            MIDIHandler::noteWithOctave(ev.noteNumber, buf, sizeof(buf)),
            ev.channel0 + 1, ev.velocity7);
    }
}

Transports

Transport Protocol Physical Latency Requires
USB Host USB MIDI 1.0 USB-OTG cable < 1 ms ESP32-S3 / S2 / P4
USB Host MIDI 2.0 USB MIDI 2.0 (UMP) USB-OTG cable < 1 ms ESP32-S3 / S2 / P4
BLE MIDI BLE MIDI 1.0 Bluetooth LE 3-15 ms Any ESP32 with BT
ESP-NOW ESP-NOW 2.4 GHz radio 1-5 ms Any ESP32
RTP-MIDI AppleMIDI / RFC 6295 WiFi UDP 5-20 ms Any ESP32 with WiFi
Ethernet MIDI AppleMIDI / RFC 6295 Wired (W5500 / native) 2-10 ms W5500 SPI or ESP32-P4
OSC Open Sound Control WiFi UDP 5-15 ms Any ESP32 with WiFi
UART / DIN-5 Serial MIDI 1.0 DIN-5 connector < 1 ms Any ESP32

Every transport implements the same MIDITransport interface and registers with one line: midiHandler.addTransport(&t).


Quick start

Since v6.0 transports are explicit: include the header you need, declare the transport, register it with addTransport(), and call its begin(). See docs/migration-v6.md if you are upgrading from v5.x.

#include <ESP32_Host_MIDI.h>
#include <USBConnection.h>   // include only the transports you use
// Arduino IDE: Tools > USB Mode > "USB Host"

USBConnection usbHost;

void setup() {
    Serial.begin(115200);
    midiHandler.addTransport(&usbHost);
    usbHost.begin();
    midiHandler.begin();
}

void loop() {
    midiHandler.task();
    for (const auto& ev : midiHandler.getQueue()) {
        char buf[8];
        Serial.println(MIDIHandler::noteWithOctave(ev.noteNumber, buf, sizeof(buf)));
    }
}

Reading events

Each MIDIEventData in the queue exposes both MIDI 1.0 and MIDI 2.0 resolution:

for (const auto& ev : midiHandler.getQueue()) {
    ev.statusCode;   // MIDI_NOTE_ON | MIDI_NOTE_OFF | MIDI_CONTROL_CHANGE | ...
    ev.channel0;     // 0-15 (MIDI spec convention)
    ev.noteNumber;   // 0-127 (controller number for CC)
    ev.velocity7;    // 0-127 (MIDI 1.0)
    ev.velocity16;   // 0-65535 (MIDI 2.0, scaled)
    ev.pitchBend14;  // 0-16383 (center = 8192)
    ev.pitchBend32;  // 0-0xFFFFFFFF (MIDI 2.0, center = 0x80000000)
    ev.chordIndex;   // groups simultaneous notes
    ev.timestamp;    // millis() at arrival

    // Static helpers (zero allocation):
    MIDIHandler::noteName(ev.noteNumber);    // "C", "C#", "D" ...
    MIDIHandler::noteOctave(ev.noteNumber);  // -1 to 9
    MIDIHandler::statusName(ev.statusCode);  // "NoteOn", "ControlChange" ...
}

Sending and bridging

Send from any transport with the unified API. send* tries each registered transport in order and stops at the first that accepts the message (first-wins). Channel is 1 to 16.

midiHandler.sendNoteOn(1, 60, 100);
midiHandler.sendControlChange(1, 64, 127);
midiHandler.sendPitchBend(1, 0);           // -8192 to +8191, center = 0

MIDI is not routed between transports automatically. Each transport delivers its incoming MIDI into the shared event queue; your loop() decides what to forward. To bridge two transports, read the queue and resend to the target transport:

// Forward note/CC from any input to a target transport.
static int lastIndex = 0;
for (const auto& ev : midiHandler.getQueue()) {
    if (ev.index <= lastIndex) continue;
    lastIndex = ev.index;
    uint8_t msg[3] = { uint8_t(ev.statusCode | ev.channel0), ev.noteNumber, ev.velocity7 };
    target.sendMidiMessage(msg, 3);
}
Bridge Path
USB keyboard to WiFi USB keyboard -> ESP32 -> RTP-MIDI -> macOS
Modern to legacy macOS -> RTP-MIDI -> ESP32 -> DIN-5 -> 1980s drum machine
Wireless stage mesh ESP-NOW nodes -> ESP32 hub -> RTP-MIDI -> FOH computer
Creative software Max/MSP OSC -> ESP32 -> BLE -> iPad instrument

Transport reference

USB Host

Connects any class-compliant USB MIDI device (keyboards, pads, interfaces, controllers) directly to the ESP32 USB-OTG port. No hub, no driver, no OS configuration.

Boards: ESP32-S3, S2, P4 · Arduino IDE: Tools > USB Mode > "USB Host"

#include <ESP32_Host_MIDI.h>
#include <USBConnection.h>

USBConnection usbHost;

void setup() {
    midiHandler.addTransport(&usbHost);
    usbHost.begin();
    midiHandler.begin();
}

For a full host example that also decodes MIDI 2.0, see the USB-Host-MIDI2 example.

USB Host MIDI 2.0

Native USB MIDI 2.0 / UMP. USBMIDI2Connection extends USBConnection, scans the device configuration descriptor for Alt 0 (MIDI 1.0) and Alt 1 (MIDI 2.0), and prefers MIDI 2.0 when available, falling back to MIDI 1.0. After negotiation it runs read-only UMP discovery (Endpoint Info, Function Block Info). Raw 32-bit UMP words are delivered one whole packet at a time, reassembled across USB transfers.

#include <USBMIDI2Connection.h>

USBMIDI2Connection usb;

void onUMP(void*, const uint32_t* words, uint8_t count) {
    // Raw UMP words, MIDI 2.0 native resolution.
}

void setup() {
    usb.setUMPCallback(onUMP, nullptr);
    usb.setMidiCallback(onMidi, nullptr);   // MIDI 1.0 fallback
    usb.begin();
}

Query the negotiated capabilities:

if (usb.isMIDI2() && usb.isNegotiated()) {
    const auto& ep = usb.getEndpointInfo();
    Serial.printf("UMP v%d.%d, %d function blocks\n",
        ep.umpVersionMajor, ep.umpVersionMinor, ep.numFunctionBlocks);
}

Boards: ESP32-S3, S2, P4 · Examples: USB-Host-MIDI2, T-Display-S3-Piano-Flow

BLE MIDI

The ESP32 advertises as a BLE MIDI 1.0 peripheral. macOS (Audio MIDI Setup > Bluetooth), iOS (GarageBand, AUM, Loopy, Moog), and Android connect with no pairing ritual. Central (scanner) mode connects to another BLE MIDI device.

Boards: Any ESP32 with Bluetooth · Range: ~30 m · Latency: 3-15 ms

#include <ESP32_Host_MIDI.h>
#include <BLEConnection.h>

BLEConnection ble;

void setup() {
    ble.begin("ESP32 MIDI");
    midiHandler.addTransport(&ble);
    midiHandler.begin();
}

Examples: T-Display-S3-BLE-Sender, T-Display-S3-BLE-Receiver

ESP-NOW

Low-latency wireless MIDI between ESP32 boards over Espressif peer-to-peer radio. No WiFi router, no handshake, no pairing. Broadcast (every board hears everyone) or unicast.

Boards: Any ESP32 · Range: ~200 m open air · Infrastructure: none

#include <ESP32_Host_MIDI.h>
#include <ESPNowConnection.h>

ESPNowConnection espNow;

void setup() {
    espNow.begin();
    midiHandler.addTransport(&espNow);
    midiHandler.begin();
}

Examples: T-Display-S3-ESP-NOW-Jam

RTP-MIDI (Apple MIDI)

Apple MIDI (RTP-MIDI, RFC 6295) over WiFi UDP. macOS and iOS discover the ESP32 over mDNS Bonjour and show it in Audio MIDI Setup > Network with no manual configuration. Works with Logic Pro, GarageBand, Ableton, and any CoreMIDI app.

Requires: lathoub/Arduino-AppleMIDI-Library v3.x

#include <WiFi.h>
#include <ESP32_Host_MIDI.h>
#include <RTPMIDIConnection.h>

RTPMIDIConnection rtpMIDI;

void setup() {
    WiFi.begin("YourSSID", "YourPassword");
    while (WiFi.status() != WL_CONNECTED) delay(500);
    rtpMIDI.begin("ESP32 MIDI");
    midiHandler.addTransport(&rtpMIDI);
    midiHandler.begin();
}

Examples: RTP-MIDI-WiFi

Ethernet MIDI

The same RTP-MIDI / AppleMIDI protocol over a wired W5500 SPI Ethernet module or the ESP32-P4 native Ethernet MAC. Lower and more consistent latency than WiFi. Ideal for studio racks and live venues.

Requires: lathoub/Arduino-AppleMIDI-Library v3.x and the Arduino Ethernet library

#include <ESP32_Host_MIDI.h>
#include <EthernetMIDIConnection.h>

EthernetMIDIConnection ethMIDI;
static const uint8_t MAC[6] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };

void setup() {
    ethMIDI.begin(MAC);   // DHCP; pass a static IPAddress as second arg for a fixed IP
    midiHandler.addTransport(&ethMIDI);
    midiHandler.begin();
}

Examples: Ethernet-MIDI

OSC

Bidirectional OSC to MIDI bridge over WiFi UDP. Receives OSC from Max/MSP, Pure Data, SuperCollider, and TouchOSC and converts it to MIDI events, and sends every MIDI event out as OSC.

Address map: /midi/noteon, /midi/noteoff, /midi/cc, /midi/pc, /midi/pitchbend, /midi/aftertouch Requires: CNMAT/OSC library

#include <WiFi.h>
#include <ESP32_Host_MIDI.h>
#include <OSCConnection.h>

OSCConnection oscMIDI;

void setup() {
    WiFi.begin("YourSSID", "YourPassword");
    while (WiFi.status() != WL_CONNECTED) delay(500);
    oscMIDI.begin(8000, IPAddress(192, 168, 1, 100), 9000);
    midiHandler.addTransport(&oscMIDI);
    midiHandler.begin();
}

Examples: T-Display-S3-OSC

UART / DIN-5

Standard serial MIDI (31250 baud, 8N1) for vintage hardware: synthesizers, drum machines, mixers, sequencers, anything with a DIN-5 connector. Supports running status, real-time messages (Clock, Start, Stop), and multiple UART ports (the ESP32-P4 has five hardware UARTs).

Hardware: TX to DIN-5 pin 5 via 220 Ohm; PC-900V / 6N138 optocoupler on RX to DIN-5 pin 4

#include <ESP32_Host_MIDI.h>
#include <UARTConnection.h>

UARTConnection uartMIDI;

void setup() {
    uartMIDI.begin(Serial1, /*RX=*/16, /*TX=*/17);
    midiHandler.addTransport(&uartMIDI);
    midiHandler.begin();
}

Examples: UART-MIDI-Basic, P4-Dual-UART-MIDI


Architecture

INPUTS                              MIDIHandler              OUTPUTS

USB keyboard  --[USBConnection]------>  +--------------+
USB MIDI 2.0  --[USBMIDI2Connection]->  |              |
iPhone BLE    --[BLEConnection]------>  |  Event queue |--> getQueue()
macOS WiFi    --[RTPMIDIConnection]-->  |  (ring buf,  |
W5500 LAN     --[EthernetMIDIConn.]-->  |  thread-safe)|--> Active notes
Max/MSP OSC   --[OSCConnection]------>  |              |
DIN-5 serial  --[UARTConnection]----->  |  Chord       |--> Chord index
ESP32 radio   --[ESPNowConnection]--->  +------+-------+
                                               |
                                               v
                                        send* / sendMidiMessage()
                                        (first transport that accepts)

Core 0 runs the USB Host task, BLE stack, and radio / network drivers (FreeRTOS tasks). Core 1 runs midiHandler.task() and your loop(). Every transport uses ring buffers and portMUX spinlocks for thread safety.


Hardware compatibility

Chip USB Host BLE WiFi Ethernet (native) UART ESP-NOW
ESP32-S3 yes yes yes W5500 SPI yes yes
ESP32-S2 yes no yes W5500 SPI yes no
ESP32-P4 yes no no yes yes (x5) no
ESP32 (classic) no yes yes W5500 SPI yes yes
ESP32-C3 / C6 / H2 no yes yes no yes yes

W5500 SPI Ethernet works on any ESP32 through EthernetMIDIConnection. The LilyGO T-Display-S3 (ESP32-S3 + 1.9" display) is the best all-round board for USB Host, BLE, WiFi, and a live MIDI dashboard.


Examples

The examples/ folder ships runnable sketches, several with a photo and an .mp4 demo in examples/<name>/images/.

Example Transport What it shows
USB-Host-MIDI2 USB Host MIDI 2.0 Receive and decode raw UMP
T-Display-S3-Piano-Flow USB Host MIDI 2.0 Piano roll, chord with inversion, note duration
T-Display-S3-BLE-Sender BLE Send mode status + event log
T-Display-S3-BLE-Receiver BLE Receive mode + note log
T-Display-S3-ESP-NOW-Jam ESP-NOW Peer status + jam events
RTP-MIDI-WiFi RTP-MIDI Apple MIDI to macOS over WiFi
Ethernet-MIDI Ethernet Apple MIDI over W5500 / native MAC
T-Display-S3-OSC OSC + WiFi OSC to MIDI bridge with display
UART-MIDI-Basic UART / DIN-5 DIN-5 in and out
P4-Dual-UART-MIDI UART / DIN-5 Two hardware UARTs on the ESP32-P4

 

RTP-MIDI / Apple MIDI to macOS · BLE MIDI from iPhone


Installation

Arduino IDE: Sketch > Include Library > Manage Libraries, search ESP32_Host_MIDI.

PlatformIO:

[env:esp32-s3-devkitc-1]
platform = espressif32
board    = esp32-s3-devkitc-1
framework = arduino

lib_deps =
    sauloverissimo/ESP32_Host_MIDI
    # lathoub/Arduino-AppleMIDI-Library   ; RTP-MIDI + Ethernet MIDI
    # arduino-libraries/Ethernet          ; Ethernet MIDI
    # CNMAT/OSC                           ; OSC

Board package: Tools > Boards Manager, "esp32" by Espressif, version >= 3.0.0. USB Host requires arduino-esp32 >= 3.0 (TinyUSB MIDI).

Transport Required library
RTP-MIDI / Ethernet MIDI lathoub/Arduino-AppleMIDI-Library
Ethernet MIDI arduino-libraries/Ethernet
OSC CNMAT/OSC
USB Host / BLE / ESP-NOW / UART built into arduino-esp32

API reference

// Setup: every transport is explicit (v6.0+).
USBConnection usb;                 // #include <USBConnection.h>, <BLEConnection.h>, ...
BLEConnection ble;
ble.begin("My Device");            // user owns each transport's lifecycle
usb.begin();
midiHandler.addTransport(&usb);    // register each transport
midiHandler.addTransport(&ble);
midiHandler.begin();               // defaults
midiHandler.begin(cfg);            // or with a custom MIDIHandlerConfig
midiHandler.task();                // call every loop()

// Receive
const auto& q = midiHandler.getQueue();                          // event ring buffer
std::vector<std::string> n = midiHandler.getActiveNotesVector(); // ["C4","E4","G4"]
size_t count = midiHandler.getActiveNotesCount();
// SysEx: midiHandler.getSysExQueue(), setSysExCallback(cb), sendSysEx(data, len)

// Send (first transport that accepts the message wins)
midiHandler.sendNoteOn(ch, note, vel);        // ch: 1-16
midiHandler.sendNoteOff(ch, note, vel);
midiHandler.sendControlChange(ch, ctrl, val);
midiHandler.sendProgramChange(ch, prog);
midiHandler.sendPitchBend(ch, val);           // -8192..+8191, center = 0

MIDIHandlerConfig:

MIDIHandlerConfig cfg;
cfg.maxEvents         = 20;    // queue capacity (1..100)
cfg.chordTimeWindow   = 0;     // ms grouping for chord detection (0 = legacy)
cfg.velocityThreshold = 0;     // ignore NoteOn below this velocity (0..127)
cfg.historyCapacity   = 0;     // PSRAM history buffer (0 = disabled)
cfg.maxSysExSize      = 512;   // bytes per SysEx (0 = disable SysEx)
cfg.maxSysExEvents    = 8;     // SysEx queue depth
midiHandler.begin(cfg);

Custom transport: subclass MIDITransport, implement task() and isConnected(), optionally sendMidiMessage(), and call the inherited dispatchMidiData() to inject received MIDI.

class MyTransport : public MIDITransport {
    void task() override { /* read hardware, then dispatchMidiData(bytes, len) */ }
    bool isConnected() const override { return connected; }
    bool sendMidiMessage(const uint8_t* data, size_t len) override { /* ... */ return true; }
};
midiHandler.addTransport(&myTransport);

License

MIT, see LICENSE.

Built for musicians, makers, and researchers.
Issues and contributions welcome at github.com/sauloverissimo/ESP32_Host_MIDI

About

MIDI / MIDI 2.0 host transports for ESP32 . Arduino, PlatformIO, ESP-IDF. ESP32-S3/S2/P4.

Topics

Resources

License

Stars

92 stars

Watchers

6 watching

Forks

Packages

 
 
 

Contributors