The USB MIDI 2.0 class layer LUFA never shipped. C99, zero allocation, MIT. Turns an Arduino Pro Micro or Leonardo into a MIDI 2.0 device.
LUFA is the reference USB stack for 8-bit AVRs, but its USB-MIDI support stops at MIDI 1.0. midi2lufa adds the layer on top: a dual alternate-setting MIDIStreaming interface (alt 0 = USB-MIDI 1.0, alt 1 = USB-MIDI 2.0 / UMP), the Group Terminal Block descriptor, and a super-loop pump that moves whole Universal MIDI Packets between the bulk endpoints and two word rings.
Your application sees only 32-bit UMP words in and out. It never touches a descriptor, an endpoint, or an alternate setting. On a 16 MHz ATmega32U4 with 2.5 KB of SRAM, the whole transport costs about 3 KB of flash and a few hundred bytes of RAM.
midi2lufa 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 beyond the USB descriptors. Pair it with a UMP library or your own code:
your application notes, controllers, device logic
UMP library parse / build UMP, MIDI-CI, Stream discovery
midi2lufa <- you are here: UMP words <-> USB wire
LUFA USB device core
ATmega32U4 hardware
The reference pairing is the midi2 C99 core. The bundled examples/atmega32u4-device-baremetal recipe puts the two together into a full device with a 58-entry UMP catalog and a MIDI-CI responder, self-certified against the MIDI Association's MIDI 2.0 Workbench.
Two LUFA event hooks and a poll:
#include <LUFA/Drivers/USB/USB.h>
#include "midi2lufa.h"
void EVENT_USB_Device_ConfigurationChanged(void) { midi2lufa_configure_endpoints(); }
void EVENT_USB_Device_ControlRequest(void) { midi2lufa_control_request(); }
int main(void) {
USB_Init();
GlobalInterruptEnable();
for (;;) {
if (USB_DeviceState == DEVICE_STATE_Configured) {
midi2lufa_task(); // pump the endpoints
uint32_t w;
while (midi2lufa_read(&w)) {
// handle inbound UMP, or echo it back:
midi2lufa_write_word(w);
}
}
USB_USBTask();
}
}The library provides CALLBACK_USB_GetDescriptor, so a plain MIDI device writes no descriptor code of its own.
To put MIDI 2.0 next to another class (a CDC debug console, for example), compile the descriptor unit with -DMIDI2LUFA_NO_DEFAULT_DESCRIPTORS. The library then contributes only the Group Terminal Block and a helper; you write the device and configuration descriptors, embed the MIDI interface fragment, and delegate the GTB request:
static const uint8_t PROGMEM cfg[] = {
/* your 9-byte configuration header, then your CDC interfaces, then: */
MIDI2LUFA_MS_INTERFACE_DESCRIPTOR(2, 3, 3, 4, 64), // AC itf 2, MS itf 3, EP 3/4
};
uint16_t CALLBACK_USB_GetDescriptor(uint16_t wValue, uint16_t wIndex,
const void** addr) {
uint16_t size;
if (midi2lufa_get_descriptor(wValue, wIndex, addr, &size))
return size; // Group Terminal Block
/* ... your device / config / string descriptors ... */
}Set MIDI2LUFA_ITF_MIDISTREAM to the MIDI interface slot so the transport's control-request handler matches.
| Function | Purpose |
|---|---|
midi2lufa_configure_endpoints() |
configure the bulk endpoints (from ConfigurationChanged) |
midi2lufa_control_request() |
handle the alt-setting requests (from ControlRequest) |
midi2lufa_task() |
pump both endpoints once per super-loop iteration |
midi2lufa_read(&word) |
pop one received UMP word, false if empty |
midi2lufa_write(words, n) |
queue a whole UMP message atomically |
midi2lufa_write_word(word) |
queue one word (one-word message types only) |
midi2lufa_alt() |
active alternate setting: 0 = MIDI 1.0, 1 = MIDI 2.0 |
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.
Override any of these before building, with a -D flag or your build header. Defaults build a working device out of the box.
| Macro | Default | Meaning |
|---|---|---|
MIDI2LUFA_VID |
0x1209 |
USB vendor ID (pid.codes prototyping) |
MIDI2LUFA_PID |
0x0001 |
USB product ID |
MIDI2LUFA_MANUFACTURER |
L"midi2lufa" |
manufacturer string |
MIDI2LUFA_PRODUCT |
L"MIDI 2.0 (LUFA)" |
product string |
MIDI2LUFA_EP_OUT / _EP_IN |
1 / 2 |
bulk endpoint numbers |
MIDI2LUFA_EP_SIZE |
64 |
bulk packet size |
MIDI2LUFA_POWER_MA |
100 |
advertised bus power |
MIDI2LUFA_RING_WORDS |
64 |
depth of each UMP word ring |
Developed and hardware-validated on the ATmega32U4 (Arduino Pro Micro and Arduino Leonardo) against Linux ALSA UMP and Windows MIDI Services. Any USB AVR that LUFA supports (AT90USB1286 on the Teensy 2.0++, ATmega16U4, ATmega32U6, and relatives) uses the same code; adjust the endpoint numbers if the part differs.
examples/echo: the smallest device. Enumerate and echo UMP; no UMP library needed.examples/atmega32u4-device-baremetal: the full device. Device identity, the complete M2-104 message catalog and a MIDI-CI Discovery responder over the midi2 C99 core; Pro Micro and Leonardo variants.
Needs avr-gcc, avr-libc, avrdude, and a checkout of LUFA release LUFA-210130:
git clone --branch LUFA-210130 --depth 1 https://github.com/abcminiuser/lufa
LUFA_PATH=/path/to/lufa/LUFA make -C examples/echoThe host-side ring unit test needs only a C compiler:
make testLUFA is not bundled; it stays an external dependency you point at with LUFA_PATH.
The default 0x1209:0x0001 is the pid.codes prototyping allocation, fine for development and never for a shipped product. A device you distribute must carry its own VID and PID, from one of:
- pid.codes
0x1209(free, open-source friendly) - V-USB / Objective Development
0x16C0(shared, with conditions) - a purchased USB-IF vendor ID
The MIDI Manufacturer ID used at the MIDI-CI and SysEx layer is a separate concern, owned by the UMP library above this transport, not by midi2lufa.
- Not a USB stack. LUFA is, and stays external.
- Not a UMP parser or builder. That is the layer above; midi2lufa 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 with device, host and bridge classes for TinyUSB platforms.
If this saved you time, consider sponsoring.
MIT. LUFA is distributed separately under its own MIT-style license by Dean Camera.
MIDI and MIDI 2.0 are trademarks of the MIDI Manufacturers Association / MIDI Association. This project is an independent implementation and is not affiliated with or endorsed by them.
