From 46761f4398cdd03a854640f74c9149b2d38ba4b7 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 12 Feb 2026 21:36:59 +0100 Subject: [PATCH 1/4] [CoreMidi] Add missing CoreMIDI bindings and MidiEventList support. 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: https://github.com/dotnet/macios-samples/pull/10. Fixes https://github.com/dotnet/macios/issues/4452 Fixes https://github.com/dotnet/macios/issues/12489 --- docs/preview-apis.md | 12 +- src/CoreFoundation/CFUuidBytes.cs | 23 + src/CoreMidi/MidiBluetoothDriver.cs | 10 +- src/CoreMidi/MidiDriverInterface.cs | 360 +++++++ src/CoreMidi/MidiEventList.cs | 271 +++++ src/CoreMidi/MidiEventPacket.cs | 203 ++++ src/CoreMidi/MidiServices.cs | 689 +++++++++++-- src/CoreMidi/MidiStructs.cs | 162 +++ src/CoreMidi/MidiThruConnectionParams.cs | 8 +- src/coremidi.cs | 14 +- src/frameworks.sources | 4 + .../Documentation.KnownFailures.txt | 33 +- tests/cecil-tests/Documentation.cs | 3 + .../CoreMidi/MidiComprehensiveTest.cs | 973 ++++++++++++++++++ .../monotouch-test/CoreMidi/MidiDeviceTest.cs | 34 + .../CoreMidi/MidiEndpointTest.cs | 54 + .../CoreMidi/MidiEventListTest.cs | 157 +++ .../CoreMidi/MidiEventPacketTest.cs | 130 +++ .../MacCatalyst-CoreMIDI.ignore | 30 +- .../common-CoreMIDI.ignore | 2 - .../iOS-CoreMIDI.ignore | 30 +- .../macOS-CoreMIDI.ignore | 34 +- .../tvOS-CoreMIDI.ignore | 7 - 23 files changed, 3023 insertions(+), 220 deletions(-) create mode 100644 src/CoreFoundation/CFUuidBytes.cs create mode 100644 src/CoreMidi/MidiDriverInterface.cs create mode 100644 src/CoreMidi/MidiEventList.cs create mode 100644 src/CoreMidi/MidiEventPacket.cs create mode 100644 tests/monotouch-test/CoreMidi/MidiComprehensiveTest.cs create mode 100644 tests/monotouch-test/CoreMidi/MidiDeviceTest.cs create mode 100644 tests/monotouch-test/CoreMidi/MidiEventListTest.cs create mode 100644 tests/monotouch-test/CoreMidi/MidiEventPacketTest.cs delete mode 100644 tests/xtro-sharpie/api-annotations-dotnet/tvOS-CoreMIDI.ignore diff --git a/docs/preview-apis.md b/docs/preview-apis.md index f43049be8162..afbb01dd9a6c 100644 --- a/docs/preview-apis.md +++ b/docs/preview-apis.md @@ -104,11 +104,19 @@ method is Swift API we've bound manually, and as such it was marked as experimen It's no longer marked as experimental. -[1]: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.experimentalattribute?view=net-8.0 - ## Rgen (APL0003) Rgen is the new Roslyn codegenerator based binding tool. The tool is underdevelopment and its API is open to change until a stable release is announced. The diagnostic id for Rgen is APL0003. + +## CoreMidi.MidiDriver (APL0004) + +The [MIDIDevice](https://developer.apple.com/documentation/coremidi/midi-drivers) API is untested, and as such it's marked experimental until .NET 12. + +The diagnostic id for MidiDevice is APL0004. + +--- + +[1]: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis.experimentalattribute?view=net-8.0 diff --git a/src/CoreFoundation/CFUuidBytes.cs b/src/CoreFoundation/CFUuidBytes.cs new file mode 100644 index 000000000000..125a830479ff --- /dev/null +++ b/src/CoreFoundation/CFUuidBytes.cs @@ -0,0 +1,23 @@ +namespace CoreFoundation { + // This struct is only used for P/Invokes. + struct CFUuidBytes { +#pragma warning disable CS0649 // Field '...' is never assigned to, and will always have its default value 0 + public byte Byte0; + public byte Byte1; + public byte Byte2; + public byte Byte3; + public byte Byte4; + public byte Byte5; + public byte Byte6; + public byte Byte7; + public byte Byte8; + public byte Byte9; + public byte Byte10; + public byte Byte11; + public byte Byte12; + public byte Byte13; + public byte Byte14; + public byte Byte15; +#pragma warning restore CS0649 + } +} diff --git a/src/CoreMidi/MidiBluetoothDriver.cs b/src/CoreMidi/MidiBluetoothDriver.cs index 0f7d5e058b29..2fbaa9de8c2f 100644 --- a/src/CoreMidi/MidiBluetoothDriver.cs +++ b/src/CoreMidi/MidiBluetoothDriver.cs @@ -1,5 +1,4 @@ -#if !TVOS -// + // MidiBluetoothDriver.cs // // Authors: TJ Lambert (TJ.Lambert@microsoft.com) @@ -12,6 +11,7 @@ using CoreFoundation; namespace CoreMidi { + /// Provides access to the MIDI Bluetooth driver for managing Bluetooth MIDI connections. [SupportedOSPlatform ("ios16.0")] [SupportedOSPlatform ("maccatalyst16.0")] [SupportedOSPlatform ("tvos16.0")] @@ -20,11 +20,16 @@ public partial class MidiBluetoothDriver { [DllImport (Constants.CoreMidiLibrary)] static extern int MIDIBluetoothDriverActivateAllConnections (); + /// Activates all Bluetooth MIDI connections. + /// A status code indicating the result of the operation (0 for success). public static int ActivateAllConnections () => MIDIBluetoothDriverActivateAllConnections (); [DllImport (Constants.CoreMidiLibrary)] static extern unsafe int MIDIBluetoothDriverDisconnect (/* CFStringRef* */ NativeHandle uuid); + /// Disconnects a Bluetooth MIDI device identified by its UUID. + /// The UUID of the Bluetooth MIDI device to disconnect. + /// A status code indicating the result of the operation (0 for success). public static int Disconnect (NSString uuid) { int result = MIDIBluetoothDriverDisconnect (uuid.GetHandle ()); @@ -33,4 +38,3 @@ public static int Disconnect (NSString uuid) } } } -#endif diff --git a/src/CoreMidi/MidiDriverInterface.cs b/src/CoreMidi/MidiDriverInterface.cs new file mode 100644 index 000000000000..7d1980c1649f --- /dev/null +++ b/src/CoreMidi/MidiDriverInterface.cs @@ -0,0 +1,360 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Let's hope that by .NET 12 we've ironed out all the bugs in the API. +// This can of course be adjusted as needed (until we've released as stable). +#if NET120_0_OR_GREATER +#define STABLE_MIDIDRIVER +#endif + + +#if !__TVOS__ + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Threading; + +using CoreFoundation; +using ObjCRuntime; + +using MidiObjectRef = System.Int32; +using MidiClientRef = System.Int32; +using MidiDeviceRef = System.Int32; +using MidiDeviceListRef = System.Int32; +using MidiDriverRef = System.IntPtr; +using MidiPortRef = System.Int32; +using MidiEndpointRef = System.Int32; +using MidiEntityRef = System.Int32; + +using MidiEventListPointer = System.IntPtr; +using MidiPacketListPointer = System.IntPtr; + +using HRESULT = System.Int32; + +namespace CoreMidi { + /// Abstract base class for implementing custom MIDI drivers. Subclass this to create a driver that communicates with MIDI hardware. +#if !STABLE_MIDIDRIVER + [Experimental ("APL0004")] +#endif + [SupportedOSPlatform ("ios")] + [SupportedOSPlatform ("maccatalyst")] + [SupportedOSPlatform ("macos")] + public abstract class MidiDriver { +#if !COREBUILD + unsafe MidiDriverInterface* driverInterface; + + unsafe internal MidiDriverInterface* DriverInterface { get => driverInterface; } + + unsafe protected MidiDriver () + { + driverInterface = CreateDriver (); + } + + unsafe MidiDriverInterface* CreateDriver () + { + var iface = (MidiDriverInterface*) Marshal.AllocHGlobal (sizeof (MidiDriverInterface)); + iface->QueryInterface = &QueryInterface; + iface->AddRef = &AddRef; + iface->Release = &Release; + iface->FindDevices = &FindDevices; + iface->Start = &Start; + iface->Stop = &Stop; + iface->Configure = &Configure; + iface->Send = &Send; + iface->EnableSource = &EnableSource; + iface->Flush = &Flush; + iface->Monitor = &Monitor; + iface->SendPackets = &SendPackets; + iface->MonitorEvents = &MonitorEvents; + iface->gchandle = (IntPtr) GCHandle.Alloc (this, GCHandleType.Weak); + iface->referenceCount = 1; // managed code has one reference + return iface; + } + + ~MidiDriver () + { + Release (); // release managed code's reference + } + + [UnmanagedCallersOnly] + unsafe static HRESULT QueryInterface (MidiDriverInterface* self, CFUuidBytes iid, void* ppv) + { + var driver = self->GetObject (); + return driver?.QueryInterface (iid, (IntPtr) ppv) ?? 0; + } + + internal virtual HRESULT QueryInterface (CFUuidBytes iid, IntPtr ppv) + { + return 0; + } + + static List strongReferences = new List (); + + [UnmanagedCallersOnly] + unsafe static uint AddRef (MidiDriverInterface* self) + { + var driver = self->GetObject (); + return driver?.AddRef () ?? 0; + } + + unsafe internal virtual uint AddRef () + { + uint referenceCount; + lock (strongReferences) { + referenceCount = Interlocked.Increment (ref driverInterface->referenceCount); + if (referenceCount == 2) { + strongReferences.Add (this); + } + } + return referenceCount; + } + + [UnmanagedCallersOnly] + unsafe static uint Release (MidiDriverInterface* self) + { + var driver = self->GetObject (); + return driver?.Release () ?? 0; + } + + unsafe internal virtual uint Release () + { + uint referenceCount = 0; + lock (strongReferences) { + if (driverInterface is not null) { + referenceCount = Interlocked.Decrement (ref driverInterface->referenceCount); + if (referenceCount == 1) { + strongReferences.Remove (this); + } else if (referenceCount == 0) { + var gchandle = GCHandle.FromIntPtr (driverInterface->gchandle); + gchandle.Free (); + driverInterface->gchandle = IntPtr.Zero; + Marshal.FreeHGlobal ((IntPtr) driverInterface); + driverInterface = null; + } + } + } + return referenceCount; + } + + [UnmanagedCallersOnly] + unsafe static OSStatus FindDevices (MidiDriverInterface* self, MidiDeviceListRef devList) + { + var driver = self->GetObject (); + return driver?.FindDevices (devList) ?? 0; + } + + /// The server requests that the driver detects any present devices. For each detected device, call and , and then add the device to the supplied . + protected virtual OSStatus FindDevices (MidiDeviceListRef deviceList /* FIXME: strongly typed */) + { + return 0; + } + + [UnmanagedCallersOnly] + unsafe static OSStatus Start (MidiDriverInterface* self, MidiDeviceListRef devList) + { + var driver = self->GetObject (); + return driver?.Start (devList) ?? 0; + } + + /// Start MIDI I/O. + protected virtual OSStatus Start (MidiDeviceListRef deviceList /* FIXME: strongly typed */) + { + return 0; + } + + [UnmanagedCallersOnly] + unsafe static OSStatus Stop (MidiDriverInterface* self) + { + var driver = self->GetObject (); + return driver?.Stop () ?? 0; + } + + /// Stop MIDI I/O. + protected virtual OSStatus Stop () + { + return 0; + } + + [UnmanagedCallersOnly] + unsafe static OSStatus Configure (MidiDriverInterface* self, MidiDeviceRef device) + { + var driver = self->GetObject (); + return driver?.Configure (device) ?? 0; + } + + /// Not used at the moment. + protected virtual OSStatus Configure (MidiDeviceRef device) + { + return 0; + } + + [UnmanagedCallersOnly] + unsafe static OSStatus Send (MidiDriverInterface* self, MidiPacketListPointer pktList, void* destRefCon1, void* destRefCon2) + { + var driver = self->GetObject (); + return driver?.Send (pktList, destRefCon1, destRefCon2) ?? 0; + } + + /// Send a MidiPacketList to the destination endpoint. + protected unsafe virtual OSStatus Send (MidiPacketListPointer pktList, void* destRefCon1, void* destRefCon2) + { + return 0; + } + + [UnmanagedCallersOnly] + unsafe static OSStatus EnableSource (MidiDriverInterface* self, MidiEndpointRef src, byte enabled) + { + var driver = self->GetObject (); + return driver?.EnableSource (src, enabled != 0) ?? 0; + } + + /// Lets the driver know if a particular source has any listeners or not. + protected unsafe virtual OSStatus EnableSource (MidiEndpointRef src, bool enabled) + { + return 0; + } + + [UnmanagedCallersOnly] + unsafe static OSStatus Flush (MidiDriverInterface* self, MidiEndpointRef dest, void* destRefCon1, void* destRefCon2) + { + var driver = self->GetObject (); + return driver?.Flush (dest, destRefCon1, destRefCon2) ?? 0; + } + + /// Unschedule all pending output to the specified destination endpoint (or all endpoints if null). + protected unsafe virtual OSStatus Flush (MidiEndpointRef src, void* destRefCon1, void* destRefCon2) + { + return 0; + } + + [UnmanagedCallersOnly] + unsafe static OSStatus Monitor (MidiDriverInterface* self, MidiEndpointRef dest, MidiPacketListPointer pktList) + { + var driver = self->GetObject (); + return driver?.Monitor (dest, pktList) ?? 0; + } + + /// If monitoring is enabled, this method will be called with all outgoing MIDI messages. + protected unsafe virtual OSStatus Monitor (MidiEndpointRef src, MidiPacketListPointer packetList) + { + return 0; + } + + [UnmanagedCallersOnly] + unsafe static OSStatus SendPackets (MidiDriverInterface* self, MidiEventListPointer pktList, void* destRefCon1, void* destRefCon2) + { + var driver = self->GetObject (); + return driver?.SendPackets (pktList, destRefCon1, destRefCon2) ?? 0; + } + + /// Send a to the destination endpoint. + protected unsafe virtual OSStatus SendPackets (MidiEventListPointer pktList, void* destRefCon1, void* destRefCon2) + { + return 0; + } + + [UnmanagedCallersOnly] + unsafe static OSStatus MonitorEvents (MidiDriverInterface* self, MidiEndpointRef dest, MidiEventListPointer pktList) + { + var driver = self->GetObject (); + return driver?.MonitorEvents (dest, pktList) ?? 0; + } + + /// Same as , but sending a instead of a MidiPacketList. + protected unsafe virtual OSStatus MonitorEvents (MidiEndpointRef dest, MidiEventListPointer pktList) + { + return 0; + } + + [DllImport (Constants.CoreMidiLibrary)] + unsafe static extern MidiDeviceListRef MIDIGetDriverDeviceList (MidiDriverInterface** driver); + + /// Get the devices this driver owns or created. + /// If successful, a list of the device this driver owns or created. Otherwise null. + public unsafe MidiDeviceList? GetDeviceList () + { + fixed (MidiDriverInterface** driverInterfacePtr = &driverInterface) { + var rv = MIDIGetDriverDeviceList (driverInterfacePtr); + if (rv == MidiObject.InvalidRef) + return null; + return new MidiDeviceList (rv); + } + } + + [DllImport (Constants.CoreMidiLibrary)] + static extern IntPtr /* CFRunLoopRef */ MIDIGetDriverIORunLoop (); + + /// Get the high (realtime) priority run loop that can be used for asynchronous I/O completion callbacks. + /// If successful, the IO run loop. Otherwise null. + public static CFRunLoop? GetIORunLoop () + { + var rv = MIDIGetDriverIORunLoop (); + if (rv == IntPtr.Zero) + return null; + return new CFRunLoop (rv, false); + } + +#if MONOMAC + [SupportedOSPlatform ("macos")] + [UnsupportedOSPlatform ("ios")] + [UnsupportedOSPlatform ("tvos")] + [UnsupportedOSPlatform ("maccatalyst")] + [DllImport (Constants.CoreMidiLibrary)] + unsafe static extern OSStatus MIDIDriverEnableMonitoring (MidiDriverInterface** driver, byte enabled); + + /// A driver can call this method to receive all the outgoing MIDI packets in the system. + /// Whether to enable or disable monitoring. + /// A status code that describes the result of the operation. This will be in case of success. + [SupportedOSPlatform ("macos")] + [UnsupportedOSPlatform ("ios")] + [UnsupportedOSPlatform ("tvos")] + [UnsupportedOSPlatform ("maccatalyst")] + public unsafe MidiError EnableMonitoring (bool enabled) + { + fixed (MidiDriverInterface **driver = &driverInterface) { + return (MidiError) MIDIDriverEnableMonitoring (driver, enabled.AsByte ()); + } + } +#endif // MONOMAC +#endif // COREBUILD + } + +#if !COREBUILD +#if !STABLE_MIDIDRIVER + [Experimental ("APL0004")] +#endif + struct MidiDriverInterface { +#pragma warning disable CS0169 // The field 'MidiDriverInterface._reserved' is never used + IntPtr _reserved; +#pragma warning restore CS0169 + internal unsafe delegate* unmanaged QueryInterface; + internal unsafe delegate* unmanaged AddRef; + internal unsafe delegate* unmanaged Release; + internal unsafe delegate* unmanaged FindDevices; + internal unsafe delegate* unmanaged Start; + internal unsafe delegate* unmanaged Stop; + internal unsafe delegate* unmanaged Configure; + internal unsafe delegate* unmanaged Send; + internal unsafe delegate* unmanaged EnableSource; + internal unsafe delegate* unmanaged Flush; + internal unsafe delegate* unmanaged Monitor; + internal unsafe delegate* unmanaged SendPackets; + internal unsafe delegate* unmanaged MonitorEvents; + + internal IntPtr gchandle; // this is our own + internal uint referenceCount; // this is our own + + internal MidiDriver? GetObject () + { + var gchandle = this.gchandle; + if (gchandle == IntPtr.Zero) + return null; + return (MidiDriver?) GCHandle.FromIntPtr (gchandle).Target; + } + } +#endif // COREBUILD +} + +#endif // !__TVOS__ + diff --git a/src/CoreMidi/MidiEventList.cs b/src/CoreMidi/MidiEventList.cs new file mode 100644 index 000000000000..4d6fa781b199 --- /dev/null +++ b/src/CoreMidi/MidiEventList.cs @@ -0,0 +1,271 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +using Foundation; +using ObjCRuntime; + +using MidiEndpointRef = System.Int32; +using MidiPortRef = System.Int32; + +#nullable enable + +namespace CoreMidi { + /// This class represents the Objective-C struct MIDIEventList, which is a list of packets. + [SupportedOSPlatform ("ios14.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + // [NativeName ("MIDIEventList")] + public sealed class MidiEventList : IEnumerable, IDisposable { + /* This is a variable sized struct, so store all the data in a byte array. + * struct MIDIEventList + * { + * MIDIProtocolID protocol; + * UInt32 numPackets; + * MIDIEventPacket packet[1]; + * }; + */ + + // this struct is just used internally to avoid some manual pointer math + struct MIDIEventList { +#pragma warning disable CS0649 // Field '...' is never assigned to, and will always have its default value +#pragma warning disable CS0169 // The field '...' is never used + internal MidiProtocolId protocol; + internal uint numPackets; + internal MidiEventPacket packet; +#pragma warning restore CS0169 +#pragma warning restore CS0649 + } + + unsafe MIDIEventList* midiDataPointer; + int midiDataSize; + bool owns; + unsafe MidiEventPacket* currentPacket; + + const int MinimumSize = 276; /* 4 + 4 + sizeof (MidiEventPacket) */ + + /// The protocol for the packets in this list of packets. + /// The protocol for the packets in this list of packets. + public unsafe MidiProtocolId Protocol { + get { + return midiDataPointer->protocol; + } + } + + /// The number of packets in this list. + /// The number of packets in this list. + public unsafe uint PacketCount { + get { + return midiDataPointer->numPackets; + } + } + + unsafe internal void* MidiData { get => midiDataPointer; } + + /// Create a new list with the minimum size. + /// The protocol for the packets in the created list. + /// A newly created , or an exception in case of failure. + public MidiEventList (MidiProtocolId protocol) + : this (protocol, MinimumSize) + { + } + + /// Create a new for the specified protocol and size. + /// The protocol for the event list. + /// The size, in number of bytes, of the event list. Minimum size is 276 bytes. + /// A newly created , or an exception in case of failure. + public MidiEventList (MidiProtocolId protocol, int size) + { + if (size < MinimumSize) + throw new ArgumentOutOfRangeException ($"{nameof (size)} must be at least {MinimumSize}."); + + midiDataSize = size; + owns = true; + unsafe { + midiDataPointer = (MIDIEventList*) Marshal.AllocHGlobal (midiDataSize); + currentPacket = MIDIEventListInit (midiDataPointer, protocol); + if (currentPacket is null) { + Marshal.FreeHGlobal ((IntPtr) midiDataPointer); + midiDataPointer = null; + throw new Exception ($"Failed to create midi event list."); + } + } + } + + /// Create a new for a given block of memory. + /// A pointer to a block of memory with the event list. + /// A newly created , or an exception in case of failure. + public MidiEventList (IntPtr eventListPointer) + { + if (eventListPointer == IntPtr.Zero) + throw new ArgumentOutOfRangeException (nameof (eventListPointer)); + + unsafe { + midiDataPointer = (MIDIEventList*) eventListPointer; + owns = false; + midiDataSize = -1; + } + } + + /// Releases the resources associated with this . + public void Dispose () + { + Dispose (true); + GC.SuppressFinalize (this); + } + + void Dispose (bool disposing) + { + if (owns) { + unsafe { + Marshal.FreeHGlobal ((IntPtr) midiDataPointer); + midiDataPointer = null; + } + } + } + + ~MidiEventList () + { + Dispose (false); + } + +#if !__TVOS__ + /// Send the packets in this list to the specified . + /// The port through which the packets are sent. + /// The destination where the packets are sent. + /// A non-zero error code in case of failure, otherwise zero (which indicates success). + [SupportedOSPlatform ("ios14.0")] + [UnsupportedOSPlatform ("tvos")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + public unsafe int /* OSStatus */ Send (MidiPort port, MidiEndpoint destination) + { + var rv = MIDISendEventList (port.Handle, destination.Handle, midiDataPointer); + GC.KeepAlive (port); + GC.KeepAlive (destination); + return rv; + } + + /// Distribute the packets from the specified . + /// The endpoint where the packates come from. + /// A non-zero error code in case of failure, otherwise zero (which indicates success). + [SupportedOSPlatform ("ios14.0")] + [UnsupportedOSPlatform ("tvos")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + public unsafe int /* OSStatus */ Receive (MidiEndpoint source) + { + var rv = MIDIReceivedEventList (source.Handle, midiDataPointer); + GC.KeepAlive (source); + return rv; + } +#endif + + /// Add a new to this lis. + /// The timestamp for the new packet. + /// The data for the midi event to add. + /// True if successful, otherwise false (which typically means there's not enough space for the new packet). + public unsafe bool Add (ulong time, uint [] words) + { + if (midiDataSize < 0) + throw new InvalidOperationException ($"Can't add to a MidiEventList initialized from a raw pointer."); + + ArgumentNullException.ThrowIfNull (words); + + fixed (uint* wordsPtr = words) { + var rv = MIDIEventListAdd (midiDataPointer, (ulong) midiDataSize, currentPacket, time, (ulong) words.Length, wordsPtr); + if (rv is not null) { + currentPacket = rv; + return true; + } + return false; + } + } + + [DllImport (Constants.CoreMidiLibrary)] + unsafe static extern MidiEventPacket* MIDIEventListInit (MIDIEventList* evtlist, MidiProtocolId /* MIDIProtocolID */ protocol); + + [DllImport (Constants.CoreMidiLibrary)] + unsafe static extern MidiEventPacket* MIDIEventListAdd ( + MIDIEventList* evtlist, + ulong /* ByteCount = unsigned long */ listSize, + MidiEventPacket* curPacket, + ulong /* MIDITimeStamp */ time, + ulong /* ByteCount = unsigned long */ wordCount, + uint* /* const UInt32 * */ words); + +#if !__TVOS__ + [SupportedOSPlatform ("ios14.0")] + [UnsupportedOSPlatform ("tvos")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + [DllImport (Constants.CoreMidiLibrary)] + unsafe static extern int /* OSStatus */ MIDISendEventList (MidiPortRef port, MidiEndpointRef dest, MIDIEventList* evtList); + + [SupportedOSPlatform ("ios14.0")] + [UnsupportedOSPlatform ("tvos")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + [DllImport (Constants.CoreMidiLibrary)] + unsafe static extern int /* OSStatus */ MIDIReceivedEventList (MidiEndpointRef src, MIDIEventList* evtlist); +#endif // !__TVOS__ + + IEnumerator IEnumerable.GetEnumerator () + { + MidiEventPacket packetToYield; + IntPtr packetPtr; + + if (PacketCount == 0) + yield break; + + unsafe { + MidiEventPacket* packet = &midiDataPointer->packet; + packetToYield = *packet; + packetPtr = (IntPtr) packet; + } + yield return packetToYield; + + for (var i = 1; i < PacketCount; i++) { + unsafe { + MidiEventPacket* packet = (MidiEventPacket*) packetPtr; + uint* wordPointer = &packet->word_00; + packet = (MidiEventPacket*) (wordPointer + packet->WordCount); + packetToYield = *packet; + packetPtr = (IntPtr) packet; + } + yield return packetToYield; + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () + { + return ((IEnumerable) this).GetEnumerator (); + } + + /// Iterate over each in this list without allocating or copying memory. + /// The function to call for each packet. + public unsafe void Iterate (MidiEventListIterator callback) + { + if (PacketCount == 0) + return; + + MidiEventPacket* packet = &midiDataPointer->packet; + callback (ref Unsafe.AsRef (packet)); + for (var i = 1; i < PacketCount; i++) { + uint* wordPointer = &packet->word_00; + packet = (MidiEventPacket*) (wordPointer + packet->WordCount); + callback (ref Unsafe.AsRef (packet)); + } + } + } + + /// The delegate type used by . + /// The current packet found when iterating. + public delegate void MidiEventListIterator (ref MidiEventPacket packet); +} diff --git a/src/CoreMidi/MidiEventPacket.cs b/src/CoreMidi/MidiEventPacket.cs new file mode 100644 index 000000000000..bf8977fd2d44 --- /dev/null +++ b/src/CoreMidi/MidiEventPacket.cs @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +using Foundation; +using ObjCRuntime; + +using MidiEndpointRef = System.Int32; +using MidiPortRef = System.Int32; + +#nullable enable + +namespace CoreMidi { + /// This class represents the Objective-C struct MIDIEventPacket, which is a variable-sized struct. + [NativeName ("MIDIEventPacket")] + public struct MidiEventPacket { + ulong /* MIDITimeStamp */ timeStamp; + uint /* UInt32 */ wordCount; + + /* UInt32 words[64]; */ + internal uint word_00; + uint word_01; + uint word_02; + uint word_03; + uint word_04; + uint word_05; + uint word_06; + uint word_07; + uint word_08; + uint word_09; + uint word_10; + uint word_11; + uint word_12; + uint word_13; + uint word_14; + uint word_15; + uint word_16; + uint word_17; + uint word_18; + uint word_19; + uint word_20; + uint word_21; + uint word_22; + uint word_23; + uint word_24; + uint word_25; + uint word_26; + uint word_27; + uint word_28; + uint word_29; + uint word_30; + uint word_31; + uint word_32; + uint word_33; + uint word_34; + uint word_35; + uint word_36; + uint word_37; + uint word_38; + uint word_39; + uint word_40; + uint word_41; + uint word_42; + uint word_43; + uint word_44; + uint word_45; + uint word_46; + uint word_47; + uint word_48; + uint word_49; + uint word_50; + uint word_51; + uint word_52; + uint word_53; + uint word_54; + uint word_55; + uint word_56; + uint word_57; + uint word_58; + uint word_59; + uint word_60; + uint word_61; + uint word_62; + uint word_63; + + /// The timestamp for this packet. + /// The timestamp for this packet. + public ulong Timestamp { + get => timeStamp; + set => timeStamp = value; + } + + /// The number of 32-bit Midi words in this packet. + /// The number of 32-bit Midi words in this packet. + public uint WordCount { + get => wordCount; + set { + if (value > 64) + throw new ArgumentOutOfRangeException ($"WordCount can't be higher than 64."); + wordCount = value; + } + } + + /// All the 32-bit Midi words in this packet. + /// All the 32-bit Midi words in this packet. + public uint [] Words { + get { + var wc = wordCount; + if (wc > 64) + throw new ArgumentOutOfRangeException ($"WordCount can't be higher than 64."); + var rv = new uint [wc]; + unsafe { + fixed (uint* destination = rv) { + fixed (uint* source = &word_00) { + Buffer.MemoryCopy (source, destination, rv.Length * sizeof (uint), wc * sizeof (uint)); + } + } + } + return rv; + } + set { + if (value is null) + ObjCRuntime.ThrowHelper.ThrowArgumentNullException (nameof (value)); + + if (value.Length > 64) + throw new ArgumentOutOfRangeException ($"WordCount can't be higher than 64."); + wordCount = (uint) value.Length; + unsafe { + fixed (uint* destination = &word_00) { + fixed (uint* source = value) { + Buffer.MemoryCopy (source, destination, 64 * sizeof (uint), value.Length * sizeof (uint)); + } + } + } + } + } + + /// An indexer for the 32-bit Midi words in this packet. + /// The index of the 32-bit Midi word to set or get. + /// The 32-bit Midi words for specified index. + public uint this [int index] { + get { + if (index < 0) + throw new ArgumentOutOfRangeException ($"index must be positive."); + if (index >= 64) + throw new ArgumentOutOfRangeException ($"index must be less than 64."); + if (index + 1 > wordCount) + throw new ArgumentOutOfRangeException ($"index must be less than WordCount."); + unsafe { + fixed (uint* firstWord = &word_00) + return firstWord [index]; + } + } + set { + if (index < 0) + throw new ArgumentOutOfRangeException ($"index must be positive."); + if (index >= 64) + throw new ArgumentOutOfRangeException ($"index must be less than 64."); + if (index + 1 > wordCount) + throw new ArgumentOutOfRangeException ($"index must be less than WordCount."); + unsafe { + fixed (uint* firstWord = &word_00) + firstWord [index] = value; + } + } + } + +#if !__TVOS__ + + [SupportedOSPlatform ("ios17.0")] + [SupportedOSPlatform ("maccatalyst17.0")] + [SupportedOSPlatform ("macos14.0")] + [UnsupportedOSPlatform ("tvos")] + [DllImport (Constants.CoreMidiLibrary)] + unsafe extern static OSStatus MIDIEventPacketSysexBytesForGroup (MidiEventPacket* pkt, byte /* UInt8 */ groupIndex, IntPtr* /* CFDataRef __nullable * __mononull */ outData); + + /// Get MIDI 1.0 sysex bytes on the specified group. + /// The index of the target group. + /// A status code that describes the result of the operation. This will be in case of success. + /// An that contains the requested byte stream. + [SupportedOSPlatform ("ios17.0")] + [SupportedOSPlatform ("maccatalyst17.0")] + [SupportedOSPlatform ("macos14.0")] + [UnsupportedOSPlatform ("tvos")] + public unsafe NSData? GetSysexBytes (byte groupIndex, out MidiError status) + { + var handle = default (IntPtr); + + fixed (MidiEventPacket* self = &this) { + status = (MidiError) MIDIEventPacketSysexBytesForGroup (self, groupIndex, &handle); + } + if (handle == IntPtr.Zero) + return null; + return Runtime.GetNSObject (handle, false); + } +#endif // !__TVOS__ + } +} diff --git a/src/CoreMidi/MidiServices.cs b/src/CoreMidi/MidiServices.cs index b72e87f53eec..d3bd2a2c10a9 100644 --- a/src/CoreMidi/MidiServices.cs +++ b/src/CoreMidi/MidiServices.cs @@ -1,4 +1,3 @@ -#if !TVOS // // MidiServices.cs: Implementation of the MidiObject base class and its derivates // @@ -39,8 +38,22 @@ #nullable enable +// Let's hope that by .NET 12 we've ironed out all the bugs in the API. +// This can of course be adjusted as needed (until we've released as stable). +#if NET120_0_OR_GREATER +#define STABLE_MIDIDRIVER +#endif + +using System; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +using ObjCRuntime; using CoreFoundation; using MidiObjectRef = System.Int32; @@ -53,47 +66,50 @@ namespace CoreMidi { +#if !TVOS // anonymous enum - MIDIServices.h /// Errors raised by the CoreMIDI stack. /// /// public enum MidiError : int { - /// To be added. + /// The operation completed successfully. Ok = 0, - /// To be added. + /// An invalid was passed. InvalidClient = -10830, - /// To be added. + /// An invalid was passed. InvalidPort = -10831, - /// To be added. + /// A source endpoint was passed to a function expecting a destination, or vice versa. WrongEndpointType = -10832, - /// To be added. + /// Attempt to close a non-existent connection. NoConnection = -10833, - /// To be added. + /// An invalid, unknown was passed. UnknownEndpoint = -10834, - /// To be added. + /// An attempt to query a property not set on the object. UnknownProperty = -10835, - /// To be added. + /// An attempt to set a property with a value of the wrong type (e.g., setting a string as an integer). WrongPropertyType = -10836, - /// To be added. + /// Internal error; there is no current MIDI setup. NoCurrentSetup = -10837, - /// To be added. + /// An error occurred while sending a MIDI message. MessageSendErr = -10838, - /// To be added. + /// Unable to start the MIDI server. ServerStartErr = -10839, - /// To be added. + /// An error occurred while reading the MIDI setup from persistent storage. SetupFormatErr = -10840, - /// To be added. + /// A driver is calling a non-I/O function from the server's MIDI I/O thread. WrongThread = -10841, - /// To be added. + /// The specified object does not exist. ObjectNotFound = -10842, - /// To be added. + /// The specified unique ID is not unique. IDNotUnique = -10843, - /// To be added. + /// The app does not have permission to use MIDI. Ensure your Info.plist includes the 'audio' key in UIBackgroundModes. NotPermitted = -10844, } +#endif // TVOS [Flags] // SInt32 - MIDIServices.h + [NativeName ("MIDIObjectType")] enum MidiObjectType : int { Other = -1, Device, @@ -107,6 +123,7 @@ enum MidiObjectType : int { ExternalDestination = ExternalMask | Destination, } +#if !TVOS public static partial class Midi { #if !COREBUILD [DllImport (Constants.CoreMidiLibrary)] @@ -201,6 +218,80 @@ public static nint DeviceCount { return null; return new MidiDevice (h); } + + [DllImport (Constants.CoreMidiLibrary)] + unsafe extern static OSStatus MIDIExternalDeviceCreate (IntPtr /* CFStringRef */ name, IntPtr /* CFStringRef */ manufacturer, IntPtr /* CFStringRef */ model, MidiDeviceRef* outDevice); + + /// Create a new external MIDI device. + /// The name for the new device. + /// The manufacturer for the new device. + /// The model for the new device. + /// A status code that describes the result of creating the new external device. This will be in case of success. + /// A newly created external instance, null otherwise. + public static MidiDevice? CreateExternalDevice (string name, string manufacturer, string model, out MidiError status) + { + using var namePtr = new TransientCFString (name); + using var manufacturerPtr = new TransientCFString (manufacturer); + using var modelPtr = new TransientCFString (model); + + var handle = default (MidiDeviceRef); + unsafe { + status = (MidiError) MIDIExternalDeviceCreate (namePtr, manufacturerPtr, modelPtr, &handle); + } + if (status != MidiError.Ok) + return null; + return new MidiDevice (handle); + } +#endif // !COREBUILD + } + + /// This class consists of functions that customize the global state of the MIDI system. + public static class MidiSetup { +#if !COREBUILD + [DllImport (Constants.CoreMidiLibrary)] + unsafe extern static OSStatus MIDISetupAddDevice (MidiDeviceRef device); + + /// Add a device to the current MIDI setup. + /// The device to add to the current MIDI setup. + /// A status code that describes the result of the operation. This will be in case of success. + public static MidiError AddDevice (MidiDevice device) + { + return (MidiError) MIDISetupAddDevice (device.GetCheckedHandle ()); + } + + + [DllImport (Constants.CoreMidiLibrary)] + unsafe extern static OSStatus MIDISetupRemoveDevice (MidiDeviceRef device); + + /// Remove a device from the current MIDI setup. + /// The device to remove from the current MIDI setup. + /// A status code that describes the result of the operation. This will be in case of success. + public static MidiError RemoveDevice (MidiDevice device) + { + return (MidiError) MIDISetupRemoveDevice (device.GetCheckedHandle ()); + } + + [DllImport (Constants.CoreMidiLibrary)] + unsafe extern static OSStatus MIDISetupAddExternalDevice (MidiDeviceRef device); + + /// Add an external device to the current MIDI setup. + /// The external device to add to the current MIDI setup. + /// A status code that describes the result of the operation. This will be in case of success. + public static MidiError AddExternalDevice (MidiDevice device) + { + return (MidiError) MIDISetupAddExternalDevice (device.GetCheckedHandle ()); + } + + [DllImport (Constants.CoreMidiLibrary)] + unsafe extern static OSStatus MIDISetupRemoveExternalDevice (MidiDeviceRef device); + + /// Remove a device from the current MIDI setup. + /// The device to remove from the current MIDI setup. + /// A status code that describes the result of the operation. This will be in case of success. + public static MidiError RemoveExternalDevice (MidiDevice device) + { + return (MidiError) MIDISetupRemoveExternalDevice (device.GetCheckedHandle ()); + } #endif // !COREBUILD } @@ -519,10 +610,7 @@ internal MidiException (MidiError code) : base (code == MidiError.NotPermitted ? } /// Contains the underlying MIDI error code. - /// - /// - /// - /// + /// The error code that caused this exception. public MidiError ErrorCode { get; private set; } } @@ -543,11 +631,18 @@ public class MidiClient : MidiObject { [SupportedOSPlatform ("ios")] [SupportedOSPlatform ("maccatalyst")] [SupportedOSPlatform ("macos")] - [ObsoletedOSPlatform ("macos11.0")] - [ObsoletedOSPlatform ("ios14.0")] - [ObsoletedOSPlatform ("maccatalyst14.0")] + [ObsoletedOSPlatform ("macos11.0", "Call 'MIDISourceCreateWithProtocol' instead.")] + [ObsoletedOSPlatform ("ios14.0", "Call 'MIDISourceCreateWithProtocol' instead.")] + [ObsoletedOSPlatform ("maccatalyst14.0", "Call 'MIDISourceCreateWithProtocol' instead.")] + [DllImport (Constants.CoreMidiLibrary)] + unsafe extern static int /* OSStatus = SInt32 */ MIDISourceCreate (MidiObjectRef handle, IntPtr name, MidiEndpointRef* endpoint); + + [SupportedOSPlatform ("ios14.0")] + [SupportedOSPlatform ("maccatalyst")] + [SupportedOSPlatform ("macos")] + [UnsupportedOSPlatform ("tvos")] [DllImport (Constants.CoreMidiLibrary)] - unsafe extern static int /* OSStatus = SInt32 */ MIDISourceCreate (MidiObjectRef handle, IntPtr name, MidiObjectRef* endpoint); + unsafe extern static OSStatus MIDISourceCreateWithProtocol (MidiClientRef client, IntPtr /* CFStringRef */ name, MidiProtocolId protocol, MidiEndpointRef* outSrc); GCHandle gch; @@ -601,45 +696,59 @@ public override string ToString () return Name; } - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. + /// Create a virtual source for this client. + /// The name for the virtual source. + /// A status code that describes the result of this operation. This will be in case of success. + /// A newly created if successful, otherwise null. [SupportedOSPlatform ("ios")] [SupportedOSPlatform ("maccatalyst")] [SupportedOSPlatform ("macos")] - [ObsoletedOSPlatform ("macos11.0")] - [ObsoletedOSPlatform ("ios14.0")] - [ObsoletedOSPlatform ("maccatalyst14.0")] + [ObsoletedOSPlatform ("macos11.0", "Call 'CreateVirtualSource (string, MidiProtocolId, out MidiError)' instead.")] + [ObsoletedOSPlatform ("ios14.0", "Call 'CreateVirtualSource (string, MidiProtocolId, out MidiError)' instead.")] + [ObsoletedOSPlatform ("maccatalyst14.0", "Call 'CreateVirtualSource (string, MidiProtocolId, out MidiError)' instead.")] public MidiEndpoint? CreateVirtualSource (string name, out MidiError statusCode) { - using (var nsstr = new NSString (name)) { - MidiObjectRef ret; - int code; - unsafe { - code = MIDISourceCreate (handle, nsstr.Handle, &ret); - } - if (code != 0) { - statusCode = (MidiError) code; - return null; - } - statusCode = MidiError.Ok; - return new MidiEndpoint (ret, true); + using var namePtr = new TransientCFString (name); + var endpointHandle = default (MidiEndpointRef); + unsafe { + statusCode = (MidiError) MIDISourceCreate (GetCheckedHandle (), namePtr, &endpointHandle); } + if (endpointHandle == MidiObject.InvalidRef) + return null; + return new MidiEndpoint (endpointHandle, true); } - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. + /// Create a virtual source for this client. + /// The name for the virtual source. + /// The MIDI protocol for the data this source will produce. + /// A status code that describes the result of this operation. This will be in case of success. + /// A newly created if successful, otherwise null. + [SupportedOSPlatform ("ios14.0")] + [SupportedOSPlatform ("maccatalyst")] + [SupportedOSPlatform ("macos")] + [UnsupportedOSPlatform ("tvos")] + public MidiEndpoint? CreateVirtualSource (string name, MidiProtocolId protocol, out MidiError status) + { + using var namePtr = new TransientCFString (name); + var handle = default (MidiEndpointRef); + unsafe { + status = (MidiError) MIDISourceCreateWithProtocol (GetCheckedHandle (), namePtr, protocol, &handle); + } + if (handle == MidiObject.InvalidRef) + return null; + return new MidiEndpoint (handle, true); + } + + /// Create a virtual destination for this client. + /// The name for the virtual destination. + /// A status code that describes the result of this operation. This will be in case of success. + /// A newly created if successful, otherwise null. [SupportedOSPlatform ("ios")] [SupportedOSPlatform ("maccatalyst")] [SupportedOSPlatform ("macos")] - [ObsoletedOSPlatform ("macos11.0")] - [ObsoletedOSPlatform ("ios14.0")] - [ObsoletedOSPlatform ("maccatalyst14.0")] + [ObsoletedOSPlatform ("macos11.0", "Call the other 'CreateVirtualDestination' overload instead.")] + [ObsoletedOSPlatform ("ios14.0", "Call the other 'CreateVirtualDestination' overload instead.")] + [ObsoletedOSPlatform ("maccatalyst14.0", "Call the other 'CreateVirtualDestination' overload instead.")] public MidiEndpoint? CreateVirtualDestination (string name, out MidiError status) { var m = new MidiEndpoint (this, name, out status); @@ -650,6 +759,32 @@ public override string ToString () return null; } + /// Create a virtual destination for this client. + /// The name for the virtual destination. + /// The MIDI protocol for the data this destination will receive. + /// The callback that will be called when the destination receives MIDI data. + /// A status code that describes the result of this operation. This will be in case of success. + /// A newly created if successful, otherwise null. + /// The callback receives two pointers: the first is a pointer to the MIDIEventList, and the second is a pointer to the source MIDIEndpointRef. Use to wrap the event list pointer. + public unsafe MidiEndpoint? CreateVirtualDestination (string name, MidiProtocolId protocol, delegate* unmanaged readBlock, out MidiError status) + { + using var namePtr = new TransientCFString (name); + var handle = default (MidiEndpointRef); + unsafe { + status = (MidiError) MIDIDestinationCreateWithProtocol (GetCheckedHandle (), namePtr, protocol, &handle, readBlock); + } + if (handle == MidiObject.InvalidRef) + return null; + return new MidiEndpoint (handle, name, true); + } + + [SupportedOSPlatform ("ios14.0")] + [SupportedOSPlatform ("maccatalyst")] + [SupportedOSPlatform ("macos")] + [UnsupportedOSPlatform ("tvos")] + [DllImport (Constants.CoreMidiLibrary)] + unsafe extern static OSStatus MIDIDestinationCreateWithProtocol (MidiClientRef client, IntPtr /* CFStringRef */ name, MidiProtocolId protocol, MidiEndpointRef* outSrc, delegate* unmanaged readBlock); + /// name for the input port. /// Creates a new MIDI input port. /// @@ -670,6 +805,41 @@ public MidiPort CreateOutputPort (string name) return new MidiPort (this, name, false); } + [SupportedOSPlatform ("ios14.0")] + [SupportedOSPlatform ("maccatalyst")] + [SupportedOSPlatform ("macos")] + [UnsupportedOSPlatform ("tvos")] + [DllImport (Constants.CoreMidiLibrary)] + unsafe extern static OSStatus MIDIInputPortCreateWithProtocol ( + MidiClientRef client, + IntPtr /* CFStringRef */ name, + MidiProtocolId protocol, + MidiPortRef* outPort, + delegate* unmanaged receiveBlock); + + /// Create a input port for this client. + /// The name for the port. + /// The MIDI protocol for the data this port will receive. + /// The callback that will be called when the port receives MIDI data. + /// A status code that describes the result of this operation. This will be in case of success. + /// A newly created if successful, otherwise null. + /// The callback receives two pointers: the first is a pointer to the MIDIEventList, and the second is a pointer to the source MIDIEndpointRef. Use to wrap the event list pointer. + [SupportedOSPlatform ("ios14.0")] + [SupportedOSPlatform ("maccatalyst")] + [SupportedOSPlatform ("macos")] + [UnsupportedOSPlatform ("tvos")] + public unsafe MidiPort? CreateInputPort (string name, MidiProtocolId protocol, delegate* unmanaged readBlock, out MidiError status) + { + using var namePtr = new TransientCFString (name); + var handle = default (MidiEndpointRef); + unsafe { + status = (MidiError) MIDIInputPortCreateWithProtocol (GetCheckedHandle (), namePtr, protocol, &handle, readBlock); + } + if (handle == MidiObject.InvalidRef) + return null; + return new MidiPort (handle, true, this, name); + } + public event EventHandler? SetupChanged; public event EventHandler? ObjectAdded; public event EventHandler? ObjectRemoved; @@ -837,20 +1007,18 @@ public MidiPacket (long timestamp, ushort length, IntPtr bytes) byteptr = bytes; } - /// Timestamp for the packet. - /// To be added. - /// To be added. - /// To be added. + /// Create a new with the specified timestamp and MIDI data. + /// The timestamp for the packet. + /// The MIDI data for the packet. public MidiPacket (long timestamp, byte [] bytes) : this (timestamp, bytes, 0, bytes.Length, false) { } - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. - /// To be added. + /// Create a new with the specified timestamp and a range of MIDI data. + /// The timestamp for the packet. + /// The byte array containing the MIDI data. + /// The starting index in for the MIDI data. + /// The number of bytes to include from . public MidiPacket (long timestamp, byte [] bytes, int start, int len) : this (timestamp, bytes, start, len, true) { } @@ -1052,6 +1220,13 @@ public class MidiPort : MidiObject { GCHandle gch; bool input; + internal MidiPort (MidiPortRef handle, bool owns, MidiClient client, string portName) + : base (handle, owns) + { + Client = client; + PortName = portName; + } + internal MidiPort (MidiClient client, string portName, bool input) { using (var nsstr = new NSString (portName)) { @@ -1787,6 +1962,19 @@ public bool UmpCanTransmitGroupless { SetInt (MidiPropertyExtensions.kMIDIPropertyUMPCanTransmitGroupless, value ? 1 : 0); } } + + [DllImport (Constants.CoreMidiLibrary)] + extern static OSStatus MIDIEntityAddOrRemoveEndpoints (MidiEntityRef entity, nuint numSourceEndpoints, nuint numDestinationEndpoints); + + /// Sets the number of endpoints for this entity. + /// The number of source endpoints this entity will have. + /// The number of destination endpoints this entity will have. + /// if successful, an error code otherwise. + public MidiError AddOrRemoveEndpoints (nuint numberOfSourceEndpoints, nuint numberOfDestinationEndpoints) + { + return (MidiError) MIDIEntityAddOrRemoveEndpoints (GetCheckedHandle (), numberOfSourceEndpoints, numberOfDestinationEndpoints); + } + #endif // !COREBUILD } // MidiEntity @@ -1813,12 +2001,38 @@ public class MidiDevice : MidiObject { [DllImport (Constants.CoreMidiLibrary)] extern static MidiEntityRef MIDIDeviceGetEntity (MidiDeviceRef handle, nint item); + [SupportedOSPlatform ("ios14.0")] + [SupportedOSPlatform ("maccatalyst14.0")] + [SupportedOSPlatform ("macos")] + [DllImport (Constants.CoreMidiLibrary)] + unsafe extern static OSStatus MIDIDeviceNewEntity (MidiDeviceRef device, /* CFString */ IntPtr name, /* MIDIProtocolId */ MidiProtocolId protocol, byte embedded, /* ItemCount */ nuint numSourceEndpoints, /* ItemCount */ nuint numDestinationEndpoints, MidiEntityRef* newEntity); + + /// Create and add a new entity to an external device. + /// The name for the new entity. + /// The protocol in use for the new entity. + /// Whether the new entity is inside the device (true), or if it consists of external connectors (false). + /// The number of source endpoints in the new entity. + /// The number of destination endpoints in the new entity. + /// A status code that describes the result of creating the new entity. This will be in case of success. + /// A newly created entity in case of success, null otherwise. In case of failure, will contain an error code. + public MidiEntity? CreateEntity (string name, MidiProtocolId protocol, bool embedded, nuint numberOfSourceEndpoints, nuint numberOfDestinationEndpoints, out MidiError status) + { + using var namePtr = new TransientCFString (name); + var handle = default (MidiEntityRef); + unsafe { + status = (MidiError) MIDIDeviceNewEntity (GetCheckedHandle (), namePtr, protocol, embedded.AsByte (), numberOfSourceEndpoints, numberOfDestinationEndpoints, &handle); + } + if (handle == MidiObject.InvalidRef) + return null; + return new MidiEntity (handle); + } + [SupportedOSPlatform ("ios")] [SupportedOSPlatform ("maccatalyst")] [SupportedOSPlatform ("macos")] - [ObsoletedOSPlatform ("macos11.0")] - [ObsoletedOSPlatform ("ios14.0")] - [ObsoletedOSPlatform ("maccatalyst14.0")] + [ObsoletedOSPlatform ("macos11.0", "Call 'CreateEntity' instead.")] + [ObsoletedOSPlatform ("ios14.0", "Call 'CreateEntity' instead.")] + [ObsoletedOSPlatform ("maccatalyst14.0", "Call 'CreateEntity' instead.")] [DllImport (Constants.CoreMidiLibrary)] extern static int MIDIDeviceAddEntity (MidiDeviceRef device, /* CFString */ IntPtr name, byte embedded, nuint numSourceEndpoints, nuint numDestinationEndpoints, MidiEntityRef newEntity); @@ -1833,9 +2047,9 @@ public class MidiDevice : MidiObject { [SupportedOSPlatform ("ios")] [SupportedOSPlatform ("maccatalyst")] [SupportedOSPlatform ("macos")] - [ObsoletedOSPlatform ("ios14.0")] - [ObsoletedOSPlatform ("maccatalyst14.0")] - [ObsoletedOSPlatform ("macos11.0")] + [ObsoletedOSPlatform ("macos11.0", "Call 'CreateEntity' instead.")] + [ObsoletedOSPlatform ("ios14.0", "Call 'CreateEntity' instead.")] + [ObsoletedOSPlatform ("maccatalyst14.0", "Call 'CreateEntity' instead.")] public int Add (string name, bool embedded, nuint numSourceEndpoints, nuint numDestinationEndpoints, MidiEntity newEntity) { using (NSString nsName = new NSString (name)) { @@ -1843,6 +2057,69 @@ public int Add (string name, bool embedded, nuint numSourceEndpoints, nuint numD } } + [SupportedOSPlatform ("ios")] + [SupportedOSPlatform ("maccatalyst")] + [SupportedOSPlatform ("macos")] + [DllImport (Constants.CoreMidiLibrary)] + extern static OSStatus MIDIDeviceRemoveEntity (MidiDeviceRef device, MidiEntityRef entity); + + /// Remove the specified entity from this device. + /// The entity to remove. + /// if successful, an error code otherwise. + public MidiError Remove (MidiEntity entity) + { + return (MidiError) MIDIDeviceRemoveEntity (GetCheckedHandle (), entity.GetCheckedHandle ()); + } + +#if !STABLE_MIDIDRIVER + [Experimental ("APL0004")] +#endif + [DllImport (Constants.CoreMidiLibrary)] + unsafe extern static OSStatus MIDIDeviceCreate (MidiDriverInterface** /* MidiDriverRef _nullable */ owner, IntPtr /* CFStringRef */ name, IntPtr /* CFStringRef */ manufacturer, IntPtr /* CFStringRef */ model, MidiDeviceRef* outDevice); + + /// Create a new device corresponding to a specific piece of hardware. + /// The driver that owns the new device. Pass null if the owner isn't a driver. + /// The name for the new device. + /// The manufacturer for the new device. + /// The model for the new device. + /// A status code that describes the result of creating the new device. This will be in case of success. + /// A newly created instance, null otherwise. +#if !STABLE_MIDIDRIVER + [Experimental ("APL0004")] +#endif + public static MidiDevice? Create (MidiDriver? owner, string name, string manufacturer, string model, out MidiError status) + { + var handle = default (MidiDeviceRef); + using var namePtr = new TransientCFString (name); + using var manufacturerPtr = new TransientCFString (manufacturer); + using var modelPtr = new TransientCFString (model); + + unsafe { + MidiDriverInterface* driverInterfacePtr = owner is null ? null : owner.DriverInterface; + MidiDriverInterface** driverPtr = null; + if (owner is not null) + driverPtr = &driverInterfacePtr; + status = (MidiError) MIDIDeviceCreate (driverPtr, namePtr, manufacturerPtr, modelPtr, &handle); + } + + GC.KeepAlive (owner); + + if (handle == MidiObject.InvalidRef) + return null; + return new MidiDevice (handle); + } + + [DllImport (Constants.CoreMidiLibrary)] + unsafe extern static OSStatus MIDIDeviceDispose (MidiDeviceRef device); + + /// Dispose of devices that haven't yet been added to the system with . + /// if successful, an error code otherwise. + /// Only drivers can call this method, and only before calling . Once has been called, use instead to destroy the device. + public MidiError DisposeDevice () + { + return (MidiError) MIDIDeviceDispose (GetCheckedHandle ()); + } + /// Returns the number of MIDI entities in this device. /// /// @@ -2644,17 +2921,21 @@ internal MidiEndpoint (MidiEndpointRef handle, string endpointName, bool owns) : return new MidiEndpoint (h, "Destination" + destinationIndex, false); } - internal MidiEndpoint (MidiClient client, string name, out MidiError code) + internal MidiEndpoint (MidiClient client, string name, out MidiError status) { - using (var nsstr = new NSString (name)) { - GCHandle gch = GCHandle.Alloc (this); - unsafe { - MidiEndpointRef tempHandle; - code = MIDIDestinationCreate (client.handle, nsstr.Handle, &Read, GCHandle.ToIntPtr (gch), &tempHandle); - handle = tempHandle; - } - EndpointName = name; + EndpointName = name; + + using var namePtr = new TransientCFString (name); + var handle = default (MidiEndpointRef); + gch = GCHandle.Alloc (this); + unsafe { + status = (MidiError) MIDIDestinationCreate (client.GetCheckedHandle (), namePtr, &Read, GCHandle.ToIntPtr (gch), &handle); } + if (handle == MidiObject.InvalidRef) { + gch.Free (); + return; + } + this.handle = handle; } /// @@ -2991,11 +3272,259 @@ public int AssociatedEndpoint { SetInt (MidiPropertyExtensions.kMIDIPropertyAssociatedEndpoint, value); } } + + [DllImport (Constants.CoreMidiLibrary)] + unsafe extern static OSStatus MIDIEndpointGetRefCons (MidiEndpointRef endpoint, IntPtr* ref1, IntPtr* ref2); + + [DllImport (Constants.CoreMidiLibrary)] + extern static OSStatus MIDIEndpointSetRefCons (MidiEndpointRef endpoint, IntPtr ref1, IntPtr ref2); + + /// Get the refcons for this endpoint. + /// Returns the first refcon if successful. + /// Returns the second refcon if successful. + /// if successful, an error code otherwise. + public MidiError GetRefCons (out IntPtr ref1, out IntPtr ref2) + { + ref1 = IntPtr.Zero; + ref2 = IntPtr.Zero; + unsafe { + return (MidiError) MIDIEndpointGetRefCons (GetCheckedHandle (), (IntPtr*) Unsafe.AsRef (ref ref1), (IntPtr*) Unsafe.AsRef (ref ref2)); + } + } + + /// Set the refcons for this endpoint. + /// The first refcon. + /// The second refcon. + /// if successful, an error code otherwise. + public MidiError SetRefCons (IntPtr ref1, IntPtr ref2) + { + unsafe { + return (MidiError) MIDIEndpointSetRefCons (GetCheckedHandle (), ref1, ref2); + } + } + + [DllImport (Constants.CoreMidiLibrary)] + unsafe extern static OSStatus MIDISendSysex (MidiSysexSendRequest* request); + + /// Asynchronously sends a single system-exclusive event. + /// The data to send. + /// An optional cancellation token that can be used to cancel the request. + /// A value for the request. This will be if the request was successful, an error code otherwise. + public unsafe Task SendSysexAsync (byte [] data, CancellationToken? cancellationToken = null) + { + if (data is null) + ThrowHelper.ThrowArgumentNullException (nameof (data)); + + var tcs = new TaskCompletionSource (); + var request = new SysexRequest (this, data, tcs); + var rv = (MidiError) MIDISendSysex (request.GetSysexRequestStruct (cancellationToken)); + if (rv != MidiError.Ok) { + request.Dispose (); + tcs.TrySetResult (rv); + } + + return tcs.Task; + } + + [DllImport (Constants.CoreMidiLibrary)] + [SupportedOSPlatform ("ios17.0")] + [SupportedOSPlatform ("maccatalyst17.0")] + [SupportedOSPlatform ("macos14.0")] + [UnsupportedOSPlatform ("tvos")] + unsafe extern static OSStatus MIDISendUMPSysex (MidiSysexSendRequestUmp* request); + + /// Asynchronously sends a single UMP system-exclusive event. + /// The data to send. + /// An optional cancellation token that can be used to cancel the request. + /// A value for the request. This will be if the request was successful, an error code otherwise. + [SupportedOSPlatform ("ios17.0")] + [SupportedOSPlatform ("maccatalyst17.0")] + [SupportedOSPlatform ("macos14.0")] + [UnsupportedOSPlatform ("tvos")] + public unsafe Task SendSysexUmpAsync (uint [] data, CancellationToken? cancellationToken = null) + { + if (data is null) + ThrowHelper.ThrowArgumentNullException (nameof (data)); + + var tcs = new TaskCompletionSource (); + var request = new SysexRequest (this, data, tcs); + var rv = (MidiError) MIDISendUMPSysex (request.GetSysexUmpRequestStruct (cancellationToken)); + if (rv != MidiError.Ok) { + request.Dispose (); + tcs.TrySetResult (rv); + } + + return tcs.Task; + } + + [SupportedOSPlatform ("ios17.0")] + [SupportedOSPlatform ("maccatalyst17.0")] + [SupportedOSPlatform ("macos14.0")] + [UnsupportedOSPlatform ("tvos")] + [DllImport (Constants.CoreMidiLibrary)] + unsafe extern static OSStatus MIDISendUMPSysex8 (MidiSysexSendRequestUmp* request); + + /// Asynchronously sends a single 8-bit system-exclusive event. + /// The data to send. + /// An optional cancellation token that can be used to cancel the request. + /// A value for the request. This will be if the request was successful, an error code otherwise. + [SupportedOSPlatform ("ios17.0")] + [SupportedOSPlatform ("maccatalyst17.0")] + [SupportedOSPlatform ("macos14.0")] + [UnsupportedOSPlatform ("tvos")] + public unsafe Task SendSysexUmp8Async (uint [] data, CancellationToken? cancellationToken = null) + { + if (data is null) + ThrowHelper.ThrowArgumentNullException (nameof (data)); + + var tcs = new TaskCompletionSource (); + var request = new SysexRequest (this, data, tcs); + var rv = (MidiError) MIDISendUMPSysex8 (request.GetSysexUmpRequestStruct (cancellationToken)); + if (rv != MidiError.Ok) { + request.Dispose (); + tcs.TrySetResult (rv); + } + + return tcs.Task; + } + + class SysexRequest : IDisposable { + IntPtr structPointer; + MidiEndpoint endpoint; + byte []? byteData; + uint []? uintData; + GCHandle dataHandle; + GCHandle thisHandle; + TaskCompletionSource onCompletion; + CancellationTokenRegistration? cancellationTokenRegistration; + + public SysexRequest (MidiEndpoint endpoint, byte [] data, TaskCompletionSource onCompletion) + { + this.endpoint = endpoint; + this.byteData = data; + this.onCompletion = onCompletion; + + unsafe { + structPointer = Marshal.AllocHGlobal (sizeof (MidiSysexSendRequest)); + } + dataHandle = GCHandle.Alloc (byteData, GCHandleType.Pinned); + thisHandle = GCHandle.Alloc (this); + } + + public SysexRequest (MidiEndpoint endpoint, uint [] data, TaskCompletionSource onCompletion) + { + this.endpoint = endpoint; + this.uintData = data; + this.onCompletion = onCompletion; + + unsafe { + structPointer = Marshal.AllocHGlobal (sizeof (MidiSysexSendRequestUmp)); + } + dataHandle = GCHandle.Alloc (uintData, GCHandleType.Pinned); + thisHandle = GCHandle.Alloc (this); + } + + public unsafe MidiSysexSendRequest* GetSysexRequestStruct (CancellationToken? cancellationToken) + { + if (byteData is null) + throw new InvalidOperationException ($"No byte[] data specified."); + + var rv = (MidiSysexSendRequest*) structPointer; + + rv->Destination = endpoint.GetCheckedHandle (); + rv->Data = dataHandle.AddrOfPinnedObject (); + rv->BytesToSend = (uint) byteData.Length; + rv->CompletionProcedure = &SysexCompletion; + rv->Context = GCHandle.ToIntPtr (thisHandle); + + cancellationTokenRegistration = cancellationToken?.Register (SysexCancellationRequest); + + return rv; + } + + public unsafe MidiSysexSendRequestUmp* GetSysexUmpRequestStruct (CancellationToken? cancellationToken) + { + if (uintData is null) + throw new InvalidOperationException ($"No uint[] data specified."); + + var rv = (MidiSysexSendRequestUmp*) structPointer; + + rv->Destination = endpoint.GetCheckedHandle (); + rv->Words = dataHandle.AddrOfPinnedObject (); + rv->WordsToSend = (uint) uintData.Length; + rv->CompletionProcedure = &UmpSysexCompletion; + rv->Context = GCHandle.ToIntPtr (thisHandle); + + cancellationTokenRegistration = cancellationToken?.Register (UmpSysexCancellationRequest); + + return rv; + } + + void OnCompleted () + { + onCompletion.TrySetResult (MidiError.Ok); + Dispose (); + } + + [UnmanagedCallersOnly] + unsafe static void SysexCompletion (MidiSysexSendRequest* request) + { + var obj = (SysexRequest?) GCHandle.FromIntPtr (request->Context).Target; + obj?.OnCompleted (); + } + + [UnmanagedCallersOnly] + unsafe static void UmpSysexCompletion (MidiSysexSendRequestUmp* request) + { + var obj = (SysexRequest?) GCHandle.FromIntPtr (request->Context).Target; + obj?.OnCompleted (); + } + + unsafe void SysexCancellationRequest () + { + var rv = (MidiSysexSendRequest*) structPointer; + if (rv is null) + return; + rv->Complete = true; + } + + unsafe void UmpSysexCancellationRequest () + { + var rv = (MidiSysexSendRequestUmp*) structPointer; + if (rv is null) + return; + rv->Complete = true; + } + + public void Dispose () + { + cancellationTokenRegistration?.Dispose (); + cancellationTokenRegistration = null; + if (structPointer != IntPtr.Zero) { + Marshal.FreeHGlobal (structPointer); + structPointer = IntPtr.Zero; + } + if (dataHandle.IsAllocated) + dataHandle.Free (); + if (thisHandle.IsAllocated) + thisHandle.Free (); + GC.SuppressFinalize (this); + } + + ~SysexRequest () + { + Dispose (); + } + } + // MidiEndpoint #endif // !COREBUILD } +#endif // TVOS + // SInt32 - MIDIServices.h + [NativeName ("MIDINotificationMessageID")] enum MidiNotificationMessageId : int { SetupChanged = 1, ObjectAdded, @@ -3008,10 +3537,12 @@ enum MidiNotificationMessageId : int { [SupportedOSPlatform ("ios")] [SupportedOSPlatform ("maccatalyst")] [UnsupportedOSPlatform ("macos")] + [UnsupportedOSPlatform ("tvos")] InternalStart = 0x1000, #endif } +#if !TVOS // // The notification EventArgs // @@ -3185,5 +3716,5 @@ protected virtual void Dispose (bool disposing) } #endif // !COREBUILD } +#endif // TVOS } -#endif diff --git a/src/CoreMidi/MidiStructs.cs b/src/CoreMidi/MidiStructs.cs index 96f24b7987a0..f4a064e0a50c 100644 --- a/src/CoreMidi/MidiStructs.cs +++ b/src/CoreMidi/MidiStructs.cs @@ -12,6 +12,7 @@ using MidiEntityRef = System.Int32; namespace CoreMidi { + /// Represents a MIDI 2.0 device manufacturer identifier, consisting of a 3-byte SysEx ID. [SupportedOSPlatform ("ios18.0")] [SupportedOSPlatform ("maccatalyst18.0")] [SupportedOSPlatform ("macos15.0")] @@ -23,6 +24,10 @@ public struct Midi2DeviceManufacturer { byte sysExIdByte1; byte sysExIdByte2; + /// Gets or sets the 3-byte SysEx manufacturer ID. Single-byte SysEx IDs should be padded with trailing zeroes. + /// A 3-element byte array with the SysEx manufacturer ID bytes. + /// Thrown when the value is null. + /// Thrown when the array length is not exactly 3. public byte [] SysExIdByte { get { return new byte [] { sysExIdByte0, sysExIdByte1, sysExIdByte2 }; @@ -40,6 +45,7 @@ public byte [] SysExIdByte { } } + /// Represents a MIDI 2.0 device revision level, consisting of a 4-byte revision level identifier. [SupportedOSPlatform ("ios18.0")] [SupportedOSPlatform ("maccatalyst18.0")] [SupportedOSPlatform ("macos15.0")] @@ -52,6 +58,10 @@ public struct Midi2DeviceRevisionLevel { byte revisionLevel2; byte revisionLevel3; + /// Gets or sets the 4-byte device revision level. + /// A 4-element byte array with the device revision level. + /// Thrown when the value is null. + /// Thrown when the array length is not exactly 4. public byte [] RevisionLevel { get { return new byte [] { revisionLevel0, revisionLevel1, revisionLevel2, revisionLevel3 }; @@ -70,32 +80,46 @@ public byte [] RevisionLevel { } } + /// Represents a standard MIDI-CI profile identifier with profile bank, number, version, and level fields. [SupportedOSPlatform ("ios18.0")] [SupportedOSPlatform ("maccatalyst18.0")] [SupportedOSPlatform ("macos15.0")] [SupportedOSPlatform ("tvos18.0")] [NativeName ("MIDICIProfileIDStandard")] public struct MidiCIProfileIdStandard { + /// The first byte of the standard profile identifier. public byte /* MIDIUInteger7 */ ProfileIdByte1; + /// The profile bank number. public byte /* MIDIUInteger7 */ ProfileBank; + /// The profile number within the bank. public byte /* MIDIUInteger7 */ ProfileNumber; + /// The version of the profile. public byte /* MIDIUInteger7 */ ProfileVersion; + /// The level of the profile. public byte /* MIDIUInteger7 */ ProfileLevel; } + /// Represents a manufacturer-specific MIDI-CI profile identifier with SysEx ID and info fields. [SupportedOSPlatform ("ios18.0")] [SupportedOSPlatform ("maccatalyst18.0")] [SupportedOSPlatform ("macos15.0")] [SupportedOSPlatform ("tvos18.0")] [NativeName ("MIDICIProfileIDManufacturerSpecific")] public struct MidiCIProfileIdManufacturerSpecific { + /// The first byte of the manufacturer SysEx ID. public byte /* MIDIUInteger7 */ SysExId1; + /// The second byte of the manufacturer SysEx ID. public byte /* MIDIUInteger7 */ SysExId2; + /// The third byte of the manufacturer SysEx ID. public byte /* MIDIUInteger7 */ SysExId3; + /// The first manufacturer-specific info byte. public byte /* MIDIUInteger7 */ Info1; + /// The second manufacturer-specific info byte. public byte /* MIDIUInteger7 */ Info2; } + /// Represents a MIDI-CI profile identifier, which can be either a standard or manufacturer-specific profile. + /// This is a union type. Access the property for standard profiles or for manufacturer-specific profiles. [SupportedOSPlatform ("ios18.0")] [SupportedOSPlatform ("maccatalyst18.0")] [SupportedOSPlatform ("macos15.0")] @@ -110,6 +134,8 @@ public struct MidiCIProfileId { byte /* MIDIUInteger7 */ Value3; byte /* MIDIUInteger7 */ Value4; + /// Gets or sets this profile ID interpreted as a standard MIDI-CI profile identifier. + /// A that represents this profile ID. public unsafe MidiCIProfileIdStandard Standard { get { fixed (MidiCIProfileId* self = &this) { @@ -123,6 +149,8 @@ public unsafe MidiCIProfileIdStandard Standard { } } + /// Gets or sets this profile ID interpreted as a manufacturer-specific MIDI-CI profile identifier. + /// A that represents this profile ID. public unsafe MidiCIProfileIdManufacturerSpecific ManufacturerSpecific { get { fixed (MidiCIProfileId* self = &this) { @@ -136,5 +164,139 @@ public unsafe MidiCIProfileIdManufacturerSpecific ManufacturerSpecific { } } } + + /// A struct that represents a request to transmit a single system-exclusive event. + [NativeName ("MIDISysexSendRequest")] + struct MidiSysexSendRequest { + MidiEndpointRef destination; + IntPtr /* const Byte * */ data; + uint bytesToSend; + byte /* Boolean */ complete; +#pragma warning disable CS0169 // The field '...' is never used + byte reserved1; + byte reserved2; + byte reserved3; +#pragma warning restore CS0169 + unsafe delegate* unmanaged /* MIDICompletionProc */ completionProc; + IntPtr /* void * __nullable */ completionRefCon; + + /// The endpoint where the request is sent. + public MidiEndpointRef Destination { + get => destination; + set => destination = value; + } + + /// A pointer to the data to send. + /// The MIDI system will update this value as the request progresses. + public IntPtr Data { + get => data; + set => data = value; + } + + /// The number of bytes to send. + /// The MIDI system will update this value as the request progresses. + public uint BytesToSend { + get => bytesToSend; + set => bytesToSend = value; + } + + /// The client can set true to immediately stop the request. The MIDI system will set it to true when the request is complete. + public bool Complete { + get => complete != 0; + set => complete = value.AsByte (); + } + + /// The callback that is called when all the data has been sent and the request is complete. + /// Also called if the client sets to true before the request is complete. + public unsafe delegate* unmanaged CompletionProcedure { + get => completionProc; + set => completionProc = value; + } + + /// A context value that's passed to the callback. + public IntPtr Context { + get => completionRefCon; + set => completionRefCon = value; + } + } + + + /*! + @struct MIDISysexSendRequestUMP + @abstract A request to transmit a UMP system-exclusive event. + + @discussion + This represents a request to send a single UMP system-exclusive MIDI event to + a MIDI destination asynchronously. + + @field destination + The endpoint to which the event is to be sent. + @field words + Initially, a pointer to the UMP SysEx event to be sent. + MIDISendUMPSysex will advance this pointer as data is + sent. + @field wordsToSend + Initially, the number of words to be sent. MIDISendUMPSysex + will decrement this counter as data is sent. + @field complete + The client may set this to true at any time to abort + transmission. The implementation sets this to true when + all data been transmitted. + @field completionProc + Called when all bytes have been sent, or after the client + has set complete to true. + @field completionRefCon + Passed as a refCon to completionProc. + */ + + /// A struct that represents a request to transmit a single UMP system-exclusive event. + [NativeName ("MIDISysexSendRequestUMP")] + struct MidiSysexSendRequestUmp { + MidiEndpointRef destination; + IntPtr /* UInt32* */ words; + uint /* UInt32 */ wordsToSend; + byte /* Boolean */ complete; + unsafe delegate* unmanaged /* MIDICompletionProcUMP */ completionProc; + IntPtr /* void* __nullable */ completionRefCon; + + /// The endpoint where the request is sent. + public MidiEndpointRef Destination { + get => destination; + set => destination = value; + } + + /// A pointer to the 32-bit word(s) to send. + /// The MIDI system will update this value as the request progresses. + public IntPtr Words { + get => words; + set => words = value; + } + + /// The number of 32-bit words to send. + /// The MIDI system will update this value as the request progresses. + public uint WordsToSend { + get => wordsToSend; + set => wordsToSend = value; + } + + /// The client can set true to immediately stop the request. The MIDI system will set it to true when the request is complete. + public bool Complete { + get => complete != 0; + set => complete = value.AsByte (); + } + + /// The callback that is called when all the data has been sent and the request is complete. + /// Also called if the client sets to true before the request is complete. + public unsafe delegate* unmanaged CompletionProcedure { + get => completionProc; + set => completionProc = value; + } + + /// A context value that's passed to the callback. + public IntPtr Context { + get => completionRefCon; + set => completionRefCon = value; + } + }; } #endif diff --git a/src/CoreMidi/MidiThruConnectionParams.cs b/src/CoreMidi/MidiThruConnectionParams.cs index d563a4e94284..8cda044e4b49 100644 --- a/src/CoreMidi/MidiThruConnectionParams.cs +++ b/src/CoreMidi/MidiThruConnectionParams.cs @@ -1,5 +1,4 @@ -#if !TVOS -// + // MidiThruConnectionParams.cs: A C# wrapper around MidiThruConnectionParamsStruct // // Authors: Alex Soto (alex.soto@xamarin.com) @@ -19,6 +18,7 @@ namespace CoreMidi { /// MIDI transform types. /// To be added. + [NativeName ("MIDITransformType")] public enum MidiTransformType : ushort { /// To be added. None = 0, @@ -40,6 +40,7 @@ public enum MidiTransformType : ushort { /// MIDI Control Transformation Type. /// To be added. + [NativeName ("MIDITransformControlType")] public enum MidiTransformControlType : byte { /// To be added. SevenBit = 0, @@ -55,6 +56,7 @@ public enum MidiTransformControlType : byte { FourteenBitNRpn = 5, } +#if !TVOS /// Object that defines how a MIDI event is transformed. /// To be added. [SupportedOSPlatform ("ios")] @@ -648,5 +650,5 @@ internal NSData WriteStruct () } } #endif // !COREBUILD +#endif // TVOS } -#endif diff --git a/src/coremidi.cs b/src/coremidi.cs index e76c8058c0d5..1aa22becb62b 100644 --- a/src/coremidi.cs +++ b/src/coremidi.cs @@ -884,13 +884,6 @@ interface MidiCIResponder { void Stop (); } - [Internal] - [NoTV, NoiOS, NoMacCatalyst] - enum MidiDriverProperty { - [Field ("kMIDIDriverPropertyUsesSerial")] - UsesSerial, - } - [Internal] enum MidiProperty { [NoTV] @@ -1098,6 +1091,13 @@ enum MidiProperty { AssociatedEndpoint, } + [Internal] + [NoiOS, NoMacCatalyst, NoTV] + enum MidiDriverProperty { + [Field ("kMIDIDriverPropertyUsesSerial")] + UsesSerial, + } + [NoTV, Mac (15, 0), iOS (18, 0), MacCatalyst (18, 0)] [BaseType (typeof (NSObject), Name = "MIDICIDevice")] [DisableDefaultCtor] diff --git a/src/frameworks.sources b/src/frameworks.sources index 0b7237d278a2..bad26718ae9e 100644 --- a/src/frameworks.sources +++ b/src/frameworks.sources @@ -460,6 +460,7 @@ COREFOUNDATION_CORE_SOURCES = \ CoreFoundation/CFMutableString.cs \ CoreFoundation/CFRunLoop.cs \ CoreFoundation/CFString.cs \ + CoreFoundation/CFUuidBytes.cs \ CoreFoundation/Dispatch.cs \ CoreFoundation/DispatchData.cs \ CoreFoundation/NativeObject.cs \ @@ -624,6 +625,7 @@ COREMEDIA_SOURCES = \ COREMIDI_CORE_SOURCES = \ CoreMidi/MidiCIDeviceIdentification.cs \ + CoreMidi/MidiDriverInterface.cs \ CoreMidi/MidiServices.cs \ CoreMidi/MidiStructs.cs \ CoreMidi/MidiThruConnection.cs \ @@ -631,6 +633,8 @@ COREMIDI_CORE_SOURCES = \ COREMIDI_SOURCES = \ CoreMidi/MidiBluetoothDriver.cs \ + CoreMidi/MidiEventList.cs \ + CoreMidi/MidiEventPacket.cs \ # CoreML diff --git a/tests/cecil-tests/Documentation.KnownFailures.txt b/tests/cecil-tests/Documentation.KnownFailures.txt index be4383668c70..30a8b11ce7d2 100644 --- a/tests/cecil-tests/Documentation.KnownFailures.txt +++ b/tests/cecil-tests/Documentation.KnownFailures.txt @@ -2607,16 +2607,6 @@ F:CoreMidi.MidiCIProcessInquiryMessageType.InquiryMidiMessageReport F:CoreMidi.MidiCIProcessInquiryMessageType.InquiryProcessInquiryCapabilities F:CoreMidi.MidiCIProcessInquiryMessageType.ReplyToMidiMessageReport F:CoreMidi.MidiCIProcessInquiryMessageType.ReplyToProcessInquiryCapabilities -F:CoreMidi.MidiCIProfileIdManufacturerSpecific.Info1 -F:CoreMidi.MidiCIProfileIdManufacturerSpecific.Info2 -F:CoreMidi.MidiCIProfileIdManufacturerSpecific.SysExId1 -F:CoreMidi.MidiCIProfileIdManufacturerSpecific.SysExId2 -F:CoreMidi.MidiCIProfileIdManufacturerSpecific.SysExId3 -F:CoreMidi.MidiCIProfileIdStandard.ProfileBank -F:CoreMidi.MidiCIProfileIdStandard.ProfileIdByte1 -F:CoreMidi.MidiCIProfileIdStandard.ProfileLevel -F:CoreMidi.MidiCIProfileIdStandard.ProfileNumber -F:CoreMidi.MidiCIProfileIdStandard.ProfileVersion F:CoreMidi.MidiCIProfileMessageType.DetailsInquiry F:CoreMidi.MidiCIProfileMessageType.ProfileAdded F:CoreMidi.MidiCIProfileMessageType.ProfileDisabledReport @@ -10487,7 +10477,7 @@ M:CoreFoundation.CFNetwork.ExecuteProxyAutoConfigurationScriptAsync(System.Strin M:CoreFoundation.CFNetwork.ExecuteProxyAutoConfigurationUrl(System.Uri,System.Uri,Foundation.NSError@) M:CoreFoundation.CFNetwork.ExecuteProxyAutoConfigurationUrlAsync(System.Uri,System.Uri,System.Threading.CancellationToken) M:CoreFoundation.CFRange.#ctor(System.IntPtr,System.IntPtr) -M:CoreFoundation.CFReadStream.DoSetClient(.method,System.IntPtr,System.IntPtr) +M:CoreFoundation.CFReadStream.DoSetClient(,System.IntPtr,System.IntPtr) M:CoreFoundation.CFRunLoop.RunInMode(System.String,System.Double,System.Boolean) M:CoreFoundation.CFSocket.add_AcceptEvent(System.EventHandler{CoreFoundation.CFSocket.CFSocketAcceptEventArgs}) M:CoreFoundation.CFSocket.add_ConnectEvent(System.EventHandler{CoreFoundation.CFSocket.CFSocketConnectEventArgs}) @@ -10510,7 +10500,7 @@ M:CoreFoundation.CFStream.CreateBoundPair(CoreFoundation.CFReadStream@,CoreFound M:CoreFoundation.CFStream.CreateForHTTPRequest(CFNetwork.CFHTTPMessage) M:CoreFoundation.CFStream.CreateForStreamedHTTPRequest(CFNetwork.CFHTTPMessage,CoreFoundation.CFReadStream) M:CoreFoundation.CFStream.CreateForStreamedHTTPRequest(CFNetwork.CFHTTPMessage,Foundation.NSInputStream) -M:CoreFoundation.CFStream.DoSetClient(.method,System.IntPtr,System.IntPtr) +M:CoreFoundation.CFStream.DoSetClient(,System.IntPtr,System.IntPtr) M:CoreFoundation.CFStream.remove_CanAcceptBytesEvent(System.EventHandler{CoreFoundation.CFStream.StreamEventArgs}) M:CoreFoundation.CFStream.remove_ClosedEvent(System.EventHandler{CoreFoundation.CFStream.StreamEventArgs}) M:CoreFoundation.CFStream.remove_ErrorEvent(System.EventHandler{CoreFoundation.CFStream.StreamEventArgs}) @@ -10518,7 +10508,7 @@ M:CoreFoundation.CFStream.remove_HasBytesAvailableEvent(System.EventHandler{Core M:CoreFoundation.CFStream.remove_OpenCompletedEvent(System.EventHandler{CoreFoundation.CFStream.StreamEventArgs}) M:CoreFoundation.CFString.op_Implicit(CoreFoundation.CFString)~System.String M:CoreFoundation.CFString.op_Implicit(System.String)~CoreFoundation.CFString -M:CoreFoundation.CFWriteStream.DoSetClient(.method,System.IntPtr,System.IntPtr) +M:CoreFoundation.CFWriteStream.DoSetClient(,System.IntPtr,System.IntPtr) M:CoreFoundation.CFWriteStream.Write(System.Byte[],System.IntPtr,System.IntPtr) M:CoreFoundation.DispatchBlock.op_Explicit(CoreFoundation.DispatchBlock)~System.Action M:CoreFoundation.DispatchData.CreateMap(System.IntPtr@,System.UIntPtr@) @@ -10728,7 +10718,7 @@ M:CoreGraphics.CGPDFObject.TryGetValue(System.IntPtr@) M:CoreGraphics.CGPDFObject.TryGetValue(System.Runtime.InteropServices.NFloat@) M:CoreGraphics.CGPDFOperatorTable.Release M:CoreGraphics.CGPDFOperatorTable.Retain -M:CoreGraphics.CGPDFOperatorTable.SetCallback(System.String,.method) +M:CoreGraphics.CGPDFOperatorTable.SetCallback(System.String,) M:CoreGraphics.CGPDFPage.Release M:CoreGraphics.CGPDFPage.Retain M:CoreGraphics.CGPDFPageInfo.#ctor @@ -11179,8 +11169,6 @@ M:CoreMidi.IMidiCIProfileResponderDelegate.InitiatorDisconnected(Foundation.NSNu M:CoreMidi.IMidiCIProfileResponderDelegate.WillSetProfile(CoreMidi.MidiCIProfile,System.Byte,System.Boolean) M:CoreMidi.Midi2DeviceInfo.#ctor(CoreMidi.Midi2DeviceManufacturer,System.UInt16,System.UInt16,CoreMidi.Midi2DeviceRevisionLevel) M:CoreMidi.MidiBluetoothDriver.#ctor -M:CoreMidi.MidiBluetoothDriver.ActivateAllConnections -M:CoreMidi.MidiBluetoothDriver.Disconnect(Foundation.NSString) M:CoreMidi.MidiCIDeviceInfo.#ctor(CoreMidi.MidiEndpoint,Foundation.NSData,Foundation.NSData,Foundation.NSData,Foundation.NSData) M:CoreMidi.MidiCIDeviceInfo.GetMidiDestination M:CoreMidi.MidiCIDiscoveredNode.GetDestination @@ -11217,12 +11205,15 @@ M:CoreMidi.MidiClient.remove_ThruConnectionsChanged(System.EventHandler) M:CoreMidi.MidiDevice.Add(System.String,System.Boolean,System.UIntPtr,System.UIntPtr,CoreMidi.MidiEntity) M:CoreMidi.MidiDevice.GetEntity(System.IntPtr) M:CoreMidi.MidiDeviceList.Get(System.UIntPtr) +M:CoreMidi.MidiDriver.#ctor +M:CoreMidi.MidiDriver.Finalize M:CoreMidi.MidiEndpoint.add_MessageReceived(System.EventHandler{CoreMidi.MidiPacketsEventArgs}) M:CoreMidi.MidiEndpoint.GetDestination(System.IntPtr) M:CoreMidi.MidiEndpoint.GetSource(System.IntPtr) M:CoreMidi.MidiEndpoint.remove_MessageReceived(System.EventHandler{CoreMidi.MidiPacketsEventArgs}) M:CoreMidi.MidiEntity.GetDestination(System.IntPtr) M:CoreMidi.MidiEntity.GetSource(System.IntPtr) +M:CoreMidi.MidiEventList.Finalize M:CoreMidi.MidiNetworkSession.GetDestinationEndPoint M:CoreMidi.MidiNetworkSession.GetSourceEndpoint M:CoreMidi.MidiObject.Finalize @@ -20607,8 +20598,6 @@ P:CoreMidi.Midi2DeviceInfo.Family P:CoreMidi.Midi2DeviceInfo.ManufacturerId P:CoreMidi.Midi2DeviceInfo.ModelNumber P:CoreMidi.Midi2DeviceInfo.RevisionLevel -P:CoreMidi.Midi2DeviceManufacturer.SysExIdByte -P:CoreMidi.Midi2DeviceRevisionLevel.RevisionLevel P:CoreMidi.MidiCIDevice.DeviceInfo P:CoreMidi.MidiCIDevice.DeviceType P:CoreMidi.MidiCIDevice.MaxPropertyExchangeRequests @@ -20630,8 +20619,6 @@ P:CoreMidi.MidiCIDiscoveredNode.MaximumSysExSize P:CoreMidi.MidiCIDiscoveredNode.SupportsProfiles P:CoreMidi.MidiCIDiscoveredNode.SupportsProperties P:CoreMidi.MidiCIDiscoveryManager.SharedInstance -P:CoreMidi.MidiCIProfileId.ManufacturerSpecific -P:CoreMidi.MidiCIProfileId.Standard P:CoreMidi.MidiCIProfileState.MidiChannel P:CoreMidi.MidiCIResponder.DeviceInfo P:CoreMidi.MidiCIResponder.Initiators @@ -26286,9 +26273,6 @@ T:CoreMedia.CMTaggedBufferGroupFormatType T:CoreMedia.CMTextFormatType T:CoreMedia.LensStabilizationStatus T:CoreMidi.Midi2DeviceInfo -T:CoreMidi.Midi2DeviceManufacturer -T:CoreMidi.Midi2DeviceRevisionLevel -T:CoreMidi.MidiBluetoothDriver T:CoreMidi.MidiCICategoryOptions T:CoreMidi.MidiCIDevice T:CoreMidi.MidiCIDeviceInfo @@ -26302,9 +26286,6 @@ T:CoreMidi.MidiCIManagementMessageType T:CoreMidi.MidiCIProcessInquiryMessageType T:CoreMidi.MidiCIProfile T:CoreMidi.MidiCIProfileChangedHandler -T:CoreMidi.MidiCIProfileId -T:CoreMidi.MidiCIProfileIdManufacturerSpecific -T:CoreMidi.MidiCIProfileIdStandard T:CoreMidi.MidiCIProfileMessageType T:CoreMidi.MidiCIProfileSpecificDataHandler T:CoreMidi.MidiCIProfileState diff --git a/tests/cecil-tests/Documentation.cs b/tests/cecil-tests/Documentation.cs index e580685e79ac..ee6195b39012 100644 --- a/tests/cecil-tests/Documentation.cs +++ b/tests/cecil-tests/Documentation.cs @@ -321,6 +321,9 @@ static string GetDocId (PropertyDefinition pd) static string GetDocId (TypeReference tr) { + if (tr is FunctionPointerType) + return ""; + string name = ""; if (tr.IsNested) { var decl = tr.DeclaringType; diff --git a/tests/monotouch-test/CoreMidi/MidiComprehensiveTest.cs b/tests/monotouch-test/CoreMidi/MidiComprehensiveTest.cs new file mode 100644 index 000000000000..6aa8e832a3dd --- /dev/null +++ b/tests/monotouch-test/CoreMidi/MidiComprehensiveTest.cs @@ -0,0 +1,973 @@ +// +// Comprehensive tests for CoreMidi APIs +// +// Copyright 2025 Microsoft Corp. All rights reserved. +// + +#if HAS_COREMIDI && !__TVOS__ + +#pragma warning disable APL0004 // MidiDevice.Create is experimental + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +using CoreMidi; +using Foundation; + +using NUnit.Framework; + +namespace MonoTouchFixtures.CoreMidi { + static class MidiTestHelpers { + public static void AssertStatusOkOrInconclusive (MidiError status, string message) + { + if (status == MidiError.NotPermitted) + Assert.Inconclusive ("MIDI permission not granted in this environment."); + + Assert.That (status, Is.EqualTo (MidiError.Ok), message); + } + } + + [TestFixture] + [Preserve (AllMembers = true)] + public class MidiTest { + [Test] + public void Restart () + { + Assert.DoesNotThrow (() => Midi.Restart (), "Restart"); + } + + [Test] + public void SourceCount () + { + Assert.That ((int) Midi.SourceCount, Is.GreaterThanOrEqualTo (0), "SourceCount"); + } + + [Test] + public void DestinationCount () + { + Assert.That ((int) Midi.DestinationCount, Is.GreaterThanOrEqualTo (0), "DestinationCount"); + } + + [Test] + public void DeviceCount () + { + Assert.That ((int) Midi.DeviceCount, Is.GreaterThanOrEqualTo (0), "DeviceCount"); + } + + [Test] + public void ExternalDeviceCount () + { + Assert.That ((int) Midi.ExternalDeviceCount, Is.GreaterThanOrEqualTo (0), "ExternalDeviceCount"); + } + + [Test] + public void GetDevice () + { + // Get any device that might exist; if none, verify null for invalid index + if (Midi.DeviceCount > 0) { + var device = Midi.GetDevice (0); + Assert.That (device, Is.Not.Null, "GetDevice (0)"); + } + + // Out of range should return null + var invalid = Midi.GetDevice (99999); + Assert.That (invalid, Is.Null, "GetDevice (99999)"); + } + + [Test] + public void GetExternalDevice () + { + if (Midi.ExternalDeviceCount > 0) { + var device = Midi.GetExternalDevice (0); + Assert.That (device, Is.Not.Null, "GetExternalDevice (0)"); + } + } + + [Test] + public void CreateExternalDevice () + { + MidiError status; + using var device = Midi.CreateExternalDevice ("Test Device", "Test Manufacturer", "Test Model", out status); + Assert.That (status, Is.EqualTo (MidiError.Ok), "Status"); + Assert.That (device, Is.Not.Null, "Device"); + + // Clean up + var removeStatus = MidiSetup.RemoveExternalDevice (device!); + Assert.That (removeStatus, Is.EqualTo (MidiError.Ok), "RemoveExternalDevice"); + } + } + + [TestFixture] + [Preserve (AllMembers = true)] + public class MidiSetupTest { + [Test] + public void AddRemoveDevice () + { + // MIDIDeviceCreate requires a MIDI driver context and returns -50 (paramErr) in user-space. + // Verify the API doesn't crash and returns a meaningful error. + var device = MidiDevice.Create (null, "TestSetupDevice", "TestManufacturer", "TestModel", out var createStatus); + // -50 is paramErr - expected when not running as a MIDI driver + Assert.That ((int) createStatus, Is.EqualTo (-50), "Create returns paramErr without driver"); + Assert.That (device, Is.Null, "Device is null without driver"); + } + + [Test] + public void AddRemoveExternalDevice () + { + var device = Midi.CreateExternalDevice ("TestExtDevice", "TestExtManufacturer", "TestExtModel", out var createStatus); + Assert.That (createStatus, Is.EqualTo (MidiError.Ok), "Create"); + Assert.That (device, Is.Not.Null, "Device not null"); + + var addStatus = MidiSetup.AddExternalDevice (device!); + Assert.That (addStatus, Is.EqualTo (MidiError.Ok), "AddExternalDevice"); + + var removeStatus = MidiSetup.RemoveExternalDevice (device!); + Assert.That (removeStatus, Is.EqualTo (MidiError.Ok), "RemoveExternalDevice"); + } + } + + [TestFixture] + [Preserve (AllMembers = true)] + public class MidiClientTest_Comprehensive { + [Test] + public void CreateClient () + { + using var client = new MidiClient ("TestClient"); + Assert.That (client.Name, Is.EqualTo ("TestClient"), "Name"); + Assert.That (client.Handle, Is.Not.EqualTo (0), "Handle"); + } + + [Test] + public void CreateOutputPort () + { + using var client = new MidiClient ("TestOutputPortClient"); + using var port = client.CreateOutputPort ("TestOutputPort"); + Assert.That (port, Is.Not.Null, "Port not null"); + Assert.That (port.PortName, Is.EqualTo ("TestOutputPort"), "PortName"); + } + + [Test] + public void CreateInputPort () + { + using var client = new MidiClient ("TestInputPortClient"); + using var port = client.CreateInputPort ("TestInputPort"); + Assert.That (port, Is.Not.Null, "Port not null"); + Assert.That (port.PortName, Is.EqualTo ("TestInputPort"), "PortName"); + } + + [Test] + public void CreateVirtualSource_Legacy () + { + using var client = new MidiClient ("TestVirtualSourceClient"); +#pragma warning disable CS0618 // Type or member is obsolete + var source = client.CreateVirtualSource ("TestVirtualSource", out var status); +#pragma warning restore CS0618 + MidiTestHelpers.AssertStatusOkOrInconclusive (status, "Status"); + Assert.That (source, Is.Not.Null, "Source not null"); + source?.Dispose (); + } + + [Test] + public void CreateVirtualSource_WithProtocol () + { + using var client = new MidiClient ("TestVirtualSourceClient2"); + var source = client.CreateVirtualSource ("TestVirtualSource2", MidiProtocolId.Protocol_1_0, out var status); + MidiTestHelpers.AssertStatusOkOrInconclusive (status, "Status"); + Assert.That (source, Is.Not.Null, "Source not null"); + source?.Dispose (); + } + + [Test] + public void CreateVirtualDestination_Legacy () + { + using var client = new MidiClient ("TestVirtualDestClient"); +#pragma warning disable CS0618 // Type or member is obsolete + var dest = client.CreateVirtualDestination ("TestVirtualDest", out var status); +#pragma warning restore CS0618 + MidiTestHelpers.AssertStatusOkOrInconclusive (status, "Status"); + Assert.That (dest, Is.Not.Null, "Destination not null"); + dest?.Dispose (); + } + + [Test] + public void Events () + { + using var client = new MidiClient ("TestEventsClient"); + // We can't easily trigger these events in a test, but verify the subscription doesn't crash + Assert.DoesNotThrow (() => { + client.ObjectAdded += (sender, args) => { }; + client.ObjectRemoved += (sender, args) => { }; + client.PropertyChanged += (sender, args) => { }; + client.ThruConnectionsChanged += (sender, args) => { }; + client.SerialPortOwnerChanged += (sender, args) => { }; + client.IOError += (sender, args) => { }; + }, "Event subscriptions"); + } + } + + [TestFixture] + [Preserve (AllMembers = true)] + public class MidiPortTest { + [Test] + public void PortToString () + { + using var client = new MidiClient ("TestPortToStringClient"); + using var outputPort = client.CreateOutputPort ("Output1"); + Assert.That (outputPort.ToString (), Does.Contain ("Output1"), "OutputPort.ToString"); + Assert.That (outputPort.ToString (), Does.Contain ("output"), "OutputPort contains 'output'"); + } + + [Test] + public void SendAndReceive () + { + // Create client, source, destination, and output port + using var client = new MidiClient ("TestSendReceiveClient"); + using var outputPort = client.CreateOutputPort ("TestOutput"); + + var source = client.CreateVirtualSource ("TestSendSource", MidiProtocolId.Protocol_1_0, out var srcStatus); + MidiTestHelpers.AssertStatusOkOrInconclusive (srcStatus, "Source status"); + Assert.That (source, Is.Not.Null, "Source not null"); + source?.Dispose (); + } + } + + [TestFixture] + [Preserve (AllMembers = true)] + public class MidiDeviceTest_Comprehensive { + [Test] + public void Create () + { + // MIDIDeviceCreate requires a MIDI driver context, returns -50 (paramErr) in user-space + var device = MidiDevice.Create (null, "TestDevice", "Manufacturer", "Model", out var status); + Assert.That ((int) status, Is.EqualTo (-50), "Create returns paramErr without driver"); + Assert.That (device, Is.Null, "Device is null without driver"); + } + + [Test] + public void EntityCount () + { + // Use an existing device if available, otherwise verify the API exists + if (Midi.DeviceCount > 0) { + var device = Midi.GetDevice (0); + Assert.That (device, Is.Not.Null, "Device"); + Assert.That ((int) device!.EntityCount, Is.GreaterThanOrEqualTo (0), "EntityCount >= 0"); + } + } + + [Test] + public void CreateEntity () + { + // MIDIDeviceCreate requires a driver context, can't test entity creation in user-space + var device = MidiDevice.Create (null, "TestEntityDevice", "Manufacturer", "Model", out var status); + Assert.That ((int) status, Is.EqualTo (-50), "Create returns paramErr"); + } + + [Test] + public void RemoveEntity () + { + // MIDIDeviceCreate requires a driver context, can't test entity removal in user-space + var device = MidiDevice.Create (null, "TestRemoveEntityDevice", "Manufacturer", "Model", out var status); + Assert.That ((int) status, Is.EqualTo (-50), "Create returns paramErr"); + } + + [Test] + public void GetEntity () + { + // Use an existing device if available + if (Midi.DeviceCount > 0) { + var device = Midi.GetDevice (0); + Assert.That (device, Is.Not.Null, "Device"); + if (device!.EntityCount > 0) { + var entity = device.GetEntity (0); + Assert.That (entity, Is.Not.Null, "GetEntity (0)"); + } + var outOfRange = device.GetEntity (99999); + Assert.That (outOfRange, Is.Null, "GetEntity (99999)"); + } + } + + [Test] + public void UniqueID () + { + // Use an existing device if available + if (Midi.DeviceCount > 0) { + var device = Midi.GetDevice (0); + Assert.That (device, Is.Not.Null, "Device"); + Assert.DoesNotThrow (() => { + var uniqueId = device!.UniqueID; + }, "UniqueID getter"); + } + } + } + + [TestFixture] + [Preserve (AllMembers = true)] + public class MidiEntityTest { + [Test] + public void SourcesAndDestinations () + { + // Use an existing device/entity if available + if (Midi.DeviceCount > 0) { + var device = Midi.GetDevice (0); + if (device is not null && device.EntityCount > 0) { + var entity = device.GetEntity (0); + Assert.That (entity, Is.Not.Null, "Entity"); + Assert.That ((int) entity!.Sources, Is.GreaterThanOrEqualTo (0), "Sources"); + Assert.That ((int) entity.Destinations, Is.GreaterThanOrEqualTo (0), "Destinations"); + } + } + } + + [Test] + public void AddOrRemoveEndpoints () + { + // MIDIDeviceCreate requires a driver context, can't test in user-space + var device = MidiDevice.Create (null, "TestAddRemoveEndpointsDevice", "Manufacturer", "Model", out var status); + Assert.That ((int) status, Is.EqualTo (-50), "Create returns paramErr"); + } + + [Test] + public void Device () + { + // Use an existing device/entity if available + if (Midi.DeviceCount > 0) { + var device = Midi.GetDevice (0); + if (device is not null && device.EntityCount > 0) { + var entity = device.GetEntity (0); + Assert.That (entity, Is.Not.Null, "Entity"); + var entityDevice = entity!.Device; + Assert.That (entityDevice, Is.Not.Null, "Device"); + } + } + } + } + + [TestFixture] + [Preserve (AllMembers = true)] + public class MidiEndpointTest_Comprehensive { + [Test] + public void GetSource () + { + if (Midi.SourceCount > 0) { + var source = MidiEndpoint.GetSource (0); + Assert.That (source, Is.Not.Null, "GetSource (0)"); + Assert.That (source!.Handle, Is.Not.EqualTo (0), "Handle"); + } + } + + [Test] + public void GetDestination () + { + if (Midi.DestinationCount > 0) { + var dest = MidiEndpoint.GetDestination (0); + Assert.That (dest, Is.Not.Null, "GetDestination (0)"); + Assert.That (dest!.Handle, Is.Not.EqualTo (0), "Handle"); + } + } + + [Test] + public void FlushOutput () + { + // Create a virtual destination and flush it + using var client = new MidiClient ("TestFlushClient"); +#pragma warning disable CS0618 + var dest = client.CreateVirtualDestination ("TestFlushDest", out var status); +#pragma warning restore CS0618 + MidiTestHelpers.AssertStatusOkOrInconclusive (status, "Create"); + Assert.DoesNotThrow (() => dest!.FlushOutput (), "FlushOutput"); + dest?.Dispose (); + } + + [Test] + public void EndpointName () + { + using var client = new MidiClient ("TestEndpointNameClient"); + var source = client.CreateVirtualSource ("TestNameSource", MidiProtocolId.Protocol_1_0, out var status); + MidiTestHelpers.AssertStatusOkOrInconclusive (status, "Create"); + Assert.That (source!.EndpointName, Is.Not.Null, "EndpointName"); + source?.Dispose (); + } + + [Test] + public void Entity_ForVirtualEndpoint () + { + using var client = new MidiClient ("TestEntityClient"); + var source = client.CreateVirtualSource ("TestEntitySource", MidiProtocolId.Protocol_1_0, out var status); + MidiTestHelpers.AssertStatusOkOrInconclusive (status, "Create"); + // Virtual endpoints don't have a parent entity + var entity = source!.Entity; + Assert.That (entity, Is.Null, "Entity should be null for virtual endpoints"); + source.Dispose (); + } + + [Test] + public void Properties () + { + // Use a virtual source to test endpoint properties + using var client = new MidiClient ("TestPropsClient"); + var source = client.CreateVirtualSource ("TestPropsSource", MidiProtocolId.Protocol_1_0, out var srcStatus); + MidiTestHelpers.AssertStatusOkOrInconclusive (srcStatus, "Create source"); + Assert.That (source, Is.Not.Null, "source"); + + // Test properties that should be readable + Assert.DoesNotThrow (() => { + _ = source!.MaxSysExSpeed; + }, "MaxSysExSpeed"); + + source?.Dispose (); + } + } + + [TestFixture] + [Preserve (AllMembers = true)] + public class MidiObjectTest { + [Test] + public void FindByUniqueId () + { + // Use an existing device to find by unique ID + if (Midi.DeviceCount > 0) { + var device = Midi.GetDevice (0); + Assert.That (device, Is.Not.Null, "Device"); + var uniqueId = device!.UniqueID; + var findStatus = MidiObject.FindByUniqueId (uniqueId, out var found); + Assert.That (findStatus, Is.EqualTo (MidiError.Ok), "FindByUniqueId"); + Assert.That (found, Is.Not.Null, "Found object not null"); + } else { + // If no devices exist, verify the API can handle "not found" + var findStatus = MidiObject.FindByUniqueId (12345, out var found); + Assert.That (findStatus, Is.EqualTo (MidiError.ObjectNotFound), "ObjectNotFound"); + } + } + + [Test] + public void FindByUniqueId_NotFound () + { + var findStatus = MidiObject.FindByUniqueId (-999999, out var found); + Assert.That (findStatus, Is.EqualTo (MidiError.ObjectNotFound), "FindByUniqueId for non-existent ID"); + Assert.That (found, Is.Null, "Found object is null"); + } + + [Test] + public void GetDictionaryProperties () + { + // Use a virtual source instead of creating a device (which requires driver context) + using var client = new MidiClient ("TestDictPropsClient"); + var source = client.CreateVirtualSource ("TestDictPropsSource", MidiProtocolId.Protocol_1_0, out var status); + MidiTestHelpers.AssertStatusOkOrInconclusive (status, "Create"); + + var dict = source!.GetDictionaryProperties (false); + Assert.That (dict, Is.Not.Null, "GetDictionaryProperties"); + Assert.That ((int) dict!.Count, Is.GreaterThan (0), "Properties count > 0"); + + source.Dispose (); + } + } + + [TestFixture] + [Preserve (AllMembers = true)] + public class MidiEventListTest_Comprehensive { + [Test] + public void Ctor_Protocol10 () + { + using var list = new MidiEventList (MidiProtocolId.Protocol_1_0); + Assert.That (list.Protocol, Is.EqualTo (MidiProtocolId.Protocol_1_0), "Protocol"); + Assert.That (list.PacketCount, Is.EqualTo (0), "PacketCount"); + } + + [Test] + public void Ctor_Protocol20 () + { + using var list = new MidiEventList (MidiProtocolId.Protocol_2_0); + Assert.That (list.Protocol, Is.EqualTo (MidiProtocolId.Protocol_2_0), "Protocol"); + Assert.That (list.PacketCount, Is.EqualTo (0), "PacketCount"); + } + + [Test] + public void Add_MultiplePackets () + { + // Use a large list to hold multiple packets + using var list = new MidiEventList (MidiProtocolId.Protocol_2_0, 1024); + + var rv1 = list.Add (100, new uint [] { 0x20906040 }); // Note On + Assert.That (rv1, Is.True, "Add 1"); + Assert.That (list.PacketCount, Is.EqualTo (1), "PacketCount 1"); + + var rv2 = list.Add (200, new uint [] { 0x20806040 }); // Note Off + Assert.That (rv2, Is.True, "Add 2"); + Assert.That (list.PacketCount, Is.EqualTo (2), "PacketCount 2"); + + var rv3 = list.Add (300, new uint [] { 0x20906050 }); // Another Note On + Assert.That (rv3, Is.True, "Add 3"); + Assert.That (list.PacketCount, Is.EqualTo (3), "PacketCount 3"); + + // Verify packets via enumeration + var packets = list.ToArray (); + Assert.That (packets.Length, Is.EqualTo (3), "Enumerated length"); + Assert.That (packets [0].Timestamp, Is.EqualTo (100), "Packet 0 timestamp"); + Assert.That (packets [1].Timestamp, Is.EqualTo (200), "Packet 1 timestamp"); + Assert.That (packets [2].Timestamp, Is.EqualTo (300), "Packet 2 timestamp"); + } + + [Test] + public void Add_FromRawPointer_Throws () + { + var ptr = Marshal.AllocHGlobal (512); + try { + var list = new MidiEventList (ptr); + Assert.Throws (() => list.Add (0, new uint [] { 1 }), "Add to raw pointer list"); + } finally { + Marshal.FreeHGlobal (ptr); + } + } + + [Test] + public void Iterate_MultiplePackets () + { + using var list = new MidiEventList (MidiProtocolId.Protocol_1_0, 1024); + + list.Add (100, new uint [] { 0x20906040 }); + list.Add (200, new uint [] { 0x20806040 }); + + var packetList = new List<(ulong Timestamp, uint [] Words)> (); + list.Iterate ((ref MidiEventPacket packet) => { + packetList.Add ((packet.Timestamp, packet.Words)); + }); + + Assert.That (packetList.Count, Is.EqualTo (2), "Count"); + Assert.That (packetList [0].Timestamp, Is.EqualTo (100), "Timestamp 0"); + Assert.That (packetList [1].Timestamp, Is.EqualTo (200), "Timestamp 1"); + } + + [Test] + public void Iterate_EmptyList () + { + using var list = new MidiEventList (MidiProtocolId.Protocol_1_0); + var count = 0; + list.Iterate ((ref MidiEventPacket packet) => { count++; }); + Assert.That (count, Is.EqualTo (0), "Empty iteration count"); + } + + [Test] + public void Enumerator_EmptyList () + { + using var list = new MidiEventList (MidiProtocolId.Protocol_1_0); + var packets = list.ToArray (); + Assert.That (packets.Length, Is.EqualTo (0), "Empty enumeration"); + } + + [Test] + public void Dispose_Idempotent () + { + var list = new MidiEventList (MidiProtocolId.Protocol_1_0); + Assert.DoesNotThrow (() => { + list.Dispose (); + list.Dispose (); + }, "Double dispose"); + } + + [Test] + public void SendAndReceive () + { + using var client = new MidiClient ("TestEventListSendClient"); + using var outputPort = client.CreateOutputPort ("TestEventListOutput"); + + var source = client.CreateVirtualSource ("TestEventListSource", MidiProtocolId.Protocol_1_0, out var status); + MidiTestHelpers.AssertStatusOkOrInconclusive (status, "CreateVirtualSource"); + + using var list = new MidiEventList (MidiProtocolId.Protocol_1_0, 1024); + // MIDI 1.0 Note On: channel 0, note 60 (middle C), velocity 127 + list.Add (0, new uint [] { 0x20903C7F }); + + // Send from source (distribute to listeners) + var sendStatus = list.Receive (source!); + Assert.That (sendStatus, Is.EqualTo (0), "Receive status"); + + source?.Dispose (); + } + + /// + /// Test creating a MIDI event list with the notes for "Happy Birthday" melody. + /// Uses MIDI 1.0 protocol with Note On/Off messages. + /// + [Test] + public void HappyBirthday () + { + using var list = new MidiEventList (MidiProtocolId.Protocol_1_0, 4096); + Assert.That (list.Protocol, Is.EqualTo (MidiProtocolId.Protocol_1_0), "Protocol"); + + // MIDI 1.0 channel voice messages encoded as UMP (Universal MIDI Packet): + // Type 2 (MIDI 1.0 Channel Voice), Group 0 + // Status: 0x90 = Note On channel 0, 0x80 = Note Off channel 0 + // Format: 0x2tssnnvv where t=type(0=group0), ss=status, nn=note, vv=velocity + // + // "Happy Birthday to You" melody notes (in MIDI note numbers): + // C4=60, D4=62, E4=64, F4=65, G4=67, A4=69, Bb4=70, C5=72 + // + // Melody: C C D C F E | C C D C G F | C C C' A F E D | Bb Bb A F G F + byte [] melody = { + 60, 60, 62, 60, 65, 64, // Hap-py Birth-day to You + 60, 60, 62, 60, 67, 65, // Hap-py Birth-day to You + 60, 60, 72, 69, 65, 64, 62, // Hap-py Birth-day dear friend + 70, 70, 69, 65, 67, 65 // Hap-py Birth-day to You + }; + + // Duration in ticks (arbitrary units) for each note + ulong [] durations = { + 250, 250, 500, 500, 500, 1000, // line 1 + 250, 250, 500, 500, 500, 1000, // line 2 + 250, 250, 500, 500, 500, 500, 1000, // line 3 + 250, 250, 500, 500, 500, 1000 // line 4 + }; + + byte velocity = 100; + ulong currentTime = 0; + + for (int i = 0; i < melody.Length; i++) { + // Note On: 0x2090NNVV + uint noteOn = (uint) (0x20900000 | (melody [i] << 8) | velocity); + var addedOn = list.Add (currentTime, new uint [] { noteOn }); + Assert.That (addedOn, Is.True, $"Add NoteOn {i}"); + + // Note Off: 0x2080NN00 + uint noteOff = (uint) (0x20800000 | (melody [i] << 8)); + var addedOff = list.Add (currentTime + durations [i], new uint [] { noteOff }); + Assert.That (addedOff, Is.True, $"Add NoteOff {i}"); + + currentTime += durations [i]; + } + + // MIDIEventListAdd merges events with the same timestamp into a single packet. + // The NoteOff of note i and NoteOn of note i+1 share the same timestamp, + // so they are merged into one packet. This gives us: + // 1 NoteOn at time 0 + 24 merged (NoteOff + NoteOn) packets + 1 final NoteOff = 26 packets + Assert.That ((int) list.PacketCount, Is.EqualTo (26), "PacketCount (merged by timestamp)"); + + // Verify the melody by iterating and collecting all words + var allWords = new List<(ulong Timestamp, uint Word)> (); + list.Iterate ((ref MidiEventPacket packet) => { + var words = packet.Words; + for (int w = 0; w < words.Length; w++) + allWords.Add ((packet.Timestamp, words [w])); + }); + + Assert.That (allWords.Count, Is.EqualTo (melody.Length * 2), "Total word count"); + + // Verify first note: C4 Note On at time 0 + Assert.That (allWords [0].Timestamp, Is.EqualTo (0UL), "First note timestamp"); + Assert.That (allWords [0].Word & 0xFF00, Is.EqualTo ((uint) (60 << 8)), "First note is C4 (60)"); + + // Verify the sequence contains all happy birthday notes + var noteOnMessages = allWords.Where (n => (n.Word & 0x00F00000) == 0x00900000).Select (n => (byte) ((n.Word >> 8) & 0xFF)).ToArray (); + Assert.That (noteOnMessages, Is.EqualTo (melody), "Happy Birthday melody matches"); + } + } + + [TestFixture] + [Preserve (AllMembers = true)] + public class MidiEventPacketTest_Comprehensive { + [Test] + public void DefaultValues () + { + var packet = new MidiEventPacket (); + Assert.That (packet.Timestamp, Is.EqualTo (0UL), "Timestamp default"); + Assert.That (packet.WordCount, Is.EqualTo (0U), "WordCount default"); + Assert.That (packet.Words.Length, Is.EqualTo (0), "Words default length"); + } + + [Test] + public void Timestamp_SetGet () + { + var packet = new MidiEventPacket (); + packet.Timestamp = ulong.MaxValue; + Assert.That (packet.Timestamp, Is.EqualTo (ulong.MaxValue), "MaxValue"); + + packet.Timestamp = 0; + Assert.That (packet.Timestamp, Is.EqualTo (0UL), "Zero"); + + packet.Timestamp = 12345678UL; + Assert.That (packet.Timestamp, Is.EqualTo (12345678UL), "Arbitrary"); + } + + [Test] + public void WordCount_Validation () + { + var packet = new MidiEventPacket (); + Assert.DoesNotThrow (() => packet.WordCount = 0, "0 is valid"); + Assert.DoesNotThrow (() => packet.WordCount = 64, "64 is valid"); + Assert.Throws (() => packet.WordCount = 65, "65 is invalid"); + } + + [Test] + public void Words_SetGet () + { + var packet = new MidiEventPacket (); + var words = new uint [] { 0xDEADBEEF, 0xCAFEBABE, 0x12345678 }; + packet.Words = words; + + Assert.That (packet.WordCount, Is.EqualTo (3U), "WordCount after set"); + Assert.That (packet.Words, Is.EqualTo (words), "Words match"); + } + + [Test] + public void Words_MaxWords () + { + var packet = new MidiEventPacket (); + var words = Enumerable.Range (1, 64).Select (v => (uint) v).ToArray (); + packet.Words = words; + + Assert.That (packet.WordCount, Is.EqualTo (64U), "WordCount = 64"); + Assert.That (packet.Words, Is.EqualTo (words), "Words match"); + } + + [Test] + public void Words_TooMany () + { + var packet = new MidiEventPacket (); + var words = Enumerable.Range (1, 65).Select (v => (uint) v).ToArray (); + Assert.Throws (() => packet.Words = words, "65 words is too many"); + } + + [Test] + public void Indexer () + { + var packet = new MidiEventPacket (); + packet.Words = new uint [] { 10, 20, 30, 40, 50 }; + + Assert.That (packet [0], Is.EqualTo (10U), "Index 0"); + Assert.That (packet [4], Is.EqualTo (50U), "Index 4"); + + packet [2] = 999; + Assert.That (packet [2], Is.EqualTo (999U), "Modified index 2"); + } + + [Test] + public void Indexer_OutOfRange () + { + var packet = new MidiEventPacket (); + packet.Words = new uint [] { 1, 2, 3 }; + + Assert.Throws (() => { var _ = packet [-1]; }, "Negative index"); + Assert.Throws (() => { var _ = packet [64]; }, "Index 64"); + Assert.Throws (() => { var _ = packet [3]; }, "Beyond WordCount"); + } + + [Test] + public void NoteOnOffRoundtrip () + { + // Construct a MIDI 1.0 Note On as UMP + var packet = new MidiEventPacket (); + packet.Timestamp = 1000; + // UMP Type 2 (MIDI 1.0 CV), Group 0, Note On, Channel 0, Note 60, Velocity 127 + packet.Words = new uint [] { 0x20903C7F }; + + Assert.That (packet.Timestamp, Is.EqualTo (1000UL), "Timestamp"); + Assert.That (packet.WordCount, Is.EqualTo (1U), "WordCount"); + + var word = packet [0]; + var messageType = (word >> 28) & 0xF; + var group = (word >> 24) & 0xF; + var status = (word >> 16) & 0xFF; + var note = (word >> 8) & 0xFF; + var velocity = word & 0xFF; + + Assert.That (messageType, Is.EqualTo (2U), "Message type (MIDI 1.0 CV)"); + Assert.That (group, Is.EqualTo (0U), "Group 0"); + Assert.That (status, Is.EqualTo (0x90U), "Note On status"); + Assert.That (note, Is.EqualTo (60U), "Middle C (note 60)"); + Assert.That (velocity, Is.EqualTo (127U), "Velocity 127"); + } + } + + [TestFixture] + [Preserve (AllMembers = true)] + public class MidiPacketTest { + [Test] + public void Ctor_IntPtr () + { + var bytes = new byte [] { 0x90, 60, 100 }; + var handle = Marshal.AllocHGlobal (bytes.Length); + try { + Marshal.Copy (bytes, 0, handle, bytes.Length); + using var packet = new MidiPacket (12345, (ushort) bytes.Length, handle); + Assert.That (packet.TimeStamp, Is.EqualTo (12345L), "TimeStamp"); + Assert.That (packet.Length, Is.EqualTo (3), "Length"); + Assert.That (packet.BytePointer, Is.EqualTo (handle), "BytePointer"); + } finally { + Marshal.FreeHGlobal (handle); + } + } + + [Test] + public void Ctor_ByteArray () + { + var bytes = new byte [] { 0x90, 60, 100 }; + using var packet = new MidiPacket (54321, bytes); + Assert.That (packet.TimeStamp, Is.EqualTo (54321L), "TimeStamp"); + Assert.That (packet.Length, Is.EqualTo (3), "Length"); + Assert.That (packet.ByteArray, Is.EqualTo (bytes), "ByteArray"); + } + + [Test] + public void Ctor_ByteArrayRange () + { + var bytes = new byte [] { 0x00, 0x90, 60, 100, 0x00 }; + using var packet = new MidiPacket (0, bytes, 1, 3); + Assert.That (packet.Length, Is.EqualTo (3), "Length"); + Assert.That (packet.ByteArray, Is.Not.Null, "ByteArray not null"); + } + + [Test] + public void Ctor_NullBytes () + { + // The public constructor dereferences bytes.Length before the null check in the + // private constructor, so it throws NullReferenceException rather than ArgumentNullException. + Assert.Throws (() => new MidiPacket (0, (byte []) null!), "Null bytes"); + } + + [Test] + public void Ctor_TooLong () + { + var bytes = new byte [ushort.MaxValue + 1]; + Assert.Throws (() => new MidiPacket (0, bytes), "Too long"); + } + + [Test] + public void Dispose_ClearsPointer () + { + var bytes = new byte [] { 0x90, 60, 100 }; + var handle = Marshal.AllocHGlobal (bytes.Length); + try { + Marshal.Copy (bytes, 0, handle, bytes.Length); + var packet = new MidiPacket (0, (ushort) bytes.Length, handle); + Assert.That (packet.BytePointer, Is.Not.EqualTo (IntPtr.Zero), "Before dispose"); + packet.Dispose (); + Assert.That (packet.BytePointer, Is.EqualTo (IntPtr.Zero), "After dispose"); + } finally { + Marshal.FreeHGlobal (handle); + } + } + } + + [TestFixture] + [Preserve (AllMembers = true)] + public class MidiExceptionTest { + [Test] + public void ErrorCode_Property () + { + // MidiException has an ErrorCode property with the underlying MidiError value. + // Verify by creating a client with a null name, which should fail on some platforms. + // Since we can't construct MidiException directly, verify the type exists and + // its ErrorCode property is accessible via reflection. + var type = typeof (MidiException); + Assert.That (type, Is.Not.Null, "MidiException type exists"); + var prop = type.GetProperty ("ErrorCode"); + Assert.That (prop, Is.Not.Null, "ErrorCode property exists"); + Assert.That (prop!.PropertyType, Is.EqualTo (typeof (MidiError)), "ErrorCode type"); + } + } + + [TestFixture] + [Preserve (AllMembers = true)] + public class MidiMidi2StructsTest { + [Test] + public void Midi2DeviceManufacturer_SysExIdByte () + { + var mfg = new Midi2DeviceManufacturer (); + mfg.SysExIdByte = new byte [] { 0x7E, 0x01, 0x02 }; + Assert.That (mfg.SysExIdByte, Is.EqualTo (new byte [] { 0x7E, 0x01, 0x02 }), "SysExIdByte roundtrip"); + } + + [Test] + public void Midi2DeviceManufacturer_WrongLength () + { + var mfg = new Midi2DeviceManufacturer (); + Assert.Throws (() => mfg.SysExIdByte = new byte [] { 1, 2 }, "Too short"); + Assert.Throws (() => mfg.SysExIdByte = new byte [] { 1, 2, 3, 4 }, "Too long"); + } + + [Test] + public void Midi2DeviceRevisionLevel_RevisionLevel () + { + var rev = new Midi2DeviceRevisionLevel (); + rev.RevisionLevel = new byte [] { 1, 2, 3, 4 }; + Assert.That (rev.RevisionLevel, Is.EqualTo (new byte [] { 1, 2, 3, 4 }), "RevisionLevel roundtrip"); + } + + [Test] + public void Midi2DeviceRevisionLevel_WrongLength () + { + var rev = new Midi2DeviceRevisionLevel (); + Assert.Throws (() => rev.RevisionLevel = new byte [] { 1, 2, 3 }, "Too short"); + Assert.Throws (() => rev.RevisionLevel = new byte [] { 1, 2, 3, 4, 5 }, "Too long"); + } + + [Test] + public void MidiCIProfileId_Standard () + { + var id = new MidiCIProfileId (); + id.Standard = new MidiCIProfileIdStandard { + ProfileIdByte1 = 1, + ProfileBank = 2, + ProfileNumber = 3, + ProfileVersion = 4, + ProfileLevel = 5, + }; + + Assert.That (id.Standard.ProfileIdByte1, Is.EqualTo (1), "ProfileIdByte1"); + Assert.That (id.Standard.ProfileBank, Is.EqualTo (2), "ProfileBank"); + Assert.That (id.Standard.ProfileNumber, Is.EqualTo (3), "ProfileNumber"); + Assert.That (id.Standard.ProfileVersion, Is.EqualTo (4), "ProfileVersion"); + Assert.That (id.Standard.ProfileLevel, Is.EqualTo (5), "ProfileLevel"); + } + + [Test] + public void MidiCIProfileId_ManufacturerSpecific () + { + var id = new MidiCIProfileId (); + id.ManufacturerSpecific = new MidiCIProfileIdManufacturerSpecific { + SysExId1 = 0x7E, + SysExId2 = 0x01, + SysExId3 = 0x02, + Info1 = 0x10, + Info2 = 0x20, + }; + + Assert.That (id.ManufacturerSpecific.SysExId1, Is.EqualTo (0x7E), "SysExId1"); + Assert.That (id.ManufacturerSpecific.SysExId2, Is.EqualTo (0x01), "SysExId2"); + Assert.That (id.ManufacturerSpecific.SysExId3, Is.EqualTo (0x02), "SysExId3"); + Assert.That (id.ManufacturerSpecific.Info1, Is.EqualTo (0x10), "Info1"); + Assert.That (id.ManufacturerSpecific.Info2, Is.EqualTo (0x20), "Info2"); + } + } + + [TestFixture] + [Preserve (AllMembers = true)] + public class MidiErrorTest { + [Test] + public void ErrorValues () + { + Assert.That ((int) MidiError.Ok, Is.EqualTo (0), "Ok"); + Assert.That ((int) MidiError.InvalidClient, Is.EqualTo (-10830), "InvalidClient"); + Assert.That ((int) MidiError.InvalidPort, Is.EqualTo (-10831), "InvalidPort"); + Assert.That ((int) MidiError.WrongEndpointType, Is.EqualTo (-10832), "WrongEndpointType"); + Assert.That ((int) MidiError.NoConnection, Is.EqualTo (-10833), "NoConnection"); + Assert.That ((int) MidiError.UnknownEndpoint, Is.EqualTo (-10834), "UnknownEndpoint"); + Assert.That ((int) MidiError.UnknownProperty, Is.EqualTo (-10835), "UnknownProperty"); + Assert.That ((int) MidiError.WrongPropertyType, Is.EqualTo (-10836), "WrongPropertyType"); + Assert.That ((int) MidiError.NoCurrentSetup, Is.EqualTo (-10837), "NoCurrentSetup"); + Assert.That ((int) MidiError.MessageSendErr, Is.EqualTo (-10838), "MessageSendErr"); + Assert.That ((int) MidiError.ServerStartErr, Is.EqualTo (-10839), "ServerStartErr"); + Assert.That ((int) MidiError.SetupFormatErr, Is.EqualTo (-10840), "SetupFormatErr"); + Assert.That ((int) MidiError.WrongThread, Is.EqualTo (-10841), "WrongThread"); + Assert.That ((int) MidiError.ObjectNotFound, Is.EqualTo (-10842), "ObjectNotFound"); + Assert.That ((int) MidiError.IDNotUnique, Is.EqualTo (-10843), "IDNotUnique"); + Assert.That ((int) MidiError.NotPermitted, Is.EqualTo (-10844), "NotPermitted"); + } + } +} + +#endif diff --git a/tests/monotouch-test/CoreMidi/MidiDeviceTest.cs b/tests/monotouch-test/CoreMidi/MidiDeviceTest.cs new file mode 100644 index 000000000000..56aa4d80a9cc --- /dev/null +++ b/tests/monotouch-test/CoreMidi/MidiDeviceTest.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +#if !__TVOS__ +using System; +using System.Diagnostics; + +using CoreMidi; +using Foundation; + +using NUnit.Framework; + +namespace MonoTouchFixtures.CoreMidi { + [TestFixture] + [Preserve (AllMembers = true)] + public class MidiDeviceTest { + [Test] + public void ExternalDevice () + { + using var device = Midi.CreateExternalDevice ("MonoTouchTestMidiTestDevice", "MonoTouchTestMidiTestManufacturer", "MonoTouchTestMidiTestModel", out var status); + Assert.That (device, Is.Not.Null, "Device"); + Assert.That (status, Is.EqualTo (MidiError.Ok), "Status"); + if (device is not null) { + var rv = MidiSetup.AddExternalDevice (device); + Assert.That (rv, Is.EqualTo (MidiError.Ok), "Add Status"); + rv = MidiSetup.RemoveExternalDevice (device); + Assert.That (rv, Is.EqualTo (MidiError.Ok), "Remove Status"); + } + } + } +} +#endif diff --git a/tests/monotouch-test/CoreMidi/MidiEndpointTest.cs b/tests/monotouch-test/CoreMidi/MidiEndpointTest.cs index 503876358950..39e4eb1bd464 100644 --- a/tests/monotouch-test/CoreMidi/MidiEndpointTest.cs +++ b/tests/monotouch-test/CoreMidi/MidiEndpointTest.cs @@ -9,8 +9,15 @@ // #if !__TVOS__ +using System; +using System.Collections.Generic; + +using AudioToolbox; +using Foundation; using CoreMidi; +using NUnit.Framework; + namespace MonoTouchFixtures.CoreMidi { [TestFixture] [Preserve (AllMembers = true)] @@ -30,6 +37,53 @@ public void CorrectDisposeTest () } }); } + + [Test] + public void SendTest () + { + var anyChecks = false; + + for (var i = 0; i < Midi.DeviceCount; i++) { + using var device = Midi.GetDevice (i); + Assert.IsNotNull (device, "Device"); + for (var e = 0; e < device.EntityCount; e++) { + using var entity = device.GetEntity (e); + var endpoints = new List (); + for (var d = 0; d < entity.Destinations; d++) + endpoints.Add (entity.GetDestination (d)); + for (var d = 0; d < entity.Sources; d++) + endpoints.Add (entity.GetSource (d)); + + foreach (var ep in endpoints) { + Assert.NotNull (ep, "EndPoint"); + + // 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"); + Assert.AreEqual (ref2, IntPtr.Zero, "GetRefCons A 2"); + + ref1 = unchecked((IntPtr) 0xfee1600d); + ref2 = 0x42f00f00; + Assert.AreEqual (AudioQueueStatus.GeneralParamError, (AudioQueueStatus) ep.SetRefCons (ref1, ref2), "SetRefCons B"); + Assert.AreEqual (AudioQueueStatus.GeneralParamError, (AudioQueueStatus) ep.GetRefCons (out ref1, out ref2), "GetRefCons C"); + Assert.AreEqual (ref1, IntPtr.Zero /* 0xfee1600d */, "GetRefCons C 1"); + Assert.AreEqual (ref2, IntPtr.Zero /* 0x42f00f00 */, "GetRefCons C 2"); + + Assert.AreEqual (AudioQueueStatus.GeneralParamError, (AudioQueueStatus) ep.SetRefCons (IntPtr.Zero, IntPtr.Zero), "SetRefCons D"); + + Assert.AreEqual (AudioQueueStatus.GeneralParamError, (AudioQueueStatus) ep.GetRefCons (out ref1, out ref2), "GetRefCons E"); + Assert.AreEqual (ref1, IntPtr.Zero, "GetRefCons E 1"); + Assert.AreEqual (ref2, IntPtr.Zero, "GetRefCons E 2"); + + anyChecks = true; + } + } + } + + if (!anyChecks) + Assert.Inconclusive ("No applicable MidiEntity found."); + } } } #endif diff --git a/tests/monotouch-test/CoreMidi/MidiEventListTest.cs b/tests/monotouch-test/CoreMidi/MidiEventListTest.cs new file mode 100644 index 000000000000..a75ad26ace8b --- /dev/null +++ b/tests/monotouch-test/CoreMidi/MidiEventListTest.cs @@ -0,0 +1,157 @@ +// +// Unit tests for MidiEventPacket +// +// Copyright 2025 Microsoft Corp. All rights reserved. +// + +#if HAS_COREMIDI + +using System; +using System.Collections.Generic; +using System.Linq; + +using CoreMidi; +using Foundation; + +using NUnit.Framework; + +namespace MonoTouchFixtures.CoreMidi { + [TestFixture] + [Preserve (AllMembers = true)] + public class MidiEventListTest { + [Test] + 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"); + }); + } + + [Test] + public void CtorTest_Size () + { + Exception ex; + + Assert.Multiple (() => { + ex = Assert.Throws (() => 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 (() => 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 (() => 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 (() => 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"); + }); + } + + [Test] + 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"); + }); + } + + [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"); + }); + } + + [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"); + }); + } + + [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"); + }); + } + + [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 (); + 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"); + }); + } + } +} + +#endif diff --git a/tests/monotouch-test/CoreMidi/MidiEventPacketTest.cs b/tests/monotouch-test/CoreMidi/MidiEventPacketTest.cs new file mode 100644 index 000000000000..c4823cb0b95d --- /dev/null +++ b/tests/monotouch-test/CoreMidi/MidiEventPacketTest.cs @@ -0,0 +1,130 @@ +// +// Unit tests for MidiEventPacket +// +// Copyright 2025 Microsoft Corp. All rights reserved. +// + +#if HAS_COREMIDI + +using System; +using System.Linq; + +using CoreMidi; +using Foundation; + +using NUnit.Framework; + +namespace MonoTouchFixtures.CoreMidi { + [TestFixture] + [Preserve (AllMembers = true)] + public class MidiEventPacketTest { + [Test] + public void Default () + { + Assert.Multiple (() => { + Exception ex; + uint v; + + var value = new MidiEventPacket (); + Assert.That (value.Timestamp, Is.EqualTo (0), "Timestamp"); + Assert.That (value.WordCount, Is.EqualTo (0), "WordCount"); + Assert.That (value.Words.Length, Is.EqualTo (0), "WordCount"); + + ex = Assert.Throws (() => v = value [-1], $"Index #-1"); + Assert.That (ex.Message, Does.Contain ("index must be positive."), $"Index #-1 message"); + + for (var i = 0; i < 64; i++) { + ex = Assert.Throws (() => v = value [i], $"Index #{i}"); + Assert.That (ex.Message, Does.Contain ("index must be less than WordCount."), $"Index #{i} message"); + } + + ex = Assert.Throws (() => v = value [64], $"Index #64"); + Assert.That (ex.Message, Does.Contain ("index must be less than 64."), $"Index #64 message"); + }); + } + + [Test] + public void Roundtrips () + { + Assert.Multiple (() => { + Exception ex; + + var value = new MidiEventPacket (); + + // Timestamp + value.Timestamp = 2; + Assert.That (value.Timestamp, Is.EqualTo (2), "Timestamp"); + + value.Timestamp = ulong.MinValue; + Assert.That (value.Timestamp, Is.EqualTo (ulong.MinValue), "Timestamp #2"); + + value.Timestamp = ulong.MaxValue; + Assert.That (value.Timestamp, Is.EqualTo (ulong.MaxValue), "Timestamp #3"); + + // WordCount + + value.WordCount = 3; + Assert.That (value.WordCount, Is.EqualTo (3), "WordCount"); + Assert.That (value.Words.Length, Is.EqualTo (3), "WordCount"); + + value.WordCount = uint.MinValue; + Assert.That (value.WordCount, Is.EqualTo (uint.MinValue), "WordCount #2"); + Assert.That (value.Words.Length, Is.EqualTo (uint.MinValue), "WordCount #2"); + + ex = Assert.Throws (() => value.WordCount = uint.MaxValue, "WordCount #3"); + Assert.That (ex.Message, Does.Contain ("WordCount can't be higher than 64."), $"WordCount #3 message"); + + ex = Assert.Throws (() => value.WordCount = 65, "WordCount #4"); + Assert.That (ex.Message, Does.Contain ("WordCount can't be higher than 64."), $"WordCount #4 message"); + + value.WordCount = 64; + Assert.That (value.WordCount, Is.EqualTo (64), "WordCount #5"); + Assert.That (value.Words.Length, Is.EqualTo (64), "WordCount #5"); + for (var i = 0; i < value.WordCount; i++) + Assert.That (value.Words [i], Is.EqualTo (0), $"WordCount #5 - {i}"); + for (var i = 0; i < value.WordCount; i++) + Assert.That (value [i], Is.EqualTo (0), $"WordCount #5 - idx {i}"); + + // Words + + Assert.Throws (() => value.Words = null, "Words Null"); + + value.Words = new uint [0]; + Assert.That (value.WordCount, Is.EqualTo (0), "Words #1"); + Assert.That (value.Words.Length, Is.EqualTo (0), "Words #1 - Length"); + + value.Words = new uint [] { 2 }; + Assert.That (value.WordCount, Is.EqualTo (1), "Words #2"); + Assert.That (value.Words.Length, Is.EqualTo (1), "Words #2 - Length"); + Assert.That (value.Words [0], Is.EqualTo (2), "Words #2 - element"); + Assert.That (value [0], Is.EqualTo (2), "Words #2 - idx"); + + ex = Assert.Throws (() => value.Words = new uint [65], "Words #3"); + Assert.That (ex.Message, Does.Contain ("WordCount can't be higher than 64."), $"Words #3 message"); + + var array = Enumerable.Range (1, 64).Select (v => (uint) (v * 2)).ToArray (); + value.Words = Enumerable.Range (1, 64).Select (v => (uint) (v * 2)).ToArray (); + Assert.That (value.WordCount, Is.EqualTo (64), "Words #5"); + Assert.That (value.Words.Length, Is.EqualTo (64), "Words #5 - Length"); + for (var i = 0; i < 64; i++) { + Assert.That (value.Words [i], Is.EqualTo ((i + 1) * 2), $"Words #5 - element {i}"); + Assert.That (value [i], Is.EqualTo ((i + 1) * 2), $"Words #5 - indexer {i}"); + Assert.That (array [i], Is.EqualTo ((i + 1) * 2), $"Words #5 - array {i}"); + } + + // indexer + value.Words = new uint [64]; + Assert.That (value.WordCount, Is.EqualTo (64), "indexer #1"); + Assert.That (value.Words.Length, Is.EqualTo (64), "indexer #1 - Length"); + for (var i = 0; i < 64; i++) { + Assert.That (value [i], Is.EqualTo (0), $"indexer #1 - element {i} - 1"); + var v = (uint) ((i + 3) * 3); + value [i] = v; + Assert.That (value [i], Is.EqualTo (v), $"indexer #1 - element {i} - 2"); + } + }); + } + } +} + +#endif // HAS_COREMIDI diff --git a/tests/xtro-sharpie/api-annotations-dotnet/MacCatalyst-CoreMIDI.ignore b/tests/xtro-sharpie/api-annotations-dotnet/MacCatalyst-CoreMIDI.ignore index ca7683dfa9c4..5dd862cb92f5 100644 --- a/tests/xtro-sharpie/api-annotations-dotnet/MacCatalyst-CoreMIDI.ignore +++ b/tests/xtro-sharpie/api-annotations-dotnet/MacCatalyst-CoreMIDI.ignore @@ -1,30 +1,2 @@ -# no known use -!missing-pinvoke! MIDIEndpointGetRefCons is not bound -!missing-pinvoke! MIDIEndpointSetRefCons is not bound - -# old apis that have been ignored, please refer to issue https://github.com/dotnet/macios/issues/4452 +# There's no need to bind this P/Invoke, we bind/use 'MIDIClientCreate' which provide the exact same capabilities. !missing-pinvoke! MIDIClientCreateWithBlock is not bound -!missing-pinvoke! MIDIDeviceCreate is not bound -!missing-pinvoke! MIDIDeviceDispose is not bound -!missing-pinvoke! MIDIDeviceRemoveEntity is not bound -!missing-pinvoke! MIDIEntityAddOrRemoveEndpoints is not bound -!missing-pinvoke! MIDIExternalDeviceCreate is not bound -!missing-pinvoke! MIDIGetDriverDeviceList is not bound -!missing-pinvoke! MIDIGetDriverIORunLoop is not bound -!missing-pinvoke! MIDISendSysex is not bound -!missing-pinvoke! MIDISetupAddDevice is not bound -!missing-pinvoke! MIDISetupAddExternalDevice is not bound -!missing-pinvoke! MIDISetupRemoveDevice is not bound -!missing-pinvoke! MIDISetupRemoveExternalDevice is not bound - -# same as the above, we should bind all of them, these have been added on Xcode 12 beta 2 -# https://github.com/dotnet/macios/issues/4452#issuecomment-660220392 -!missing-pinvoke! MIDIDestinationCreateWithProtocol is not bound -!missing-pinvoke! MIDIDeviceNewEntity is not bound -!missing-pinvoke! MIDIInputPortCreateWithProtocol is not bound -!missing-pinvoke! MIDIReceivedEventList is not bound -!missing-pinvoke! MIDISendEventList is not bound -!missing-pinvoke! MIDISourceCreateWithProtocol is not bound -!missing-pinvoke! MIDIEventPacketSysexBytesForGroup is not bound -!missing-pinvoke! MIDISendUMPSysex is not bound -!missing-pinvoke! MIDISendUMPSysex8 is not bound diff --git a/tests/xtro-sharpie/api-annotations-dotnet/common-CoreMIDI.ignore b/tests/xtro-sharpie/api-annotations-dotnet/common-CoreMIDI.ignore index 166e9451a6be..d5d48a5d41b7 100644 --- a/tests/xtro-sharpie/api-annotations-dotnet/common-CoreMIDI.ignore +++ b/tests/xtro-sharpie/api-annotations-dotnet/common-CoreMIDI.ignore @@ -1,4 +1,2 @@ # https://github.com/dotnet/macios/issues/4452#issuecomment-660220392 -!missing-pinvoke! MIDIEventListAdd is not bound -!missing-pinvoke! MIDIEventListInit is not bound !missing-pinvoke! MIDIEventListForEachEvent is not bound diff --git a/tests/xtro-sharpie/api-annotations-dotnet/iOS-CoreMIDI.ignore b/tests/xtro-sharpie/api-annotations-dotnet/iOS-CoreMIDI.ignore index ca7683dfa9c4..5dd862cb92f5 100644 --- a/tests/xtro-sharpie/api-annotations-dotnet/iOS-CoreMIDI.ignore +++ b/tests/xtro-sharpie/api-annotations-dotnet/iOS-CoreMIDI.ignore @@ -1,30 +1,2 @@ -# no known use -!missing-pinvoke! MIDIEndpointGetRefCons is not bound -!missing-pinvoke! MIDIEndpointSetRefCons is not bound - -# old apis that have been ignored, please refer to issue https://github.com/dotnet/macios/issues/4452 +# There's no need to bind this P/Invoke, we bind/use 'MIDIClientCreate' which provide the exact same capabilities. !missing-pinvoke! MIDIClientCreateWithBlock is not bound -!missing-pinvoke! MIDIDeviceCreate is not bound -!missing-pinvoke! MIDIDeviceDispose is not bound -!missing-pinvoke! MIDIDeviceRemoveEntity is not bound -!missing-pinvoke! MIDIEntityAddOrRemoveEndpoints is not bound -!missing-pinvoke! MIDIExternalDeviceCreate is not bound -!missing-pinvoke! MIDIGetDriverDeviceList is not bound -!missing-pinvoke! MIDIGetDriverIORunLoop is not bound -!missing-pinvoke! MIDISendSysex is not bound -!missing-pinvoke! MIDISetupAddDevice is not bound -!missing-pinvoke! MIDISetupAddExternalDevice is not bound -!missing-pinvoke! MIDISetupRemoveDevice is not bound -!missing-pinvoke! MIDISetupRemoveExternalDevice is not bound - -# same as the above, we should bind all of them, these have been added on Xcode 12 beta 2 -# https://github.com/dotnet/macios/issues/4452#issuecomment-660220392 -!missing-pinvoke! MIDIDestinationCreateWithProtocol is not bound -!missing-pinvoke! MIDIDeviceNewEntity is not bound -!missing-pinvoke! MIDIInputPortCreateWithProtocol is not bound -!missing-pinvoke! MIDIReceivedEventList is not bound -!missing-pinvoke! MIDISendEventList is not bound -!missing-pinvoke! MIDISourceCreateWithProtocol is not bound -!missing-pinvoke! MIDIEventPacketSysexBytesForGroup is not bound -!missing-pinvoke! MIDISendUMPSysex is not bound -!missing-pinvoke! MIDISendUMPSysex8 is not bound diff --git a/tests/xtro-sharpie/api-annotations-dotnet/macOS-CoreMIDI.ignore b/tests/xtro-sharpie/api-annotations-dotnet/macOS-CoreMIDI.ignore index 83b130198d75..5dd862cb92f5 100644 --- a/tests/xtro-sharpie/api-annotations-dotnet/macOS-CoreMIDI.ignore +++ b/tests/xtro-sharpie/api-annotations-dotnet/macOS-CoreMIDI.ignore @@ -1,34 +1,2 @@ -# deprecated pinvokes and not binded. -!missing-pinvoke! MIDIDriverEnableMonitoring is not bound - -# no known use -!missing-pinvoke! MIDIEndpointGetRefCons is not bound -!missing-pinvoke! MIDIEndpointSetRefCons is not bound - -# old apis that have been ignored, please refer to issue https://github.com/dotnet/macios/issues/4452 +# There's no need to bind this P/Invoke, we bind/use 'MIDIClientCreate' which provide the exact same capabilities. !missing-pinvoke! MIDIClientCreateWithBlock is not bound -!missing-pinvoke! MIDIDeviceCreate is not bound -!missing-pinvoke! MIDIDeviceDispose is not bound -!missing-pinvoke! MIDIDeviceRemoveEntity is not bound -!missing-pinvoke! MIDIEntityAddOrRemoveEndpoints is not bound -!missing-pinvoke! MIDIExternalDeviceCreate is not bound -!missing-pinvoke! MIDIGetDriverDeviceList is not bound -!missing-pinvoke! MIDIGetDriverIORunLoop is not bound -!missing-pinvoke! MIDISendSysex is not bound -!missing-pinvoke! MIDISetupAddDevice is not bound -!missing-pinvoke! MIDISetupAddExternalDevice is not bound -!missing-pinvoke! MIDISetupRemoveDevice is not bound -!missing-pinvoke! MIDISetupRemoveExternalDevice is not bound - -# same as the above, we should bind all of them, these have been added on Xcode 12 beta 2 -# https://github.com/dotnet/macios/issues/4452#issuecomment-660220392 -!missing-pinvoke! MIDIDestinationCreateWithProtocol is not bound -!missing-pinvoke! MIDIDeviceNewEntity is not bound -!missing-pinvoke! MIDIInputPortCreateWithProtocol is not bound -!missing-pinvoke! MIDIReceivedEventList is not bound -!missing-pinvoke! MIDISendEventList is not bound -!missing-pinvoke! MIDISourceCreateWithProtocol is not bound - -!missing-pinvoke! MIDIEventPacketSysexBytesForGroup is not bound -!missing-pinvoke! MIDISendUMPSysex is not bound -!missing-pinvoke! MIDISendUMPSysex8 is not bound diff --git a/tests/xtro-sharpie/api-annotations-dotnet/tvOS-CoreMIDI.ignore b/tests/xtro-sharpie/api-annotations-dotnet/tvOS-CoreMIDI.ignore deleted file mode 100644 index 9e8bc217096d..000000000000 --- a/tests/xtro-sharpie/api-annotations-dotnet/tvOS-CoreMIDI.ignore +++ /dev/null @@ -1,7 +0,0 @@ -# CoreMIDI not supported for TV in Xcode14 -!missing-enum! MIDINotificationMessageID not bound -!missing-enum! MIDIObjectType not bound -!missing-enum! MIDITransformControlType not bound -!missing-enum! MIDITransformType not bound -!missing-pinvoke! MIDIBluetoothDriverActivateAllConnections is not bound -!missing-pinvoke! MIDIBluetoothDriverDisconnect is not bound From 5247eb1d20762aac29f16c961d7ad7d12c935161 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 23 Jun 2026 16:13:28 +0200 Subject: [PATCH 2/4] [CoreMidi] Add MIDIEventListForEachEvent binding and MidiUniversalMessage. 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> --- src/CoreMidi/MidiEventList.cs | 39 ++ src/CoreMidi/MidiUniversalMessage.cs | 608 ++++++++++++++++++ src/frameworks.sources | 1 + .../CoreMidi/MidiEventListTest.cs | 97 +++ 4 files changed, 745 insertions(+) create mode 100644 src/CoreMidi/MidiUniversalMessage.cs diff --git a/src/CoreMidi/MidiEventList.cs b/src/CoreMidi/MidiEventList.cs index 4d6fa781b199..7b59ebe03bc1 100644 --- a/src/CoreMidi/MidiEventList.cs +++ b/src/CoreMidi/MidiEventList.cs @@ -263,8 +263,47 @@ public unsafe void Iterate (MidiEventListIterator callback) callback (ref Unsafe.AsRef (packet)); } } + + /// Parse each Universal MIDI Packet (UMP) in this list, invoking the specified for each parsed message. + /// The function to call for each parsed message. Unknown messages are reported with their raw words. + /// This is a binding for the native MIDIEventListForEachEvent function, which parses each UMP and fills a struct. + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + public unsafe void ForEachEvent (MidiUniversalMessageVisitor visitor) + { + ArgumentNullException.ThrowIfNull (visitor); + + var gch = GCHandle.Alloc (visitor); + try { + MIDIEventListForEachEvent (midiDataPointer, &TrampolineForEachEvent, (void*) GCHandle.ToIntPtr (gch)); + } finally { + gch.Free (); + } + } + + [UnmanagedCallersOnly] + static unsafe void TrampolineForEachEvent (void* context, ulong timeStamp, MidiUniversalMessage message) + { + var gch = GCHandle.FromIntPtr ((IntPtr) context); + if (gch.Target is MidiUniversalMessageVisitor visitor) + visitor (timeStamp, message); + } + + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + [DllImport (Constants.CoreMidiLibrary)] + unsafe static extern void MIDIEventListForEachEvent (MIDIEventList* evtlist, delegate* unmanaged visitor, void* visitorContext); } + /// The delegate type used by . + /// The timestamp of the parsed message. + /// The parsed message. + public delegate void MidiUniversalMessageVisitor (ulong timeStamp, MidiUniversalMessage message); + /// The delegate type used by . /// The current packet found when iterating. public delegate void MidiEventListIterator (ref MidiEventPacket packet); diff --git a/src/CoreMidi/MidiUniversalMessage.cs b/src/CoreMidi/MidiUniversalMessage.cs new file mode 100644 index 000000000000..d2df7badf034 --- /dev/null +++ b/src/CoreMidi/MidiUniversalMessage.cs @@ -0,0 +1,608 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +#nullable enable + +#pragma warning disable CS0649 // Field '...' is never assigned to, and will always have its default value +#pragma warning disable CS0169 // The field '...' is never used + +namespace CoreMidi { + /// A representation of all possible messages stored in a Universal MIDI packet (UMP). + /// + /// This is the managed representation of the native MIDIUniversalMessage struct. The active variant is determined by the property: only the union member matching the message type contains valid data. + /// + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Explicit, Size = 28)] + public struct MidiUniversalMessage { + [FieldOffset (0)] + MidiMessageType type; + [FieldOffset (4)] + byte group; + [FieldOffset (8)] + MidiUniversalMessageUtility utility; + [FieldOffset (8)] + MidiUniversalMessageSystem system; + [FieldOffset (8)] + MidiUniversalMessageChannelVoice1 channelVoice1; + [FieldOffset (8)] + MidiUniversalMessageSysEx sysEx; + [FieldOffset (8)] + MidiUniversalMessageChannelVoice2 channelVoice2; + [FieldOffset (8)] + MidiUniversalMessageData128 data128; + [FieldOffset (8)] + MidiUniversalMessageUnknown unknown; + + /// The message type. Determines which variant in the union is valid. + public MidiMessageType Type => type; + + /// The 4-bit MIDI group this message belongs to. + public byte Group => group; + + /// The utility message data. Valid when is . + public MidiUniversalMessageUtility Utility => utility; + + /// The system message data. Valid when is . + public MidiUniversalMessageSystem System => system; + + /// The MIDI 1.0 channel voice message data. Valid when is . + public MidiUniversalMessageChannelVoice1 ChannelVoice1 => channelVoice1; + + /// The system exclusive (SysEx) message data. Valid when is . + public MidiUniversalMessageSysEx SysEx => sysEx; + + /// The MIDI 2.0 channel voice message data. Valid when is . + public MidiUniversalMessageChannelVoice2 ChannelVoice2 => channelVoice2; + + /// The 128-bit data message data. Valid when is . + public MidiUniversalMessageData128 Data128 => data128; + + /// The raw words of an unknown message. Valid when is not a recognized message type. + public MidiUniversalMessageUnknown Unknown => unknown; + } + + /// A utility message in a . + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Explicit, Size = 8)] + public struct MidiUniversalMessageUtility { + [FieldOffset (0)] + MidiUtilityStatus status; + [FieldOffset (4)] + ushort jitterReductionClock; + [FieldOffset (4)] + ushort jitterReductionTimestamp; + + /// The status determining which value is valid. + public MidiUtilityStatus Status => status; + + /// The jitter reduction clock. Valid when is . + public ushort JitterReductionClock => jitterReductionClock; + + /// The jitter reduction timestamp. Valid when is . + public ushort JitterReductionTimestamp => jitterReductionTimestamp; + } + + /// A system message in a . + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Explicit, Size = 8)] + public struct MidiUniversalMessageSystem { + [FieldOffset (0)] + MidiSystemStatus status; + [FieldOffset (4)] + byte timeCode; + [FieldOffset (4)] + ushort songPositionPointer; + [FieldOffset (4)] + byte songSelect; + + /// The status determining which value is valid. + public MidiSystemStatus Status => status; + + /// The MIDI time code. Valid when is . + public byte TimeCode => timeCode; + + /// The song position pointer. Valid when is . + public ushort SongPositionPointer => songPositionPointer; + + /// The selected song. Valid when is . + public byte SongSelect => songSelect; + } + + /// The note data of a MIDI 1.0 channel voice message. + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + public struct MidiUniversalMessageChannelVoice1Note { + byte number; + byte velocity; + + /// The 7-bit note number. + public byte Number => number; + + /// The 7-bit note velocity. + public byte Velocity => velocity; + } + + /// The poly pressure data of a MIDI 1.0 channel voice message. + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + public struct MidiUniversalMessageChannelVoice1PolyPressure { + byte noteNumber; + byte pressure; + + /// The 7-bit note number. + public byte NoteNumber => noteNumber; + + /// The 7-bit poly pressure data. + public byte Pressure => pressure; + } + + /// The control change data of a MIDI 1.0 channel voice message. + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + public struct MidiUniversalMessageChannelVoice1ControlChange { + byte index; + byte data; + + /// The 7-bit index of the control parameter. + public byte Index => index; + + /// The 7-bit value of the control parameter. + public byte Data => data; + } + + /// A MIDI 1.0 channel voice message in a . + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Explicit, Size = 12)] + public struct MidiUniversalMessageChannelVoice1 { + [FieldOffset (0)] + MidiCVStatus status; + [FieldOffset (4)] + byte channel; + [FieldOffset (8)] + MidiUniversalMessageChannelVoice1Note note; + [FieldOffset (8)] + MidiUniversalMessageChannelVoice1PolyPressure polyPressure; + [FieldOffset (8)] + MidiUniversalMessageChannelVoice1ControlChange controlChange; + [FieldOffset (8)] + byte program; + [FieldOffset (8)] + byte channelPressure; + [FieldOffset (8)] + ushort pitchBend; + + /// The status determining which value is valid. + public MidiCVStatus Status => status; + + /// The MIDI channel (0-15). + public byte Channel => channel; + + /// The note data. Valid when is or . + public MidiUniversalMessageChannelVoice1Note Note => note; + + /// The poly pressure data. Valid when is . + public MidiUniversalMessageChannelVoice1PolyPressure PolyPressure => polyPressure; + + /// The control change data. Valid when is . + public MidiUniversalMessageChannelVoice1ControlChange ControlChange => controlChange; + + /// The 7-bit program number. Valid when is . + public byte Program => program; + + /// The 7-bit channel pressure. Valid when is . + public byte ChannelPressure => channelPressure; + + /// The pitch bend value. Valid when is . + public ushort PitchBend => pitchBend; + } + + /// A system exclusive (SysEx) message in a . + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Sequential)] + public unsafe struct MidiUniversalMessageSysEx { + MidiSysExStatus status; + byte channel; + fixed byte data [6]; + byte reserved; + + /// The status determining how the message should be interpreted. + public MidiSysExStatus Status => status; + + /// The MIDI channel (0-15). + public byte Channel => channel; + + /// The SysEx data (6 bytes, 7-bit values each). + /// A 6-element array with the SysEx data. + public byte [] Data { + get { + var rv = new byte [6]; + for (var i = 0; i < rv.Length; i++) + rv [i] = data [i]; + return rv; + } + } + } + + /// The note data of a MIDI 2.0 channel voice message. + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + public struct MidiUniversalMessageChannelVoice2Note { + byte number; + MidiNoteAttribute attributeType; + ushort velocity; + ushort attribute; + + /// The 7-bit note number. + public byte Number => number; + + /// The attribute type. + public MidiNoteAttribute AttributeType => attributeType; + + /// The note velocity. + public ushort Velocity => velocity; + + /// The attribute data. + public ushort Attribute => attribute; + } + + /// The poly pressure data of a MIDI 2.0 channel voice message. + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + public struct MidiUniversalMessageChannelVoice2PolyPressure { + byte noteNumber; + byte reserved; + uint pressure; + + /// The 7-bit note number. + public byte NoteNumber => noteNumber; + + /// The pressure value. + public uint Pressure => pressure; + } + + /// The control change data of a MIDI 2.0 channel voice message. + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + public struct MidiUniversalMessageChannelVoice2ControlChange { + byte index; + byte reserved; + uint data; + + /// The 7-bit controller number. + public byte Index => index; + + /// The controller value. + public uint Data => data; + } + + /// The program change data of a MIDI 2.0 channel voice message. + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + public struct MidiUniversalMessageChannelVoice2ProgramChange { + MidiProgramChangeOptions options; + byte program; + byte reserved0; + byte reserved1; + ushort bank; + + /// The program change options. + public MidiProgramChangeOptions Options => options; + + /// The 7-bit program number. + public byte Program => program; + + /// The 14-bit bank. + public ushort Bank => bank; + } + + /// The channel pressure data of a MIDI 2.0 channel voice message. + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + public struct MidiUniversalMessageChannelVoice2ChannelPressure { + uint data; + byte reserved0; + byte reserved1; + + /// The channel pressure data. + public uint Data => data; + } + + /// The pitch bend data of a MIDI 2.0 channel voice message. + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + public struct MidiUniversalMessageChannelVoice2PitchBend { + uint data; + byte reserved0; + byte reserved1; + + /// The pitch bend data. + public uint Data => data; + } + + /// The per-note controller data of a MIDI 2.0 channel voice message. + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + public struct MidiUniversalMessageChannelVoice2PerNoteController { + byte noteNumber; + byte index; + uint data; + + /// The 7-bit note number. + public byte NoteNumber => noteNumber; + + /// The 7-bit controller number. + public byte Index => index; + + /// The controller data. + public uint Data => data; + } + + /// The registered/assignable controller data of a MIDI 2.0 channel voice message. + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + public struct MidiUniversalMessageChannelVoice2Controller { + byte bank; + byte index; + uint data; + + /// The 7-bit bank. + public byte Bank => bank; + + /// The 7-bit controller number. + public byte Index => index; + + /// The controller data. + public uint Data => data; + } + + /// The per-note pitch bend data of a MIDI 2.0 channel voice message. + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + public struct MidiUniversalMessageChannelVoice2PerNotePitchBend { + byte noteNumber; + byte reserved; + uint bend; + + /// The 7-bit note number. + public byte NoteNumber => noteNumber; + + /// The per-note pitch bend value. + public uint Bend => bend; + } + + /// The per-note management data of a MIDI 2.0 channel voice message. + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + public struct MidiUniversalMessageChannelVoice2PerNoteManagement { + byte note; + MidiPerNoteManagementOptions options; + byte reserved0; + byte reserved1; + byte reserved2; + byte reserved3; + + /// The 7-bit note number. + public byte Note => note; + + /// The per-note management options. + public MidiPerNoteManagementOptions Options => options; + } + + /// A MIDI 2.0 channel voice message in a . + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Explicit, Size = 16)] + public struct MidiUniversalMessageChannelVoice2 { + [FieldOffset (0)] + MidiCVStatus status; + [FieldOffset (4)] + byte channel; + [FieldOffset (8)] + MidiUniversalMessageChannelVoice2Note note; + [FieldOffset (8)] + MidiUniversalMessageChannelVoice2PolyPressure polyPressure; + [FieldOffset (8)] + MidiUniversalMessageChannelVoice2ControlChange controlChange; + [FieldOffset (8)] + MidiUniversalMessageChannelVoice2ProgramChange programChange; + [FieldOffset (8)] + MidiUniversalMessageChannelVoice2ChannelPressure channelPressure; + [FieldOffset (8)] + MidiUniversalMessageChannelVoice2PitchBend pitchBend; + [FieldOffset (8)] + MidiUniversalMessageChannelVoice2PerNoteController perNoteController; + [FieldOffset (8)] + MidiUniversalMessageChannelVoice2Controller controller; + [FieldOffset (8)] + MidiUniversalMessageChannelVoice2PerNotePitchBend perNotePitchBend; + [FieldOffset (8)] + MidiUniversalMessageChannelVoice2PerNoteManagement perNoteManagement; + + /// The status determining which value is valid. + public MidiCVStatus Status => status; + + /// The MIDI channel. + public byte Channel => channel; + + /// The note data. Valid when is or . + public MidiUniversalMessageChannelVoice2Note Note => note; + + /// The poly pressure data. Valid when is . + public MidiUniversalMessageChannelVoice2PolyPressure PolyPressure => polyPressure; + + /// The control change data. Valid when is . + public MidiUniversalMessageChannelVoice2ControlChange ControlChange => controlChange; + + /// The program change data. Valid when is . + public MidiUniversalMessageChannelVoice2ProgramChange ProgramChange => programChange; + + /// The channel pressure data. Valid when is . + public MidiUniversalMessageChannelVoice2ChannelPressure ChannelPressure => channelPressure; + + /// The pitch bend data. Valid when is . + public MidiUniversalMessageChannelVoice2PitchBend PitchBend => pitchBend; + + /// The per-note controller data. Valid when is or . + public MidiUniversalMessageChannelVoice2PerNoteController PerNoteController => perNoteController; + + /// The registered/assignable controller data. Valid when is one of , , or . + public MidiUniversalMessageChannelVoice2Controller Controller => controller; + + /// The per-note pitch bend data. Valid when is . + public MidiUniversalMessageChannelVoice2PerNotePitchBend PerNotePitchBend => perNotePitchBend; + + /// The per-note management data. Valid when is . + public MidiUniversalMessageChannelVoice2PerNoteManagement PerNoteManagement => perNoteManagement; + } + + /// The 8-bit system exclusive (SysEx8) data of a 128-bit data message. + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Sequential)] + public unsafe struct MidiUniversalMessageSysEx8 { + byte byteCount; + byte streamID; + fixed byte data [13]; + byte reserved; + + /// The byte count of the data including the stream ID (1-14 bytes). + public byte ByteCount => byteCount; + + /// The stream ID. + public byte StreamId => streamID; + + /// The SysEx8 data (13 bytes). + /// A 13-element array with the SysEx8 data. + public byte [] Data { + get { + var rv = new byte [13]; + for (var i = 0; i < rv.Length; i++) + rv [i] = data [i]; + return rv; + } + } + } + + /// The mixed data set of a 128-bit data message. + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Sequential)] + public unsafe struct MidiUniversalMessageMixedDataSet { + byte mdsID; + fixed byte data [14]; + byte reserved; + + /// The mixed data set ID. + public byte MixedDataSetId => mdsID; + + /// The mixed data set data (14 bytes). + /// A 14-element array with the mixed data set data. + public byte [] Data { + get { + var rv = new byte [14]; + for (var i = 0; i < rv.Length; i++) + rv [i] = data [i]; + return rv; + } + } + } + + /// A 128-bit data message in a . + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Explicit, Size = 20)] + public struct MidiUniversalMessageData128 { + [FieldOffset (0)] + MidiSysExStatus status; + [FieldOffset (4)] + MidiUniversalMessageSysEx8 sysEx8; + [FieldOffset (4)] + MidiUniversalMessageMixedDataSet mixedDataSet; + + /// The status determining which value is valid. + public MidiSysExStatus Status => status; + + /// The SysEx8 data. Valid when is one of , , or . + public MidiUniversalMessageSysEx8 SysEx8 => sysEx8; + + /// The mixed data set. Valid when is or . + public MidiUniversalMessageMixedDataSet MixedDataSet => mixedDataSet; + } + + /// The raw words of an unknown message in a . + [SupportedOSPlatform ("ios15.0")] + [SupportedOSPlatform ("tvos15.0")] + [SupportedOSPlatform ("macos")] + [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Sequential)] + public unsafe struct MidiUniversalMessageUnknown { + fixed uint words [4]; + + /// The raw words of the message (up to four 32-bit words). + /// A 4-element array with the raw words. + public uint [] Words { + get { + var rv = new uint [4]; + for (var i = 0; i < rv.Length; i++) + rv [i] = words [i]; + return rv; + } + } + } +} + +#pragma warning restore CS0649 +#pragma warning restore CS0169 diff --git a/src/frameworks.sources b/src/frameworks.sources index bad26718ae9e..377cb8263e5d 100644 --- a/src/frameworks.sources +++ b/src/frameworks.sources @@ -635,6 +635,7 @@ COREMIDI_SOURCES = \ CoreMidi/MidiBluetoothDriver.cs \ CoreMidi/MidiEventList.cs \ CoreMidi/MidiEventPacket.cs \ + CoreMidi/MidiUniversalMessage.cs \ # CoreML diff --git a/tests/monotouch-test/CoreMidi/MidiEventListTest.cs b/tests/monotouch-test/CoreMidi/MidiEventListTest.cs index a75ad26ace8b..a38d1cc1d82c 100644 --- a/tests/monotouch-test/CoreMidi/MidiEventListTest.cs +++ b/tests/monotouch-test/CoreMidi/MidiEventListTest.cs @@ -151,6 +151,103 @@ public void IteratorTest () Assert.That (packetList [0].Words, Is.EqualTo (new uint [] { 1, 2, 3, 4, 5, 6 }), "packetList[0].Words"); }); } + + // Build a MIDI 1.0 channel voice Note On UMP (a single 32-bit word). + static uint Midi1NoteOn (byte group, byte channel, byte note, byte velocity) + => ((uint) MidiMessageType.ChannelVoice1 << 28) | ((uint) group << 24) | (0x9u << 20) | ((uint) channel << 16) | ((uint) note << 8) | velocity; + + [Test] + public void ForEachEventTest_Null () + { + var obj = new MidiEventList (MidiProtocolId.Protocol_1_0); + Assert.Throws (() => obj.ForEachEvent (null), "ForEachEvent (null)"); + } + + [Test] + public void ForEachEventTest_Empty () + { + var obj = new MidiEventList (MidiProtocolId.Protocol_1_0); + var count = 0; + obj.ForEachEvent ((ulong timeStamp, MidiUniversalMessage message) => count++); + Assert.That (count, Is.EqualTo (0), "count"); + } + + [Test] + public void ForEachEventTest_Midi1NoteOn () + { + var obj = new MidiEventList (MidiProtocolId.Protocol_1_0); + Assert.That (obj.Add (1234, new uint [] { Midi1NoteOn (0, 3, 60, 100) }), Is.True, "Add"); + + var messages = new List<(ulong TimeStamp, MidiUniversalMessage Message)> (); + obj.ForEachEvent ((ulong timeStamp, MidiUniversalMessage message) => messages.Add ((timeStamp, message))); + + Assert.That (messages.Count, Is.EqualTo (1), "Count"); + Assert.Multiple (() => { + Assert.That (messages [0].TimeStamp, Is.EqualTo ((ulong) 1234), "TimeStamp"); + var message = messages [0].Message; + Assert.That (message.Type, Is.EqualTo (MidiMessageType.ChannelVoice1), "Type"); + Assert.That (message.ChannelVoice1.Status, Is.EqualTo (MidiCVStatus.NoteOn), "Status"); + Assert.That (message.ChannelVoice1.Channel, Is.EqualTo (3), "Channel"); + Assert.That (message.ChannelVoice1.Note.Number, Is.EqualTo (60), "Note.Number"); + Assert.That (message.ChannelVoice1.Note.Velocity, Is.EqualTo (100), "Note.Velocity"); + }); + } + + [Test] + public void ForEachEventTest_Midi2NoteOn () + { + var obj = new MidiEventList (MidiProtocolId.Protocol_2_0); + // A MIDI 2.0 channel voice Note On UMP (two 32-bit words). + var word0 = ((uint) MidiMessageType.ChannelVoice2 << 28) | (0x9u << 20) | (2u << 16) | (60u << 8) | (uint) MidiNoteAttribute.None; + var word1 = (0xCAFEu << 16) | 0xBEEFu; + Assert.That (obj.Add (4321, new uint [] { word0, word1 }), Is.True, "Add"); + + var messages = new List<(ulong TimeStamp, MidiUniversalMessage Message)> (); + obj.ForEachEvent ((ulong timeStamp, MidiUniversalMessage message) => messages.Add ((timeStamp, message))); + + Assert.That (messages.Count, Is.EqualTo (1), "Count"); + Assert.Multiple (() => { + Assert.That (messages [0].TimeStamp, Is.EqualTo ((ulong) 4321), "TimeStamp"); + var message = messages [0].Message; + Assert.That (message.Type, Is.EqualTo (MidiMessageType.ChannelVoice2), "Type"); + Assert.That (message.ChannelVoice2.Status, Is.EqualTo (MidiCVStatus.NoteOn), "Status"); + Assert.That (message.ChannelVoice2.Channel, Is.EqualTo (2), "Channel"); + Assert.That (message.ChannelVoice2.Note.Number, Is.EqualTo (60), "Note.Number"); + Assert.That (message.ChannelVoice2.Note.AttributeType, Is.EqualTo (MidiNoteAttribute.None), "Note.AttributeType"); + Assert.That (message.ChannelVoice2.Note.Velocity, Is.EqualTo (0xCAFE), "Note.Velocity"); + Assert.That (message.ChannelVoice2.Note.Attribute, Is.EqualTo (0xBEEF), "Note.Attribute"); + }); + } + + [Test] + public void ForEachEventTest_HappyBirthday () + { + // The "Happy Birthday" melody as MIDI note numbers. + var melody = new byte [] { + 67, 67, 69, 67, 72, 71, + 67, 67, 69, 67, 74, 72, + 67, 67, 79, 76, 72, 71, 69, + 77, 77, 76, 72, 74, 72, + }; + + var obj = new MidiEventList (MidiProtocolId.Protocol_1_0, 4096); + for (var i = 0; i < melody.Length; i++) + Assert.That (obj.Add ((ulong) (i + 1), new uint [] { Midi1NoteOn (0, 0, melody [i], 96) }), Is.True, $"Add #{i}"); + + var notes = new List (); + var timeStamps = new List (); + obj.ForEachEvent ((ulong timeStamp, MidiUniversalMessage message) => { + Assert.That (message.Type, Is.EqualTo (MidiMessageType.ChannelVoice1), "Type"); + Assert.That (message.ChannelVoice1.Status, Is.EqualTo (MidiCVStatus.NoteOn), "Status"); + notes.Add (message.ChannelVoice1.Note.Number); + timeStamps.Add (timeStamp); + }); + + Assert.Multiple (() => { + Assert.That (notes, Is.EqualTo (new List (melody)), "notes"); + Assert.That (timeStamps, Is.EqualTo (Enumerable.Range (1, melody.Length).Select (v => (ulong) v).ToList ()), "timeStamps"); + }); + } } } From 01b1410d6c7c4d512edcfcb2ad724e52353a6c0d Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 23 Jun 2026 16:39:02 +0200 Subject: [PATCH 3/4] [CoreMidi] Make MidiUniversalMessage blittable (no explicit layout). 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> --- src/CoreMidi/MidiUniversalMessage.cs | 381 +++++++++++------- .../common-CoreMIDI.ignore | 2 - 2 files changed, 232 insertions(+), 151 deletions(-) delete mode 100644 tests/xtro-sharpie/api-annotations-dotnet/common-CoreMIDI.ignore diff --git a/src/CoreMidi/MidiUniversalMessage.cs b/src/CoreMidi/MidiUniversalMessage.cs index d2df7badf034..e30c443ce96c 100644 --- a/src/CoreMidi/MidiUniversalMessage.cs +++ b/src/CoreMidi/MidiUniversalMessage.cs @@ -19,26 +19,19 @@ namespace CoreMidi { [SupportedOSPlatform ("tvos15.0")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] - [StructLayout (LayoutKind.Explicit, Size = 28)] - public struct MidiUniversalMessage { - [FieldOffset (0)] + [StructLayout (LayoutKind.Sequential)] + public unsafe struct MidiUniversalMessage { MidiMessageType type; - [FieldOffset (4)] byte group; - [FieldOffset (8)] - MidiUniversalMessageUtility utility; - [FieldOffset (8)] - MidiUniversalMessageSystem system; - [FieldOffset (8)] - MidiUniversalMessageChannelVoice1 channelVoice1; - [FieldOffset (8)] - MidiUniversalMessageSysEx sysEx; - [FieldOffset (8)] - MidiUniversalMessageChannelVoice2 channelVoice2; - [FieldOffset (8)] - MidiUniversalMessageData128 data128; - [FieldOffset (8)] - MidiUniversalMessageUnknown unknown; + byte reserved0; + byte reserved1; + byte reserved2; + // 20 bytes of union storage, starting at offset 8. + uint storage0; + uint storage1; + uint storage2; + uint storage3; + uint storage4; /// The message type. Determines which variant in the union is valid. public MidiMessageType Type => type; @@ -47,25 +40,60 @@ public struct MidiUniversalMessage { public byte Group => group; /// The utility message data. Valid when is . - public MidiUniversalMessageUtility Utility => utility; + public MidiUniversalMessageUtility Utility { + get { + var self = this; + return *(MidiUniversalMessageUtility*) ((byte*) &self + 8); + } + } /// The system message data. Valid when is . - public MidiUniversalMessageSystem System => system; + public MidiUniversalMessageSystem System { + get { + var self = this; + return *(MidiUniversalMessageSystem*) ((byte*) &self + 8); + } + } /// The MIDI 1.0 channel voice message data. Valid when is . - public MidiUniversalMessageChannelVoice1 ChannelVoice1 => channelVoice1; + public MidiUniversalMessageChannelVoice1 ChannelVoice1 { + get { + var self = this; + return *(MidiUniversalMessageChannelVoice1*) ((byte*) &self + 8); + } + } /// The system exclusive (SysEx) message data. Valid when is . - public MidiUniversalMessageSysEx SysEx => sysEx; + public MidiUniversalMessageSysEx SysEx { + get { + var self = this; + return *(MidiUniversalMessageSysEx*) ((byte*) &self + 8); + } + } /// The MIDI 2.0 channel voice message data. Valid when is . - public MidiUniversalMessageChannelVoice2 ChannelVoice2 => channelVoice2; + public MidiUniversalMessageChannelVoice2 ChannelVoice2 { + get { + var self = this; + return *(MidiUniversalMessageChannelVoice2*) ((byte*) &self + 8); + } + } /// The 128-bit data message data. Valid when is . - public MidiUniversalMessageData128 Data128 => data128; + public MidiUniversalMessageData128 Data128 { + get { + var self = this; + return *(MidiUniversalMessageData128*) ((byte*) &self + 8); + } + } /// The raw words of an unknown message. Valid when is not a recognized message type. - public MidiUniversalMessageUnknown Unknown => unknown; + public MidiUniversalMessageUnknown Unknown { + get { + var self = this; + return *(MidiUniversalMessageUnknown*) ((byte*) &self + 8); + } + } } /// A utility message in a . @@ -73,23 +101,19 @@ public struct MidiUniversalMessage { [SupportedOSPlatform ("tvos15.0")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] - [StructLayout (LayoutKind.Explicit, Size = 8)] + [StructLayout (LayoutKind.Sequential)] public struct MidiUniversalMessageUtility { - [FieldOffset (0)] MidiUtilityStatus status; - [FieldOffset (4)] - ushort jitterReductionClock; - [FieldOffset (4)] - ushort jitterReductionTimestamp; + ushort union0; /// The status determining which value is valid. public MidiUtilityStatus Status => status; /// The jitter reduction clock. Valid when is . - public ushort JitterReductionClock => jitterReductionClock; + public ushort JitterReductionClock => union0; /// The jitter reduction timestamp. Valid when is . - public ushort JitterReductionTimestamp => jitterReductionTimestamp; + public ushort JitterReductionTimestamp => union0; } /// A system message in a . @@ -97,28 +121,22 @@ public struct MidiUniversalMessageUtility { [SupportedOSPlatform ("tvos15.0")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] - [StructLayout (LayoutKind.Explicit, Size = 8)] + [StructLayout (LayoutKind.Sequential)] public struct MidiUniversalMessageSystem { - [FieldOffset (0)] MidiSystemStatus status; - [FieldOffset (4)] - byte timeCode; - [FieldOffset (4)] - ushort songPositionPointer; - [FieldOffset (4)] - byte songSelect; + ushort union0; /// The status determining which value is valid. public MidiSystemStatus Status => status; /// The MIDI time code. Valid when is . - public byte TimeCode => timeCode; + public byte TimeCode => (byte) union0; /// The song position pointer. Valid when is . - public ushort SongPositionPointer => songPositionPointer; + public ushort SongPositionPointer => union0; /// The selected song. Valid when is . - public byte SongSelect => songSelect; + public byte SongSelect => (byte) union0; } /// The note data of a MIDI 1.0 channel voice message. @@ -126,6 +144,7 @@ public struct MidiUniversalMessageSystem { [SupportedOSPlatform ("tvos15.0")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Sequential)] public struct MidiUniversalMessageChannelVoice1Note { byte number; byte velocity; @@ -142,6 +161,7 @@ public struct MidiUniversalMessageChannelVoice1Note { [SupportedOSPlatform ("tvos15.0")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Sequential)] public struct MidiUniversalMessageChannelVoice1PolyPressure { byte noteNumber; byte pressure; @@ -158,6 +178,7 @@ public struct MidiUniversalMessageChannelVoice1PolyPressure { [SupportedOSPlatform ("tvos15.0")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Sequential)] public struct MidiUniversalMessageChannelVoice1ControlChange { byte index; byte data; @@ -174,24 +195,14 @@ public struct MidiUniversalMessageChannelVoice1ControlChange { [SupportedOSPlatform ("tvos15.0")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] - [StructLayout (LayoutKind.Explicit, Size = 12)] - public struct MidiUniversalMessageChannelVoice1 { - [FieldOffset (0)] + [StructLayout (LayoutKind.Sequential)] + public unsafe struct MidiUniversalMessageChannelVoice1 { MidiCVStatus status; - [FieldOffset (4)] byte channel; - [FieldOffset (8)] - MidiUniversalMessageChannelVoice1Note note; - [FieldOffset (8)] - MidiUniversalMessageChannelVoice1PolyPressure polyPressure; - [FieldOffset (8)] - MidiUniversalMessageChannelVoice1ControlChange controlChange; - [FieldOffset (8)] - byte program; - [FieldOffset (8)] - byte channelPressure; - [FieldOffset (8)] - ushort pitchBend; + byte reserved0; + byte reserved1; + byte reserved2; + ushort union0; /// The status determining which value is valid. public MidiCVStatus Status => status; @@ -200,22 +211,37 @@ public struct MidiUniversalMessageChannelVoice1 { public byte Channel => channel; /// The note data. Valid when is or . - public MidiUniversalMessageChannelVoice1Note Note => note; + public MidiUniversalMessageChannelVoice1Note Note { + get { + var self = this; + return *(MidiUniversalMessageChannelVoice1Note*) ((byte*) &self + 8); + } + } /// The poly pressure data. Valid when is . - public MidiUniversalMessageChannelVoice1PolyPressure PolyPressure => polyPressure; + public MidiUniversalMessageChannelVoice1PolyPressure PolyPressure { + get { + var self = this; + return *(MidiUniversalMessageChannelVoice1PolyPressure*) ((byte*) &self + 8); + } + } /// The control change data. Valid when is . - public MidiUniversalMessageChannelVoice1ControlChange ControlChange => controlChange; + public MidiUniversalMessageChannelVoice1ControlChange ControlChange { + get { + var self = this; + return *(MidiUniversalMessageChannelVoice1ControlChange*) ((byte*) &self + 8); + } + } /// The 7-bit program number. Valid when is . - public byte Program => program; + public byte Program => (byte) union0; /// The 7-bit channel pressure. Valid when is . - public byte ChannelPressure => channelPressure; + public byte ChannelPressure => (byte) union0; /// The pitch bend value. Valid when is . - public ushort PitchBend => pitchBend; + public ushort PitchBend => union0; } /// A system exclusive (SysEx) message in a . @@ -224,10 +250,15 @@ public struct MidiUniversalMessageChannelVoice1 { [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] [StructLayout (LayoutKind.Sequential)] - public unsafe struct MidiUniversalMessageSysEx { + public struct MidiUniversalMessageSysEx { MidiSysExStatus status; byte channel; - fixed byte data [6]; + byte data0; + byte data1; + byte data2; + byte data3; + byte data4; + byte data5; byte reserved; /// The status determining how the message should be interpreted. @@ -238,14 +269,7 @@ public unsafe struct MidiUniversalMessageSysEx { /// The SysEx data (6 bytes, 7-bit values each). /// A 6-element array with the SysEx data. - public byte [] Data { - get { - var rv = new byte [6]; - for (var i = 0; i < rv.Length; i++) - rv [i] = data [i]; - return rv; - } - } + public byte [] Data => new byte [] { data0, data1, data2, data3, data4, data5 }; } /// The note data of a MIDI 2.0 channel voice message. @@ -253,6 +277,7 @@ public byte [] Data { [SupportedOSPlatform ("tvos15.0")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Sequential)] public struct MidiUniversalMessageChannelVoice2Note { byte number; MidiNoteAttribute attributeType; @@ -277,6 +302,7 @@ public struct MidiUniversalMessageChannelVoice2Note { [SupportedOSPlatform ("tvos15.0")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Sequential)] public struct MidiUniversalMessageChannelVoice2PolyPressure { byte noteNumber; byte reserved; @@ -294,6 +320,7 @@ public struct MidiUniversalMessageChannelVoice2PolyPressure { [SupportedOSPlatform ("tvos15.0")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Sequential)] public struct MidiUniversalMessageChannelVoice2ControlChange { byte index; byte reserved; @@ -311,6 +338,7 @@ public struct MidiUniversalMessageChannelVoice2ControlChange { [SupportedOSPlatform ("tvos15.0")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Sequential)] public struct MidiUniversalMessageChannelVoice2ProgramChange { MidiProgramChangeOptions options; byte program; @@ -333,6 +361,7 @@ public struct MidiUniversalMessageChannelVoice2ProgramChange { [SupportedOSPlatform ("tvos15.0")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Sequential)] public struct MidiUniversalMessageChannelVoice2ChannelPressure { uint data; byte reserved0; @@ -347,6 +376,7 @@ public struct MidiUniversalMessageChannelVoice2ChannelPressure { [SupportedOSPlatform ("tvos15.0")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Sequential)] public struct MidiUniversalMessageChannelVoice2PitchBend { uint data; byte reserved0; @@ -361,6 +391,7 @@ public struct MidiUniversalMessageChannelVoice2PitchBend { [SupportedOSPlatform ("tvos15.0")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Sequential)] public struct MidiUniversalMessageChannelVoice2PerNoteController { byte noteNumber; byte index; @@ -381,6 +412,7 @@ public struct MidiUniversalMessageChannelVoice2PerNoteController { [SupportedOSPlatform ("tvos15.0")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Sequential)] public struct MidiUniversalMessageChannelVoice2Controller { byte bank; byte index; @@ -401,6 +433,7 @@ public struct MidiUniversalMessageChannelVoice2Controller { [SupportedOSPlatform ("tvos15.0")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Sequential)] public struct MidiUniversalMessageChannelVoice2PerNotePitchBend { byte noteNumber; byte reserved; @@ -418,6 +451,7 @@ public struct MidiUniversalMessageChannelVoice2PerNotePitchBend { [SupportedOSPlatform ("tvos15.0")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] + [StructLayout (LayoutKind.Sequential)] public struct MidiUniversalMessageChannelVoice2PerNoteManagement { byte note; MidiPerNoteManagementOptions options; @@ -438,32 +472,15 @@ public struct MidiUniversalMessageChannelVoice2PerNoteManagement { [SupportedOSPlatform ("tvos15.0")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] - [StructLayout (LayoutKind.Explicit, Size = 16)] - public struct MidiUniversalMessageChannelVoice2 { - [FieldOffset (0)] + [StructLayout (LayoutKind.Sequential)] + public unsafe struct MidiUniversalMessageChannelVoice2 { MidiCVStatus status; - [FieldOffset (4)] byte channel; - [FieldOffset (8)] - MidiUniversalMessageChannelVoice2Note note; - [FieldOffset (8)] - MidiUniversalMessageChannelVoice2PolyPressure polyPressure; - [FieldOffset (8)] - MidiUniversalMessageChannelVoice2ControlChange controlChange; - [FieldOffset (8)] - MidiUniversalMessageChannelVoice2ProgramChange programChange; - [FieldOffset (8)] - MidiUniversalMessageChannelVoice2ChannelPressure channelPressure; - [FieldOffset (8)] - MidiUniversalMessageChannelVoice2PitchBend pitchBend; - [FieldOffset (8)] - MidiUniversalMessageChannelVoice2PerNoteController perNoteController; - [FieldOffset (8)] - MidiUniversalMessageChannelVoice2Controller controller; - [FieldOffset (8)] - MidiUniversalMessageChannelVoice2PerNotePitchBend perNotePitchBend; - [FieldOffset (8)] - MidiUniversalMessageChannelVoice2PerNoteManagement perNoteManagement; + byte reserved0; + byte reserved1; + byte reserved2; + uint union0; + uint union1; /// The status determining which value is valid. public MidiCVStatus Status => status; @@ -472,34 +489,84 @@ public struct MidiUniversalMessageChannelVoice2 { public byte Channel => channel; /// The note data. Valid when is or . - public MidiUniversalMessageChannelVoice2Note Note => note; + public MidiUniversalMessageChannelVoice2Note Note { + get { + var self = this; + return *(MidiUniversalMessageChannelVoice2Note*) ((byte*) &self + 8); + } + } /// The poly pressure data. Valid when is . - public MidiUniversalMessageChannelVoice2PolyPressure PolyPressure => polyPressure; + public MidiUniversalMessageChannelVoice2PolyPressure PolyPressure { + get { + var self = this; + return *(MidiUniversalMessageChannelVoice2PolyPressure*) ((byte*) &self + 8); + } + } /// The control change data. Valid when is . - public MidiUniversalMessageChannelVoice2ControlChange ControlChange => controlChange; + public MidiUniversalMessageChannelVoice2ControlChange ControlChange { + get { + var self = this; + return *(MidiUniversalMessageChannelVoice2ControlChange*) ((byte*) &self + 8); + } + } /// The program change data. Valid when is . - public MidiUniversalMessageChannelVoice2ProgramChange ProgramChange => programChange; + public MidiUniversalMessageChannelVoice2ProgramChange ProgramChange { + get { + var self = this; + return *(MidiUniversalMessageChannelVoice2ProgramChange*) ((byte*) &self + 8); + } + } /// The channel pressure data. Valid when is . - public MidiUniversalMessageChannelVoice2ChannelPressure ChannelPressure => channelPressure; + public MidiUniversalMessageChannelVoice2ChannelPressure ChannelPressure { + get { + var self = this; + return *(MidiUniversalMessageChannelVoice2ChannelPressure*) ((byte*) &self + 8); + } + } /// The pitch bend data. Valid when is . - public MidiUniversalMessageChannelVoice2PitchBend PitchBend => pitchBend; + public MidiUniversalMessageChannelVoice2PitchBend PitchBend { + get { + var self = this; + return *(MidiUniversalMessageChannelVoice2PitchBend*) ((byte*) &self + 8); + } + } /// The per-note controller data. Valid when is or . - public MidiUniversalMessageChannelVoice2PerNoteController PerNoteController => perNoteController; + public MidiUniversalMessageChannelVoice2PerNoteController PerNoteController { + get { + var self = this; + return *(MidiUniversalMessageChannelVoice2PerNoteController*) ((byte*) &self + 8); + } + } /// The registered/assignable controller data. Valid when is one of , , or . - public MidiUniversalMessageChannelVoice2Controller Controller => controller; + public MidiUniversalMessageChannelVoice2Controller Controller { + get { + var self = this; + return *(MidiUniversalMessageChannelVoice2Controller*) ((byte*) &self + 8); + } + } /// The per-note pitch bend data. Valid when is . - public MidiUniversalMessageChannelVoice2PerNotePitchBend PerNotePitchBend => perNotePitchBend; + public MidiUniversalMessageChannelVoice2PerNotePitchBend PerNotePitchBend { + get { + var self = this; + return *(MidiUniversalMessageChannelVoice2PerNotePitchBend*) ((byte*) &self + 8); + } + } /// The per-note management data. Valid when is . - public MidiUniversalMessageChannelVoice2PerNoteManagement PerNoteManagement => perNoteManagement; + public MidiUniversalMessageChannelVoice2PerNoteManagement PerNoteManagement { + get { + var self = this; + return *(MidiUniversalMessageChannelVoice2PerNoteManagement*) ((byte*) &self + 8); + } + } } /// The 8-bit system exclusive (SysEx8) data of a 128-bit data message. @@ -508,10 +575,22 @@ public struct MidiUniversalMessageChannelVoice2 { [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] [StructLayout (LayoutKind.Sequential)] - public unsafe struct MidiUniversalMessageSysEx8 { + public struct MidiUniversalMessageSysEx8 { byte byteCount; byte streamID; - fixed byte data [13]; + byte data0; + byte data1; + byte data2; + byte data3; + byte data4; + byte data5; + byte data6; + byte data7; + byte data8; + byte data9; + byte data10; + byte data11; + byte data12; byte reserved; /// The byte count of the data including the stream ID (1-14 bytes). @@ -522,14 +601,7 @@ public unsafe struct MidiUniversalMessageSysEx8 { /// The SysEx8 data (13 bytes). /// A 13-element array with the SysEx8 data. - public byte [] Data { - get { - var rv = new byte [13]; - for (var i = 0; i < rv.Length; i++) - rv [i] = data [i]; - return rv; - } - } + public byte [] Data => new byte [] { data0, data1, data2, data3, data4, data5, data6, data7, data8, data9, data10, data11, data12 }; } /// The mixed data set of a 128-bit data message. @@ -538,9 +610,22 @@ public byte [] Data { [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] [StructLayout (LayoutKind.Sequential)] - public unsafe struct MidiUniversalMessageMixedDataSet { + public struct MidiUniversalMessageMixedDataSet { byte mdsID; - fixed byte data [14]; + byte data0; + byte data1; + byte data2; + byte data3; + byte data4; + byte data5; + byte data6; + byte data7; + byte data8; + byte data9; + byte data10; + byte data11; + byte data12; + byte data13; byte reserved; /// The mixed data set ID. @@ -548,14 +633,7 @@ public unsafe struct MidiUniversalMessageMixedDataSet { /// The mixed data set data (14 bytes). /// A 14-element array with the mixed data set data. - public byte [] Data { - get { - var rv = new byte [14]; - for (var i = 0; i < rv.Length; i++) - rv [i] = data [i]; - return rv; - } - } + public byte [] Data => new byte [] { data0, data1, data2, data3, data4, data5, data6, data7, data8, data9, data10, data11, data12, data13 }; } /// A 128-bit data message in a . @@ -563,23 +641,32 @@ public byte [] Data { [SupportedOSPlatform ("tvos15.0")] [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] - [StructLayout (LayoutKind.Explicit, Size = 20)] - public struct MidiUniversalMessageData128 { - [FieldOffset (0)] + [StructLayout (LayoutKind.Sequential)] + public unsafe struct MidiUniversalMessageData128 { MidiSysExStatus status; - [FieldOffset (4)] - MidiUniversalMessageSysEx8 sysEx8; - [FieldOffset (4)] - MidiUniversalMessageMixedDataSet mixedDataSet; + uint union0; + uint union1; + uint union2; + uint union3; /// The status determining which value is valid. public MidiSysExStatus Status => status; /// The SysEx8 data. Valid when is one of , , or . - public MidiUniversalMessageSysEx8 SysEx8 => sysEx8; + public MidiUniversalMessageSysEx8 SysEx8 { + get { + var self = this; + return *(MidiUniversalMessageSysEx8*) ((byte*) &self + 4); + } + } /// The mixed data set. Valid when is or . - public MidiUniversalMessageMixedDataSet MixedDataSet => mixedDataSet; + public MidiUniversalMessageMixedDataSet MixedDataSet { + get { + var self = this; + return *(MidiUniversalMessageMixedDataSet*) ((byte*) &self + 4); + } + } } /// The raw words of an unknown message in a . @@ -588,19 +675,15 @@ public struct MidiUniversalMessageData128 { [SupportedOSPlatform ("macos")] [SupportedOSPlatform ("maccatalyst")] [StructLayout (LayoutKind.Sequential)] - public unsafe struct MidiUniversalMessageUnknown { - fixed uint words [4]; + public struct MidiUniversalMessageUnknown { + uint word0; + uint word1; + uint word2; + uint word3; /// The raw words of the message (up to four 32-bit words). /// A 4-element array with the raw words. - public uint [] Words { - get { - var rv = new uint [4]; - for (var i = 0; i < rv.Length; i++) - rv [i] = words [i]; - return rv; - } - } + public uint [] Words => new uint [] { word0, word1, word2, word3 }; } } diff --git a/tests/xtro-sharpie/api-annotations-dotnet/common-CoreMIDI.ignore b/tests/xtro-sharpie/api-annotations-dotnet/common-CoreMIDI.ignore deleted file mode 100644 index d5d48a5d41b7..000000000000 --- a/tests/xtro-sharpie/api-annotations-dotnet/common-CoreMIDI.ignore +++ /dev/null @@ -1,2 +0,0 @@ -# https://github.com/dotnet/macios/issues/4452#issuecomment-660220392 -!missing-pinvoke! MIDIEventListForEachEvent is not bound From a8f368c4af4d8f06d0a7612b95d861cb269c4504 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 25 Jun 2026 13:50:20 +0200 Subject: [PATCH 4/4] [CoreMidi] Address PR review comments * 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 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> --- src/CoreFoundation/CFUuidBytes.cs | 5 ++++ src/CoreMidi/MidiEventList.cs | 2 +- src/CoreMidi/MidiEventPacket.cs | 18 ++++++------ src/CoreMidi/MidiServices.cs | 4 +-- src/CoreMidi/MidiStructs.cs | 28 ------------------- .../CoreMidi/MidiEndpointTest.cs | 12 ++++---- 6 files changed, 23 insertions(+), 46 deletions(-) diff --git a/src/CoreFoundation/CFUuidBytes.cs b/src/CoreFoundation/CFUuidBytes.cs index 125a830479ff..0b3fc8912189 100644 --- a/src/CoreFoundation/CFUuidBytes.cs +++ b/src/CoreFoundation/CFUuidBytes.cs @@ -1,3 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + namespace CoreFoundation { // This struct is only used for P/Invokes. struct CFUuidBytes { diff --git a/src/CoreMidi/MidiEventList.cs b/src/CoreMidi/MidiEventList.cs index 7b59ebe03bc1..27efcbe43d9f 100644 --- a/src/CoreMidi/MidiEventList.cs +++ b/src/CoreMidi/MidiEventList.cs @@ -83,7 +83,7 @@ public MidiEventList (MidiProtocolId protocol) public MidiEventList (MidiProtocolId protocol, int size) { if (size < MinimumSize) - throw new ArgumentOutOfRangeException ($"{nameof (size)} must be at least {MinimumSize}."); + throw new ArgumentOutOfRangeException (nameof (size), $"{nameof (size)} must be at least {MinimumSize}."); midiDataSize = size; owns = true; diff --git a/src/CoreMidi/MidiEventPacket.cs b/src/CoreMidi/MidiEventPacket.cs index bf8977fd2d44..565549cdff29 100644 --- a/src/CoreMidi/MidiEventPacket.cs +++ b/src/CoreMidi/MidiEventPacket.cs @@ -101,7 +101,7 @@ public uint WordCount { get => wordCount; set { if (value > 64) - throw new ArgumentOutOfRangeException ($"WordCount can't be higher than 64."); + throw new ArgumentOutOfRangeException (nameof (value), "WordCount can't be higher than 64."); wordCount = value; } } @@ -112,7 +112,7 @@ public uint [] Words { get { var wc = wordCount; if (wc > 64) - throw new ArgumentOutOfRangeException ($"WordCount can't be higher than 64."); + throw new ArgumentOutOfRangeException (nameof (WordCount), "WordCount can't be higher than 64."); var rv = new uint [wc]; unsafe { fixed (uint* destination = rv) { @@ -128,7 +128,7 @@ public uint [] Words { ObjCRuntime.ThrowHelper.ThrowArgumentNullException (nameof (value)); if (value.Length > 64) - throw new ArgumentOutOfRangeException ($"WordCount can't be higher than 64."); + throw new ArgumentOutOfRangeException (nameof (value), "WordCount can't be higher than 64."); wordCount = (uint) value.Length; unsafe { fixed (uint* destination = &word_00) { @@ -146,11 +146,11 @@ public uint [] Words { public uint this [int index] { get { if (index < 0) - throw new ArgumentOutOfRangeException ($"index must be positive."); + throw new ArgumentOutOfRangeException (nameof (index), "index must be positive."); if (index >= 64) - throw new ArgumentOutOfRangeException ($"index must be less than 64."); + throw new ArgumentOutOfRangeException (nameof (index), "index must be less than 64."); if (index + 1 > wordCount) - throw new ArgumentOutOfRangeException ($"index must be less than WordCount."); + throw new ArgumentOutOfRangeException (nameof (index), "index must be less than WordCount."); unsafe { fixed (uint* firstWord = &word_00) return firstWord [index]; @@ -158,11 +158,11 @@ public uint this [int index] { } set { if (index < 0) - throw new ArgumentOutOfRangeException ($"index must be positive."); + throw new ArgumentOutOfRangeException (nameof (index), "index must be positive."); if (index >= 64) - throw new ArgumentOutOfRangeException ($"index must be less than 64."); + throw new ArgumentOutOfRangeException (nameof (index), "index must be less than 64."); if (index + 1 > wordCount) - throw new ArgumentOutOfRangeException ($"index must be less than WordCount."); + throw new ArgumentOutOfRangeException (nameof (index), "index must be less than WordCount."); unsafe { fixed (uint* firstWord = &word_00) firstWord [index] = value; diff --git a/src/CoreMidi/MidiServices.cs b/src/CoreMidi/MidiServices.cs index d3bd2a2c10a9..b21a6c8307a1 100644 --- a/src/CoreMidi/MidiServices.cs +++ b/src/CoreMidi/MidiServices.cs @@ -822,7 +822,7 @@ unsafe extern static OSStatus MIDIInputPortCreateWithProtocol ( /// The MIDI protocol for the data this port will receive. /// The callback that will be called when the port receives MIDI data. /// A status code that describes the result of this operation. This will be in case of success. - /// A newly created if successful, otherwise null. + /// A newly created if successful, otherwise null. /// The callback receives two pointers: the first is a pointer to the MIDIEventList, and the second is a pointer to the source MIDIEndpointRef. Use to wrap the event list pointer. [SupportedOSPlatform ("ios14.0")] [SupportedOSPlatform ("maccatalyst")] @@ -831,7 +831,7 @@ unsafe extern static OSStatus MIDIInputPortCreateWithProtocol ( public unsafe MidiPort? CreateInputPort (string name, MidiProtocolId protocol, delegate* unmanaged readBlock, out MidiError status) { using var namePtr = new TransientCFString (name); - var handle = default (MidiEndpointRef); + var handle = default (MidiPortRef); unsafe { status = (MidiError) MIDIInputPortCreateWithProtocol (GetCheckedHandle (), namePtr, protocol, &handle, readBlock); } diff --git a/src/CoreMidi/MidiStructs.cs b/src/CoreMidi/MidiStructs.cs index f4a064e0a50c..e999dfe624c8 100644 --- a/src/CoreMidi/MidiStructs.cs +++ b/src/CoreMidi/MidiStructs.cs @@ -221,34 +221,6 @@ public IntPtr Context { } - /*! - @struct MIDISysexSendRequestUMP - @abstract A request to transmit a UMP system-exclusive event. - - @discussion - This represents a request to send a single UMP system-exclusive MIDI event to - a MIDI destination asynchronously. - - @field destination - The endpoint to which the event is to be sent. - @field words - Initially, a pointer to the UMP SysEx event to be sent. - MIDISendUMPSysex will advance this pointer as data is - sent. - @field wordsToSend - Initially, the number of words to be sent. MIDISendUMPSysex - will decrement this counter as data is sent. - @field complete - The client may set this to true at any time to abort - transmission. The implementation sets this to true when - all data been transmitted. - @field completionProc - Called when all bytes have been sent, or after the client - has set complete to true. - @field completionRefCon - Passed as a refCon to completionProc. - */ - /// A struct that represents a request to transmit a single UMP system-exclusive event. [NativeName ("MIDISysexSendRequestUMP")] struct MidiSysexSendRequestUmp { diff --git a/tests/monotouch-test/CoreMidi/MidiEndpointTest.cs b/tests/monotouch-test/CoreMidi/MidiEndpointTest.cs index 39e4eb1bd464..d669a2c94f89 100644 --- a/tests/monotouch-test/CoreMidi/MidiEndpointTest.cs +++ b/tests/monotouch-test/CoreMidi/MidiEndpointTest.cs @@ -60,21 +60,21 @@ public void SendTest () // 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"); - Assert.AreEqual (ref2, IntPtr.Zero, "GetRefCons A 2"); + Assert.That (ref1, Is.EqualTo (IntPtr.Zero), "GetRefCons A 1"); + Assert.That (ref2, Is.EqualTo (IntPtr.Zero), "GetRefCons A 2"); ref1 = unchecked((IntPtr) 0xfee1600d); ref2 = 0x42f00f00; Assert.AreEqual (AudioQueueStatus.GeneralParamError, (AudioQueueStatus) ep.SetRefCons (ref1, ref2), "SetRefCons B"); Assert.AreEqual (AudioQueueStatus.GeneralParamError, (AudioQueueStatus) ep.GetRefCons (out ref1, out ref2), "GetRefCons C"); - Assert.AreEqual (ref1, IntPtr.Zero /* 0xfee1600d */, "GetRefCons C 1"); - Assert.AreEqual (ref2, IntPtr.Zero /* 0x42f00f00 */, "GetRefCons C 2"); + Assert.That (ref1, Is.EqualTo (IntPtr.Zero) /* 0xfee1600d */, "GetRefCons C 1"); + Assert.That (ref2, Is.EqualTo (IntPtr.Zero) /* 0x42f00f00 */, "GetRefCons C 2"); Assert.AreEqual (AudioQueueStatus.GeneralParamError, (AudioQueueStatus) ep.SetRefCons (IntPtr.Zero, IntPtr.Zero), "SetRefCons D"); Assert.AreEqual (AudioQueueStatus.GeneralParamError, (AudioQueueStatus) ep.GetRefCons (out ref1, out ref2), "GetRefCons E"); - Assert.AreEqual (ref1, IntPtr.Zero, "GetRefCons E 1"); - Assert.AreEqual (ref2, IntPtr.Zero, "GetRefCons E 2"); + Assert.That (ref1, Is.EqualTo (IntPtr.Zero), "GetRefCons E 1"); + Assert.That (ref2, Is.EqualTo (IntPtr.Zero), "GetRefCons E 2"); anyChecks = true; }