From 2081371d861e659192eff905acba75e443bb9c79 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Mon, 13 Jun 2022 01:36:32 -0700 Subject: [PATCH] `.send(events:)`: Improved reliability of sending SysEx in a multi-event array when using old Core MIDI API --- .../MIDIPacketList Utilities.swift | 1 + .../MIDIIOSendsMIDIMessagesProtocol.swift | 10 +++- .../Integration Tests/Round Trip Tests.swift | 6 ++- Tests/MIDIKitTests/Tests Helpers.swift | 53 +++++++++++++++++++ 4 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 Tests/MIDIKitTests/Tests Helpers.swift diff --git a/Sources/MIDIKit/IO/Core MIDI/MIDIPacketLIst/MIDIPacketList Utilities.swift b/Sources/MIDIKit/IO/Core MIDI/MIDIPacketLIst/MIDIPacketList Utilities.swift index 310d4dee16..3f72c46953 100644 --- a/Sources/MIDIKit/IO/Core MIDI/MIDIPacketLIst/MIDIPacketList Utilities.swift +++ b/Sources/MIDIKit/IO/Core MIDI/MIDIPacketLIst/MIDIPacketList Utilities.swift @@ -74,6 +74,7 @@ extension UnsafeMutablePointer where Pointee == MIDIPacketList { /// 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. + /// - Note: System Exclusive messages must each be packed in a dedicated MIDIPacketList with no other events, otherwise MIDIPacketList may fail. @inline(__always) internal init(data: [[MIDI.Byte]]) throws { diff --git a/Sources/MIDIKit/IO/Managed/Protocols/MIDIIOSendsMIDIMessagesProtocol.swift b/Sources/MIDIKit/IO/Managed/Protocols/MIDIIOSendsMIDIMessagesProtocol.swift index aab5b8acc6..e18d211b16 100644 --- a/Sources/MIDIKit/IO/Managed/Protocols/MIDIIOSendsMIDIMessagesProtocol.swift +++ b/Sources/MIDIKit/IO/Managed/Protocols/MIDIIOSendsMIDIMessagesProtocol.swift @@ -152,7 +152,14 @@ extension _MIDIIOSendsMIDIMessagesProtocol { switch api { case .legacyCoreMIDI: - try send(rawMessages: events.map { $0.midi1RawBytes }) + if events.contains(where: { $0.isSystemExclusive }) { + // System Exclusive events must be the only event in a MIDIPacketList + // so force each event to be sent in its own packet + try events.forEach { try send(event: $0) } + } else { + // combine events into a single MIDIPacketList + try send(rawMessages: events.map { $0.midi1RawBytes }) + } case .newCoreMIDI: guard #available(macOS 11, iOS 14, macCatalyst 14, tvOS 14, watchOS 7, *) else { @@ -172,4 +179,3 @@ extension _MIDIIOSendsMIDIMessagesProtocol { } } - diff --git a/Tests/MIDIKitTests/Integration Tests/Round Trip Tests.swift b/Tests/MIDIKitTests/Integration Tests/Round Trip Tests.swift index d27d0e881d..bd40ef5c6d 100644 --- a/Tests/MIDIKitTests/Integration Tests/Round Trip Tests.swift +++ b/Tests/MIDIKitTests/Integration Tests/Round Trip Tests.swift @@ -262,8 +262,10 @@ open class RoundTrip_Tests_Base: XCTestCase { receivedEvents.reserveCapacity(sourceEvents.count) - for event in sourceEvents { - try output.send(event: event) + // send several events at once to test packing + // multiple packets into a single MIDIPacketList / MIDIEventList + for eventGroup in sourceEvents.split(every: 2) { + try output.send(events: Array(eventGroup)) } wait(sec: 0.5) diff --git a/Tests/MIDIKitTests/Tests Helpers.swift b/Tests/MIDIKitTests/Tests Helpers.swift new file mode 100644 index 0000000000..70cda8242f --- /dev/null +++ b/Tests/MIDIKitTests/Tests Helpers.swift @@ -0,0 +1,53 @@ +/// ------------------------------------------------------------------------------------ +/// ------------------------------------------------------------------------------------ +/// Borrowed from [OTCore 1.4.1](https://github.com/orchetect/OTCore) under MIT license. +/// Methods herein are unit tested in OTCore, so no unit tests are necessary in MIDIKit. +/// ------------------------------------------------------------------------------------ +/// ------------------------------------------------------------------------------------ + +#if shouldTestCurrentPlatform + +import Foundation + +extension Collection { + + /// **OTCore:** + /// Splits a `Collection` or `String` into groups of `length` characters, grouping from left-to-right. If `backwards` is true, right-to-left. + @_disfavoredOverload + internal func split(every: Int, + backwards: Bool = false) -> [SubSequence] { + + var result: [SubSequence] = [] + + for i in stride(from: 0, to: count, by: every) { + + switch backwards { + case true: + let offsetEndIndex = index(endIndex, offsetBy: -i) + let offsetStartIndex = index(offsetEndIndex, + offsetBy: -every, + limitedBy: startIndex) + ?? startIndex + + result.insert(self[offsetStartIndex..