Skip to content

Repository files navigation

midi2lufa

midi2lufa

USB MIDI 2.0 device transport for LUFA on 8-bit AVRs.

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.

License: MIT Platform Sponsor


The library

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.

Where it fits

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.

Quick start

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.

Composite devices

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.

API

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.

Configuration

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

Supported hardware

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

  • 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.

Building

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/echo

The host-side ring unit test needs only a C compiler:

make test

LUFA is not bundled; it stays an external dependency you point at with LUFA_PATH.

Identity

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:

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.

What this is not

  • 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.

Related projects

  • 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.

Sponsor

If this saved you time, consider sponsoring.

License

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.

About

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.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages