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