Skip to content

USB MIDI 2.0 transport: UMP device and host support#703

Open
sauloverissimo wants to merge 3 commits into
electro-smith:masterfrom
sauloverissimo:feat/usb-midi2-transport
Open

USB MIDI 2.0 transport: UMP device and host support#703
sauloverissimo wants to merge 3 commits into
electro-smith:masterfrom
sauloverissimo:feat/usb-midi2-transport

Conversation

@sauloverissimo

@sauloverissimo sauloverissimo commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds USB MIDI 2.0 (Universal MIDI Packet) transport support to
MidiUsbTransport, on both the device and the host side. The MIDI 1.0
byte path is unchanged: MIDI 2.0 traffic flows only when the active
MIDIStreaming interface is Alternate Setting 1.

What was added

Device side

win_properties_daisy

The peripheral now enumerates with the USB MIDI 2.0 descriptor layout:
Alternate Setting 0 keeps the existing MIDI 1.0 interface for backward
compatibility, Alternate Setting 1 exposes the UMP endpoints (bcdMSC
0x0200). A Group Terminal Block descriptor (1 bidirectional block,
group 0, protocol MIDI 2.0) is served via the class GET_DESCRIPTOR
request. Hosts that do not know MIDI 2.0 stay on Alt 0 and see a
regular MIDI 1.0 device.
win_properties_daisy_details

Host side

USBH_MIDI now checks attached devices for a MIDIStreaming Alt 1. When
present, it drives SET_INTERFACE to completion before opening the IN/OUT
pipes, so the device has switched to UMP framing when the host starts
reading. Devices without Alt 1 keep working exactly as before (MIDI 1.0
CIN packets).

USBH_MAX_NUM_INTERFACES goes from 2 to 4 so Itf_Desc[] can hold the
three entries a MIDI 2.0 device exposes (AudioControl, MIDIStreaming
Alt 0, MIDIStreaming Alt 1).

API

One new callback on MidiUsbTransport, delivering complete UMP
messages (1 to 4 words, split by message type per the UMP spec):

midi.SetUmpCallback(cb, context);   // RX, MIDI 2.0 Alt 1 only
midi.Tx(bytes, size);               // TX picks framing from the
                                    // active alt setting

The MIDI 1.0 parse callback registered through StartRx() keeps
working; both can be registered at the same time.

Examples

Two paired examples provide a reproducible stress test:

  • examples/MIDI2_USB_Device: deterministic UMP emitter. Per burst,
    2000 NoteOn + 2000 NoteOff (sequence number embedded in the NoteOn
    velocity) + 1 marker CC, then 400 ms of silence.
  • examples/MIDI2_Host: validator. Counts every received word in
    cumulative counters inside the RX callback (no work in the hot path)
    and prints a once-per-second tally with an OK/FAIL verdict: the span
    between the first and last marker must hold an exact multiple of
    4001 packets, with zero sequence gaps.

Hardware validation

Setup Duration Result
Daisy Seed (device) to Daisy Seed (host), USB-A 5 min 1,471,348 messages, received count exact (367 bursts x 4001), 0 gaps, 0 unexpected
RP2040 UMP emitter to Daisy Seed (host) 5 min 74,497 messages, 0 gaps, 0 unexpected, 300/300 OK
Daisy Seed (device) to Linux ALSA (/dev/snd/umpC*D0) 60 s 72 consecutive bursts of exactly 4001 packets, marker ids contiguous, 0 gaps

Descriptors verified against the USB Device Class Definition for MIDI
Devices v2.0 (Alt 1 endpoint descriptors are the 7-byte form per Table
B-17, Group Terminal Block per Table B-21/B-22) and enumeration checked
with lsusb -v on Linux 6.17 (ALSA creates the UMP endpoint and
selects Alt 1 automatically).
stack_daisy
win_stream_daisy

How to test

Device side needs nothing beyond a USB cable: flash
examples/MIDI2_USB_Device and watch the UMP stream on any MIDI 2.0
capable host (on Linux, /dev/snd/umpC*D0).

Host side needs a USB-A receptacle wired to the Seed:

USB-A pin Daisy Seed
1 VBUS external 5V supply
2 D- pin 36 (D29)
3 D+ pin 37 (D30)
4 GND pin 40 (DGND), shared with the 5V supply ground
shell / ID pin 1 (D0, USB ID) tied to GND

Flash examples/MIDI2_Host, open the serial console, plug the emitter
into the USB-A jack and watch the HEARTBEAT lines.

Backward compatibility

  • Alt 0 (MIDI 1.0) descriptors and byte path are unchanged.
  • examples/MIDI_USBH_Input rebuilt and retested against a MIDI 1.0
    device, behavior unchanged.
  • The CDC (serial logging) mode is untouched.

Device side exposes MIDI 2.0 descriptors (Alt 0 MIDI 1.0 compat + Alt 1
UMP, group terminal block served via GET_DESCRIPTOR). Host side detects
Alt 1 on attached devices, drives SET_INTERFACE to completion before
opening pipes, and hands complete UMP messages to a registered callback.
MidiUsbTransport keeps the existing MIDI 1.0 byte path on Alt 0; UMP
framing applies only when the active interface is Alt 1.

USBH_MAX_NUM_INTERFACES goes from 2 to 4 so Itf_Desc holds the three
entries a MIDI 2.0 device exposes (AudioControl + MIDIStreaming Alt 0 +
Alt 1). USBH_SelectInterface receives the Alt 0 index, which satisfies
its bNumInterfaces bounds check; the Alt 1 index drives the endpoint
scan and SET_INTERFACE.
Paired stress-test examples for the USB MIDI 2.0 transport. The device
emits a deterministic UMP stream (4001 packets per burst, sequence
number embedded in NoteOn velocity, CC 119 marker per burst). The host
validates reception with cumulative counters and reports a once-per-
second tally. Validated on hardware: 1.47M messages between two Daisy
Seeds over 5 minutes with zero loss, and 72 consecutive bursts of
exactly 4001 packets captured via Linux ALSA on the device side.
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown

Test Results

167 tests  ±0   167 ✅ ±0   0s ⏱️ ±0s
  1 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit 56799ee. ± Comparison against base commit 24be4af.

♻️ This comment has been updated with latest results.

@stephenhensley

Copy link
Copy Markdown
Collaborator

Woah! Very cool. Can't wait to check this out.

I've got a few of the older PRs, and things outside of libDaisy to get through today/next week before I get to reviewing this, but I should have some time to check it out in the next few weeks!

@sauloverissimo

Copy link
Copy Markdown
Contributor Author

Thanks, Stephen! Appreciate the reply.

Taking it slow makes total sense. The idea was to start with the transport layer, since that is the foundation: raw UMP words in and out, with no opinion about what to do with them. With that solid, everything else builds on top.

I've been experimenting quite a bit with the fork and put together a few devices on top of it that work really well. If you want to play with them, they live in midi2cpp, a C++ MIDI 2.0 library I maintain: the daisyseed-midi2 recipe builds a full USB MIDI 2.0 device (32-bit channel voice, per-note controllers, Flex Data, UMP Stream identity, and MIDI-CI) on top of this PR's UMP callback and Tx, using libDaisy's own makefile. I revalidated it against the head of this branch and it runs cleanly under Linux ALSA and Windows MIDI Services.

When it makes sense to think about the layers above transport, MIDI 2.0 has a few specifics compared to 1.0 (group terminal blocks, function blocks, protocol negotiation over MIDI-CI), and I'm happy to help think those decisions through if it's ever useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants