[CoreMidi] Add missing CoreMIDI bindings and MidiEventList support.#24738
[CoreMidi] Add missing CoreMIDI bindings and MidiEventList support.#24738rolfbjarne wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds comprehensive CoreMIDI bindings for missing APIs and implements MIDI 2.0 Universal MIDI Packet (UMP) support, addressing issues #4452 and #12489. The changes enable developers to work with MIDI devices at a lower level, including custom driver implementation, and provide full support for the modern MIDI 2.0 protocol.
Changes:
- Added MidiEventList and MidiEventPacket classes for MIDI 2.0 UMP support, enabling Universal MIDI Packet handling
- Implemented missing CoreMIDI device/entity management APIs (Create, Dispose, AddOrRemoveEndpoints, etc.) and sysex sending functionality
- Introduced experimental MidiDriver abstract class for implementing custom MIDI drivers with full lifecycle management
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/xtro-sharpie/api-annotations-dotnet/*.ignore | Removed API ignore entries for newly bound CoreMIDI APIs across all platforms |
| tests/cecil-tests/Documentation.cs | Added FunctionPointerType handling to return empty doc IDs for unmanaged function pointers |
| tests/cecil-tests/Documentation.KnownFailures.txt | Removed known failures for newly documented CoreMIDI types and fixed function pointer doc IDs |
| tests/monotouch-test/CoreMidi/MidiEventPacketTest.cs | Comprehensive unit tests for MidiEventPacket struct including bounds checking and data manipulation |
| tests/monotouch-test/CoreMidi/MidiEventListTest.cs | Unit tests for MidiEventList covering construction, adding packets, iteration, and edge cases |
| tests/monotouch-test/CoreMidi/MidiEndpointTest.cs | Added tests for GetRefCons/SetRefCons endpoint reference management APIs |
| tests/monotouch-test/CoreMidi/MidiDeviceTest.cs | Tests for external device creation and MidiSetup add/remove operations |
| tests/monotouch-test/CoreMidi/MidiComprehensiveTest.cs | Extensive integration tests including a complete "Happy Birthday" melody demonstration |
| src/frameworks.sources | Registered new CoreMidi and CoreFoundation source files in the build system |
| src/coremidi.cs | Added MidiDriverProperty enum for driver-specific properties |
| src/CoreMidi/MidiThruConnectionParams.cs | Moved enum definitions outside TVOS conditional to fix tvOS build issues |
| src/CoreMidi/MidiStructs.cs | Implemented MIDI 2.0 structs (Midi2DeviceManufacturer, Midi2DeviceRevisionLevel, MidiCIProfileId variants, MidiSysexSendRequest) |
| src/CoreMidi/MidiServices.cs | Added device/entity creation, external device support, sysex async sending, and protocol-aware port/endpoint creation methods |
| src/CoreMidi/MidiEventPacket.cs | Implemented MidiEventPacket struct with 64-word capacity, indexer, and GetSysexBytes method |
| src/CoreMidi/MidiEventList.cs | Implemented MidiEventList as IDisposable wrapper with Add, Send, Receive, and iteration support |
| src/CoreMidi/MidiDriverInterface.cs | Created experimental MidiDriver abstract class with COM-style interface for custom driver implementation |
| src/CoreMidi/MidiBluetoothDriver.cs | Removed TVOS conditional and added XML documentation for Bluetooth driver methods |
| src/CoreFoundation/CFUuidBytes.cs | Added CFUuidBytes struct for CoreMIDI driver interface QueryInterface method |
| docs/preview-apis.md | Documented MidiDriver as experimental (APL0004) until .NET 12 |
| public void CtorTest_Size () | ||
| { | ||
| Exception ex; | ||
|
|
||
| Assert.Multiple (() => { | ||
| ex = Assert.Throws<ArgumentOutOfRangeException> (() => new MidiEventList (MidiProtocolId.Protocol_1_0, int.MinValue), "AOORE int.MinValue"); | ||
| Assert.That (ex.Message, Does.Contain ("size must be at least 276."), "AOORE msg int.MinValue"); | ||
| ex = Assert.Throws<ArgumentOutOfRangeException> (() => new MidiEventList (MidiProtocolId.Protocol_1_0, -1), "AOORE -1"); | ||
| Assert.That (ex.Message, Does.Contain ("size must be at least 276."), "AOORE msg -1"); | ||
| ex = Assert.Throws<ArgumentOutOfRangeException> (() => new MidiEventList (MidiProtocolId.Protocol_1_0, 0), "AOORE 0"); | ||
| Assert.That (ex.Message, Does.Contain ("size must be at least 276."), "AOORE msg 0"); | ||
| ex = Assert.Throws<ArgumentOutOfRangeException> (() => new MidiEventList (MidiProtocolId.Protocol_1_0, 275), "AOORE 275"); | ||
| Assert.That (ex.Message, Does.Contain ("size must be at least 276."), "AOORE msg 275"); | ||
|
|
||
| var obj = new MidiEventList (MidiProtocolId.Protocol_1_0, 276); | ||
| Assert.That (obj.Protocol, Is.EqualTo (MidiProtocolId.Protocol_1_0), "Protocol"); | ||
| Assert.That (obj.PacketCount, Is.EqualTo (0), "PacketCount"); | ||
| var packets = obj.ToArray (); | ||
| Assert.That (packets.Length, Is.EqualTo (0), "ToArray ().Length"); | ||
| }); |
There was a problem hiding this comment.
The MidiEventList instances created in these tests are not being disposed. Since MidiEventList allocates unmanaged memory when owns=true, these should be wrapped in using statements to ensure proper cleanup. For example: using var obj = new MidiEventList (MidiProtocolId.Protocol_1_0, 276);
| public void AddTest () | ||
| { | ||
| Assert.Multiple (() => { | ||
| var obj = new MidiEventList (MidiProtocolId.Protocol_2_0); | ||
| Assert.That (obj.Protocol, Is.EqualTo (MidiProtocolId.Protocol_2_0), "Protocol"); | ||
| Assert.That (obj.PacketCount, Is.EqualTo (0), "PacketCount"); | ||
|
|
||
| var rv = obj.Add (123, new uint [] { 1, 2, 3 }); | ||
| Assert.That (rv, Is.EqualTo (true), "Add B"); | ||
| Assert.That (obj.Protocol, Is.EqualTo (MidiProtocolId.Protocol_2_0), "Protocol B"); | ||
| Assert.That (obj.PacketCount, Is.EqualTo (1), "PacketCount B"); | ||
|
|
||
| var packets = obj.ToArray (); | ||
| Assert.That (packets.Length, Is.EqualTo (1), "ToArray ().Length"); | ||
| Assert.That (packets [0].Timestamp, Is.EqualTo (123), "Item[0].Timestamp"); | ||
| Assert.That (packets [0].WordCount, Is.EqualTo (3), "Item[0].WordCount"); | ||
| Assert.That (packets [0].Words, Is.EqualTo (new uint [] { 1, 2, 3 }), "Item[0].Words"); | ||
| }); | ||
| } |
There was a problem hiding this comment.
The MidiEventList instance created in this test is not being disposed. Since MidiEventList allocates unmanaged memory when owns=true, this should be wrapped in a using statement to ensure proper cleanup. For example: using var obj = new MidiEventList (MidiProtocolId.Protocol_2_0);
| [Test] | ||
| public void AddTest_ManyWords () | ||
| { | ||
| Assert.Multiple (() => { | ||
| var obj = new MidiEventList (MidiProtocolId.Protocol_2_0); | ||
| Assert.That (obj.Protocol, Is.EqualTo (MidiProtocolId.Protocol_2_0), "Protocol"); | ||
| Assert.That (obj.PacketCount, Is.EqualTo (0), "PacketCount"); | ||
|
|
||
| var manyWords = Enumerable.Range (1, 65).Select (v => (uint) v).ToArray (); | ||
| var rv = obj.Add (123, manyWords); | ||
| Assert.That (rv, Is.EqualTo (false), "Add B"); | ||
| }); | ||
| } |
There was a problem hiding this comment.
The MidiEventList instance created in this test is not being disposed. Since MidiEventList allocates unmanaged memory when owns=true, this should be wrapped in a using statement to ensure proper cleanup.
| [Test] | ||
| public void AddTest_NotEnoughSpace () | ||
| { | ||
| Assert.Multiple (() => { | ||
| var obj = new MidiEventList (MidiProtocolId.Protocol_2_0); | ||
| Assert.That (obj.Protocol, Is.EqualTo (MidiProtocolId.Protocol_2_0), "Protocol"); | ||
| Assert.That (obj.PacketCount, Is.EqualTo (0), "PacketCount"); | ||
|
|
||
| var fitsTwice = Enumerable.Range (1, 24).Select (v => (uint) v).ToArray (); | ||
| var rv = obj.Add (123, fitsTwice); | ||
| Assert.That (rv, Is.EqualTo (true), "Add B"); | ||
| rv = obj.Add (456, fitsTwice); | ||
| Assert.That (rv, Is.EqualTo (true), "Add C"); | ||
| rv = obj.Add (789, fitsTwice); | ||
| Assert.That (rv, Is.EqualTo (false), "Add C"); | ||
| }); | ||
| } |
There was a problem hiding this comment.
The MidiEventList instance created in this test is not being disposed. Since MidiEventList allocates unmanaged memory when owns=true, this should be wrapped in a using statement to ensure proper cleanup.
| [Test] | ||
| public void EnumeratorTest () | ||
| { | ||
| Assert.Multiple (() => { | ||
| var obj = new MidiEventList (MidiProtocolId.Protocol_2_0); | ||
| var rv = obj.Add (789, new uint [] { 4, 5, 6 }); | ||
| Assert.That (rv, Is.EqualTo (true), "Add B"); | ||
| Assert.That (obj.Protocol, Is.EqualTo (MidiProtocolId.Protocol_2_0), "Protocol B"); | ||
| Assert.That (obj.PacketCount, Is.EqualTo (1), "PacketCount B"); | ||
|
|
||
| var packets = obj.ToArray (); | ||
| Assert.That (packets.Length, Is.EqualTo (1), "ToArray ().Length"); | ||
| Assert.That (packets [0].Timestamp, Is.EqualTo (789), "Item[0].Timestamp"); | ||
| Assert.That (packets [0].WordCount, Is.EqualTo (3), "Item[0].WordCount"); | ||
| Assert.That (packets [0].Words, Is.EqualTo (new uint [] { 4, 5, 6 }), "Item[0].Words"); | ||
| }); | ||
| } |
There was a problem hiding this comment.
The MidiEventList instance created in this test is not being disposed. Since MidiEventList allocates unmanaged memory when owns=true, this should be wrapped in a using statement to ensure proper cleanup.
| [Test] | ||
| public void IteratorTest () | ||
| { | ||
| Assert.Multiple (() => { | ||
| var obj = new MidiEventList (MidiProtocolId.Protocol_2_0); | ||
| var rv = obj.Add (456, new uint [] { 1, 2, 3, 4, 5, 6 }); | ||
| Assert.That (rv, Is.EqualTo (true), "Add B"); | ||
| Assert.That (obj.Protocol, Is.EqualTo (MidiProtocolId.Protocol_2_0), "Protocol B"); | ||
| Assert.That (obj.PacketCount, Is.EqualTo (1), "PacketCount B"); | ||
|
|
||
| var packets = obj.ToArray (); | ||
| Assert.That (packets.Length, Is.EqualTo (1), "ToArray ().Length"); | ||
| Assert.That (packets [0].Timestamp, Is.EqualTo (456), "Item[0].Timestamp"); | ||
| Assert.That (packets [0].WordCount, Is.EqualTo (6), "Item[0].WordCount"); | ||
| Assert.That (packets [0].Words, Is.EqualTo (new uint [] { 1, 2, 3, 4, 5, 6 }), "Item[0].Words"); | ||
|
|
||
| var packetList = new List<MidiEventPacket> (); | ||
| obj.Iterate ((ref MidiEventPacket packet) => { | ||
| packetList.Add (packet); | ||
| }); | ||
| Assert.That (packetList.Count, Is.EqualTo (1), "packetList.Length"); | ||
| Assert.That (packetList [0].Timestamp, Is.EqualTo (456), "packetList[0].Timestamp"); | ||
| Assert.That (packetList [0].WordCount, Is.EqualTo (6), "packetList[0].WordCount"); | ||
| Assert.That (packetList [0].Words, Is.EqualTo (new uint [] { 1, 2, 3, 4, 5, 6 }), "packetList[0].Words"); | ||
| }); | ||
| } |
There was a problem hiding this comment.
The MidiEventList instance created in this test is not being disposed. Since MidiEventList allocates unmanaged memory when owns=true, this should be wrapped in a using statement to ensure proper cleanup.
| } | ||
| #endif | ||
|
|
||
| /// <summary>Add a new <see cref="MidiEventPacket" /> to this lis.</summary> |
There was a problem hiding this comment.
There's a typo in the XML documentation: "lis" should be "list".
| /// <summary>Add a new <see cref="MidiEventPacket" /> to this lis.</summary> | |
| /// <summary>Add a new <see cref="MidiEventPacket" /> to this list.</summary> |
| } | ||
|
|
||
| /// <summary>Distribute the packets from the specified <paramref name="source" />.</summary> | ||
| /// <param name="source">The endpoint where the packates come from.</param> |
There was a problem hiding this comment.
There's a typo in the XML documentation: "packates" should be "packets".
| /// <param name="source">The endpoint where the packates come from.</param> | |
| /// <param name="source">The endpoint where the packets come from.</param> |
| return 0; | ||
| } | ||
|
|
||
| static List<MidiDriver> strongReferences = new List<MidiDriver> (); |
There was a problem hiding this comment.
The static List<MidiDriver> field should be initialized as readonly to prevent accidental reassignment. Consider: static readonly List<MidiDriver> strongReferences = new List<MidiDriver> ();
| static List<MidiDriver> strongReferences = new List<MidiDriver> (); | |
| static readonly List<MidiDriver> strongReferences = new List<MidiDriver> (); |
| public void CtorTest () | ||
| { | ||
| Assert.Multiple (() => { | ||
| var obj = new MidiEventList (MidiProtocolId.Protocol_1_0); | ||
| Assert.That (obj.Protocol, Is.EqualTo (MidiProtocolId.Protocol_1_0), "Protocol"); | ||
| Assert.That (obj.PacketCount, Is.EqualTo (0), "PacketCount"); | ||
| var packets = obj.ToArray (); | ||
| Assert.That (packets.Length, Is.EqualTo (0), "ToArray ().Length"); | ||
| }); |
There was a problem hiding this comment.
The MidiEventList instances created in these tests are not being disposed. Since MidiEventList allocates unmanaged memory when owns=true, these should be wrapped in using statements to ensure proper cleanup. For example: using var obj = new MidiEventList (MidiProtocolId.Protocol_1_0);
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Bind the following missing CoreMIDI APIs: - MIDIDeviceCreate/MIDIDeviceDispose - MIDIExternalDeviceCreate - MIDISetupAddDevice/MIDISetupRemoveDevice - MIDISetupAddExternalDevice/MIDISetupRemoveExternalDevice - MIDIEntityAddOrRemoveEndpoints - MIDIDeviceRemoveEntity - MIDIEndPointGetRefCons/MIDIEndPointSetRefCons - MIDIDriverEnableMonitoring - MIDIGetDriverDeviceList/MIDIGetDriverIORunLoop - MIDISendSysex/MIDISendUMPSysex - MIDIDestinationCreateWithProtocol - MIDISourceCreateWithProtocol - MIDIInputPortCreateWithProtocol - MIDIClientCreateWithBlock - MIDIEventPacketSysexBytesForGroup - MIDI 2.0 structs (MIDI2DeviceManufacturer, MIDI2DeviceRevisionLevel, MIDICIProfileID, MIDISysexSendRequest, MIDISysexSendRequestUMP) - MidiDriver abstract class for implementing custom MIDI drivers Add MidiEventList and MidiEventPacket classes for MIDI 2.0 Universal MIDI Packet (UMP) support (MIDIEventList/MIDIEventPacket structs). Add comprehensive tests including a Happy Birthday melody test. There's a sample project in progress here: dotnet/macios-samples#10. Fixes #4452 Fixes #12489
…sage. Add a binding for the native MIDIEventListForEachEvent function, which parses each Universal MIDI Packet (UMP) in a MidiEventList and invokes a callback with the parsed message. This adds: * A faithful managed MidiUniversalMessage struct (and its variant structs) mirroring the native MIDIUniversalMessage union. * A MidiEventList.ForEachEvent method + MidiUniversalMessageVisitor delegate. * Thorough tests, including parsing a MIDI 1.0 note on, a MIDI 2.0 note on, and the Happy Birthday melody. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Explicit struct layout (with overlapping union fields) makes a struct non-blittable, which forces the runtime to generate marshaling code and prevents passing the struct by value through a 'delegate* unmanaged' function pointer without overhead. Rewrite MidiUniversalMessage and its variant structs to use sequential blittable layout with opaque storage fields, exposing the native unions through unsafe accessor properties that reinterpret the storage. Also replace fixed buffers / array fields with named byte fields. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
9db33b7 to
01b1410
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
rolfbjarne
left a comment
There was a problem hiding this comment.
🤖 Code Review — CoreMIDI bindings
Verdict: MidiUniversalMessage/MidiEventPacket avoid explicit layout/arrays to stay blittable for delegate* unmanaged), and the sysex async lifetime management (disposing the CancellationTokenRegistration before freeing native memory) are all sound. A few correctness/polish items inline.
Positive callouts
- No API-breaking changes; removals handled via
[ObsoletedOSPlatform]with actionable "Call 'X' instead." messages. MidiDriverCOM ref-counting (AddRef/Releaseunder lock, weakGCHandle, finalizer releasing the managed ref) is coherent; experimental surface gated behindAPL0004and documented indocs/preview-apis.md..ignoreentries for the now-boundMIDIEventListForEachEvent/Init/Addand the tvOS enums are correctly removed.- Thorough XML docs replacing
"To be added."stubs; Happy Birthday test present. 🎂
Severity counts: 0 ❌ · 1
Reviewed against origin/main (HEAD 01b1410). CI status not evaluated in this review.
| get => wordCount; | ||
| set { | ||
| if (value > 64) | ||
| throw new ArgumentOutOfRangeException ($"WordCount can't be higher than 64."); |
There was a problem hiding this comment.
🤖 new ArgumentOutOfRangeException ($"WordCount can't be higher than 64.") uses the single-string constructor, which treats the string as paramName, not the message. The thrown text becomes "Specified argument was out of the range of valid values. (Parameter 'WordCount can't be higher than 64.')" — misleading. Use the two-arg form, e.g. throw new ArgumentOutOfRangeException (nameof (value), "WordCount can't be higher than 64.") (or nameof (index)).
This pattern repeats throughout this file (lines 104, 115, 131, 149, 151, 153, 161, 163, 165) and in src/CoreMidi/MidiEventList.cs:86.
Rule: Error handling — use correct exception constructor
| @@ -0,0 +1,23 @@ | |||
| namespace CoreFoundation { | |||
There was a problem hiding this comment.
🤖 💡 Patterns — This new file is missing the repo's MIT license header and #nullable enable. New files should start with:
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.Rule: New-file conventions
| /// <param name="protocol">The MIDI protocol for the data this port will receive.</param> | ||
| /// <param name="readBlock">The callback that will be called when the port receives MIDI data.</param> | ||
| /// <param name="status">A status code that describes the result of this operation. This will be <see cref="MidiError.Ok" /> in case of success.</param> | ||
| /// <returns>A newly created <see cref="MidiEndpoint" /> if successful, otherwise null.</returns> |
There was a problem hiding this comment.
🤖 💡 Documentation — CreateInputPort returns MidiPort?, but this <returns> says "A newly created MidiEndpoint…". Likely copy-pasted from the virtual-source/destination overloads. The local var handle = default (MidiEndpointRef); in the method body is also semantically a MidiPortRef (both alias int, so it compiles). Fix the doc and the alias for clarity.
Rule: Documentation accuracy
|
|
||
|
|
||
| /*! | ||
| @struct MIDISysexSendRequestUMP |
There was a problem hiding this comment.
🤖 💡 Code organization — This raw Objective-C header block (@struct MIDISysexSendRequestUMP …) duplicates the XML docs added just below for MidiSysexSendRequestUmp. Consider removing the leftover native comment to reduce noise.
Rule: Avoid redundant comments
| // These APIs returns -50 (GeneralParamError) no matter what I do :/ | ||
|
|
||
| Assert.AreEqual (AudioQueueStatus.GeneralParamError, (AudioQueueStatus) ep.GetRefCons (out var ref1, out var ref2), "GetRefCons A"); | ||
| Assert.AreEqual (ref1, IntPtr.Zero, "GetRefCons A 1"); |
There was a problem hiding this comment.
🤖 💡 Testing — Assert.AreEqual (ref1, IntPtr.Zero, …) has actual/expected swapped (NUnit's signature is (expected, actual)), so failure messages will read backwards. Prefer the constraint form: Assert.That (ref1, Is.EqualTo (IntPtr.Zero), …). Same applies to the sibling assertions in this block.
Rule: Assertion style for good failure messages
* Fix ArgumentOutOfRangeException constructor misuse (string was passed as paramName instead of message) in MidiEventPacket and MidiEventList. * Add MIT license header and #nullable enable to CFUuidBytes.cs. * Fix CreateInputPort <returns> doc and local alias (MidiPort, not MidiEndpoint). * Remove redundant native @struct comment block in MidiStructs.cs. * Use Assert.That/Is.EqualTo with correct argument order in MidiEndpointTest. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
✅ [PR Build #a8f368c] Build passed (Detect API changes) ✅Pipeline on Agent |
This comment has been minimized.
This comment has been minimized.
✅ [PR Build #a8f368c] Build passed (Build packages) ✅Pipeline on Agent |
✅ API diff for current PR / commitNET (empty diffs)✅ API diff vs stableNET (empty diffs)ℹ️ Generator diffGenerator Diff: vsdrops (html) vsdrops (raw diff) gist (raw diff) - Please review changes) Pipeline on Agent |
✅ [PR Build #a8f368c] Build passed (Build macOS tests) ✅Pipeline on Agent |
🔥 [CI Build #a8f368c] Test results 🔥Test results❌ Tests failed on VSTS: test results 0 tests crashed, 8 tests failed, 199 tests passed. Failures❌ introspection tests2 tests failed, 2 tests passed.Failed tests
Html Report (VSDrops) Download ❌ monotouch tests (MacCatalyst)4 tests failed, 15 tests passed.Failed tests
Html Report (VSDrops) Download ❌ monotouch tests (macOS)2 tests failed, 18 tests passed.Failed tests
Html Report (VSDrops) Download Successes✅ assembly-processing: All 1 tests passed. Html Report (VSDrops) Download macOS tests✅ Tests on macOS Monterey (12): All 5 tests passed. Html Report (VSDrops) Download Linux Build VerificationPipeline on Agent |
Bind the following missing CoreMIDI APIs:
MIDICIProfileID, MIDISysexSendRequest, MIDISysexSendRequestUMP)
Add MidiEventList and MidiEventPacket classes for MIDI 2.0 Universal MIDI
Packet (UMP) support (MIDIEventList/MIDIEventPacket structs).
Add comprehensive tests including a Happy Birthday melody test.
There's a sample project in progress here: dotnet/macios-samples#10.
Fixes #4452
Fixes #12489