From 6addd59a23b37c25508b53dbe1868b06f6d80ec3 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 9 Sep 2021 16:18:24 -0700 Subject: [PATCH 1/2] Prevented `MIDI.IO` `Input/InputConnection` `queue.sync { }` from overlapping --- Sources/MIDIKit/IO/Managed/Input.swift | 8 ++++++++ Sources/MIDIKit/IO/Managed/InputConnection.swift | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/Sources/MIDIKit/IO/Managed/Input.swift b/Sources/MIDIKit/IO/Managed/Input.swift index 47ea23a75e..59f93e2af8 100644 --- a/Sources/MIDIKit/IO/Managed/Input.swift +++ b/Sources/MIDIKit/IO/Managed/Input.swift @@ -27,6 +27,8 @@ extension MIDI.IO { internal var receiveHandler: ReceiveHandler + internal var isReceiveReady: Bool = false + internal init(name: String, uniqueID: MIDI.IO.InputEndpoint.UniqueID? = nil, receiveHandler: ReceiveHandler.Definition, @@ -74,6 +76,8 @@ extension MIDI.IO.Input { internal func create(in manager: MIDI.IO.Manager) throws { + isReceiveReady = false + if uniqueIDExistsInSystem != nil { // if uniqueID is already in use, set it to nil here // so MIDIDestinationCreateWithBlock can return a new unused ID; @@ -93,6 +97,7 @@ extension MIDI.IO.Input { &newPortRef, { [weak self] packetListPtr, srcConnRefCon in guard let strongSelf = self else { return } + guard strongSelf.isReceiveReady else { return } // this must be sync and not async, otherwise the pointer gets freed before we can use it strongSelf.midiManager?.queue.sync { @@ -114,6 +119,7 @@ extension MIDI.IO.Input { &newPortRef, { [weak self] eventListPtr, srcConnRefCon in guard let strongSelf = self else { return } + guard strongSelf.isReceiveReady else { return } // this must be sync and not async, otherwise the pointer gets freed before we can use it strongSelf.midiManager?.queue.sync { @@ -141,6 +147,8 @@ extension MIDI.IO.Input { uniqueID = .init(MIDI.IO.getUniqueID(of: newPortRef)) } + isReceiveReady = true + } /// Disposes of the the virtual port if it's already been created in the system via the `create()` method. diff --git a/Sources/MIDIKit/IO/Managed/InputConnection.swift b/Sources/MIDIKit/IO/Managed/InputConnection.swift index 6bbc6f910c..1dad852844 100644 --- a/Sources/MIDIKit/IO/Managed/InputConnection.swift +++ b/Sources/MIDIKit/IO/Managed/InputConnection.swift @@ -28,6 +28,8 @@ extension MIDI.IO { public private(set) var isConnected: Bool = false + internal var isReceiveReady: Bool = false + internal init(toOutput: MIDI.IO.EndpointIDCriteria, receiveHandler: ReceiveHandler.Definition, midiManager: MIDI.IO.Manager, @@ -59,6 +61,8 @@ extension MIDI.IO.InputConnection { if isConnected { return } + isReceiveReady = false + // if previously connected, clean the old connection _ = try? disconnect() @@ -91,6 +95,7 @@ extension MIDI.IO.InputConnection { &newConnection, { [weak self] packetListPtr, srcConnRefCon in guard let strongSelf = self else { return } + guard strongSelf.isReceiveReady else { return } // this must be sync and not async, otherwise the pointer gets freed before we can use it strongSelf.midiManager?.queue.sync { @@ -112,6 +117,7 @@ extension MIDI.IO.InputConnection { &newConnection, { [weak self] eventListPtr, srcConnRefCon in guard let strongSelf = self else { return } + guard strongSelf.isReceiveReady else { return } // this must be sync and not async, otherwise the pointer gets freed before we can use it strongSelf.midiManager?.queue.sync { @@ -134,6 +140,8 @@ extension MIDI.IO.InputConnection { isConnected = true + isReceiveReady = true + } /// Disconnects the connection if it's currently connected. From 293caa3c4dd321995ec251d3bd5d1e5c1c1cd832 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Sat, 18 Sep 2021 19:44:04 -0700 Subject: [PATCH 2/2] Threading, I/O and UMP improvements --- Sources/MIDIKit/Common/Types/UMPWord.swift | 31 ++ .../MIDIKit/Events/Event/Event rawBytes.swift | 202 +++++++---- Sources/MIDIKit/IO/API/APIVersion.swift | 1 + Sources/MIDIKit/IO/API/ProtocolVersion.swift | 43 ++- .../MIDIEventList Utilities.swift | 109 ++++++ .../MIDIPacketList Utilities.swift | 64 ++-- Sources/MIDIKit/IO/IO Constants.swift | 14 +- Sources/MIDIKit/IO/Managed/Input.swift | 44 ++- .../MIDIKit/IO/Managed/InputConnection.swift | 50 ++- Sources/MIDIKit/IO/Managed/Output.swift | 36 +- .../MIDIKit/IO/Managed/OutputConnection.swift | 33 +- .../Protocols/MIDIIOManagedProtocol.swift | 2 +- .../MIDIIOReceivesMIDIMessagesProtocol.swift | 3 +- .../MIDIIOSendsMIDIMessagesProtocol.swift | 79 ++++- .../MIDIKit/IO/Managed/ThruConnection.swift | 10 +- .../IO/Manager/Manager Add and Remove.swift | 26 +- .../MIDIKit/IO/Manager/Manager State.swift | 2 +- Sources/MIDIKit/IO/Manager/Manager.swift | 22 +- .../IO/ReceiveHandler/Handlers/Events.swift | 15 +- .../Handlers/EventsLogging.swift | 15 +- .../IO/ReceiveHandler/Handlers/Group.swift | 15 +- .../IO/ReceiveHandler/Handlers/RawData.swift | 15 +- .../Handlers/RawDataLogging.swift | 15 +- .../MIDIIOReceiveHandlerProtocol.swift | 15 +- .../IO/ReceiveHandler/ReceiveHandler.swift | 15 +- Sources/MIDIKit/Parser/MIDI2Parser.swift | 329 +++++++++++++++--- 26 files changed, 861 insertions(+), 344 deletions(-) create mode 100644 Sources/MIDIKit/Common/Types/UMPWord.swift create mode 100644 Sources/MIDIKit/IO/Core MIDI/MIDIEventList/MIDIEventList Utilities.swift diff --git a/Sources/MIDIKit/Common/Types/UMPWord.swift b/Sources/MIDIKit/Common/Types/UMPWord.swift new file mode 100644 index 0000000000..73728767d3 --- /dev/null +++ b/Sources/MIDIKit/Common/Types/UMPWord.swift @@ -0,0 +1,31 @@ +// +// UMPWord.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// + +extension MIDI { + + /// Universal MIDI Packet Word: Type representing four 8-bit bytes + public typealias UMPWord = UInt32 + +} + +extension MIDI.UMPWord { + + /// Internal: Pack a UInt32 with four 8-bit bytes. + @inline(__always) internal init( + _ byte0: MIDI.Byte, + _ byte1: MIDI.Byte, + _ byte2: MIDI.Byte, + _ byte3: MIDI.Byte + ) { + + self = + (Self(byte0) << 24) + + (Self(byte1) << 16) + + (Self(byte2) << 8) + + Self(byte3) + + } + +} diff --git a/Sources/MIDIKit/Events/Event/Event rawBytes.swift b/Sources/MIDIKit/Events/Event/Event rawBytes.swift index b88e45dfe7..fe4597bc1e 100644 --- a/Sources/MIDIKit/Events/Event/Event rawBytes.swift +++ b/Sources/MIDIKit/Events/Event/Event rawBytes.swift @@ -60,6 +60,7 @@ extension MIDI.Event { let bytePair = value.bytePair return [0xE0 + channel.uInt8Value, bytePair.lsb, bytePair.msb] + // ---------------------- // MARK: System Exclusive // ---------------------- @@ -85,6 +86,7 @@ extension MIDI.Event { + data + [0xF7] + // ------------------- // MARK: System Common // ------------------- @@ -113,6 +115,7 @@ extension MIDI.Event { return [0xF6] + // ---------------------- // MARK: System Real Time // ---------------------- @@ -149,7 +152,7 @@ extension MIDI.Event { extension MIDI.Event { - public var umpRawBytes: [MIDI.Byte] { + public var umpRawWords: [MIDI.UMPWord] { #warning("> this is incomplete and needs testing; for the time being MIDIKit will only use MIDI 1.0 event raw bytes") @@ -200,10 +203,12 @@ extension MIDI.Event { let mtAndGroup = (messageType.rawValue.uInt8Value << 4) + group - return [mtAndGroup, - 0x90 + channel.uInt8Value, - note.uInt8Value, - velocity.uInt8Value] + let word = MIDI.UMPWord(mtAndGroup, + 0x90 + channel.uInt8Value, + note.uInt8Value, + velocity.uInt8Value) + + return [word] case .noteOff(note: let note, velocity: let velocity, @@ -212,10 +217,12 @@ extension MIDI.Event { let mtAndGroup = (messageType.rawValue.uInt8Value << 4) + group - return [mtAndGroup, - 0x80 + channel.uInt8Value, - note.uInt8Value, - velocity.uInt8Value] + let word = MIDI.UMPWord(mtAndGroup, + 0x80 + channel.uInt8Value, + note.uInt8Value, + velocity.uInt8Value) + + return [word] case .polyAftertouch(note: let note, pressure: let pressure, @@ -224,10 +231,12 @@ extension MIDI.Event { let mtAndGroup = (messageType.rawValue.uInt8Value << 4) + group - return [mtAndGroup, - 0xA0 + channel.uInt8Value, - note.uInt8Value, - pressure.uInt8Value] + let word = MIDI.UMPWord(mtAndGroup, + 0xA0 + channel.uInt8Value, + note.uInt8Value, + pressure.uInt8Value) + + return [word] case .cc(controller: let controller, value: let value, @@ -236,10 +245,12 @@ extension MIDI.Event { let mtAndGroup = (messageType.rawValue.uInt8Value << 4) + group - return [mtAndGroup, - 0xB0 + channel.uInt8Value, - controller.uInt8Value, - value.uInt8Value] + let word = MIDI.UMPWord(mtAndGroup, + 0xB0 + channel.uInt8Value, + controller.uInt8Value, + value.uInt8Value) + + return [word] case .programChange(program: let program, channel: let channel, @@ -247,10 +258,12 @@ extension MIDI.Event { let mtAndGroup = (messageType.rawValue.uInt8Value << 4) + group - return [mtAndGroup, - 0xC0 + channel.uInt8Value, - program.uInt8Value, - 0x00] // pad an empty byte to fill 4 bytes + let word = MIDI.UMPWord(mtAndGroup, + 0xC0 + channel.uInt8Value, + program.uInt8Value, + 0x00) // pad an empty byte to fill 4 bytes + + return [word] case .chanAftertouch(pressure: let pressure, channel: let channel, @@ -258,10 +271,12 @@ extension MIDI.Event { let mtAndGroup = (messageType.rawValue.uInt8Value << 4) + group - return [mtAndGroup, - 0xD0 + channel.uInt8Value, - pressure.uInt8Value, - 0x00] // pad an empty byte to fill 4 bytes + let word = MIDI.UMPWord(mtAndGroup, + 0xD0 + channel.uInt8Value, + pressure.uInt8Value, + 0x00) // pad an empty byte to fill 4 bytes + + return [word] case .pitchBend(value: let value, channel: let channel, @@ -271,10 +286,13 @@ extension MIDI.Event { let bytePair = value.bytePair - return [mtAndGroup, - 0xE0 + channel.uInt8Value, - bytePair.lsb, - bytePair.msb] + let word = MIDI.UMPWord(mtAndGroup, + 0xE0 + channel.uInt8Value, + bytePair.lsb, + bytePair.msb) + + return [word] + // ---------------------- // MARK: System Exclusive @@ -288,7 +306,11 @@ extension MIDI.Event { let mtAndGroup = (messageType.rawValue.uInt8Value << 4) + group - return [mtAndGroup, 0xF0] + manufacturer.bytes + data + [0xF7] + _ = manufacturer + _ = data + _ = group + + return [] case .sysExUniversal(universalType: let universalType, deviceID: let deviceID, @@ -301,14 +323,15 @@ extension MIDI.Event { let mtAndGroup = (messageType.rawValue.uInt8Value << 4) + group - return [mtAndGroup, - 0xF0, - MIDI.Byte(universalType.rawValue), - deviceID.uInt8Value, - subID1.uInt8Value, - subID2.uInt8Value] - + data - + [0xF7] + _ = universalType + _ = deviceID + _ = subID1 + _ = subID2 + _ = data + _ = group + + return [] + // ------------------- // MARK: System Common @@ -319,10 +342,12 @@ extension MIDI.Event { let mtAndGroup = (messageType.rawValue.uInt8Value << 4) + group - return [mtAndGroup, - 0xF1, - byte, - 0x00] // pad an empty byte to fill 4 bytes + let word = MIDI.UMPWord(mtAndGroup, + 0xF1, + byte, + 0x00) // pad an empty byte to fill 4 bytes + + return [word] case .songPositionPointer(midiBeat: let midiBeat, group: let group): @@ -331,36 +356,47 @@ extension MIDI.Event { let bytePair = midiBeat.bytePair - return [mtAndGroup, - 0xF2, - bytePair.lsb, - bytePair.msb] + let word = MIDI.UMPWord(mtAndGroup, + 0xF2, + bytePair.lsb, + bytePair.msb) + + return [word] case .songSelect(number: let number, group: let group): let mtAndGroup = (messageType.rawValue.uInt8Value << 4) + group - return [mtAndGroup, - 0xF3, - number.uInt8Value, - 0x00] // pad an empty byte to fill 4 bytes + let word = MIDI.UMPWord(mtAndGroup, + 0xF3, + number.uInt8Value, + 0x00) // pad an empty byte to fill 4 bytes + + return [word] case .unofficialBusSelect(group: let group): let mtAndGroup = (messageType.rawValue.uInt8Value << 4) + group - return [mtAndGroup, - 0xF5, - 0x00, 0x00] // pad empty bytes to fill 4 bytes + let word = MIDI.UMPWord(mtAndGroup, + 0xF5, + 0x00, // pad empty bytes to fill 4 bytes + 0x00) // pad empty bytes to fill 4 bytes + + return [word] case .tuneRequest(group: let group): let mtAndGroup = (messageType.rawValue.uInt8Value << 4) + group - return [mtAndGroup, - 0xF6, - 0x00, 0x00] // pad empty bytes to fill 4 bytes + let word = MIDI.UMPWord(mtAndGroup, + 0xF6, + 0x00, // pad empty bytes to fill 4 bytes + 0x00) // pad empty bytes to fill 4 bytes + + return [word] + // ---------------------- // MARK: System Real Time @@ -370,49 +406,67 @@ extension MIDI.Event { let mtAndGroup = (messageType.rawValue.uInt8Value << 4) + group - return [mtAndGroup, - 0xF8, - 0x00, 0x00] // pad empty bytes to fill 4 bytes + let word = MIDI.UMPWord(mtAndGroup, + 0xF8, + 0x00, // pad empty bytes to fill 4 bytes + 0x00) // pad empty bytes to fill 4 bytes + + return [word] case .start(group: let group): let mtAndGroup = (messageType.rawValue.uInt8Value << 4) + group - return [mtAndGroup, - 0xFA, - 0x00, 0x00] // pad empty bytes to fill 4 bytes + let word = MIDI.UMPWord(mtAndGroup, + 0xFA, + 0x00, // pad empty bytes to fill 4 bytes + 0x00) // pad empty bytes to fill 4 bytes + + return [word] case .continue(group: let group): let mtAndGroup = (messageType.rawValue.uInt8Value << 4) + group - return [mtAndGroup, - 0xFB, - 0x00, 0x00] // pad empty bytes to fill 4 bytes + let word = MIDI.UMPWord(mtAndGroup, + 0xFB, + 0x00, // pad empty bytes to fill 4 bytes + 0x00) // pad empty bytes to fill 4 bytes + + return [word] case .stop(group: let group): let mtAndGroup = (messageType.rawValue.uInt8Value << 4) + group - return [mtAndGroup, - 0xFC, - 0x00, 0x00] // pad empty bytes to fill 4 bytes + let word = MIDI.UMPWord(mtAndGroup, + 0xFC, + 0x00, // pad empty bytes to fill 4 bytes + 0x00) // pad empty bytes to fill 4 bytes + + return [word] case .activeSensing(group: let group): let mtAndGroup = (messageType.rawValue.uInt8Value << 4) + group - return [mtAndGroup, - 0xFE, - 0x00, 0x00] // pad empty bytes to fill 4 bytes + let word = MIDI.UMPWord(mtAndGroup, + 0xFE, + 0x00, // pad empty bytes to fill 4 bytes + 0x00) // pad empty bytes to fill 4 bytes + + return [word] case .systemReset(group: let group): let mtAndGroup = (messageType.rawValue.uInt8Value << 4) + group - return [mtAndGroup, - 0xFF, - 0x00, 0x00] // pad empty bytes to fill 4 bytes + let word = MIDI.UMPWord(mtAndGroup, + 0xFF, + 0x00, // pad empty bytes to fill 4 bytes + 0x00) // pad empty bytes to fill 4 bytes + + return [word] } diff --git a/Sources/MIDIKit/IO/API/APIVersion.swift b/Sources/MIDIKit/IO/API/APIVersion.swift index 5225bdd19c..6bb7f7145b 100644 --- a/Sources/MIDIKit/IO/API/APIVersion.swift +++ b/Sources/MIDIKit/IO/API/APIVersion.swift @@ -30,6 +30,7 @@ extension MIDI.IO.APIVersion { public static func bestForPlatform() -> Self { if #available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) { + #warning("> switch to new API in future") // return legacy for now, since new API is buggy; // in future, this should return .newCoreMIDI when new API is more stable return .legacyCoreMIDI diff --git a/Sources/MIDIKit/IO/API/ProtocolVersion.swift b/Sources/MIDIKit/IO/API/ProtocolVersion.swift index fa5b71e7ab..4dd6263669 100644 --- a/Sources/MIDIKit/IO/API/ProtocolVersion.swift +++ b/Sources/MIDIKit/IO/API/ProtocolVersion.swift @@ -3,10 +3,10 @@ // MIDIKit • https://github.com/orchetect/MIDIKit // +import CoreMIDI + extension MIDI.IO { - #warning("> this is currently unused") - /// MIDI protocol version. public enum ProtocolVersion { @@ -19,3 +19,42 @@ extension MIDI.IO { } } + +extension MIDI.IO.ProtocolVersion { + + /// Initializes from the corresponding Core MIDI Protocol. + @available(macOS 11.0, macCatalyst 14.0, iOS 14.0, *) + @inlinable + internal init(_ coreMIDIProtocol: MIDIProtocolID) { + + switch coreMIDIProtocol { + case ._1_0: + self = ._1_0 + + case ._2_0: + self = ._2_0 + + @unknown default: + self = ._2_0 + + } + + } + + /// Returns the corresponding Core MIDI Protocol. + @available(macOS 11.0, macCatalyst 14.0, iOS 14.0, *) + @inlinable + internal var coreMIDIProtocol: MIDIProtocolID { + + switch self { + case ._1_0: + return ._1_0 + + case ._2_0: + return ._2_0 + + } + + } + +} diff --git a/Sources/MIDIKit/IO/Core MIDI/MIDIEventList/MIDIEventList Utilities.swift b/Sources/MIDIKit/IO/Core MIDI/MIDIEventList/MIDIEventList Utilities.swift new file mode 100644 index 0000000000..4096947f02 --- /dev/null +++ b/Sources/MIDIKit/IO/Core MIDI/MIDIEventList/MIDIEventList Utilities.swift @@ -0,0 +1,109 @@ +// +// MIDIEventList Utilities.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// + +import CoreMIDI + +extension MIDIEventPacket { + + /// Internal use. + /// Assembles a Core MIDI `MIDIEventPacket` (Universal MIDI Packet) from a `UInt32` word array. + @available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) + @inlinable internal init( + words: [MIDI.UMPWord] + ) throws { + + guard words.count > 0 else { + throw MIDI.IO.MIDIError.malformed( + "A Universal MIDI Packet cannot contain zero UInt32 words." + ) + } + + guard words.count <= 64 else { + throw MIDI.IO.MIDIError.malformed( + "A Universal MIDI Packet cannot contain more than 64 UInt32 words." + ) + } + + var packet = MIDIEventPacket() + + // time stamp + packet.timeStamp = .zero // zero means "now" when sending MIDI + + // word count + packet.wordCount = UInt32(words.count) + + // words + let mutablePtr = UnsafeMutableMIDIEventPacketPointer(&packet) + for wordsIndex in 0.. 0 else { +// throw MIDI.IO.MIDIError.malformed( +// "A Universal MIDI Packet cannot contain zero UInt32 words." +// ) +// } +// +// guard words.count <= 64 else { +// throw MIDI.IO.MIDIError.malformed( +// "A Universal MIDI Packet cannot contain more than 64 UInt32 words." +// ) +// } +// +// let packetBuilder = MIDIEventPacket +// .Builder(maximumNumberMIDIWords: words.count) +// +// words.forEach { packetBuilder.append($0) } +// +// let packet = try packetBuilder +// .withUnsafePointer { unsafePtr -> Result in +// // this "works", but when ASAN is enabled accessing pointee throws a heap overflow error +// // I filed a radar with Apple on Sep 18, 2021: FB9637098 +// .success(unsafePtr.pointee) +// } +// .get() +// +// self = packet +// +// } +// +//} + +extension MIDIEventList { + + /// Internal use. + /// Assembles a single Core MIDI `MIDIEventPacket` from a Universal MIDI Packet `UInt32` word array and wraps it in a Core MIDI `MIDIEventList`. + @available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) + @inlinable internal init( + protocol midiProtocol: MIDIProtocolID, + packetWords: [MIDI.UMPWord] + ) throws { + + let packet = try MIDIEventPacket(words: packetWords) + + self = MIDIEventList(protocol: midiProtocol, + numPackets: 1, + packet: packet) + + } + +} diff --git a/Sources/MIDIKit/IO/Core MIDI/MIDIPacketLIst/MIDIPacketList Utilities.swift b/Sources/MIDIKit/IO/Core MIDI/MIDIPacketLIst/MIDIPacketList Utilities.swift index b02a6d25d6..712e0f50a7 100644 --- a/Sources/MIDIKit/IO/Core MIDI/MIDIPacketLIst/MIDIPacketList Utilities.swift +++ b/Sources/MIDIKit/IO/Core MIDI/MIDIPacketLIst/MIDIPacketList Utilities.swift @@ -5,17 +5,41 @@ import CoreMIDI -extension MIDI.IO { +extension MIDIPacketList { + + /// Internal use. + /// Assembles a single Core MIDI `MIDIPacket` from a MIDI message byte array and wraps it in a Core MIDI `MIDIPacketList`. + @inlinable internal init(data: [MIDI.Byte]) { + + let packetList = UnsafeMutablePointer(data: data) + self = packetList.pointee + packetList.deallocate() + + } + + /// Experimental. + /// Assembles an array of `Byte` arrays into Core MIDI `MIDIPacket`s and wraps them in a `MIDIPacketList`. + @inlinable internal init(data: [[MIDI.Byte]]) throws { + + let packetList = try UnsafeMutablePointer(data: data) + self = packetList.pointee + packetList.deallocate() + + } + +} + +extension UnsafeMutablePointer where Pointee == MIDIPacketList { /// Internal use. /// Assembles a single Core MIDI `MIDIPacket` from a MIDI message byte array and wraps it in a Core MIDI `MIDIPacketList`. /// /// - Note: You must deallocate the pointer when finished with it. - @inlinable internal static func assemblePacketList(data: [MIDI.Byte]) throws -> UnsafeMutablePointer { + @inlinable internal init(data: [MIDI.Byte]) { // Create a buffer that is big enough to hold the data to be sent and // all the necessary headers. - let bufferSize = data.count + MIDI.IO.kSizeOfMIDICombinedHeaders + let bufferSize = data.count + MIDI.IO.kSizeOfMIDIPacketCombinedHeaders // the discussion section of MIDIPacketListAdd states that "The maximum // size of a packet list is 65536 bytes." Checking for that limit here. @@ -29,24 +53,17 @@ extension MIDI.IO { let packetListPointer: UnsafeMutablePointer = .allocate(capacity: 1) // prepare packet - var currentPacket: UnsafeMutablePointer? - currentPacket = MIDIPacketListInit(packetListPointer) + var currentPacket: UnsafeMutablePointer = MIDIPacketListInit(packetListPointer) - // returns NULL if there was not room in the packet list for the event + // returns NULL if there was not room in the packet list for the event (?) currentPacket = MIDIPacketListAdd(packetListPointer, bufferSize, - currentPacket!, + currentPacket, timeTag, data.count, data) - guard currentPacket != nil else { - throw MIDIError.malformed( - "Failed to add packet to packet list." - ) - } - - return packetListPointer + self = packetListPointer } @@ -54,7 +71,7 @@ extension MIDI.IO { /// Assembles an array of `Byte` arrays into Core MIDI `MIDIPacket`s and wraps them in a `MIDIPacketList`. /// /// - Note: You must deallocate the pointer when finished with it. - @inlinable internal static func assemblePacketList(data: [[MIDI.Byte]]) throws -> UnsafeMutablePointer { + @inlinable internal init(data: [[MIDI.Byte]]) throws { // Create a buffer that is big enough to hold the data to be sent and // all the necessary headers. @@ -64,7 +81,7 @@ extension MIDI.IO { // MIDIPacketListAdd's discussion section states that "The maximum size of a packet list is 65536 bytes." guard bufferSize <= 65536 else { - throw MIDIError.malformed( + throw MIDI.IO.MIDIError.malformed( "Data array is too large (\(bufferSize) bytes). Maximum size is 65536 bytes." ) } @@ -74,28 +91,21 @@ extension MIDI.IO { let packetListPointer: UnsafeMutablePointer = .allocate(capacity: 1) // prepare packet - var currentPacket: UnsafeMutablePointer? - - currentPacket = MIDIPacketListInit(packetListPointer) + var currentPacket: UnsafeMutablePointer = MIDIPacketListInit(packetListPointer) for dataBlock in 0...size + + /// Size of Core MIDI `MIDIEventPacket` struct memory. + @inline(__always) @usableFromInline + internal static let kSizeOfMIDIEventPacket = MemoryLayout.size } diff --git a/Sources/MIDIKit/IO/Managed/Input.swift b/Sources/MIDIKit/IO/Managed/Input.swift index 59f93e2af8..cda40d9a25 100644 --- a/Sources/MIDIKit/IO/Managed/Input.swift +++ b/Sources/MIDIKit/IO/Managed/Input.swift @@ -13,9 +13,8 @@ extension MIDI.IO { // MIDIIOManagedProtocol public weak var midiManager: Manager? - - // MIDIIOManagedProtocol - public private(set) var apiVersion: APIVersion + public private(set) var api: APIVersion + public private(set) var `protocol`: MIDI.IO.ProtocolVersion /// The port name as displayed in the system. public private(set) var endpointName: String = "" @@ -27,19 +26,19 @@ extension MIDI.IO { internal var receiveHandler: ReceiveHandler - internal var isReceiveReady: Bool = false - internal init(name: String, uniqueID: MIDI.IO.InputEndpoint.UniqueID? = nil, receiveHandler: ReceiveHandler.Definition, midiManager: MIDI.IO.Manager, - api: APIVersion = .bestForPlatform()) { + api: APIVersion = .bestForPlatform(), + protocol midiProtocol: MIDI.IO.ProtocolVersion = ._2_0) { self.endpointName = name self.uniqueID = uniqueID self.receiveHandler = receiveHandler.createReceiveHandler() self.midiManager = midiManager - self.apiVersion = api.isValidOnCurrentPlatform ? api : .bestForPlatform() + self.api = api.isValidOnCurrentPlatform ? api : .bestForPlatform() + self.protocol = api == .legacyCoreMIDI ? ._1_0 : midiProtocol } @@ -76,8 +75,6 @@ extension MIDI.IO.Input { internal func create(in manager: MIDI.IO.Manager) throws { - isReceiveReady = false - if uniqueIDExistsInSystem != nil { // if uniqueID is already in use, set it to nil here // so MIDIDestinationCreateWithBlock can return a new unused ID; @@ -87,21 +84,20 @@ extension MIDI.IO.Input { var newPortRef = MIDIPortRef() - switch apiVersion { + switch api { case .legacyCoreMIDI: // MIDIDestinationCreateWithBlock is deprecated after macOS 11 / iOS 14 - try MIDIDestinationCreateWithBlock( manager.clientRef, endpointName as CFString, &newPortRef, { [weak self] packetListPtr, srcConnRefCon in guard let strongSelf = self else { return } - guard strongSelf.isReceiveReady else { return } - // this must be sync and not async, otherwise the pointer gets freed before we can use it - strongSelf.midiManager?.queue.sync { - strongSelf.receiveHandler.midiReadBlock(packetListPtr, srcConnRefCon) + let packets = packetListPtr.packets() + + strongSelf.midiManager?.eventQueue.async { + strongSelf.receiveHandler.packetListReceived(packets) } } ) @@ -109,21 +105,25 @@ extension MIDI.IO.Input { case .newCoreMIDI: guard #available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) else { - throw MIDI.IO.MIDIError.internalInconsistency("\(self) is not valid on this platform.") + throw MIDI.IO.MIDIError.internalInconsistency( + "New Core MIDI API is not accessible on this platform." + ) } try MIDIDestinationCreateWithProtocol( manager.clientRef, endpointName as CFString, - ._1_0, + self.protocol.coreMIDIProtocol, &newPortRef, { [weak self] eventListPtr, srcConnRefCon in guard let strongSelf = self else { return } - guard strongSelf.isReceiveReady else { return } - // this must be sync and not async, otherwise the pointer gets freed before we can use it - strongSelf.midiManager?.queue.sync { - strongSelf.receiveHandler.midiReceiveBlock(eventListPtr, srcConnRefCon) + let packets = eventListPtr.packets() + let midiProtocol = MIDI.IO.ProtocolVersion(eventListPtr.pointee.protocol) + + strongSelf.midiManager?.eventQueue.async { + strongSelf.receiveHandler.eventListReceived(packets, + protocol: midiProtocol) } } ) @@ -147,8 +147,6 @@ extension MIDI.IO.Input { uniqueID = .init(MIDI.IO.getUniqueID(of: newPortRef)) } - isReceiveReady = true - } /// Disposes of the the virtual port if it's already been created in the system via the `create()` method. diff --git a/Sources/MIDIKit/IO/Managed/InputConnection.swift b/Sources/MIDIKit/IO/Managed/InputConnection.swift index 1dad852844..6338e9ee50 100644 --- a/Sources/MIDIKit/IO/Managed/InputConnection.swift +++ b/Sources/MIDIKit/IO/Managed/InputConnection.swift @@ -14,9 +14,8 @@ extension MIDI.IO { // MIDIIOManagedProtocol public weak var midiManager: Manager? - - // MIDIIOManagedProtocol - public private(set) var apiVersion: APIVersion + public private(set) var api: APIVersion + public private(set) var `protocol`: MIDI.IO.ProtocolVersion public private(set) var outputCriteria: MIDI.IO.EndpointIDCriteria @@ -28,17 +27,17 @@ extension MIDI.IO { public private(set) var isConnected: Bool = false - internal var isReceiveReady: Bool = false - internal init(toOutput: MIDI.IO.EndpointIDCriteria, receiveHandler: ReceiveHandler.Definition, midiManager: MIDI.IO.Manager, - api: APIVersion = .bestForPlatform()) { + api: APIVersion = .bestForPlatform(), + protocol midiProtocol: MIDI.IO.ProtocolVersion = ._2_0) { self.outputCriteria = toOutput self.receiveHandler = receiveHandler.createReceiveHandler() self.midiManager = midiManager - self.apiVersion = api.isValidOnCurrentPlatform ? api : .bestForPlatform() + self.api = api.isValidOnCurrentPlatform ? api : .bestForPlatform() + self.protocol = api == .legacyCoreMIDI ? ._1_0 : midiProtocol } @@ -61,8 +60,6 @@ extension MIDI.IO.InputConnection { if isConnected { return } - isReceiveReady = false - // if previously connected, clean the old connection _ = try? disconnect() @@ -85,21 +82,20 @@ extension MIDI.IO.InputConnection { // connection name must be unique, otherwise process might hang (?) - switch apiVersion { + switch api { case .legacyCoreMIDI: // MIDIInputPortCreateWithBlock is deprecated after macOS 11 / iOS 14 - try MIDIInputPortCreateWithBlock( manager.clientRef, UUID().uuidString as CFString, &newConnection, { [weak self] packetListPtr, srcConnRefCon in guard let strongSelf = self else { return } - guard strongSelf.isReceiveReady else { return } - // this must be sync and not async, otherwise the pointer gets freed before we can use it - strongSelf.midiManager?.queue.sync { - strongSelf.receiveHandler.midiReadBlock(packetListPtr, srcConnRefCon) + let packets = packetListPtr.packets() + + strongSelf.midiManager?.eventQueue.async { + strongSelf.receiveHandler.packetListReceived(packets) } } ) @@ -107,21 +103,25 @@ extension MIDI.IO.InputConnection { case .newCoreMIDI: guard #available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) else { - throw MIDI.IO.MIDIError.internalInconsistency("\(self) is not valid on this platform.") + throw MIDI.IO.MIDIError.internalInconsistency( + "New Core MIDI API is not accessible on this platform." + ) } try MIDIInputPortCreateWithProtocol( manager.clientRef, UUID().uuidString as CFString, - ._1_0, + self.protocol.coreMIDIProtocol, &newConnection, { [weak self] eventListPtr, srcConnRefCon in guard let strongSelf = self else { return } - guard strongSelf.isReceiveReady else { return } - // this must be sync and not async, otherwise the pointer gets freed before we can use it - strongSelf.midiManager?.queue.sync { - strongSelf.receiveHandler.midiReceiveBlock(eventListPtr, srcConnRefCon) + let packets = eventListPtr.packets() + let midiProtocol = MIDI.IO.ProtocolVersion(eventListPtr.pointee.protocol) + + strongSelf.midiManager?.eventQueue.async { + strongSelf.receiveHandler.eventListReceived(packets, + protocol: midiProtocol) } } ) @@ -140,8 +140,6 @@ extension MIDI.IO.InputConnection { isConnected = true - isReceiveReady = true - } /// Disconnects the connection if it's currently connected. @@ -151,12 +149,12 @@ extension MIDI.IO.InputConnection { isConnected = false - guard let upwrappedInputPortRef = self.inputPortRef, - let upwrappedOutputEndpointRef = self.outputEndpointRef else { return } + guard let unwrappedInputPortRef = self.inputPortRef, + let unwrappedOutputEndpointRef = self.outputEndpointRef else { return } defer { self.inputPortRef = nil } - try MIDIPortDisconnectSource(upwrappedInputPortRef, upwrappedOutputEndpointRef) + try MIDIPortDisconnectSource(unwrappedInputPortRef, unwrappedOutputEndpointRef) .throwIfOSStatusErr() } diff --git a/Sources/MIDIKit/IO/Managed/Output.swift b/Sources/MIDIKit/IO/Managed/Output.swift index 961686f77e..567e10d6cb 100644 --- a/Sources/MIDIKit/IO/Managed/Output.swift +++ b/Sources/MIDIKit/IO/Managed/Output.swift @@ -13,9 +13,8 @@ extension MIDI.IO { // MIDIIOManagedProtocol public weak var midiManager: Manager? - - // MIDIIOManagedProtocol - public private(set) var apiVersion: APIVersion + public private(set) var api: APIVersion + public private(set) var `protocol`: MIDI.IO.ProtocolVersion /// The port name as displayed in the system. public private(set) var endpointName: String = "" @@ -28,12 +27,14 @@ extension MIDI.IO { internal init(name: String, uniqueID: MIDI.IO.OutputEndpoint.UniqueID? = nil, midiManager: MIDI.IO.Manager, - api: APIVersion = .bestForPlatform()) { + api: APIVersion = .bestForPlatform(), + protocol midiProtocol: MIDI.IO.ProtocolVersion = ._2_0) { self.endpointName = name self.uniqueID = uniqueID self.midiManager = midiManager - self.apiVersion = api.isValidOnCurrentPlatform ? api : .bestForPlatform() + self.api = api.isValidOnCurrentPlatform ? api : .bestForPlatform() + self.protocol = api == .legacyCoreMIDI ? ._1_0 : midiProtocol } @@ -52,11 +53,11 @@ extension MIDI.IO.Output { /// Queries the system and returns true if the endpoint exists (by matching port name and unique ID) public var uniqueIDExistsInSystem: MIDIEndpointRef? { - guard let upwrappedUniqueID = self.uniqueID else { + guard let unwrappedUniqueID = self.uniqueID else { return nil } - if let endpoint = MIDI.IO.getSystemSourceEndpoint(matching: upwrappedUniqueID.coreMIDIUniqueID) { + if let endpoint = MIDI.IO.getSystemSourceEndpoint(matching: unwrappedUniqueID.coreMIDIUniqueID) { return endpoint } @@ -79,10 +80,9 @@ extension MIDI.IO.Output { var newPortRef = MIDIPortRef() - switch apiVersion { + switch api { case .legacyCoreMIDI: // MIDISourceCreate is deprecated after macOS 11 / iOS 14 - try MIDISourceCreate( manager.clientRef, endpointName as CFString, @@ -92,13 +92,15 @@ extension MIDI.IO.Output { case .newCoreMIDI: guard #available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) else { - throw MIDI.IO.MIDIError.internalInconsistency("\(self) is not valid on this platform.") + throw MIDI.IO.MIDIError.internalInconsistency( + "New Core MIDI API is not accessible on this platform." + ) } try MIDISourceCreateWithProtocol( manager.clientRef, endpointName as CFString, - ._1_0, + self.protocol.coreMIDIProtocol, &newPortRef ) .throwIfOSStatusErr() @@ -128,11 +130,11 @@ extension MIDI.IO.Output { /// Errors thrown can be safely ignored and are typically only useful for debugging purposes. internal func dispose() throws { - guard let upwrappedPortRef = self.portRef else { return } + guard let unwrappedPortRef = self.portRef else { return } defer { self.portRef = nil } - try MIDIEndpointDispose(upwrappedPortRef) + try MIDIEndpointDispose(unwrappedPortRef) .throwIfOSStatusErr() } @@ -158,13 +160,13 @@ extension MIDI.IO.Output: MIDIIOSendsMIDIMessagesProtocol { public func send(packetList: UnsafeMutablePointer) throws { - guard let upwrappedPortRef = self.portRef else { + guard let unwrappedPortRef = self.portRef else { throw MIDI.IO.MIDIError.internalInconsistency( "Port reference is nil." ) } - try MIDIReceived(upwrappedPortRef, packetList) + try MIDIReceived(unwrappedPortRef, packetList) .throwIfOSStatusErr() } @@ -172,13 +174,13 @@ extension MIDI.IO.Output: MIDIIOSendsMIDIMessagesProtocol { @available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) public func send(eventList: UnsafeMutablePointer) throws { - guard let upwrappedPortRef = self.portRef else { + guard let unwrappedPortRef = self.portRef else { throw MIDI.IO.MIDIError.internalInconsistency( "Port reference is nil." ) } - try MIDIReceivedEventList(upwrappedPortRef, eventList) + try MIDIReceivedEventList(unwrappedPortRef, eventList) .throwIfOSStatusErr() } diff --git a/Sources/MIDIKit/IO/Managed/OutputConnection.swift b/Sources/MIDIKit/IO/Managed/OutputConnection.swift index 4b53199de4..7d4076cdb8 100644 --- a/Sources/MIDIKit/IO/Managed/OutputConnection.swift +++ b/Sources/MIDIKit/IO/Managed/OutputConnection.swift @@ -14,9 +14,8 @@ extension MIDI.IO { // MIDIIOManagedProtocol public weak var midiManager: Manager? - - // MIDIIOManagedProtocol - public private(set) var apiVersion: APIVersion + public private(set) var api: APIVersion + public private(set) var `protocol`: MIDI.IO.ProtocolVersion public var inputCriteria: MIDI.IO.EndpointIDCriteria @@ -28,11 +27,13 @@ extension MIDI.IO { internal init( toInput: MIDI.IO.EndpointIDCriteria, - api: APIVersion = .bestForPlatform() + api: APIVersion = .bestForPlatform(), + protocol midiProtocol: MIDI.IO.ProtocolVersion = ._2_0 ) { self.inputCriteria = toInput - self.apiVersion = api.isValidOnCurrentPlatform ? api : .bestForPlatform() + self.api = api.isValidOnCurrentPlatform ? api : .bestForPlatform() + self.protocol = api == .legacyCoreMIDI ? ._1_0 : midiProtocol } @@ -96,15 +97,15 @@ extension MIDI.IO.OutputConnection { isConnected = false - guard let upwrappedOutputPortRef = self.portRef, - let upwrappedInputEndpointRef = self.inputEndpointRef else { return } + guard let unwrappedOutputPortRef = self.portRef, + let unwrappedInputEndpointRef = self.inputEndpointRef else { return } defer { self.portRef = nil self.inputEndpointRef = nil } - try MIDIPortDisconnectSource(upwrappedOutputPortRef, upwrappedInputEndpointRef) + try MIDIPortDisconnectSource(unwrappedOutputPortRef, unwrappedInputEndpointRef) .throwIfOSStatusErr() } @@ -160,20 +161,20 @@ extension MIDI.IO.OutputConnection: MIDIIOSendsMIDIMessagesProtocol { public func send(packetList: UnsafeMutablePointer) throws { - guard let upwrappedOutputPortRef = self.portRef else { + guard let unwrappedOutputPortRef = self.portRef else { throw MIDI.IO.MIDIError.internalInconsistency( "Output port reference is nil." ) } - guard let upwrappedInputEndpointRef = self.inputEndpointRef else { + guard let unwrappedInputEndpointRef = self.inputEndpointRef else { throw MIDI.IO.MIDIError.internalInconsistency( "Input port reference is nil." ) } - try MIDISend(upwrappedOutputPortRef, - upwrappedInputEndpointRef, + try MIDISend(unwrappedOutputPortRef, + unwrappedInputEndpointRef, packetList) .throwIfOSStatusErr() @@ -182,20 +183,20 @@ extension MIDI.IO.OutputConnection: MIDIIOSendsMIDIMessagesProtocol { @available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) public func send(eventList: UnsafeMutablePointer) throws { - guard let upwrappedOutputPortRef = self.portRef else { + guard let unwrappedOutputPortRef = self.portRef else { throw MIDI.IO.MIDIError.internalInconsistency( "Output port reference is nil." ) } - guard let upwrappedInputEndpointRef = self.inputEndpointRef else { + guard let unwrappedInputEndpointRef = self.inputEndpointRef else { throw MIDI.IO.MIDIError.internalInconsistency( "Input port reference is nil." ) } - try MIDISendEventList(upwrappedOutputPortRef, - upwrappedInputEndpointRef, + try MIDISendEventList(unwrappedOutputPortRef, + unwrappedInputEndpointRef, eventList) .throwIfOSStatusErr() diff --git a/Sources/MIDIKit/IO/Managed/Protocols/MIDIIOManagedProtocol.swift b/Sources/MIDIKit/IO/Managed/Protocols/MIDIIOManagedProtocol.swift index 55f91a3cb6..afa28f81d2 100644 --- a/Sources/MIDIKit/IO/Managed/Protocols/MIDIIOManagedProtocol.swift +++ b/Sources/MIDIKit/IO/Managed/Protocols/MIDIIOManagedProtocol.swift @@ -9,6 +9,6 @@ public protocol MIDIIOManagedProtocol { /* public weak */ var midiManager: MIDI.IO.Manager? { get set } /// Core MIDI API version used to create the endpoint and send/receive MIDI messages (if applicable). - /* public private(set) */ var apiVersion: MIDI.IO.APIVersion { get } + /* public private(set) */ var api: MIDI.IO.APIVersion { get } } diff --git a/Sources/MIDIKit/IO/Managed/Protocols/MIDIIOReceivesMIDIMessagesProtocol.swift b/Sources/MIDIKit/IO/Managed/Protocols/MIDIIOReceivesMIDIMessagesProtocol.swift index 38ddfa2322..5c0a7f83df 100644 --- a/Sources/MIDIKit/IO/Managed/Protocols/MIDIIOReceivesMIDIMessagesProtocol.swift +++ b/Sources/MIDIKit/IO/Managed/Protocols/MIDIIOReceivesMIDIMessagesProtocol.swift @@ -7,6 +7,7 @@ import CoreMIDI public protocol MIDIIOReceivesMIDIMessagesProtocol: MIDIIOManagedProtocol { - // empty + /// MIDI Spec version used for this endpoint. + /* public private(set) */ var `protocol`: MIDI.IO.ProtocolVersion { get } } diff --git a/Sources/MIDIKit/IO/Managed/Protocols/MIDIIOSendsMIDIMessagesProtocol.swift b/Sources/MIDIKit/IO/Managed/Protocols/MIDIIOSendsMIDIMessagesProtocol.swift index 88df6986da..1de47a3106 100644 --- a/Sources/MIDIKit/IO/Managed/Protocols/MIDIIOSendsMIDIMessagesProtocol.swift +++ b/Sources/MIDIKit/IO/Managed/Protocols/MIDIIOSendsMIDIMessagesProtocol.swift @@ -7,6 +7,9 @@ import CoreMIDI public protocol MIDIIOSendsMIDIMessagesProtocol: MIDIIOManagedProtocol { + /// MIDI Spec version used for this endpoint. + /* public private(set) */ var `protocol`: MIDI.IO.ProtocolVersion { get } + /// Core MIDI Port Ref var portRef: MIDIPortRef? { get } @@ -29,6 +32,10 @@ public protocol MIDIIOSendsMIDIMessagesProtocol: MIDIIOManagedProtocol { @available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) func send(eventList: UnsafeMutablePointer) throws + /// Send a Universal MIDI Packet (MIDI 2.0). + @available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) + func send(rawWords: [MIDI.UMPWord]) throws + } extension MIDIIOSendsMIDIMessagesProtocol { @@ -38,14 +45,13 @@ extension MIDIIOSendsMIDIMessagesProtocol { /// - Parameter rawMessage: MIDI message @inlinable public func send(rawMessage: [MIDI.Byte]) throws { - switch apiVersion { + switch api { case .legacyCoreMIDI: - let packetListPointer = try MIDI.IO.assemblePacketList(data: rawMessage) + var packetList = MIDIPacketList(data: rawMessage) - try send(packetList: packetListPointer) - - // we HAVE to deallocate this here after we're done with it - packetListPointer.deallocate() + try withUnsafeMutablePointer(to: &packetList) { ptr in + try send(packetList: ptr) + } case .newCoreMIDI: #warning("> code this") @@ -60,14 +66,13 @@ extension MIDIIOSendsMIDIMessagesProtocol { /// - Parameter rawMessages: Array of MIDI messages @inlinable public func send(rawMessages: [[MIDI.Byte]]) throws { - switch apiVersion { + switch api { case .legacyCoreMIDI: - let packetListPointer = try MIDI.IO.assemblePacketList(data: rawMessages) - - try send(packetList: packetListPointer) + var packetList = try MIDIPacketList(data: rawMessages) - // we HAVE to deallocate this here after we're done with it - packetListPointer.deallocate() + try withUnsafeMutablePointer(to: &packetList) { ptr in + try send(packetList: ptr) + } case .newCoreMIDI: #warning("> code this") @@ -77,6 +82,32 @@ extension MIDIIOSendsMIDIMessagesProtocol { } + /// Send a MIDI message inside a Universal MIDI Packet. + /// + /// - Parameter rawWords: Array of `UInt32` words + @available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) + @inlinable public func send(rawWords: [MIDI.UMPWord]) throws { + + switch api { + case .legacyCoreMIDI: + throw MIDI.IO.MIDIError.internalInconsistency( + "Universal MIDI Packet words cannot be sent using old Core MIDI API." + ) + + case .newCoreMIDI: + var eventList = try MIDIEventList( + protocol: self.protocol.coreMIDIProtocol, + packetWords: rawWords + ) + + try withUnsafeMutablePointer(to: &eventList) { ptr in + try send(eventList: ptr) + } + + } + + } + } extension MIDIIOSendsMIDIMessagesProtocol { @@ -84,13 +115,18 @@ extension MIDIIOSendsMIDIMessagesProtocol { /// Send a MIDI Message. @inlinable public func send(event: MIDI.Event) throws { - switch apiVersion { + switch api { case .legacyCoreMIDI: try send(rawMessage: event.midi1RawBytes) case .newCoreMIDI: - #warning("> could use send(eventList:) here") - try send(rawMessage: event.midi1RawBytes) + guard #available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) else { + throw MIDI.IO.MIDIError.internalInconsistency( + "New Core MIDI API is not accessible on this platform." + ) + } + + try send(rawWords: event.umpRawWords) } } @@ -98,13 +134,20 @@ extension MIDIIOSendsMIDIMessagesProtocol { /// Send multiple MIDI Messages. @inlinable public func send(events: [MIDI.Event]) throws { - switch apiVersion { + switch api { case .legacyCoreMIDI: try send(rawMessages: events.map { $0.midi1RawBytes }) case .newCoreMIDI: - #warning("> could use send(eventList:) here") - try send(rawMessages: events.map { $0.midi1RawBytes }) + guard #available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) else { + throw MIDI.IO.MIDIError.internalInconsistency( + "New Core MIDI API is not accessible on this platform." + ) + } + + for event in events { + try send(rawWords: event.umpRawWords) + } } } diff --git a/Sources/MIDIKit/IO/Managed/ThruConnection.swift b/Sources/MIDIKit/IO/Managed/ThruConnection.swift index 22f6383ee1..e2065d8a1c 100644 --- a/Sources/MIDIKit/IO/Managed/ThruConnection.swift +++ b/Sources/MIDIKit/IO/Managed/ThruConnection.swift @@ -23,9 +23,7 @@ extension MIDI.IO { // MIDIIOManagedProtocol public weak var midiManager: Manager? - - // MIDIIOManagedProtocol - public private(set) var apiVersion: APIVersion + public private(set) var api: APIVersion public private(set) var thruConnectionRef: MIDIThruConnectionRef? = nil @@ -57,7 +55,7 @@ extension MIDI.IO { self.lifecycle = lifecycle self.params = params self.midiManager = midiManager - self.apiVersion = api.isValidOnCurrentPlatform ? api : .bestForPlatform() + self.api = api.isValidOnCurrentPlatform ? api : .bestForPlatform() } @@ -196,13 +194,13 @@ extension MIDI.IO.ThruConnection { /// Errors thrown can be safely ignored and are typically only useful for debugging purposes. internal func dispose() throws { - guard let upwrappedThruConnectionRef = self.thruConnectionRef else { return } + guard let unwrappedThruConnectionRef = self.thruConnectionRef else { return } defer { self.thruConnectionRef = nil } - try MIDIThruConnectionDispose(upwrappedThruConnectionRef) + try MIDIThruConnectionDispose(unwrappedThruConnectionRef) .throwIfOSStatusErr() } diff --git a/Sources/MIDIKit/IO/Manager/Manager Add and Remove.swift b/Sources/MIDIKit/IO/Manager/Manager Add and Remove.swift index 1863608a45..64addcd218 100644 --- a/Sources/MIDIKit/IO/Manager/Manager Add and Remove.swift +++ b/Sources/MIDIKit/IO/Manager/Manager Add and Remove.swift @@ -35,14 +35,15 @@ extension MIDI.IO.Manager { receiveHandler: MIDI.IO.ReceiveHandler.Definition ) throws { - try queue.sync { + try eventQueue.sync { let newVD = MIDI.IO.Input( name: name, uniqueID: uniqueID.readID(), receiveHandler: receiveHandler, midiManager: self, - api: preferredAPI + api: preferredAPI, + protocol: ._1_0 // hard-coded to 1.0 until full 2.0 support is added ) managedInputs[tag] = newVD @@ -75,13 +76,14 @@ extension MIDI.IO.Manager { receiveHandler: MIDI.IO.ReceiveHandler.Definition ) throws { - try queue.sync { + try eventQueue.sync { let newCD = MIDI.IO.InputConnection( toOutput: toOutput, receiveHandler: receiveHandler, midiManager: self, - api: preferredAPI + api: preferredAPI, + protocol: ._1_0 // hard-coded to 1.0 until full 2.0 support is added ) // store the connection object in the manager, @@ -118,13 +120,14 @@ extension MIDI.IO.Manager { uniqueID: MIDI.IO.UniqueIDPersistence ) throws { - try queue.sync { + try eventQueue.sync { let newVS = MIDI.IO.Output( name: name, uniqueID: uniqueID.readID(), midiManager: self, - api: preferredAPI + api: preferredAPI, + protocol: ._1_0 // hard-coded to 1.0 until full 2.0 support is added ) managedOutputs[tag] = newVS @@ -155,11 +158,12 @@ extension MIDI.IO.Manager { tag: String ) throws { - try queue.sync { + try eventQueue.sync { let newCS = MIDI.IO.OutputConnection( toInput: toInput, - api: preferredAPI + api: preferredAPI, + protocol: ._1_0 // hard-coded to 1.0 until full 2.0 support is added ) // store the connection object in the manager, @@ -200,7 +204,7 @@ extension MIDI.IO.Manager { params: MIDIThruConnectionParams? = nil ) throws { - try queue.sync { + try eventQueue.sync { let newCT = MIDI.IO.ThruConnection( outputs: outputs, @@ -252,7 +256,7 @@ extension MIDI.IO.Manager { public func remove(_ type: ManagedType, _ tagSelection: TagSelection) { - queue.sync { + eventQueue.sync { switch type { case .inputConnection: @@ -311,7 +315,7 @@ extension MIDI.IO.Manager { /// - `manufacturer` property public func removeAll() { - // `self.remove(...)` internally uses queue.sync{} + // `self.remove(...)` internally uses operationQueue.sync{} // so don't need to wrap this with it here for managedEndpointType in ManagedType.allCases { diff --git a/Sources/MIDIKit/IO/Manager/Manager State.swift b/Sources/MIDIKit/IO/Manager/Manager State.swift index cf214fcc49..bcca07bdaa 100644 --- a/Sources/MIDIKit/IO/Manager/Manager State.swift +++ b/Sources/MIDIKit/IO/Manager/Manager State.swift @@ -14,7 +14,7 @@ extension MIDI.IO.Manager { /// - Throws: `MIDI.IO.MIDIError.osStatus` public func start() throws { - try queue.sync { + try eventQueue.sync { // if start() was already called, return guard clientRef == MIDIClientRef() else { return } diff --git a/Sources/MIDIKit/IO/Manager/Manager.swift b/Sources/MIDIKit/IO/Manager/Manager.swift index 5a0cf0967b..d6353c4fda 100644 --- a/Sources/MIDIKit/IO/Manager/Manager.swift +++ b/Sources/MIDIKit/IO/Manager/Manager.swift @@ -81,8 +81,8 @@ extension MIDI.IO { // MARK: - Internal dispatch queue - /// Thread for all Manager operations and I/O. - internal var queue: DispatchQueue + /// Thread for MIDI event I/O. + internal var eventQueue: DispatchQueue // MARK: - Init @@ -107,15 +107,17 @@ extension MIDI.IO { preferredAPI = APIVersion.legacyCoreMIDI.isValidOnCurrentPlatform ? .legacyCoreMIDI : .bestForPlatform() - // set up dedicated manager queue + // queue client name var clientNameForQueue = clientName.onlyAlphanumerics if clientNameForQueue.isEmpty { clientNameForQueue = UUID().uuidString } - let queueName = (Bundle.main.bundleIdentifier ?? "unknown") + ".midiManager." + clientNameForQueue - queue = DispatchQueue(label: queueName, - qos: .userInteractive, - attributes: [], - autoreleaseFrequency: .workItem, - target: .global(qos: .userInteractive)) + + // manager event queue + let eventQueueName = (Bundle.main.bundleIdentifier ?? "unknown") + ".midiManager." + clientNameForQueue + ".events" + eventQueue = DispatchQueue(label: eventQueueName, + qos: .userInteractive, + attributes: [], + autoreleaseFrequency: .workItem, + target: .global(qos: .userInteractive)) // assign other properties self.clientName = clientName @@ -126,7 +128,7 @@ extension MIDI.IO { } deinit { - queue.sync { + eventQueue.sync { let result = MIDIClientDispose(clientRef) if result != noErr { diff --git a/Sources/MIDIKit/IO/ReceiveHandler/Handlers/Events.swift b/Sources/MIDIKit/IO/ReceiveHandler/Handlers/Events.swift index 2abd5c046a..0d84994f7d 100644 --- a/Sources/MIDIKit/IO/ReceiveHandler/Handlers/Events.swift +++ b/Sources/MIDIKit/IO/ReceiveHandler/Handlers/Events.swift @@ -17,12 +17,11 @@ extension MIDI.IO.ReceiveHandler { internal let midi1Parser = MIDI.MIDI1Parser() internal let midi2Parser = MIDI.MIDI2Parser() - @inline(__always) public func midiReadBlock( - _ packetListPtr: UnsafePointer, - _ srcConnRefCon: UnsafeMutableRawPointer? + @inline(__always) public func packetListReceived( + _ packets: [MIDI.Packet.PacketData] ) { - for midiPacket in packetListPtr.packets() { + for midiPacket in packets { let events = midi1Parser.parsedEvents(in: midiPacket) handler(events) } @@ -30,12 +29,12 @@ extension MIDI.IO.ReceiveHandler { } @available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) - @inline(__always) public func midiReceiveBlock( - _ eventListPtr: UnsafePointer, - _ srcConnRefCon: UnsafeMutableRawPointer? + @inline(__always) public func eventListReceived( + _ packets: [MIDI.Packet.UniversalPacketData], + protocol midiProtocol: MIDI.IO.ProtocolVersion ) { - for midiPacket in eventListPtr.packets() { + for midiPacket in packets { let events = midi2Parser.parsedEvents(in: midiPacket) handler(events) } diff --git a/Sources/MIDIKit/IO/ReceiveHandler/Handlers/EventsLogging.swift b/Sources/MIDIKit/IO/ReceiveHandler/Handlers/EventsLogging.swift index 4e9b5cd2e8..38fefda219 100644 --- a/Sources/MIDIKit/IO/ReceiveHandler/Handlers/EventsLogging.swift +++ b/Sources/MIDIKit/IO/ReceiveHandler/Handlers/EventsLogging.swift @@ -22,12 +22,11 @@ extension MIDI.IO.ReceiveHandler { @inline(__always) public var filterActiveSensingAndClock = false - @inline(__always) public func midiReadBlock( - _ packetListPtr: UnsafePointer, - _ srcConnRefCon: UnsafeMutableRawPointer? + @inline(__always) public func packetListReceived( + _ packets: [MIDI.Packet.PacketData] ) { - for midiPacket in packetListPtr.packets() { + for midiPacket in packets { let events = midi1Parser.parsedEvents(in: midiPacket) logEvents(events) } @@ -35,12 +34,12 @@ extension MIDI.IO.ReceiveHandler { } @available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) - @inline(__always) public func midiReceiveBlock( - _ eventListPtr: UnsafePointer, - _ srcConnRefCon: UnsafeMutableRawPointer? + @inline(__always) public func eventListReceived( + _ packets: [MIDI.Packet.UniversalPacketData], + protocol midiProtocol: MIDI.IO.ProtocolVersion ) { - for midiPacket in eventListPtr.packets() { + for midiPacket in packets { let events = midi2Parser.parsedEvents(in: midiPacket) logEvents(events) } diff --git a/Sources/MIDIKit/IO/ReceiveHandler/Handlers/Group.swift b/Sources/MIDIKit/IO/ReceiveHandler/Handlers/Group.swift index 9e5b6bd7ef..73b72ab0a5 100644 --- a/Sources/MIDIKit/IO/ReceiveHandler/Handlers/Group.swift +++ b/Sources/MIDIKit/IO/ReceiveHandler/Handlers/Group.swift @@ -13,25 +13,24 @@ extension MIDI.IO.ReceiveHandler { public var receiveHandlers: [MIDI.IO.ReceiveHandler] = [] - @inline(__always) public func midiReadBlock( - _ packetListPtr: UnsafePointer, - _ srcConnRefCon: UnsafeMutableRawPointer? + @inline(__always) public func packetListReceived( + _ packets: [MIDI.Packet.PacketData] ) { for handler in receiveHandlers { - handler.midiReadBlock(packetListPtr, srcConnRefCon) + handler.packetListReceived(packets) } } @available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) - @inline(__always) public func midiReceiveBlock( - _ eventListPtr: UnsafePointer, - _ srcConnRefCon: UnsafeMutableRawPointer? + @inline(__always) public func eventListReceived( + _ packets: [MIDI.Packet.UniversalPacketData], + protocol midiProtocol: MIDI.IO.ProtocolVersion ) { for handler in receiveHandlers { - handler.midiReceiveBlock(eventListPtr, srcConnRefCon) + handler.eventListReceived(packets, protocol: midiProtocol) } } diff --git a/Sources/MIDIKit/IO/ReceiveHandler/Handlers/RawData.swift b/Sources/MIDIKit/IO/ReceiveHandler/Handlers/RawData.swift index c6f6e1e74c..6f2cecaa2c 100644 --- a/Sources/MIDIKit/IO/ReceiveHandler/Handlers/RawData.swift +++ b/Sources/MIDIKit/IO/ReceiveHandler/Handlers/RawData.swift @@ -14,12 +14,11 @@ extension MIDI.IO.ReceiveHandler { @inline(__always) public var handler: Handler - @inline(__always) public func midiReadBlock( - _ packetListPtr: UnsafePointer, - _ srcConnRefCon: UnsafeMutableRawPointer? + @inline(__always) public func packetListReceived( + _ packets: [MIDI.Packet.PacketData] ) { - for midiPacket in packetListPtr.packets() { + for midiPacket in packets { let typeErasedPacket = MIDI.Packet.packet(midiPacket) handler(typeErasedPacket) } @@ -27,12 +26,12 @@ extension MIDI.IO.ReceiveHandler { } @available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) - @inline(__always) public func midiReceiveBlock( - _ eventListPtr: UnsafePointer, - _ srcConnRefCon: UnsafeMutableRawPointer? + @inline(__always) public func eventListReceived( + _ packets: [MIDI.Packet.UniversalPacketData], + protocol midiProtocol: MIDI.IO.ProtocolVersion ) { - for midiPacket in eventListPtr.packets() { + for midiPacket in packets { let typeErasedPacket = MIDI.Packet.universalPacket(midiPacket) handler(typeErasedPacket) } diff --git a/Sources/MIDIKit/IO/ReceiveHandler/Handlers/RawDataLogging.swift b/Sources/MIDIKit/IO/ReceiveHandler/Handlers/RawDataLogging.swift index 52061601b4..88907b0d19 100644 --- a/Sources/MIDIKit/IO/ReceiveHandler/Handlers/RawDataLogging.swift +++ b/Sources/MIDIKit/IO/ReceiveHandler/Handlers/RawDataLogging.swift @@ -21,24 +21,23 @@ extension MIDI.IO.ReceiveHandler { @inline(__always) public var filterActiveSensingAndClock = false - @inline(__always) public func midiReadBlock( - _ packetListPtr: UnsafePointer, - _ srcConnRefCon: UnsafeMutableRawPointer? + @inline(__always) public func packetListReceived( + _ packets: [MIDI.Packet.PacketData] ) { - for midiPacket in packetListPtr.packets() { + for midiPacket in packets { handleBytes(midiPacket.bytes) } } @available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) - @inline(__always) public func midiReceiveBlock( - _ eventListPtr: UnsafePointer, - _ srcConnRefCon: UnsafeMutableRawPointer? + @inline(__always) public func eventListReceived( + _ packets: [MIDI.Packet.UniversalPacketData], + protocol midiProtocol: MIDI.IO.ProtocolVersion ) { - for midiPacket in eventListPtr.packets() { + for midiPacket in packets { handleBytes(midiPacket.bytes) } diff --git a/Sources/MIDIKit/IO/ReceiveHandler/MIDIIOReceiveHandlerProtocol.swift b/Sources/MIDIKit/IO/ReceiveHandler/MIDIIOReceiveHandlerProtocol.swift index b183637991..1c8a582372 100644 --- a/Sources/MIDIKit/IO/ReceiveHandler/MIDIIOReceiveHandlerProtocol.swift +++ b/Sources/MIDIKit/IO/ReceiveHandler/MIDIIOReceiveHandlerProtocol.swift @@ -10,19 +10,18 @@ import CoreMIDI /// For operating system backwards compatibility, both `MIDIReadBlock` (old Core MIDI API) and `MIDIReceiveBlock` (new Core MIDI API) must be handled. public protocol MIDIIOReceiveHandlerProtocol { - /// CoreMIDI `MIDIReadBlock` signature + /// CoreMIDI `MIDIReadBlock` /// (deprecated after macOS 11 / iOS 14) - @inline(__always) func midiReadBlock( - _ packetListPtr: UnsafePointer, - _ srcConnRefCon: UnsafeMutableRawPointer? + @inline(__always) func packetListReceived( + _ packets: [MIDI.Packet.PacketData] ) - /// CoreMIDI `MIDIReceiveBlock` signature + /// CoreMIDI `MIDIReceiveBlock` /// (introduced in macOS 11 / iOS 14) @available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) - @inline(__always) func midiReceiveBlock( - _ eventListPtr: UnsafePointer, - _ srcConnRefCon: UnsafeMutableRawPointer? + @inline(__always) func eventListReceived( + _ packets: [MIDI.Packet.UniversalPacketData], + protocol midiProtocol: MIDI.IO.ProtocolVersion ) } diff --git a/Sources/MIDIKit/IO/ReceiveHandler/ReceiveHandler.swift b/Sources/MIDIKit/IO/ReceiveHandler/ReceiveHandler.swift index 404171e582..9565392018 100644 --- a/Sources/MIDIKit/IO/ReceiveHandler/ReceiveHandler.swift +++ b/Sources/MIDIKit/IO/ReceiveHandler/ReceiveHandler.swift @@ -17,22 +17,21 @@ extension MIDI.IO { @inline(__always) public var handler: MIDIIOReceiveHandlerProtocol - @inline(__always) public func midiReadBlock( - _ packetListPtr: UnsafePointer, - _ srcConnRefCon: UnsafeMutableRawPointer? + @inline(__always) public func packetListReceived( + _ packets: [MIDI.Packet.PacketData] ) { - handler.midiReadBlock(packetListPtr, srcConnRefCon) + handler.packetListReceived(packets) } @available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) - @inline(__always) public func midiReceiveBlock( - _ eventListPtr: UnsafePointer, - _ srcConnRefCon: UnsafeMutableRawPointer? + @inline(__always) public func eventListReceived( + _ packets: [MIDI.Packet.UniversalPacketData], + protocol midiProtocol: MIDI.IO.ProtocolVersion ) { - handler.midiReceiveBlock(eventListPtr, srcConnRefCon) + handler.eventListReceived(packets, protocol: midiProtocol) } diff --git a/Sources/MIDIKit/Parser/MIDI2Parser.swift b/Sources/MIDIKit/Parser/MIDI2Parser.swift index 2e67558e43..3f589eaf78 100644 --- a/Sources/MIDIKit/Parser/MIDI2Parser.swift +++ b/Sources/MIDIKit/Parser/MIDI2Parser.swift @@ -47,6 +47,7 @@ extension MIDI { switch messageType { case .utility: // 0x0 + // TODO: nothing implemented here yet break case .systemRealTimeAndCommon: // 0x1 @@ -56,12 +57,14 @@ extension MIDI { // byte 2: [MIDI1 data byte 1, or 0x00 if not applicable] // byte 3: [MIDI1 data byte 2, or 0x00 if not applicable] - let parsedMIDI1Event = Self.midi1Parser.parsedEvents( - in: Array(bytes[bytes.startIndex.advanced(by: 1)...]), - umpGroup: group - ) + let midiBytes = bytes[bytes.startIndex.advanced(by: 1)...] - events.append(contentsOf: parsedMIDI1Event) + if let parsedEvent = parseSystemRealTimeAndCommon( + bytes: midiBytes, + group: group + ) { + events.append(parsedEvent) + } case .midi1ChannelVoice: // 0x2 // always 32 bits (4 bytes) @@ -70,62 +73,27 @@ extension MIDI { // byte 2: [MIDI1 data byte 1, or 0x00 if not applicable] // byte 3: [MIDI1 data byte 2, or 0x00 if not applicable] - let parsedMIDI1Event = Self.midi1Parser.parsedEvents( - in: Array(bytes[bytes.startIndex.advanced(by: 1)...]), - umpGroup: group - ) + let midiBytes = bytes[bytes.startIndex.advanced(by: 1)...] - events.append(contentsOf: parsedMIDI1Event) + if let parsedEvent = parseMIDI1ChannelVoice( + bytes: midiBytes, + group: group + ) { + events.append(parsedEvent) + } case .data64bit: // 0x3 // SysEx 7 - // MIDI 2.0 Spec: - // "The MIDI 1.0 Protocol bracketing method with 0xF0 Start and 0xF7 End Status bytes is not used in the UMP Format. Instead, the SysEx payload is carried in one or more 64-bit UMPs, discarding the 0xF0 and 0xF7 bytes. The standard ID Number (Manufacturer ID, Special ID 0x7D, or Universal System Exclusive ID), Device ID, and Sub-ID#1 & Sub-ID#2 (if applicable) are included in the initial data bytes, just as they are in MIDI 1.0 Protocol message equivalents." - - let byte1Nibbles = bytes[bytes.startIndex.advanced(by: 1)].nibbles - - guard let sysExStatusField = MIDI.Packet.UniversalPacketData - .SysExStatusField(rawValue: byte1Nibbles.high) - else { - return events - } - - let numberOfBytes = byte1Nibbles.low.intValue + let midiBytes = bytes[bytes.startIndex.advanced(by: 1)...] - switch sysExStatusField { - case .complete: - guard bytes.count >= numberOfBytes + 2 - else { return events } - - let payloadBytes = bytes[ - bytes.startIndex.advanced(by: 2) - ..< - bytes.startIndex.advanced(by: 2 + numberOfBytes) - ] - - guard let parsedSysEx = try? MIDI.Event.SysEx - .parsed(from: [0xF0] + payloadBytes + [0xF7]) - else { return events } - - events.append(parsedSysEx) - - case .start: - #warning("> handle multi-packet SysEx Messages") - return events - - case .continue: - #warning("> handle multi-packet SysEx Messages") - return events - - case .end: - #warning("> handle multi-packet SysEx Messages") - return events - + if let parsedEvent = parseData64Bit( + bytes: midiBytes, + group: group + ) { + events.append(parsedEvent) } - break - case .midi2ChannelVoice: // 0x4 // always 64 bits (8 bytes) // byte 0: [high nibble: message type, low nibble: group] @@ -133,7 +101,7 @@ extension MIDI { // byte 2&3: [index] // byte 4...7: [data bytes] - // currently MIDIKit can only receive MIDI 1.0 events because + // TODO: currently MIDIKit can only receive MIDI 1.0 events because // we are forcing Protocol 1.0 since MIDI 2.0 events are not // implemented yet, so this should never happen (for the time being) break @@ -141,6 +109,8 @@ extension MIDI { case .data128bit: // 0x5 // can contain a SysEx 8 message + // TODO: needs implementation + break } @@ -149,6 +119,257 @@ extension MIDI { } + internal static func parseSystemRealTimeAndCommon( + bytes: Array.SubSequence, + group: MIDI.UInt4 + ) -> MIDI.Event? { + + let statusByte = bytes[bytes.startIndex] + + func dataByte1() -> MIDI.Byte? { + bytes.count > 1 + ? bytes[bytes.startIndex.advanced(by: 1)] : nil + } + + func dataByte2() -> MIDI.Byte? { + bytes.count > 2 + ? bytes[bytes.startIndex.advanced(by: 2)] : nil + } + + switch statusByte { + case 0xF0: // System Common - SysEx Start + return nil + + case 0xF1: // System Common - timecode quarter-frame + guard let unwrappedDataByte1 = dataByte1() + else { return nil } + + return .timecodeQuarterFrame(byte: unwrappedDataByte1) + + case 0xF2: // System Common - Song Position Pointer + guard let unwrappedDataByte1 = dataByte1(), + let unwrappedDataByte2 = dataByte2() + else { return nil } + + let uint14 = MIDI.UInt14(bytePair: .init(msb: unwrappedDataByte2, lsb: unwrappedDataByte1)) + return .songPositionPointer(midiBeat: uint14) + + case 0xF3: // System Common - Song Select + guard let songNumber = dataByte1()?.toMIDIUInt7Exactly + else { return nil } + + return .songSelect(number: songNumber) + + case 0xF4, 0xF5: // System Common - Undefined + // MIDI 1.0 Spec: ignore these events + return nil + + case 0xF6: // System Common - Tune Request + return .tuneRequest(group: group) + + case 0xF7: // System Common - System Exclusive End (EOX / End Of Exclusive) + // on its own, 0xF7 is ignored + return nil + + case 0xF8: // System Real Time - Timing Clock + return .timingClock(group: group) + + case 0xF9: // Real Time - undefined + return nil + + case 0xFA: // System Real Time - Start + return .start(group: group) + + case 0xFB: // System Real Time - Continue + return .continue(group: group) + + case 0xFC: // System Real Time - Stop + return .stop(group: group) + + case 0xFD: // Real Time - undefined + return nil + + case 0xFE: + return .activeSensing(group: group) + + case 0xFF: + return .systemReset(group: group) + + default: + // should never happen + //Log.debug("Unhandled System Status: \(statusByte)") + return nil + } + + } + + internal static func parseMIDI1ChannelVoice( + bytes: Array.SubSequence, + group: MIDI.UInt4 + ) -> MIDI.Event? { + + guard !bytes.isEmpty else { return nil } + + let statusByte = bytes[bytes.startIndex] + + let dataByte1: MIDI.Byte? = + bytes.count > 1 + ? bytes[bytes.startIndex.advanced(by: 1)] + : nil + + let dataByte2: MIDI.Byte? = + bytes.count > 2 + ? bytes[bytes.startIndex.advanced(by: 2)] + : nil + + switch statusByte.nibbles.high { + case 0x8: // note off + let channel = statusByte.nibbles.low + guard let note = dataByte1?.toMIDIUInt7Exactly, + let velocity = dataByte2?.toMIDIUInt7Exactly + else { return nil } + + let newEvent: MIDI.Event = .noteOff(note: note, + velocity: velocity, + channel: channel, + group: group) + + return newEvent + + case 0x9: // note on + let channel = statusByte.nibbles.low + guard let note = dataByte1?.toMIDIUInt7Exactly, + let velocity = dataByte2?.toMIDIUInt7Exactly + else { return nil } + + let newEvent: MIDI.Event = .noteOn(note: note, + velocity: velocity, + channel: channel, + group: group) + + return newEvent + + case 0xA: // poly aftertouch + let channel = statusByte.nibbles.low + guard let note = dataByte1?.toMIDIUInt7Exactly, + let pressure = dataByte2?.toMIDIUInt7Exactly + else { return nil } + + let newEvent: MIDI.Event = .polyAftertouch(note: note, + pressure: pressure, + channel: channel, + group: group) + + return newEvent + + case 0xB: // CC (incl. channel mode msgs 121-127) + let channel = statusByte.nibbles.low + guard let cc = dataByte1?.toMIDIUInt7Exactly, + let value = dataByte2?.toMIDIUInt7Exactly + else { return nil } + + let newEvent: MIDI.Event = .cc(controller: cc, + value: value, + channel: channel, + group: group) + + return newEvent + + case 0xC: // program change + let channel = statusByte.nibbles.low + guard let program = dataByte1?.toMIDIUInt7Exactly + else { return nil } + + let newEvent: MIDI.Event = .programChange(program: program, + channel: channel, + group: group) + + return newEvent + + case 0xD: // channel aftertouch + let channel = statusByte.nibbles.low + guard let pressure = dataByte1?.toMIDIUInt7Exactly + else { return nil } + + let newEvent: MIDI.Event = .chanAftertouch(pressure: pressure, + channel: channel, + group: group) + + return newEvent + + case 0xE: // pitch bend + let channel = statusByte.nibbles.low + guard let unwrappedDataByte1 = dataByte1, + let unwrappedDataByte2 = dataByte2 + else { return nil } + + let uint14 = MIDI.UInt14(bytePair: .init(msb: unwrappedDataByte2, + lsb: unwrappedDataByte1)) + + let newEvent: MIDI.Event = .pitchBend(value: uint14, + channel: channel, + group: group) + + return newEvent + + default: + return nil + + } + + } + + internal static func parseData64Bit( + bytes: Array.SubSequence, + group: MIDI.UInt4 + ) -> MIDI.Event? { + + // MIDI 2.0 Spec: + // "The MIDI 1.0 Protocol bracketing method with 0xF0 Start and 0xF7 End Status bytes is not used in the UMP Format. Instead, the SysEx payload is carried in one or more 64-bit UMPs, discarding the 0xF0 and 0xF7 bytes. The standard ID Number (Manufacturer ID, Special ID 0x7D, or Universal System Exclusive ID), Device ID, and Sub-ID#1 & Sub-ID#2 (if applicable) are included in the initial data bytes, just as they are in MIDI 1.0 Protocol message equivalents." + + let byte1Nibbles = bytes[bytes.startIndex.advanced(by: 1)].nibbles + + guard let sysExStatusField = MIDI.Packet.UniversalPacketData + .SysExStatusField(rawValue: byte1Nibbles.high) + else { + return nil + } + + let numberOfBytes = byte1Nibbles.low.intValue + + switch sysExStatusField { + case .complete: + guard bytes.count >= numberOfBytes + 2 + else { return nil } + + let payloadBytes = bytes[ + bytes.startIndex.advanced(by: 2) + ..< + bytes.startIndex.advanced(by: 2 + numberOfBytes) + ] + + guard let parsedSysEx = try? MIDI.Event.SysEx + .parsed(from: [0xF0] + payloadBytes + [0xF7]) + else { return nil } + + return parsedSysEx + + case .start: + #warning("> handle multi-packet SysEx Messages") + return nil + + case .continue: + #warning("> handle multi-packet SysEx Messages") + return nil + + case .end: + #warning("> handle multi-packet SysEx Messages") + return nil + + } + + } + } }