MIDI 2.0: Assertion failure with 16 Group Terminal Blocks on macOS

JUCE version: 8.0.12
macOS version: Tahoe 26.5.1

When a USB MIDI 2.0 device exposes 16 Group Terminal Blocks, JUCE hits an assertion failure in juce_CoreMidi_mac.mm:771 inside getSyntheticBlocks().

The root cause appears to be that CoreMIDI automatically creates a synthetic “MIDI 2.0” entity for USB MIDI 2.0 devices. It has 0 ins and 0 outs, but still shows up in the endpoint list and gets counted towards the total number of blocks. This means a device with 16 GTBs produces 17 entities. Since getSyntheticBlocks iterates both source and destination directions, that’s 34 blocks, which overflows the hardcoded Span<ump::Block, 32>.

Discussion

I’m not sure what the correct behavior should be in this scenario. I can’t find any documentation in the JUCE source explaining the decision to limit the block count to 32, but I suspect it comes from M2-104-UM section 7.1.2: “Up to 32 Function Blocks can be declared on a UMP Stream.”

I can’t find any Apple documentation about the synthetic “MIDI 2.0” entity that CoreMIDI adds, but regardless, I would expect a bidirectional Function Block to count as one block, not two. The device in question declares only 16 bidirectional GTBs; each is described by 1 bidirectional Function Block, so by my count, that plus the MIDI 2.0 entity should be 17 blocks.

The current implementation in getSyntheticBlocks iterates CoreMIDI endpoints in each direction separately, effectively double-counting bidirectional blocks. Could this be a counting error or is there some other explanation?

That’s correct, the Block type is primarily meant to model function blocks. Platform APIs are inconsistent about providing access to group terminal blocks.

Part of the problem is that we’re not really looking at group terminal blocks here, just the enabled group terminals. I don’t think CoreMidi gives us a way of inspecting the GTBs. Ideally the device would expose function blocks in addition to GTBs in order to publish its logical components.

I can think of a couple of changes that might help:

  • Maybe we could avoid this “synthetic” path entirely if we’ve already discovered the device through the systemwide UMP endpoints singleton. The synthetic path is a fallback, and it shouldn’t be required as long as the system picks up the device and exposes it through the newer UMP API.
  • If the block contains no groups, we could skip it, since a block with no groups can’t be used for sending or receiving messages anyway. I think that gets us back under the 32-block limit, because there’s a maximum of 16 potentially active groups in each direction. Again, since we’re looking at group terminals, and not GTBs, we can’t assume that the block is bidirectional - the best we can do is to report that it’s active in that direction.
  • Perhaps another option would be to report a single block that covers all active group terminals in each direction, instead of a block per group terminal. This would absolutely require that the enabled group terminals are continuous, though, which is mandated by the spec but not by design.

I’ve tried implementing the first change I mentioned, to avoid the synthetic path if we’ve already discovered the device by other means. Please could you try applying this patch, and check whether it helps?

coremidi.patch (5.9 KB)

Thank you for the patch! I generally like the approach of filtering out the endpoint when it’s non-functional as it should be in this case. I tried it out today but am still running into the same issue. I studied the execution a bit more to understand what’s happening.

The assertion that gets triggered is inside getSyntheticBlocks():

if (result >= blocks.size())
{
    jassertfalse;
    return;
}

Just before that, when the function retrieves the number of groups from the endpoint:

const auto bitmap = getUMPActiveGroupBitmap (endpoint);

if (! bitmap.has_value())
    return;

const auto numGroups = countNumberOfBits ((uint32) *bitmap);

For all of the device’s normal endpoints, bitmap takes the value (1 << n) where n is the group number for that endpoint, and numGroups correctly comes out as 1. This is all good.

However, when the mysterious “MIDI 2.0” entity is encountered, bitmap returns 15 (0b1111) and numGroups is 4 — suggesting this entity covers groups 1-4. This is surprising because earlier in execution canTransmitGroupless identified the same endpoint as having no groups. The documentation for kMIDIPropertyUMPActiveGroupBitmap states:

Any UMP-native endpoint lacking this property and the subsequently defined property kMIDIPropertyCanTransmitGroupless is assumed to handle/transmit all UMP traffic.

I’m not sure what to make of this, but I would expect the “MIDI 2.0” endpoint to have no active groups given that MIDI Studio shows it with 0 ins / 0 outs. I would expect kMIDIPropertyUMPActiveGroupBitmap to be undefined or set to 0 since kMIDIPropertyCanTransmitGroupless is truth-y. When I use MIDI 2.0 Workbench, all the USB descriptors and function block tables look to be correct for the defined GTBs.

Also an FYI, MIDIUMPEndpointManager.sharedInstance.UMPEndpoints is empty, so bidiUmpEndpoints is also empty and findAllUMPDevices never skips any device. Not sure if this changes things but the device appears this way in MIDI Studio:

- Device
	Block 1 (1 in / 1 out)
	Block 2 (1 in / 1 out)
	Block 3 (1 in / 1 out)
	...
	Block 16 (1 in / 1 out)
	MIDI 2.0 (0 ins / 0 outs)

If the “MIDI 2.0” entity’s group count were reported as 0, the original implementation would work correctly. When I use MIDI 2.0 Workbench, all the USB descriptors and function block definitions look to be correct, so I can’t think of what else might be the issue other than perhaps a bug in CoreMIDI? Unfortunately I don’t have another macOS version available to test against. It seems like the information coming back from CoreMIDI precludes the first two proposed solutions.

The idea about having one GTB covering all 16 groups would actually be the ideal, and its where I started, but for some reason the OS is not propagating the function block names to the DAWs for each group. For example, in Ableton, with that configuration the ports show up as “Device (Port 1)”, “Device (Port 2)”, etc. Providing the names through the USB descriptor resolves this, but it requires 16 GTBs, which is how I got here.

Anecdotally, increasing the blocks array capacities to 34 (in various places) works. I’m not suggesting it’s ideal, based on the spec, but it does resolve this strange edge-case.