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 222de3179f4c..62563a78fcb4 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.
///
///
@@ -1906,19 +2183,33 @@ public int UniqueID {
}
}
+#if !XAMCORE_5_0 || __MACOS__
/// To be added.
/// To be added.
/// To be added.
+ [UnsupportedOSPlatform ("tvos")]
+ [UnsupportedOSPlatform ("ios")]
+ [UnsupportedOSPlatform ("maccatalyst")]
+ [SupportedOSPlatform ("macos")]
+#if !__MACOS__
+ [EditorBrowsable (EditorBrowsableState.Never)]
+ [Obsolete ("This API does not do anything on this platform.")]
+#endif
public bool UsesSerial {
get {
- var kMIDIDriverPropertyUsesSerial = Dlfcn.GetIntPtr (Libraries.CoreMidi.Handle, "kMIDIDriverPropertyUsesSerial");
- return GetInt (kMIDIDriverPropertyUsesSerial) != 0;
+#if __MACOS__
+ return GetInt (MidiDriverPropertyExtensions.kMIDIDriverPropertyUsesSerial) != 0;
+#else
+ return false;
+#endif
}
set {
- var kMIDIDriverPropertyUsesSerial = Dlfcn.GetIntPtr (Libraries.CoreMidi.Handle, "kMIDIDriverPropertyUsesSerial");
- SetInt (kMIDIDriverPropertyUsesSerial, value ? 1 : 0);
+#if __MACOS__
+ SetInt (MidiDriverPropertyExtensions.kMIDIDriverPropertyUsesSerial, value ? 1 : 0);
+#endif
}
}
+#endif // !XAMCORE_5_0 || __MACOS__
#if !XAMCORE_5_0 || __MACOS__
/// To be added.
@@ -2631,17 +2922,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;
}
///
@@ -2978,11 +3273,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,
@@ -2995,10 +3538,12 @@ enum MidiNotificationMessageId : int {
[SupportedOSPlatform ("ios")]
[SupportedOSPlatform ("maccatalyst")]
[UnsupportedOSPlatform ("macos")]
+ [UnsupportedOSPlatform ("tvos")]
InternalStart = 0x1000,
#endif
}
+#if !TVOS
//
// The notification EventArgs
//
@@ -3172,5 +3717,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 73952ec67437..195605f3aeb7 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 05d10b04a5bb..1aa22becb62b 100644
--- a/src/coremidi.cs
+++ b/src/coremidi.cs
@@ -1091,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 049ccf36f46a..df40ac0a7c60 100644
--- a/src/frameworks.sources
+++ b/src/frameworks.sources
@@ -441,6 +441,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 \
@@ -604,6 +605,7 @@ COREMEDIA_SOURCES = \
COREMIDI_CORE_SOURCES = \
CoreMidi/MidiCIDeviceIdentification.cs \
+ CoreMidi/MidiDriverInterface.cs \
CoreMidi/MidiServices.cs \
CoreMidi/MidiStructs.cs \
CoreMidi/MidiThruConnection.cs \
@@ -611,6 +613,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 0a3dc185681b..57cad69689cc 100644
--- a/tests/cecil-tests/Documentation.KnownFailures.txt
+++ b/tests/cecil-tests/Documentation.KnownFailures.txt
@@ -2580,16 +2580,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
@@ -10432,7 +10422,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})
@@ -10455,7 +10445,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})
@@ -10463,7 +10453,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@)
@@ -10673,7 +10663,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
@@ -11123,8 +11113,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
@@ -11161,12 +11149,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
@@ -20535,8 +20526,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
@@ -20558,8 +20547,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
@@ -26206,9 +26193,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
@@ -26222,9 +26206,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 c413b9edef6e..5dd862cb92f5 100644
--- a/tests/xtro-sharpie/api-annotations-dotnet/macOS-CoreMIDI.ignore
+++ b/tests/xtro-sharpie/api-annotations-dotnet/macOS-CoreMIDI.ignore
@@ -1,36 +1,2 @@
-# deprecated pinvokes and not binded.
-!missing-pinvoke! MIDIDriverEnableMonitoring is not bound
-
-!missing-field! kMIDIDriverPropertyUsesSerial 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