Building a USB MIDI 2.0 Device – Part 2
By Andrew Mee in collaboration with the OS API Working Group.
This is the second part of a series demonstrating how a developer may go about building a USB MIDI 2.0 device. You should read Part 1 before this Part 2. Part 1 is here: https://www.midi.org/midi-articles/building-a-usb-midi-2-0-device-part-1
In the last part we created a simple set of USB MIDI 2.0 descriptors for a synthesizer. The “ACMESynth” at this stage has one function, the “Monosynth”, and a USB MIDI 2.0 Group Terminal Block was declared for this function.
In this second guide we will cover:
- UMP Discovery handling
- Function Blocks and how to expand them
UMP Discovery Handling
Our hypothetical ACMESynth is designed to be used primarily as a tone generator connected to a DAW. The goals of discovery are:
- We want the DAW to know which UMP Group is in use and information about this Group.
- We want the DAW or Operating System (OS) to use MIDI 2.0 Channel Voice messages. (Another article will cover handling of MIDI 1.0 Protocol, MIDI 2.0 Protocol, and translation.)
- We want the USB device to provide names and identifiers so that the DAW can store local information about the ACMESynth to help the user when they reload a DAW session.
The UMP and MIDI 2.0 Protocol ver 1.1 specification defines discovery mechanisms so that devices can interrogate each other and present the user options on the best way to connect to a device to achieve the goals listed above.
Note: MIDI-CI is used additionally to discover more information about a device which we will be discussing in another article.
Our device needs to be able to respond to the following Groupless messages:
- Endpoint Discovery Message
- Stream Configuration Request
- Function Block Discovery Message
Let’s relook at the data work our ACMESynth and add the Device Identity data we need:
Detail | Value |
Manufacturer Name | “ACME Enterprises” |
Product Name | “ACMESynth” |
Product Instance Id | “ABCD12345” |
Protocol | MIDI 2.0 Protocol (with a fallback to MIDI 1.0 Protocol) |
Manufacturer SysEx Id | 0x7E 0x00 0x00This example uses the Research Manufacturer System Exclusive Id set by MIDI Association. This System Exclusive Id is often shown as a single byte. However in many MIDI 2.0 Messages single byte id’s are transmitted using 3 bytes. Manufacturers should contact the MIDI Association to get their own Id. |
Model Family Id | 0x01 0x00 |
Model Id | 0x11 0x22Family Id and Model Id are defined by the Manufacturer and often messages define that these are LSB first. Please review MIDI Specifications for a detailed explanation. |
Version | 0x01 0x00 0x00 0x00 |
Function 1 | |
Function 1 Name | “Monosynth” |
Function 1 Channels Needed | 1 Channel at a time used bidirectionally |
Responding to the Endpoint Discovery Message
One of the first UMP messages that your device may receive is the Endpoint Discovery Message. This example is asking for all information available about the UMP Endpoint.
Endpoint Info Notification Message (filter & 0b1 == true)
In response to the above message, the ACMESynth formulates an Endpoint Info Notification Message:
The Endpoint Info Notification is declaring that the ACMESynth supports both MIDI 1.0 and MIDI 2.0 Protocols and that it has one static Function Block. This is reflected by the Group Terminal Block discussion in part 1.
Device Identity Notification Message (filter & 0b10 == true)
In response to the above message, the ACMESynth formulates an Device Identity Notification Message:
Endpoint Name Notification Message (filter & 0b100 == true)
In response to the above message, the ACMESynth formulates an Endpoint Name Notification Message:
Product Instance Id Notification Message (filter & 0b1000 == true)
In response to the above message the ACMESynth formulates a Product Instance Id Notification Message:
Stream Configuration Notification Message:
While the Endpoint Info Notification messages inform the other UMP Endpoint of its support for different Protocols and JR Timestamps, the Stream Configuration Notification informs the other UMP Endpoint of its current Protocol and JR Timestamp configuration.
In our startup state of the ACMESynth it will be using MIDI 1.0 Protocol. JR Timestamp is not supported so it will set this to off.
Responding to the Stream Configuration Request
The device connected to the ACMESynth may wish to change from MIDI 1.0 Protocol to MIDI 2.0 Protocol. It achieves this be sending a Stream Configuration Request:
If ACMESynth agrees with this change then it will change its Protocol in use an send a Stream Configuration Notification Message in response:
If ACMESynth was unable to change with the Protocol change it should send a response with the Protocol set to 0x01.
Responding to the Function Block Discovery
The Endpoint Info Notification message declares how many Function Blocks ACMESynth has. A separate message is received that asks us to respond with the Function Block information:
The Function Block Number (FB#) could have also been 0x00 to represent the first (and only) Function Block. This request is also asking to return both the Info and the Name of the Function Block. This request result in two replies:
Function Block Info Notification
This Function Block is providing the direction (0xb11 – bidirectional), an indication if the Function block represents a MIDI 1.0 (0b00 – Not MIDI 1.0) and Groups in use.
As Function Blocks provide more information than USB Group Terminal Blocks it also provides
- a UI Hint (0b01 – primarily a Receiver or destination for MIDI messages) – While our Monosynth supports bidirectional MIDI Messages, it mainly acts as a Tone Generator. A DAW can look at this field and have a better understanding and representation of the Devices connected.
- a the MIDI-CI Version (0x00 none or unknown) – currently set to none. Later articles will look at MIDI-CI support and how this value is affected.
- Max number of SysEx8 Streams – Our Monosynth does not support SysEx8 so this is set to zero.
Function Block Name Notification
Function Blocks and How to Expand Them
So far in the ACMESynth we have only declared a single Monosynth function. However many devices may require more than one function. Let’s take our device and add 2 more functions, a “MIDI IN” Din Port, and a “MIDI OUT” Din Port:
Let’s assume that the reason to add these functions is that we want our MIDI Application to use these DIN ports as a USB MIDI Adaptor for external MIDI 1.0 gear.
Our table of functions for the ACMESynth now looks like:
Detail | Value |
---|---|
Function 1 | |
Function 1 Name | “Monosynth” |
Function 1 Channels Needed | 1 Channel at a time used bidirectionally |
Function 1 Groups used | Only one Group is used on Function 1 |
Function 2 | |
Function 2 Name | “MIDI IN” |
Function 2 Channels Needed | 16 Channel at a time may used in one direction (inwards) |
Function 2 Groups used | Only one Group is used on Function 2 |
Function 3 | |
Function 2 Name | “MIDI OUT” |
Function 3 Channels Needed | 16 Channel at a time may used in one direction (outwards) |
Function 3 Groups used | Only one Group is used on Function 3 |
By making this change our Endpoint Info Notification Message should declare three Function Blocks:
And our Function Block Info Notification and Function Block Name messages also needs to handle the two new Function Blocks:
Function Block 2: Function Block Info Notification
Pay attention to the settings of MIDI 1.0, UI hint and Direction fields. For this Function Block we have set the direction as Input and the UI Hint also as Input.
Function Block 3: Function Block Info Notification
Function Block 3: Function Block Name Notification
What about the USB Group Terminal Blocks?
To make the USB Group Terminal Blocks reflect our new set of static Function Blocks we should extend the Group Terminal Block descriptor from part 1 (which already contained a bidirectional Group Terminal Block for the Monosynth) with the following:
Detail | Meaning | Value |
---|---|---|
bGrpTrmBlkID | Block Id | 2 |
bGrpTrmBlkType | Block Type | 0x01 – IN Group Terminals Only |
nGroupTrm | Group Terminal Block Start | 0x01 – Group 2 |
nNumGroupTrm | Number of Group Blocks | 1 |
iBlockItem | Function 2 Name | Id of String Descriptor Referenced Value – “MIDI IN” |
bMIDIProtocol | Block Protocol* | 0x01 (MIDI 1.0 Protocol) |
Detail | Meaning | Value |
---|---|---|
bGrpTrmBlkID | Block Id | 3 |
bGrpTrmBlkType | Block Type | 0x02 – OUT Group Terminals Only |
nGroupTrm | Group Terminal Block Start | 0x01 – Group 2 |
nNumGroupTrm | Number of Group Blocks | 1 |
iBlockItem | Function 3 Name | Id of String Descriptor Referenced Value – “MIDI OUT” |
bMIDIProtocol | Block Protocol* | 0x01 (MIDI 1.0 Protocol) |
USB Endpoints under the Interface will also need the following updates to their values values:
IN Endpoint:
Detail | Meaning | Value |
---|---|---|
bNumGrpTrmBlock | 2 | |
baAssoGrpTrmBlkID[0] | Block Id | 1 |
baAssoGrpTrmBlkID[1] | Block Id | 3 (MIDI OUT) |
OUT Endpoint:
Detail | Meaning | Value |
---|---|---|
bNumGrpTrmBlock | 2 | |
baAssoGrpTrmBlkID[0] | Block Id | 1 |
baAssoGrpTrmBlkID[1] | Block Id | 2 (MIDI IN) |
Note: you may also wish to update the USB MIDI 1 Descriptors to have these new functions on separate MIDI 1.0 Ports.
Static vs Non-Static Function Blocks
Up to this point we have been using static Function Blocks when declaring the functions of ACMESynth. When connecting to a DAW that is the central manager and handling the routing of MIDI Messages this is unlikely to present any problems.
This is because a DAW is likely to adapt to the devices connected to it. In this case the ACMESynth declares it has the Monosynth on Group 1 and the DAW will send MIDI Messages on Group 1.
However static Function Blocks have limitations when connecting between other devices. Much like when two devices that connect to each other must use the same channels – two UMP enabled devices must use the same Groups (and Channels) to communicate effectively.
Function Blocks also have the ability (unlike USB Group Terminal Blocks) to overlap. For example we may want a setup where the Monosynth and MIDI IN and MIDI Out functions all use the same Group. By using non-static Function Blocks the user can move these functions to the same Group.
This ability to reconfigure Function Blocks may become more important with other (upcoming) UMP transports.
Group Terminal Blocks for Non-Static Function Blocks
When using static Function Blocks it is easy to see that having matching USB Group Terminal Blocks makes sense. When Function Blocks are non-static it is best to have one bidirectional Group Terminal Block that covers all 16 Groups.
Detail | Meaning | Value |
---|---|---|
bGrpTrmBlkID | Block Id | 1 |
bGrpTrmBlkType | Block Type | 0x00 – Bidertional |
nGroupTrm | Group Terminal Block Start | 0x00 – Group 1 |
nNumGroupTrm | Number of Group Blocks | 16 |
iBlockItem | Function 2 Name | Id of String Descriptor Referenced Value – “ACMESynth” |
bMIDIProtocol | Block Protocol | 0x11 (MIDI 2.0 Protocol) |
4Endpoint Info Notification Message should declare three non-static Function Blocks:
As macOS only provides MIDI 1.0 compatibility to Groups declared by a Group Terminal this allows Functions blocks to move around freely to different Groups while still allowing MIDI 1.0 compatibility.
The downside of this is that all 16 Groups are presented as MIDI 1.0 ports even though only a handful may be connected to an internal function like the Monosynth.
Linux works somewhat differently in that all UMP connections automatically set up all 16 Groups as ALSA ports for MIDI 1.0 compatibility. These ALSA ports are then displayed to the user only if they are active. When a USB MIDI 2.0 device is first connected, it will activate the ALSA Ports based on Group Terminal Block information. It will then attempt to retrieve the current Function Blocks and then update the list of active ALSA Ports. These ALSA ports are then updated immediately based on any Function Block changes.
What to look at next…
In part 3 of this series we are going to look at how to handle advanced USB set-ups, and also look at other gotchas that a developer needs to be aware of.