From 5bb47644f9ea883232dfa27b34bd8f01143b3f99 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Sun, 3 Dec 2023 19:12:58 -0800 Subject: [PATCH 01/22] Added new `MIDIParameterNumberEvent` protocol, conformed `RPN` and `NRPN` to it --- .../RPN and NRPN/MIDIParameterNumber.swift | 2 +- .../MIDIParameterNumberEvent.swift | 24 +++++++++++++++++++ .../RPN and NRPN/NRPN/NRPN.swift | 2 ++ .../Channel Voice/RPN and NRPN/RPN/RPN.swift | 2 ++ 4 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 Sources/MIDIKitCore/Events/MIDIEvent/Channel Voice/RPN and NRPN/MIDIParameterNumberEvent.swift diff --git a/Sources/MIDIKitCore/Events/MIDIEvent/Channel Voice/RPN and NRPN/MIDIParameterNumber.swift b/Sources/MIDIKitCore/Events/MIDIEvent/Channel Voice/RPN and NRPN/MIDIParameterNumber.swift index 94dbe5dee1..30a4c3383c 100644 --- a/Sources/MIDIKitCore/Events/MIDIEvent/Channel Voice/RPN and NRPN/MIDIParameterNumber.swift +++ b/Sources/MIDIKitCore/Events/MIDIEvent/Channel Voice/RPN and NRPN/MIDIParameterNumber.swift @@ -6,7 +6,7 @@ import Foundation -/// Protocol which all Parameter Number events conform. +/// Protocol which all Parameter Numbers conform. /// This includes RPN (Registered Controller) and NRPN (Assignable Controller). public protocol MIDIParameterNumber: Sendable { /// The parameter number type. diff --git a/Sources/MIDIKitCore/Events/MIDIEvent/Channel Voice/RPN and NRPN/MIDIParameterNumberEvent.swift b/Sources/MIDIKitCore/Events/MIDIEvent/Channel Voice/RPN and NRPN/MIDIParameterNumberEvent.swift new file mode 100644 index 0000000000..4b4f681e2a --- /dev/null +++ b/Sources/MIDIKitCore/Events/MIDIEvent/Channel Voice/RPN and NRPN/MIDIParameterNumberEvent.swift @@ -0,0 +1,24 @@ +// +// MIDIParameterNumberEvent.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +import Foundation + +/// Protocol which all Parameter Number events conform. +/// This includes RPN (Registered Controller) and NRPN (Assignable Controller). +public protocol MIDIParameterNumberEvent: Sendable { + associatedtype P: MIDIParameterNumber + + /// Type-erased Parameter Number. + var parameter: P { get set } + + /// MIDI 2.0 Parameter Number value type. + /// Determines whether the value is absolute or a relative change. + /// (MIDI 1.0 will always be absolute and this property is ignored.) + var change: MIDI2ParameterNumberChange { get set } +} + +extension MIDIParameterNumberEvent { +} diff --git a/Sources/MIDIKitCore/Events/MIDIEvent/Channel Voice/RPN and NRPN/NRPN/NRPN.swift b/Sources/MIDIKitCore/Events/MIDIEvent/Channel Voice/RPN and NRPN/NRPN/NRPN.swift index 95ad4f32cc..3a8e2a5001 100644 --- a/Sources/MIDIKitCore/Events/MIDIEvent/Channel Voice/RPN and NRPN/NRPN/NRPN.swift +++ b/Sources/MIDIKitCore/Events/MIDIEvent/Channel Voice/RPN and NRPN/NRPN/NRPN.swift @@ -83,6 +83,8 @@ extension MIDIEvent { } } +extension MIDIEvent.NRPN: MIDIParameterNumberEvent { } + extension MIDIEvent.NRPN: Sendable { } extension MIDIEvent { diff --git a/Sources/MIDIKitCore/Events/MIDIEvent/Channel Voice/RPN and NRPN/RPN/RPN.swift b/Sources/MIDIKitCore/Events/MIDIEvent/Channel Voice/RPN and NRPN/RPN/RPN.swift index 1255c64325..8530bbeee9 100644 --- a/Sources/MIDIKitCore/Events/MIDIEvent/Channel Voice/RPN and NRPN/RPN/RPN.swift +++ b/Sources/MIDIKitCore/Events/MIDIEvent/Channel Voice/RPN and NRPN/RPN/RPN.swift @@ -84,6 +84,8 @@ extension MIDIEvent { } } +extension MIDIEvent.RPN: MIDIParameterNumberEvent { } + extension MIDIEvent.RPN: Sendable { } extension MIDIEvent { From c818164bfe63f7114009a1114fb144379b03be4c Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Sun, 3 Dec 2023 19:26:25 -0800 Subject: [PATCH 02/22] Added `AdvancedMIDI2Parser` with RPN/NRPN bundling --- .../Parser/AdvancedMIDI2Parser.swift | 66 ++++++++ Sources/MIDIKitIO/Parser/EventHolder.swift | 71 ++++++++ Sources/MIDIKitIO/Parser/PNBundler.swift | 156 ++++++++++++++++++ 3 files changed, 293 insertions(+) create mode 100644 Sources/MIDIKitIO/Parser/AdvancedMIDI2Parser.swift create mode 100644 Sources/MIDIKitIO/Parser/EventHolder.swift create mode 100644 Sources/MIDIKitIO/Parser/PNBundler.swift diff --git a/Sources/MIDIKitIO/Parser/AdvancedMIDI2Parser.swift b/Sources/MIDIKitIO/Parser/AdvancedMIDI2Parser.swift new file mode 100644 index 0000000000..a67479df86 --- /dev/null +++ b/Sources/MIDIKitIO/Parser/AdvancedMIDI2Parser.swift @@ -0,0 +1,66 @@ +// +// AdvancedMIDI2Parser.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +#if !os(tvOS) && !os(watchOS) + +import Foundation + +/// Wrapper for MIDI 2.0 event parser that adds certain heuristics, including RPN/NRPN bundling. +public class AdvancedMIDI2Parser { + // MARK: - Options + + public var bundleRPNAndNRPNDataEntryLSB: Bool = false + public var handleEvents: (_ events: [MIDIEvent]) -> Void + + // MARK: - Internal State + + private let parser = MIDI2Parser() + private var pnBundler: PNBundler! + + public init( + handleEvents: @escaping (_ events: [MIDIEvent]) -> Void + ) { + self.handleEvents = handleEvents + + pnBundler = PNBundler { events in + handleEvents(events) + } + } +} + +// MARK: - Public Methods + +extension AdvancedMIDI2Parser { + public func parseEvents( + in packetData: UniversalMIDIPacketData + ) { + parseEvents(in: packetData.bytes) + } + + public func parseEvents( + in bytes: [UInt8] + ) { + var events = parser.parsedEvents(in: bytes) + process(parsedEvents: &events) + } +} + +// MARK: - Internal Methods + +extension AdvancedMIDI2Parser { + // This method is broken out to make unit testing easier. + func process(parsedEvents events: inout [MIDIEvent]) { + var events = events + + if bundleRPNAndNRPNDataEntryLSB { + pnBundler.process(events: &events) + } + + handleEvents(events) + } +} + +#endif diff --git a/Sources/MIDIKitIO/Parser/EventHolder.swift b/Sources/MIDIKitIO/Parser/EventHolder.swift new file mode 100644 index 0000000000..f4d9974bae --- /dev/null +++ b/Sources/MIDIKitIO/Parser/EventHolder.swift @@ -0,0 +1,71 @@ +// +// PNCombiner.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +#if !os(tvOS) && !os(watchOS) + +import Foundation + +/// Event Holder. +final class EventHolder { + // MARK: - Options + + public typealias TimerExpiredHandler = (_ storedEvent: MIDIEvent) -> Void + public var timerExpired: TimerExpiredHandler? + + public let timeOut: TimeInterval + + public var storedEventWrapper: (T) -> MIDIEvent + + // MARK: - Parser State + + private var expirationTimer: Timer? + + var storedEvent: T? + + init( + timeOut: TimeInterval = 0.05, + storedEventWrapper: @escaping (T) -> MIDIEvent, + timerExpired: TimerExpiredHandler? = nil + ) { + self.timeOut = timeOut + self.storedEventWrapper = storedEventWrapper + self.timerExpired = timerExpired + } + + func restartTimer() { + expirationTimer?.invalidate() + expirationTimer = Timer.scheduledTimer(withTimeInterval: timeOut, repeats: false) { [self] timer in + defer { reset() } + guard let storedEvent = storedEvent else { return } + timerExpired?(storedEventWrapper(storedEvent)) + } + } + + func reset() { + expirationTimer?.invalidate() + storedEvent = nil + } + + func fireStored() { + if let storedEvent = storedEvent { + timerExpired?(storedEventWrapper(storedEvent)) + } + storedEvent = nil + } + + func fireStoredAndReset() { + expirationTimer?.invalidate() + fireStored() + } + + func returnStoredAndReset() -> T? { + let storedEvent = storedEvent + reset() + return storedEvent + } +} + +#endif diff --git a/Sources/MIDIKitIO/Parser/PNBundler.swift b/Sources/MIDIKitIO/Parser/PNBundler.swift new file mode 100644 index 0000000000..cd6c6c511a --- /dev/null +++ b/Sources/MIDIKitIO/Parser/PNBundler.swift @@ -0,0 +1,156 @@ +// +// PNBundler.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +#if !os(tvOS) && !os(watchOS) + +import Foundation + +/// RPN/NRPN bundling. +public class PNBundler { + // MARK: - Options + + public var bundleRPNAndNRPNDataEntryLSB: Bool = false + public var handleEvents: (_ events: [MIDIEvent]) -> Void + + // MARK: - Internal State + + private let parser = MIDI2Parser() + private var rpnHolder: EventHolder! + private var nrpnHolder: EventHolder! + + public init( + handleEvents: @escaping (_ events: [MIDIEvent]) -> Void + ) { + self.handleEvents = handleEvents + + rpnHolder = EventHolder( + storedEventWrapper: { event in + .rpn(event) + }, + timerExpired: { [weak self] storedEvent in + self?.handleEvents([storedEvent]) + } + ) + + nrpnHolder = EventHolder( + storedEventWrapper: { event in + .nrpn(event) + }, + timerExpired: { [weak self] storedEvent in + self?.handleEvents([storedEvent]) + } + ) + } +} + +// MARK: - Public Methods + +extension PNBundler { + public func process(events: inout [MIDIEvent]) { + var newEvents: [MIDIEvent] = [] + var indicesToRemove: [Int] = [] + + for index in events.indices { + let processed = processPN(in: events[index]) + + switch processed.eventResult { + case .noChange: break + case .remove: indicesToRemove.append(index) + case let .replace(newEvent): events[index] = newEvent + } + + newEvents.append(contentsOf: processed.newEvents) + } + indicesToRemove.sorted().reversed().forEach { events.remove(at: $0) } + events.insert(contentsOf: newEvents, at: 0) + } +} + +// MARK: - Internal Methods + +extension PNBundler { + enum ProcessResult { + case noChange + case remove + case replace(MIDIEvent) + } + + private func processPN(in event: MIDIEvent) -> (eventResult: ProcessResult, newEvents: [MIDIEvent]) { + var eventResult: ProcessResult = .noChange + var newEvents: [MIDIEvent] = [] + + if case let .rpn(event) = event { + let processed = processPN(event: event, holder: rpnHolder) + eventResult = processed.eventResult + newEvents.append(contentsOf: processed.newEvents) + } else { + if let storedEvent = rpnHolder.returnStoredAndReset() { + newEvents.append(.rpn(storedEvent)) + } + } + + if case let .nrpn(event) = event { + let processed = processPN(event: event, holder: nrpnHolder) + eventResult = processed.eventResult + newEvents.append(contentsOf: processed.newEvents) + } else { + if let storedEvent = nrpnHolder.returnStoredAndReset() { + newEvents.append(.nrpn(storedEvent)) + } + } + + return (eventResult, newEvents) + } + + private func processPN( + event: T, + holder: EventHolder + ) -> (eventResult: ProcessResult, newEvents: [MIDIEvent]) { + // could be 1st UMP in a two UMP packet series where first packet has data entry LSB of 0, + // or could be single UMP with data entry LSB of 0 + let currentEventHasDataEntryLSB0 = event.parameter.dataEntryBytes.lsb ?? 0 == 0 + + if let storedEvent = holder.storedEvent { + holder.reset() // drop stored event + + if currentEventHasDataEntryLSB0 { + // we can only assume that the stored event was 'complete' with a 0 Data Entry LSB. + // it doesn't mater if the param/LSB match; + // they could be consecutive PN messages each with a Data Entry LSB of 0. + return (eventResult: .noChange, newEvents: [holder.storedEventWrapper(storedEvent)]) + } else { + // could be either the 2nd UMP of a two UMP packet series, + // or could be its own fully contained PN UMP + + let paramAndDataEntryLSBMatch = + event.parameter.parameterBytes == storedEvent.parameter.parameterBytes && + event.parameter.dataEntryBytes.msb == storedEvent.parameter.dataEntryBytes.msb + + if paramAndDataEntryLSBMatch { + // looks like stored event is a packet #1 in a two UMP packet series + return (eventResult: .noChange, newEvents: []) // keep only new event + } else { + // looks like stored event is unrelated, so add it + return (eventResult: .noChange, newEvents: [holder.storedEventWrapper(storedEvent)]) + } + } + } else { + if currentEventHasDataEntryLSB0 { + // PN UMP packet with Data Entry MSB, but 0 LSB. + // could be complete, or could be first of 2 UMP packets. + // store it and start the timer + holder.storedEvent = event + holder.restartTimer() + return (eventResult: .remove, newEvents: []) + } else { + // fully complete PN UMP packet, pass through as-is + return (eventResult: .noChange, newEvents: []) + } + } + } +} + +#endif From 73c8f7cb2637352acd6e78a2bcaa53ec9d7db14d Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Sun, 3 Dec 2023 19:29:59 -0800 Subject: [PATCH 03/22] Added `AdvancedMIDI2Parser` unit tests --- .../Parser/AdvancedMIDI2Parser Tests.swift | 344 ++++++++++++++++++ 1 file changed, 344 insertions(+) create mode 100644 Tests/MIDIKitIOTests/Parser/AdvancedMIDI2Parser Tests.swift diff --git a/Tests/MIDIKitIOTests/Parser/AdvancedMIDI2Parser Tests.swift b/Tests/MIDIKitIOTests/Parser/AdvancedMIDI2Parser Tests.swift new file mode 100644 index 0000000000..71ff71a244 --- /dev/null +++ b/Tests/MIDIKitIOTests/Parser/AdvancedMIDI2Parser Tests.swift @@ -0,0 +1,344 @@ +// +// AdvancedMIDI2Parser Tests.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +#if shouldTestCurrentPlatform && !os(tvOS) && !os(watchOS) + +@testable import MIDIKitIO +import XCTest + +final class AdvancedMIDI2Parser_Tests: XCTestCase { + // MARK: - State + + fileprivate var parser: AdvancedMIDI2Parser! + fileprivate var receivedEvents: [MIDIEvent] = [] + + override func setUpWithError() throws { + parser = AdvancedMIDI2Parser { [self] events in + receivedEvents.append(contentsOf: events) + } + + receivedEvents = [] + } + + // MARK: - Tests + + func testBasic() throws { + let inputEvents: [MIDIEvent] = [ + .cc(1, value: .midi1(64), channel: 2) + ] + + var events = inputEvents + parser.process(parsedEvents: &events) + + wait(for: receivedEvents.count == 1, timeout: 1.0) + + XCTAssertEqual(receivedEvents, inputEvents) + } + + // MARK: - RPN + + func testHoldOff_RPN_DataEntryMSB() throws { + parser.bundleRPNAndNRPNDataEntryLSB = false + + let inputEvents: [MIDIEvent] = [ + .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: nil), channel: 2) + ] + + var events = inputEvents + parser.process(parsedEvents: &events) + + wait(sec: 0.2) // allow time for extra events if a bug exists + wait(for: receivedEvents.count == 1, timeout: 1.0) + + XCTAssertEqual(receivedEvents, inputEvents) + } + + /// This mimics the two UMP events that Core MIDI will produce when + /// translating MIDI 1.0 RPN to UMP when a Data Entry LSB is present. + func testHoldOff_RPN_DataEntryMSBAndLSB() throws { + parser.bundleRPNAndNRPNDataEntryLSB = false + + let inputEvents: [MIDIEvent] = [ + .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2), + .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x20), channel: 2) + ] + + var events = inputEvents + parser.process(parsedEvents: &events) + + wait(sec: 0.2) // allow time for extra events if a bug exists + wait(for: receivedEvents.count == 2, timeout: 1.0) + + // no bundling active, so events will just pass-thru as-is + XCTAssertEqual(receivedEvents, [ + .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2), + .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x20), channel: 2) + ]) + } + + func testHoldOn_RPN_DataEntryMSB() throws { + parser.bundleRPNAndNRPNDataEntryLSB = true + + let inputEvents: [MIDIEvent] = [ + .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: nil), channel: 2) + ] + + var events = inputEvents + parser.process(parsedEvents: &events) + + wait(sec: 0.2) // allow time for extra events if a bug exists + wait(for: receivedEvents.count == 1, timeout: 1.0) + + // we never received a 2nd UMP with Data Entry LSB, so hold timer should have expired and + // let the single message through + XCTAssertEqual(receivedEvents, inputEvents) + } + + /// This mimics the two UMP events that Core MIDI will produce when + /// translating MIDI 1.0 RPN to UMP when a Data Entry LSB is present. + func testHoldOn_RPN_DataEntryMSBAndLSB_Together() throws { + parser.bundleRPNAndNRPNDataEntryLSB = true + + let inputEvents: [MIDIEvent] = [ + .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2), + .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x20), channel: 2) + ] + + var events = inputEvents + parser.process(parsedEvents: &events) + + wait(sec: 0.2) // allow time for extra events if a bug exists + wait(for: receivedEvents.count == 1, timeout: 1.0) + + // bundleRPNAndNRPNDataEntryLSB should wait for 2nd UMP and bundle them together + XCTAssertEqual(receivedEvents, [ + .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x20), channel: 2) + ]) + } + + /// This mimics the two UMP events that Core MIDI will produce when + /// translating MIDI 1.0 RPN to UMP when a Data Entry LSB is present. + func testHoldOn_RPN_DataEntryMSBAndLSB_Apart() throws { + parser.bundleRPNAndNRPNDataEntryLSB = true + + var events1: [MIDIEvent] = [ + .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2) + ] + parser.process(parsedEvents: &events1) + + var events2: [MIDIEvent] = [ + .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x20), channel: 2) + ] + parser.process(parsedEvents: &events2) + + wait(sec: 0.2) // allow time for extra events if a bug exists + wait(for: receivedEvents.count == 1, timeout: 1.0) + + // bundleRPNAndNRPNDataEntryLSB should wait for 2nd UMP and bundle them together + XCTAssertEqual(receivedEvents, [ + .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x20), channel: 2) + ]) + } + + /// Two PN events with 0 data entry LSB. + func testHoldOn_RPN_DataEntryMSB_Duplicate_Together() throws { + parser.bundleRPNAndNRPNDataEntryLSB = true + + let inputEvents: [MIDIEvent] = [ + .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2), + .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2) + ] + + var events = inputEvents + parser.process(parsedEvents: &events) + + wait(sec: 0.2) // allow time for extra events if a bug exists + wait(for: receivedEvents.count == 2, timeout: 1.0) + + // bundleRPNAndNRPNDataEntryLSB should wait for 2nd UMP and bundle them together + XCTAssertEqual(receivedEvents, [ + .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2), + .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2) + ]) + } + + /// Two PN events with 0 data entry LSB. + func testHoldOn_RPN_DataEntryMSB_Duplicate_Apart() throws { + parser.bundleRPNAndNRPNDataEntryLSB = true + + var events1: [MIDIEvent] = [ + .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2) + ] + parser.process(parsedEvents: &events1) + + var events2: [MIDIEvent] = [ + .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2) + ] + parser.process(parsedEvents: &events2) + + wait(sec: 0.2) // allow time for extra events if a bug exists + wait(for: receivedEvents.count == 2, timeout: 1.0) + + // bundleRPNAndNRPNDataEntryLSB should wait for 2nd UMP and bundle them together + XCTAssertEqual(receivedEvents, [ + .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2), + .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2) + ]) + } + + // MARK: - NRPN + + func testHoldOff_NRPN_DataEntryMSB() throws { + parser.bundleRPNAndNRPNDataEntryLSB = false + + let inputEvents: [MIDIEvent] = [ + .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: nil), channel: 2) + ] + + var events = inputEvents + parser.process(parsedEvents: &events) + + wait(sec: 0.2) // allow time for extra events if a bug exists + wait(for: receivedEvents.count == 1, timeout: 1.0) + + XCTAssertEqual(receivedEvents, inputEvents) + } + + /// This mimics the two UMP events that Core MIDI will produce when + /// translating MIDI 1.0 RPN to UMP when a Data Entry LSB is present. + func testHoldOff_NRPN_DataEntryMSBAndLSB() throws { + parser.bundleRPNAndNRPNDataEntryLSB = false + + let inputEvents: [MIDIEvent] = [ + .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2), + .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x20), channel: 2) + ] + + var events = inputEvents + parser.process(parsedEvents: &events) + + wait(sec: 0.2) // allow time for extra events if a bug exists + wait(for: receivedEvents.count == 2, timeout: 1.0) + + // no bundling active, so events will just pass-thru as-is + XCTAssertEqual(receivedEvents, [ + .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2), + .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x20), channel: 2) + ]) + } + + func testHoldOn_NRPN_DataEntryMSB() throws { + parser.bundleRPNAndNRPNDataEntryLSB = true + + let inputEvents: [MIDIEvent] = [ + .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: nil), channel: 2) + ] + + var events = inputEvents + parser.process(parsedEvents: &events) + + wait(sec: 0.2) // allow time for extra events if a bug exists + wait(for: receivedEvents.count == 1, timeout: 1.0) + + // we never received a 2nd UMP with Data Entry LSB, so hold timer should have expired and + // let the single message through + XCTAssertEqual(receivedEvents, inputEvents) + } + + /// This mimics the two UMP events that Core MIDI will produce when + /// translating MIDI 1.0 RPN to UMP when a Data Entry LSB is present. + func testHoldOn_NRPN_DataEntryMSBAndLSB_Together() throws { + parser.bundleRPNAndNRPNDataEntryLSB = true + + let inputEvents: [MIDIEvent] = [ + .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2), + .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x20), channel: 2) + ] + + var events = inputEvents + parser.process(parsedEvents: &events) + + wait(sec: 0.2) // allow time for extra events if a bug exists + wait(for: receivedEvents.count == 1, timeout: 1.0) + + // bundleRPNAndNRPNDataEntryLSB should wait for 2nd UMP and bundle them together + XCTAssertEqual(receivedEvents, [ + .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x20), channel: 2) + ]) + } + + /// This mimics the two UMP events that Core MIDI will produce when + /// translating MIDI 1.0 RPN to UMP when a Data Entry LSB is present. + func testHoldOn_NRPN_DataEntryMSBAndLSB_Apart() throws { + parser.bundleRPNAndNRPNDataEntryLSB = true + + var events1: [MIDIEvent] = [ + .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2) + ] + parser.process(parsedEvents: &events1) + + var events2: [MIDIEvent] = [ + .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x20), channel: 2) + ] + parser.process(parsedEvents: &events2) + + wait(sec: 0.2) // allow time for extra events if a bug exists + wait(for: receivedEvents.count == 1, timeout: 1.0) + + // bundleRPNAndNRPNDataEntryLSB should wait for 2nd UMP and bundle them together + XCTAssertEqual(receivedEvents, [ + .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x20), channel: 2) + ]) + } + + /// Two PN events with 0 data entry LSB. + func testHoldOn_NRPN_DataEntryMSB_Duplicate_Together() throws { + parser.bundleRPNAndNRPNDataEntryLSB = true + + let inputEvents: [MIDIEvent] = [ + .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2), + .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2) + ] + + var events = inputEvents + parser.process(parsedEvents: &events) + + wait(sec: 0.2) // allow time for extra events if a bug exists + wait(for: receivedEvents.count == 2, timeout: 1.0) + + // bundleRPNAndNRPNDataEntryLSB should wait for 2nd UMP and bundle them together + XCTAssertEqual(receivedEvents, [ + .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2), + .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2) + ]) + } + + /// Two PN events with 0 data entry LSB. + func testHoldOn_NRPN_DataEntryMSB_Duplicate_Apart() throws { + parser.bundleRPNAndNRPNDataEntryLSB = true + + var events1: [MIDIEvent] = [ + .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2) + ] + parser.process(parsedEvents: &events1) + + var events2: [MIDIEvent] = [ + .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2) + ] + parser.process(parsedEvents: &events2) + + wait(sec: 0.2) // allow time for extra events if a bug exists + wait(for: receivedEvents.count == 2, timeout: 1.0) + + // bundleRPNAndNRPNDataEntryLSB should wait for 2nd UMP and bundle them together + XCTAssertEqual(receivedEvents, [ + .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2), + .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 2) + ]) + } +} + +#endif From 6fcd3add1f81e24cb811945d98656b5fb566b9d4 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Sun, 3 Dec 2023 19:31:07 -0800 Subject: [PATCH 04/22] Renamed `PNBundler` to `ParameterNumberEventBundler` --- Sources/MIDIKitIO/Parser/AdvancedMIDI2Parser.swift | 4 ++-- ...{PNBundler.swift => ParameterNumberEventBundler.swift} | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) rename Sources/MIDIKitIO/Parser/{PNBundler.swift => ParameterNumberEventBundler.swift} (97%) diff --git a/Sources/MIDIKitIO/Parser/AdvancedMIDI2Parser.swift b/Sources/MIDIKitIO/Parser/AdvancedMIDI2Parser.swift index a67479df86..199c9374b6 100644 --- a/Sources/MIDIKitIO/Parser/AdvancedMIDI2Parser.swift +++ b/Sources/MIDIKitIO/Parser/AdvancedMIDI2Parser.swift @@ -18,14 +18,14 @@ public class AdvancedMIDI2Parser { // MARK: - Internal State private let parser = MIDI2Parser() - private var pnBundler: PNBundler! + private var pnBundler: ParameterNumberEventBundler! public init( handleEvents: @escaping (_ events: [MIDIEvent]) -> Void ) { self.handleEvents = handleEvents - pnBundler = PNBundler { events in + pnBundler = ParameterNumberEventBundler { events in handleEvents(events) } } diff --git a/Sources/MIDIKitIO/Parser/PNBundler.swift b/Sources/MIDIKitIO/Parser/ParameterNumberEventBundler.swift similarity index 97% rename from Sources/MIDIKitIO/Parser/PNBundler.swift rename to Sources/MIDIKitIO/Parser/ParameterNumberEventBundler.swift index cd6c6c511a..45ce5bc1b0 100644 --- a/Sources/MIDIKitIO/Parser/PNBundler.swift +++ b/Sources/MIDIKitIO/Parser/ParameterNumberEventBundler.swift @@ -1,5 +1,5 @@ // -// PNBundler.swift +// ParameterNumberEventBundler.swift // MIDIKit • https://github.com/orchetect/MIDIKit // © 2021-2023 Steffan Andrews • Licensed under MIT License // @@ -9,7 +9,7 @@ import Foundation /// RPN/NRPN bundling. -public class PNBundler { +public class ParameterNumberEventBundler { // MARK: - Options public var bundleRPNAndNRPNDataEntryLSB: Bool = false @@ -48,7 +48,7 @@ public class PNBundler { // MARK: - Public Methods -extension PNBundler { +extension ParameterNumberEventBundler { public func process(events: inout [MIDIEvent]) { var newEvents: [MIDIEvent] = [] var indicesToRemove: [Int] = [] @@ -71,7 +71,7 @@ extension PNBundler { // MARK: - Internal Methods -extension PNBundler { +extension ParameterNumberEventBundler { enum ProcessResult { case noChange case remove From 9bc5ec2ef1fba786d10f8fa114e700b561411761 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Sun, 3 Dec 2023 20:37:25 -0800 Subject: [PATCH 05/22] Added `bundleRPNAndNRPNDataEntryLSB` receiver option --- .../MIDIReceiveHandler/Handlers/Events.swift | 41 +++++++++--- .../Handlers/EventsLogging.swift | 62 ++++++++++++++----- .../Handlers/EventsWithMetadata.swift | 41 +++++++++--- .../Handlers/StrongEventsReceiver.swift | 36 ++++++++--- .../Handlers/WeakEventsReceiver.swift | 37 ++++++++--- .../MIDIReceiverOptions.swift | 16 +++++ .../Parser/AdvancedMIDI2Parser.swift | 40 +++++++++--- Sources/MIDIKitIO/Parser/EventHolder.swift | 50 +++++++++++---- .../Parser/ParameterNumberEventBundler.swift | 57 ++++++++++++----- .../Parser/AdvancedMIDI2Parser Tests.swift | 2 +- 10 files changed, 293 insertions(+), 89 deletions(-) diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift index 933dfbe1c9..86fc3842f3 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift @@ -17,8 +17,10 @@ extension MIDIReceiveHandler { final class Events: MIDIReceiveHandlerProtocol { public var handler: MIDIReceiver.EventsHandler - let midi1Parser = MIDI1Parser() - let midi2Parser = MIDI2Parser() + let midi1Parser: MIDI1Parser + + var midi2Parser: MIDI2Parser? = nil + var advancedMIDI2Parser: AdvancedMIDI2Parser? = nil let options: MIDIReceiverOptions @@ -37,10 +39,15 @@ extension MIDIReceiveHandler { _ packets: [UniversalMIDIPacketData], protocol midiProtocol: MIDIProtocolVersion ) { - for midiPacket in packets { - let events = midi2Parser.parsedEvents(in: midiPacket) - guard !events.isEmpty else { continue } - handle(events: events) + if let parser = midi2Parser { + for midiPacket in packets { + let events = parser.parsedEvents(in: midiPacket) + handle(events: events) + } + } else if let parser = advancedMIDI2Parser { + for midiPacket in packets { + parser.parseEvents(in: midiPacket) + } } } @@ -50,9 +57,6 @@ extension MIDIReceiveHandler { ) { self.options = options - midi1Parser.translateNoteOnZeroVelocityToNoteOff = options - .contains(.translateMIDI1NoteOnZeroVelocityToNoteOff) - if options.contains(.filterActiveSensingAndClock) { self.handler = { events in let filtered = events.filter(sysRealTime: .dropTypes([.activeSensing, .timingClock])) @@ -62,9 +66,28 @@ extension MIDIReceiveHandler { } else { self.handler = handler } + + // MIDI 1 + + midi1Parser = MIDI1Parser() + + midi1Parser.translateNoteOnZeroVelocityToNoteOff = options + .contains(.translateMIDI1NoteOnZeroVelocityToNoteOff) + + // MIDI 2 + + if options.contains(.bundleRPNAndNRPNDataEntryLSB) { + advancedMIDI2Parser = AdvancedMIDI2Parser { [weak self] events, _, _ in + self?.handle(events: events) + } + } else { + midi2Parser = MIDI2Parser() + } } func handle(events: [MIDIEvent]) { + guard !events.isEmpty else { return } + if options.contains(.filterActiveSensingAndClock) { let filtered = events.filter(sysRealTime: .dropTypes([.activeSensing, .timingClock])) guard !filtered.isEmpty else { return } diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsLogging.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsLogging.swift index feacef3b53..38052e4d3a 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsLogging.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsLogging.swift @@ -21,8 +21,10 @@ extension MIDIReceiveHandler { final class EventsLogging: MIDIReceiveHandlerProtocol { public var handler: MIDIReceiver.EventsLoggingHandler - let midi1Parser = MIDI1Parser() - let midi2Parser = MIDI2Parser() + let midi1Parser: MIDI1Parser + + var midi2Parser: MIDI2Parser? = nil + var advancedMIDI2Parser: AdvancedMIDI2Parser? = nil public let options: MIDIReceiverOptions @@ -32,7 +34,7 @@ extension MIDIReceiveHandler { for midiPacket in packets { let events = midi1Parser.parsedEvents(in: midiPacket) guard !events.isEmpty else { continue } - log( + prepLogMessage( events: events, timeStamp: midiPacket.timeStamp, source: midiPacket.source @@ -45,14 +47,15 @@ extension MIDIReceiveHandler { _ packets: [UniversalMIDIPacketData], protocol midiProtocol: MIDIProtocolVersion ) { - for midiPacket in packets { - let events = midi2Parser.parsedEvents(in: midiPacket) - guard !events.isEmpty else { continue } - log( - events: events, - timeStamp: midiPacket.timeStamp, - source: midiPacket.source - ) + if let parser = midi2Parser { + for midiPacket in packets { + let events = parser.parsedEvents(in: midiPacket) + handle(events: events, timeStamp: midiPacket.timeStamp, source: midiPacket.source) + } + } else if let parser = advancedMIDI2Parser { + for midiPacket in packets { + parser.parseEvents(in: midiPacket) + } } } @@ -63,9 +66,6 @@ extension MIDIReceiveHandler { ) { self.options = options - midi1Parser.translateNoteOnZeroVelocityToNoteOff = options - .contains(.translateMIDI1NoteOnZeroVelocityToNoteOff) - self.handler = handler ?? { packetBytesString in #if DEBUG os_log( @@ -76,9 +76,39 @@ extension MIDIReceiveHandler { ) #endif } + + // MIDI 1 + + midi1Parser = MIDI1Parser() + + midi1Parser.translateNoteOnZeroVelocityToNoteOff = options + .contains(.translateMIDI1NoteOnZeroVelocityToNoteOff) + + // MIDI 2 + + if options.contains(.bundleRPNAndNRPNDataEntryLSB) { + advancedMIDI2Parser = AdvancedMIDI2Parser { [weak self] events, timeStamp, source in + self?.handle(events: events, timeStamp: timeStamp, source: source) + } + } else { + midi2Parser = MIDI2Parser() + } } - - func log( + + func handle( + events: [MIDIEvent], + timeStamp: CoreMIDITimeStamp, + source: MIDIOutputEndpoint? + ) { + guard !events.isEmpty else { return } + prepLogMessage( + events: events, + timeStamp: timeStamp, + source: source + ) + } + + func prepLogMessage( events: [MIDIEvent], timeStamp: CoreMIDITimeStamp, source: MIDIOutputEndpoint? diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsWithMetadata.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsWithMetadata.swift index c23e28b304..8449503054 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsWithMetadata.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsWithMetadata.swift @@ -21,8 +21,10 @@ extension MIDIReceiveHandler { final class EventsWithMetadata: MIDIReceiveHandlerProtocol { public var handler: MIDIReceiver.EventsWithMetadataHandler - let midi1Parser = MIDI1Parser() - let midi2Parser = MIDI2Parser() + let midi1Parser: MIDI1Parser + + var midi2Parser: MIDI2Parser? = nil + var advancedMIDI2Parser: AdvancedMIDI2Parser? = nil public func packetListReceived( _ packets: [MIDIPacketData] @@ -40,10 +42,17 @@ extension MIDIReceiveHandler { _ packets: [UniversalMIDIPacketData], protocol midiProtocol: MIDIProtocolVersion ) { - for midiPacket in packets { - let events = midi2Parser.parsedEvents(in: midiPacket) - guard !events.isEmpty else { continue } - handler(events, midiPacket.timeStamp, midiPacket.source) + if let parser = midi2Parser { + for midiPacket in packets { + let events = parser.parsedEvents(in: midiPacket) + guard !events.isEmpty else { continue } + + handler(events, midiPacket.timeStamp, midiPacket.source) + } + } else if let parser = advancedMIDI2Parser { + for midiPacket in packets { + parser.parseEvents(in: midiPacket) + } } } @@ -51,9 +60,6 @@ extension MIDIReceiveHandler { options: MIDIReceiverOptions, handler: @escaping MIDIReceiver.EventsWithMetadataHandler ) { - midi1Parser.translateNoteOnZeroVelocityToNoteOff = options - .contains(.translateMIDI1NoteOnZeroVelocityToNoteOff) - if options.contains(.filterActiveSensingAndClock) { self.handler = { events, timeStamp, source in let filtered = events.filter(sysRealTime: .dropTypes([.activeSensing, .timingClock])) @@ -63,6 +69,23 @@ extension MIDIReceiveHandler { } else { self.handler = handler } + + // MIDI 1 + + midi1Parser = MIDI1Parser() + + midi1Parser.translateNoteOnZeroVelocityToNoteOff = options + .contains(.translateMIDI1NoteOnZeroVelocityToNoteOff) + + // MIDI 2 + + if options.contains(.bundleRPNAndNRPNDataEntryLSB) { + advancedMIDI2Parser = AdvancedMIDI2Parser { [weak self] events, timeStamp, source in + self?.handler(events, timeStamp, source) + } + } else { + midi2Parser = MIDI2Parser() + } } } } diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/StrongEventsReceiver.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/StrongEventsReceiver.swift index bbb34adab6..4fc3fa868f 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/StrongEventsReceiver.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/StrongEventsReceiver.swift @@ -12,8 +12,10 @@ extension MIDIReceiveHandler { final class StrongEventsReceiver: MIDIReceiveHandlerProtocol { public let receiver: ReceivesMIDIEvents - let midi1Parser = MIDI1Parser() - let midi2Parser = MIDI2Parser() + let midi1Parser: MIDI1Parser + + var midi2Parser: MIDI2Parser? = nil + var advancedMIDI2Parser: AdvancedMIDI2Parser? = nil let options: MIDIReceiverOptions @@ -32,10 +34,16 @@ extension MIDIReceiveHandler { _ packets: [UniversalMIDIPacketData], protocol midiProtocol: MIDIProtocolVersion ) { - for midiPacket in packets { - let events = midi2Parser.parsedEvents(in: midiPacket) - guard !events.isEmpty else { continue } - handle(events: events) + if let parser = midi2Parser { + for midiPacket in packets { + let events = parser.parsedEvents(in: midiPacket) + guard !events.isEmpty else { continue } + handle(events: events) + } + } else if let parser = advancedMIDI2Parser { + for midiPacket in packets { + parser.parseEvents(in: midiPacket) + } } } @@ -44,10 +52,24 @@ extension MIDIReceiveHandler { receiver: ReceivesMIDIEvents ) { self.options = options + self.receiver = receiver + + // MIDI 1 + + midi1Parser = MIDI1Parser() midi1Parser.translateNoteOnZeroVelocityToNoteOff = options .contains(.translateMIDI1NoteOnZeroVelocityToNoteOff) - self.receiver = receiver + + // MIDI 2 + + if options.contains(.bundleRPNAndNRPNDataEntryLSB) { + advancedMIDI2Parser = AdvancedMIDI2Parser { [weak self] events, _, _ in + self?.handle(events: events) + } + } else { + midi2Parser = MIDI2Parser() + } } func handle(events: [MIDIEvent]) { diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift index 92e4a79060..8574079a33 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift @@ -11,8 +11,10 @@ extension MIDIReceiveHandler { final class WeakEventsReceiver: MIDIReceiveHandlerProtocol { public weak var receiver: ReceivesMIDIEvents? - let midi1Parser = MIDI1Parser() - let midi2Parser = MIDI2Parser() + let midi1Parser: MIDI1Parser + + var midi2Parser: MIDI2Parser? = nil + var advancedMIDI2Parser: AdvancedMIDI2Parser? = nil let options: MIDIReceiverOptions @@ -31,10 +33,16 @@ extension MIDIReceiveHandler { _ packets: [UniversalMIDIPacketData], protocol midiProtocol: MIDIProtocolVersion ) { - for midiPacket in packets { - let events = midi2Parser.parsedEvents(in: midiPacket) - guard !events.isEmpty else { continue } - handle(events: events) + if let parser = midi2Parser { + for midiPacket in packets { + let events = parser.parsedEvents(in: midiPacket) + guard !events.isEmpty else { continue } + handle(events: events) + } + } else if let parser = advancedMIDI2Parser { + for midiPacket in packets { + parser.parseEvents(in: midiPacket) + } } } @@ -43,9 +51,24 @@ extension MIDIReceiveHandler { receiver: ReceivesMIDIEvents ) { self.options = options + self.receiver = receiver + + // MIDI 1 + + midi1Parser = MIDI1Parser() + midi1Parser.translateNoteOnZeroVelocityToNoteOff = options .contains(.translateMIDI1NoteOnZeroVelocityToNoteOff) - self.receiver = receiver + + // MIDI 2 + + if options.contains(.bundleRPNAndNRPNDataEntryLSB) { + advancedMIDI2Parser = AdvancedMIDI2Parser { [weak self] events, _, _ in + self?.handle(events: events) + } + } else { + midi2Parser = MIDI2Parser() + } } func handle(events: [MIDIEvent]) { diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiverOptions.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiverOptions.swift index 16a696354c..d2c976250d 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiverOptions.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiverOptions.swift @@ -20,6 +20,22 @@ public struct MIDIReceiverOptions: OptionSet { /// that send these messages at a fast rate. public static let filterActiveSensingAndClock = MIDIReceiverOptions(rawValue: 1 << 1) + /// When receiving an RPN or NRPN message without a Data Entry LSB value, or a Data Entry LSB + /// value of `0`, store the message for a very brief period in order to wait for a follow-up message + /// containing a non-zero Data Entry LSB and combine the two messages into a single event. + /// + /// In the event no follow-up message arrives, a new RPN/NRPN is received, or another MIDI event + /// type is received, the stored event will be released. + /// + /// This is useful when receiving RPN or NRPN messages from a MIDI 1.0 device or endpoint while + /// MIDIKit is using the new MIDI 2.0 Core MIDI API. + /// + /// Note that this may introduce a very small amount of latency only for RPN/NRPN messages that + /// intentionally do not carry a Data Entry LSB or carry a Data Entry LSB of `0`. + /// + /// For details see [this thread](https://github.com/orchetect/MIDIKit/discussions/198). + public static let bundleRPNAndNRPNDataEntryLSB = MIDIReceiverOptions(rawValue: 1 << 2) + public init(rawValue: Int) { self.rawValue = rawValue } diff --git a/Sources/MIDIKitIO/Parser/AdvancedMIDI2Parser.swift b/Sources/MIDIKitIO/Parser/AdvancedMIDI2Parser.swift index 199c9374b6..0f94a10c6c 100644 --- a/Sources/MIDIKitIO/Parser/AdvancedMIDI2Parser.swift +++ b/Sources/MIDIKitIO/Parser/AdvancedMIDI2Parser.swift @@ -13,7 +13,13 @@ public class AdvancedMIDI2Parser { // MARK: - Options public var bundleRPNAndNRPNDataEntryLSB: Bool = false - public var handleEvents: (_ events: [MIDIEvent]) -> Void + + public typealias EventsHandler = ( + _ events: [MIDIEvent], + _ timeStamp: CoreMIDITimeStamp, + _ source: MIDIOutputEndpoint? + ) -> Void + public var handleEvents: EventsHandler // MARK: - Internal State @@ -21,12 +27,12 @@ public class AdvancedMIDI2Parser { private var pnBundler: ParameterNumberEventBundler! public init( - handleEvents: @escaping (_ events: [MIDIEvent]) -> Void + handleEvents: @escaping EventsHandler ) { self.handleEvents = handleEvents - pnBundler = ParameterNumberEventBundler { events in - handleEvents(events) + pnBundler = ParameterNumberEventBundler { events, timeStamp, source in + handleEvents(events, timeStamp, source) } } } @@ -37,14 +43,24 @@ extension AdvancedMIDI2Parser { public func parseEvents( in packetData: UniversalMIDIPacketData ) { - parseEvents(in: packetData.bytes) + parseEvents( + in: packetData.bytes, + timeStamp: packetData.timeStamp, + source: packetData.source + ) } public func parseEvents( - in bytes: [UInt8] + in bytes: [UInt8], + timeStamp: CoreMIDITimeStamp = 0, + source: MIDIOutputEndpoint? = nil ) { var events = parser.parsedEvents(in: bytes) - process(parsedEvents: &events) + process( + parsedEvents: &events, + timeStamp: timeStamp, + source: source + ) } } @@ -52,14 +68,18 @@ extension AdvancedMIDI2Parser { extension AdvancedMIDI2Parser { // This method is broken out to make unit testing easier. - func process(parsedEvents events: inout [MIDIEvent]) { + func process( + parsedEvents events: inout [MIDIEvent], + timeStamp: CoreMIDITimeStamp = 0, + source: MIDIOutputEndpoint? = nil + ) { var events = events if bundleRPNAndNRPNDataEntryLSB { - pnBundler.process(events: &events) + pnBundler.process(events: &events, timeStamp: timeStamp, source: source) } - handleEvents(events) + handleEvents(events, timeStamp, source) } } diff --git a/Sources/MIDIKitIO/Parser/EventHolder.swift b/Sources/MIDIKitIO/Parser/EventHolder.swift index f4d9974bae..b61ef02f13 100644 --- a/Sources/MIDIKitIO/Parser/EventHolder.swift +++ b/Sources/MIDIKitIO/Parser/EventHolder.swift @@ -12,18 +12,27 @@ import Foundation final class EventHolder { // MARK: - Options - public typealias TimerExpiredHandler = (_ storedEvent: MIDIEvent) -> Void + public typealias TimerExpiredHandler = (_ storedEvent: ReturnedStoredEvent) -> Void public var timerExpired: TimerExpiredHandler? - public let timeOut: TimeInterval - public var storedEventWrapper: (T) -> MIDIEvent // MARK: - Parser State private var expirationTimer: Timer? - var storedEvent: T? + public typealias StoredEvent = ( + event: T, + timeStamp: CoreMIDITimeStamp, + source: MIDIOutputEndpoint? + ) + var storedEvent: StoredEvent? + + public typealias ReturnedStoredEvent = ( + event: MIDIEvent, + timeStamp: CoreMIDITimeStamp, + source: MIDIOutputEndpoint? + ) init( timeOut: TimeInterval = 0.05, @@ -37,11 +46,11 @@ final class EventHolder { func restartTimer() { expirationTimer?.invalidate() - expirationTimer = Timer.scheduledTimer(withTimeInterval: timeOut, repeats: false) { [self] timer in - defer { reset() } - guard let storedEvent = storedEvent else { return } - timerExpired?(storedEventWrapper(storedEvent)) - } + expirationTimer = Timer + .scheduledTimer(withTimeInterval: timeOut, repeats: false) { [self] timer in + defer { reset() } + callTimerExpired() + } } func reset() { @@ -50,8 +59,8 @@ final class EventHolder { } func fireStored() { - if let storedEvent = storedEvent { - timerExpired?(storedEventWrapper(storedEvent)) + if storedEvent != nil { + callTimerExpired() } storedEvent = nil } @@ -61,11 +70,26 @@ final class EventHolder { fireStored() } - func returnStoredAndReset() -> T? { - let storedEvent = storedEvent + func returnStoredAndReset() -> ReturnedStoredEvent? { + let storedEvent = returnedStoredEvent() reset() return storedEvent } + + func callTimerExpired() { + guard let storedEvent = returnedStoredEvent() else { return } + timerExpired?(storedEvent) + } + + func returnedStoredEvent() -> ReturnedStoredEvent? { + guard let storedEvent = storedEvent else { return nil } + let wrapped = storedEventWrapper(storedEvent.event) + return ( + event: wrapped, + timeStamp: storedEvent.timeStamp, + source: storedEvent.source + ) + } } #endif diff --git a/Sources/MIDIKitIO/Parser/ParameterNumberEventBundler.swift b/Sources/MIDIKitIO/Parser/ParameterNumberEventBundler.swift index 45ce5bc1b0..de07f3c697 100644 --- a/Sources/MIDIKitIO/Parser/ParameterNumberEventBundler.swift +++ b/Sources/MIDIKitIO/Parser/ParameterNumberEventBundler.swift @@ -13,7 +13,12 @@ public class ParameterNumberEventBundler { // MARK: - Options public var bundleRPNAndNRPNDataEntryLSB: Bool = false - public var handleEvents: (_ events: [MIDIEvent]) -> Void + public typealias EventsHandler = ( + _ events: [MIDIEvent], + _ timeStamp: CoreMIDITimeStamp, + _ source: MIDIOutputEndpoint? + ) -> Void + public var handleEvents: EventsHandler // MARK: - Internal State @@ -22,7 +27,7 @@ public class ParameterNumberEventBundler { private var nrpnHolder: EventHolder! public init( - handleEvents: @escaping (_ events: [MIDIEvent]) -> Void + handleEvents: @escaping EventsHandler ) { self.handleEvents = handleEvents @@ -31,7 +36,11 @@ public class ParameterNumberEventBundler { .rpn(event) }, timerExpired: { [weak self] storedEvent in - self?.handleEvents([storedEvent]) + self?.handleEvents( + [storedEvent.event], + storedEvent.timeStamp, + storedEvent.source + ) } ) @@ -40,7 +49,11 @@ public class ParameterNumberEventBundler { .nrpn(event) }, timerExpired: { [weak self] storedEvent in - self?.handleEvents([storedEvent]) + self?.handleEvents( + [storedEvent.event], + storedEvent.timeStamp, + storedEvent.source + ) } ) } @@ -49,12 +62,16 @@ public class ParameterNumberEventBundler { // MARK: - Public Methods extension ParameterNumberEventBundler { - public func process(events: inout [MIDIEvent]) { + public func process( + events: inout [MIDIEvent], + timeStamp: CoreMIDITimeStamp, + source: MIDIOutputEndpoint? + ) { var newEvents: [MIDIEvent] = [] var indicesToRemove: [Int] = [] for index in events.indices { - let processed = processPN(in: events[index]) + let processed = processPN(in: events[index], timeStamp: timeStamp, source: source) switch processed.eventResult { case .noChange: break @@ -78,27 +95,31 @@ extension ParameterNumberEventBundler { case replace(MIDIEvent) } - private func processPN(in event: MIDIEvent) -> (eventResult: ProcessResult, newEvents: [MIDIEvent]) { + private func processPN( + in event: MIDIEvent, + timeStamp: CoreMIDITimeStamp, + source: MIDIOutputEndpoint? + ) -> (eventResult: ProcessResult, newEvents: [MIDIEvent]) { var eventResult: ProcessResult = .noChange var newEvents: [MIDIEvent] = [] if case let .rpn(event) = event { - let processed = processPN(event: event, holder: rpnHolder) + let processed = processPN(event: event, timeStamp: timeStamp, source: source, holder: rpnHolder) eventResult = processed.eventResult newEvents.append(contentsOf: processed.newEvents) } else { if let storedEvent = rpnHolder.returnStoredAndReset() { - newEvents.append(.rpn(storedEvent)) + newEvents.append(storedEvent.event) } } if case let .nrpn(event) = event { - let processed = processPN(event: event, holder: nrpnHolder) + let processed = processPN(event: event, timeStamp: timeStamp, source: source, holder: nrpnHolder) eventResult = processed.eventResult newEvents.append(contentsOf: processed.newEvents) } else { if let storedEvent = nrpnHolder.returnStoredAndReset() { - newEvents.append(.nrpn(storedEvent)) + newEvents.append(storedEvent.event) } } @@ -106,7 +127,9 @@ extension ParameterNumberEventBundler { } private func processPN( - event: T, + event: T, + timeStamp: CoreMIDITimeStamp, + source: MIDIOutputEndpoint?, holder: EventHolder ) -> (eventResult: ProcessResult, newEvents: [MIDIEvent]) { // could be 1st UMP in a two UMP packet series where first packet has data entry LSB of 0, @@ -120,21 +143,21 @@ extension ParameterNumberEventBundler { // we can only assume that the stored event was 'complete' with a 0 Data Entry LSB. // it doesn't mater if the param/LSB match; // they could be consecutive PN messages each with a Data Entry LSB of 0. - return (eventResult: .noChange, newEvents: [holder.storedEventWrapper(storedEvent)]) + return (eventResult: .noChange, newEvents: [holder.storedEventWrapper(storedEvent.event)]) } else { // could be either the 2nd UMP of a two UMP packet series, // or could be its own fully contained PN UMP let paramAndDataEntryLSBMatch = - event.parameter.parameterBytes == storedEvent.parameter.parameterBytes && - event.parameter.dataEntryBytes.msb == storedEvent.parameter.dataEntryBytes.msb + event.parameter.parameterBytes == storedEvent.event.parameter.parameterBytes && + event.parameter.dataEntryBytes.msb == storedEvent.event.parameter.dataEntryBytes.msb if paramAndDataEntryLSBMatch { // looks like stored event is a packet #1 in a two UMP packet series return (eventResult: .noChange, newEvents: []) // keep only new event } else { // looks like stored event is unrelated, so add it - return (eventResult: .noChange, newEvents: [holder.storedEventWrapper(storedEvent)]) + return (eventResult: .noChange, newEvents: [holder.storedEventWrapper(storedEvent.event)]) } } } else { @@ -142,7 +165,7 @@ extension ParameterNumberEventBundler { // PN UMP packet with Data Entry MSB, but 0 LSB. // could be complete, or could be first of 2 UMP packets. // store it and start the timer - holder.storedEvent = event + holder.storedEvent = (event, timeStamp, source) holder.restartTimer() return (eventResult: .remove, newEvents: []) } else { diff --git a/Tests/MIDIKitIOTests/Parser/AdvancedMIDI2Parser Tests.swift b/Tests/MIDIKitIOTests/Parser/AdvancedMIDI2Parser Tests.swift index 71ff71a244..6824cbec31 100644 --- a/Tests/MIDIKitIOTests/Parser/AdvancedMIDI2Parser Tests.swift +++ b/Tests/MIDIKitIOTests/Parser/AdvancedMIDI2Parser Tests.swift @@ -16,7 +16,7 @@ final class AdvancedMIDI2Parser_Tests: XCTestCase { fileprivate var receivedEvents: [MIDIEvent] = [] override func setUpWithError() throws { - parser = AdvancedMIDI2Parser { [self] events in + parser = AdvancedMIDI2Parser { [self] events, _, _ in receivedEvents.append(contentsOf: events) } From c757c2e3f5c233c8813dafb8a54ece40d3c88cff Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Mon, 4 Dec 2023 02:06:25 -0800 Subject: [PATCH 06/22] Unit test deflaking --- .../MIDIKitIOTests/Integration Tests/RPN NRPN IO Tests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/MIDIKitIOTests/Integration Tests/RPN NRPN IO Tests.swift b/Tests/MIDIKitIOTests/Integration Tests/RPN NRPN IO Tests.swift index f9e9f960c8..7317568402 100644 --- a/Tests/MIDIKitIOTests/Integration Tests/RPN NRPN IO Tests.swift +++ b/Tests/MIDIKitIOTests/Integration Tests/RPN NRPN IO Tests.swift @@ -116,9 +116,9 @@ final class RPN_NRPN_IO_Tests: XCTestCase { try manager.addInputConnection( to: .outputs([endpoint]), tag: inputConnectionTag, - receiver: .events { events in + receiver: .events { [weak self] events in DispatchQueue.main.async { - self.receivedEvents.append(contentsOf: events) + self?.receivedEvents.append(contentsOf: events) } } ) From 71052a7f2401962c15e62f4c19eb52ee89d312bf Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Wed, 6 Dec 2023 17:06:02 -0800 Subject: [PATCH 07/22] Deflaked unit tests --- .../Integration Tests/RPN NRPN IO Tests.swift | 116 ++++++++++++++++-- 1 file changed, 108 insertions(+), 8 deletions(-) diff --git a/Tests/MIDIKitIOTests/Integration Tests/RPN NRPN IO Tests.swift b/Tests/MIDIKitIOTests/Integration Tests/RPN NRPN IO Tests.swift index 7317568402..0b883a89e9 100644 --- a/Tests/MIDIKitIOTests/Integration Tests/RPN NRPN IO Tests.swift +++ b/Tests/MIDIKitIOTests/Integration Tests/RPN NRPN IO Tests.swift @@ -232,10 +232,35 @@ final class RPN_NRPN_IO_Tests: XCTestCase { 0xB2, 0x06, 0x10 // data entry MSB value 0x10 ]) + // Unit test de-flake: + // Filter received events in order to ignore a spurious initial CC message. + // + // Often systems that run these unit tests are slow or bogged down, so the discrete MIDI 1.0 + // packets may be fired with slightly too much delay between packets. + // Core MIDI allows individual CC 0,6,32,38,98,99,100,101 messages to be received from a + // MIDI 1.0 device/endpoint and be delivered to a MIDI 2.0 consumer. + // But Core MIDI also has a heuristic which attempts to detect a series of RPN/NRPN CC + // messages in discrete packets -- but only if it receives these messages in short enough + // succession with nearly no delay between them. Then it will drop the individual CC + // messages and produce a single MIDI 2.0 RPN/NRPN UMP packet containing all of the + // pertinent bytes. + // However, what happens when the MIDI 1.0 packets are sent with too much delay between them + // is Core MIDI will pass the first CC message through as-is, assuming that it is a discrete + // CC message. but once it sees the second CC message that defines the 2nd message in a + // 2-message RPN/NRPN header (CC 101 and CC 100, or CC 99 and CC 98) then it realizes that + // this 2nd message and any Data Entry MSB (CC 0) that follows should be all packaged up + // into a MIDI 2.0 RPN/NRPN UMP packet. + // The result is that we receive either a CC 101 or 99, followed by the expected RPN/NRPN + // event. + // The solution for de-flaking the unit test is to just ignore all non-RPN/NRPN events. + func filteredEvents() -> [MIDIEvent] { + receivedEvents.filter(chanVoice: .keepType(.rpn)) + } + // wait(sec: 1.0) - wait(for: receivedEvents.count == 1, timeout: 1.0) + wait(for: filteredEvents().count == 1, timeout: 1.0) - XCTAssertEqual(receivedEvents, [ + XCTAssertEqual(filteredEvents(), [ .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 0x02) @@ -275,12 +300,37 @@ final class RPN_NRPN_IO_Tests: XCTestCase { 0xB2, 0x26, 0x20 // data entry LSB value 0x20 ]) + // Unit test de-flake: + // Filter received events in order to ignore a spurious initial CC message. + // + // Often systems that run these unit tests are slow or bogged down, so the discrete MIDI 1.0 + // packets may be fired with slightly too much delay between packets. + // Core MIDI allows individual CC 0,6,32,38,98,99,100,101 messages to be received from a + // MIDI 1.0 device/endpoint and be delivered to a MIDI 2.0 consumer. + // But Core MIDI also has a heuristic which attempts to detect a series of RPN/NRPN CC + // messages in discrete packets -- but only if it receives these messages in short enough + // succession with nearly no delay between them. Then it will drop the individual CC + // messages and produce a single MIDI 2.0 RPN/NRPN UMP packet containing all of the + // pertinent bytes. + // However, what happens when the MIDI 1.0 packets are sent with too much delay between them + // is Core MIDI will pass the first CC message through as-is, assuming that it is a discrete + // CC message. but once it sees the second CC message that defines the 2nd message in a + // 2-message RPN/NRPN header (CC 101 and CC 100, or CC 99 and CC 98) then it realizes that + // this 2nd message and any Data Entry MSB (CC 0) that follows should be all packaged up + // into a MIDI 2.0 RPN/NRPN UMP packet. + // The result is that we receive either a CC 101 or 99, followed by the expected RPN/NRPN + // event. + // The solution for de-flaking the unit test is to just ignore all non-RPN/NRPN events. + func filteredEvents() -> [MIDIEvent] { + receivedEvents.filter(chanVoice: .keepType(.rpn)) + } + // wait(sec: 1.0) - wait(for: receivedEvents.count == 2, timeout: 1.0) + wait(for: filteredEvents().count == 2, timeout: 1.0) // Core MIDI translates MIDI 1.0 NRPN to a MIDI 2.0 UMP packet with MSB first, // then a second UMP packet adding the LSB to the same base packet data. - XCTAssertEqual(receivedEvents, [ + XCTAssertEqual(filteredEvents(), [ .rpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 0x02), @@ -393,10 +443,35 @@ final class RPN_NRPN_IO_Tests: XCTestCase { 0xB2, 0x06, 0x10 // data entry MSB value 0x10 ]) + // Unit test de-flake: + // Filter received events in order to ignore a spurious initial CC message. + // + // Often systems that run these unit tests are slow or bogged down, so the discrete MIDI 1.0 + // packets may be fired with slightly too much delay between packets. + // Core MIDI allows individual CC 0,6,32,38,98,99,100,101 messages to be received from a + // MIDI 1.0 device/endpoint and be delivered to a MIDI 2.0 consumer. + // But Core MIDI also has a heuristic which attempts to detect a series of RPN/NRPN CC + // messages in discrete packets -- but only if it receives these messages in short enough + // succession with nearly no delay between them. Then it will drop the individual CC + // messages and produce a single MIDI 2.0 RPN/NRPN UMP packet containing all of the + // pertinent bytes. + // However, what happens when the MIDI 1.0 packets are sent with too much delay between them + // is Core MIDI will pass the first CC message through as-is, assuming that it is a discrete + // CC message. but once it sees the second CC message that defines the 2nd message in a + // 2-message RPN/NRPN header (CC 101 and CC 100, or CC 99 and CC 98) then it realizes that + // this 2nd message and any Data Entry MSB (CC 0) that follows should be all packaged up + // into a MIDI 2.0 RPN/NRPN UMP packet. + // The result is that we receive either a CC 101 or 99, followed by the expected RPN/NRPN + // event. + // The solution for de-flaking the unit test is to just ignore all non-RPN/NRPN events. + func filteredEvents() -> [MIDIEvent] { + receivedEvents.filter(chanVoice: .keepType(.nrpn)) + } + // wait(sec: 1.0) - wait(for: receivedEvents.count == 1, timeout: 1.0) + wait(for: filteredEvents().count == 1, timeout: 1.0) - XCTAssertEqual(receivedEvents, [ + XCTAssertEqual(filteredEvents(), [ .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 0x02) @@ -436,12 +511,37 @@ final class RPN_NRPN_IO_Tests: XCTestCase { 0xB2, 0x26, 0x20 // data entry LSB value 0x20 ]) + // Unit test de-flake: + // Filter received events in order to ignore a spurious initial CC message. + // + // Often systems that run these unit tests are slow or bogged down, so the discrete MIDI 1.0 + // packets may be fired with slightly too much delay between packets. + // Core MIDI allows individual CC 0,6,32,38,98,99,100,101 messages to be received from a + // MIDI 1.0 device/endpoint and be delivered to a MIDI 2.0 consumer. + // But Core MIDI also has a heuristic which attempts to detect a series of RPN/NRPN CC + // messages in discrete packets -- but only if it receives these messages in short enough + // succession with nearly no delay between them. Then it will drop the individual CC + // messages and produce a single MIDI 2.0 RPN/NRPN UMP packet containing all of the + // pertinent bytes. + // However, what happens when the MIDI 1.0 packets are sent with too much delay between them + // is Core MIDI will pass the first CC message through as-is, assuming that it is a discrete + // CC message. but once it sees the second CC message that defines the 2nd message in a + // 2-message RPN/NRPN header (CC 101 and CC 100, or CC 99 and CC 98) then it realizes that + // this 2nd message and any Data Entry MSB (CC 0) that follows should be all packaged up + // into a MIDI 2.0 RPN/NRPN UMP packet. + // The result is that we receive either a CC 101 or 99, followed by the expected RPN/NRPN + // event. + // The solution for de-flaking the unit test is to just ignore all non-RPN/NRPN events. + func filteredEvents() -> [MIDIEvent] { + receivedEvents.filter(chanVoice: .keepType(.nrpn)) + } + // wait(sec: 1.0) - wait(for: receivedEvents.count == 2, timeout: 1.0) + wait(for: filteredEvents().count == 2, timeout: 1.0) // Core MIDI translates MIDI 1.0 NRPN to a MIDI 2.0 UMP packet with MSB first, // then a second UMP packet adding the LSB to the same base packet data. - XCTAssertEqual(receivedEvents, [ + XCTAssertEqual(filteredEvents(), [ .nrpn(parameter: .init(msb: 0x40, lsb: 0x41), data: (msb: 0x10, lsb: 0x00), channel: 0x02), From 40475c274ee14947a2ff695365561cf281aca879 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Tue, 26 Dec 2023 14:59:57 -0800 Subject: [PATCH 08/22] `MIDIEvent.pressure`: Debug description now includes both MIDI 1.0 & MIDI 2.0 value representation, consistent with other event descriptions. --- .../MIDIKitCore/Events/MIDIEvent/MIDIEvent description.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/MIDIKitCore/Events/MIDIEvent/MIDIEvent description.swift b/Sources/MIDIKitCore/Events/MIDIEvent/MIDIEvent description.swift index 222281447d..e1c65ae197 100644 --- a/Sources/MIDIKitCore/Events/MIDIEvent/MIDIEvent description.swift +++ b/Sources/MIDIKitCore/Events/MIDIEvent/MIDIEvent description.swift @@ -109,7 +109,10 @@ extension MIDIEvent: CustomStringConvertible, CustomDebugStringConvertible { } case let .pressure(event): - let valString = "midi1(\(event.amount.midi1Value))" + let midi1ValString = "midi1(\(event.amount.midi1Value))" + let midi2ValString = "midi2(\(event.amount.midi2Value))" + let valString = "\(midi1ValString)/\(midi2ValString)" + let channelString = event.channel.hexString() let groupString = event.group.hexString() From 49547459c972111f40dc94458fe3dc196bc7402e Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Tue, 26 Dec 2023 15:02:03 -0800 Subject: [PATCH 09/22] Bumped TimecodeKit to 2.0.8 --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index cd9c2d4011..4b34aa53d1 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,7 @@ let package = Package( ], dependencies: [ - .package(url: "https://github.com/orchetect/TimecodeKit", from: "2.0.6"), + .package(url: "https://github.com/orchetect/TimecodeKit", from: "2.0.8"), // testing only: .package(url: "https://github.com/orchetect/XCTestUtils", from: "1.0.3") From 82386b70416fddcda58be9663358e63914f48949 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Tue, 26 Dec 2023 15:23:22 -0800 Subject: [PATCH 10/22] `EventsLogging`: DRY refactoring --- .../Handlers/EventsLogging.swift | 136 +++++------------- .../MIDIReceiveHandler/MIDIReceiver.swift | 2 +- 2 files changed, 38 insertions(+), 100 deletions(-) diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsLogging.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsLogging.swift index 38052e4d3a..b45f2e1753 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsLogging.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsLogging.swift @@ -18,55 +18,13 @@ extension MIDIReceiveHandler { /// flag builds). /// If `handler` is provided, the event description string is supplied as a parameter and not /// automatically logged. - final class EventsLogging: MIDIReceiveHandlerProtocol { - public var handler: MIDIReceiver.EventsLoggingHandler - - let midi1Parser: MIDI1Parser - - var midi2Parser: MIDI2Parser? = nil - var advancedMIDI2Parser: AdvancedMIDI2Parser? = nil - - public let options: MIDIReceiverOptions - - public func packetListReceived( - _ packets: [MIDIPacketData] - ) { - for midiPacket in packets { - let events = midi1Parser.parsedEvents(in: midiPacket) - guard !events.isEmpty else { continue } - prepLogMessage( - events: events, - timeStamp: midiPacket.timeStamp, - source: midiPacket.source - ) - } - } - - @available(macOS 11, iOS 14, macCatalyst 14, *) - public func eventListReceived( - _ packets: [UniversalMIDIPacketData], - protocol midiProtocol: MIDIProtocolVersion - ) { - if let parser = midi2Parser { - for midiPacket in packets { - let events = parser.parsedEvents(in: midiPacket) - handle(events: events, timeStamp: midiPacket.timeStamp, source: midiPacket.source) - } - } else if let parser = advancedMIDI2Parser { - for midiPacket in packets { - parser.parseEvents(in: midiPacket) - } - } - } - - init( - options: MIDIReceiverOptions, - log: OSLog = .default, - handler: MIDIReceiver.EventsLoggingHandler? - ) { - self.options = options - - self.handler = handler ?? { packetBytesString in + static func _eventsLogging( + options: MIDIReceiverOptions, + log: OSLog = .default, + handler: MIDIReceiver.EventsLoggingHandler? + ) -> MIDIReceiveHandler.EventsWithMetadata { + let stringLogHandler: MIDIReceiver.EventsLoggingHandler = handler + ?? { packetBytesString in #if DEBUG os_log( "%{public}@", @@ -76,61 +34,41 @@ extension MIDIReceiveHandler { ) #endif } - - // MIDI 1 - - midi1Parser = MIDI1Parser() - - midi1Parser.translateNoteOnZeroVelocityToNoteOff = options - .contains(.translateMIDI1NoteOnZeroVelocityToNoteOff) - - // MIDI 2 - - if options.contains(.bundleRPNAndNRPNDataEntryLSB) { - advancedMIDI2Parser = AdvancedMIDI2Parser { [weak self] events, timeStamp, source in - self?.handle(events: events, timeStamp: timeStamp, source: source) - } - } else { - midi2Parser = MIDI2Parser() + + return MIDIReceiveHandler.EventsWithMetadata(options: options) { events, timeStamp, source in + let logString = generateLogString( + events: events, + timeStamp: timeStamp, + source: source, + options: options + ) + stringLogHandler(logString) } - } + } + + fileprivate static func generateLogString( + events: [MIDIEvent], + timeStamp: CoreMIDITimeStamp, + source: MIDIOutputEndpoint?, + options: MIDIReceiverOptions + ) -> String { + var events = events - func handle( - events: [MIDIEvent], - timeStamp: CoreMIDITimeStamp, - source: MIDIOutputEndpoint? - ) { - guard !events.isEmpty else { return } - prepLogMessage( - events: events, - timeStamp: timeStamp, - source: source - ) + if options.contains(.filterActiveSensingAndClock) { + events = events.filter(sysRealTime: .dropTypes([.activeSensing, .timingClock])) } - func prepLogMessage( - events: [MIDIEvent], - timeStamp: CoreMIDITimeStamp, - source: MIDIOutputEndpoint? - ) { - var events = events - - if options.contains(.filterActiveSensingAndClock) { - events = events.filter(sysRealTime: .dropTypes([.activeSensing, .timingClock])) - } - - var stringOutput: String = events - .map { "\($0)" } - .joined(separator: ", ") - + " timeStamp:\(timeStamp)" - - // not all packets will contain source refs - if let source { - stringOutput += " source:\(source.displayName.quoted)" - } - - handler(stringOutput) + var stringOutput: String = events + .map { "\($0)" } + .joined(separator: ", ") + + " timeStamp:\(timeStamp)" + + // not all packets will contain source refs + if let source { + stringOutput += " source:\(source.displayName.quoted)" } + + return stringOutput } } diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiver.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiver.swift index 0f9ad26f45..584569589c 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiver.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiver.swift @@ -96,7 +96,7 @@ extension MIDIReceiver { return MIDIReceiveHandler.EventsWithMetadata(options: options, handler: handler) case let .eventsLogging(options, handler): - return MIDIReceiveHandler.EventsLogging(options: options, handler: handler) + return MIDIReceiveHandler._eventsLogging(options: options, handler: handler) case let .rawData(handler): return MIDIReceiveHandler(MIDIReceiveHandler.RawData(handler: handler)) From 37db0236d6fd0d216a673037eb0a4b8f88ae860f Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Tue, 26 Dec 2023 15:31:12 -0800 Subject: [PATCH 11/22] `RawDataLogging`: DRY refactoring --- .../Handlers/RawDataLogging.swift | 95 +++++++------------ .../MIDIReceiveHandler/MIDIReceiver.swift | 2 +- 2 files changed, 37 insertions(+), 60 deletions(-) diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawDataLogging.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawDataLogging.swift index 7713c09f6f..36d56218ea 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawDataLogging.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawDataLogging.swift @@ -22,69 +22,46 @@ extension MIDIReceiveHandler { /// preprocessor flag builds). /// If `handler` is provided, the hex byte string is supplied as a parameter and not /// automatically logged. - final class RawDataLogging: MIDIReceiveHandlerProtocol { - public var handler: MIDIReceiver.RawDataLoggingHandler - - public var filterActiveSensingAndClock = false - - public func packetListReceived( - _ packets: [MIDIPacketData] - ) { - for midiPacket in packets { - log( - bytes: midiPacket.bytes, - timeStamp: midiPacket.timeStamp, - source: midiPacket.source - ) - } - } - - @available(macOS 11, iOS 14, macCatalyst 14, *) - public func eventListReceived( - _ packets: [UniversalMIDIPacketData], - protocol midiProtocol: MIDIProtocolVersion - ) { - for midiPacket in packets { - log( - bytes: midiPacket.bytes, - timeStamp: midiPacket.timeStamp, - source: midiPacket.source - ) - } + static func _rawDataLogging( + log: OSLog = .default, + handler: MIDIReceiver.RawDataLoggingHandler? + ) -> MIDIReceiveHandler.RawData { + let stringLogHandler: MIDIReceiver.RawDataLoggingHandler = handler ?? { packetBytesString in + #if DEBUG + os_log( + "%{public}@", + log: log, + type: .debug, + packetBytesString + ) + #endif } - - init( - log: OSLog = .default, - handler: MIDIReceiver.RawDataLoggingHandler? = nil - ) { - self.handler = handler ?? { packetBytesString in - #if DEBUG - os_log( - "%{public}@", - log: log, - type: .debug, - packetBytesString - ) - #endif - } + + return MIDIReceiveHandler.RawData { packet in + let logString = generateLogString( + bytes: packet.bytes, + timeStamp: packet.timeStamp, + source: packet.source + ) + stringLogHandler(logString) } + } - func log( - bytes: [UInt8], - timeStamp: CoreMIDITimeStamp, - source: MIDIOutputEndpoint? - ) { - var stringOutput = bytes - .hexString(padEachTo: 2, prefixes: false) - + " timeStamp:\(timeStamp)" - - // not all packets will contain source refs - if let source { - stringOutput += " source:\(source.displayName.quoted)" - } - - handler(stringOutput) + fileprivate static func generateLogString( + bytes: [UInt8], + timeStamp: CoreMIDITimeStamp, + source: MIDIOutputEndpoint? + ) -> String { + var stringOutput = bytes + .hexString(padEachTo: 2, prefixes: false) + + " timeStamp:\(timeStamp)" + + // not all packets will contain source refs + if let source { + stringOutput += " source:\(source.displayName.quoted)" } + + return stringOutput } } diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiver.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiver.swift index 584569589c..06cea9a9be 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiver.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiver.swift @@ -102,7 +102,7 @@ extension MIDIReceiver { return MIDIReceiveHandler(MIDIReceiveHandler.RawData(handler: handler)) case let .rawDataLogging(handler): - return MIDIReceiveHandler.RawDataLogging(handler: handler) + return MIDIReceiveHandler._rawDataLogging(handler: handler) case let .object(object, storageType, options): switch storageType { From 96157c47fe3ff5362b9f811d1aef0fb30dec4163 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Tue, 26 Dec 2023 15:52:05 -0800 Subject: [PATCH 12/22] `MIDIReceiver`: `eventsWithMetadata` receiver is now `events`. The old `events` receiver has been removed, as it is redundant. --- .../API Evolution/MIDIKit-0.9.3.swift | 6 +- .../API Evolution/MIDIKit-0.9.5.swift | 43 +++++++++ .../MIDIReceiveHandler/Handlers/Events.swift | 42 ++++----- .../Handlers/EventsLogging.swift | 5 +- .../Handlers/EventsWithMetadata.swift | 93 ------------------- .../MIDIReceiveHandler/Handlers/RawData.swift | 1 + .../Handlers/RawDataLogging.swift | 1 + .../Handlers/WeakEventsReceiver.swift | 3 +- .../MIDIReceiveHandler/MIDIReceiver.swift | 15 +-- .../MIDIThruConnectionProxy.swift | 2 +- .../Integration Tests/RPN NRPN IO Tests.swift | 2 +- .../Integration Tests/Round Trip Tests.swift | 2 +- .../MIDIInputConnection Tests.swift | 14 +-- .../MIDIOutputConnection Tests.swift | 16 ++-- .../MIDIThruConnection Tests.swift | 8 +- 15 files changed, 96 insertions(+), 157 deletions(-) create mode 100644 Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.5.swift delete mode 100644 Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsWithMetadata.swift diff --git a/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.3.swift b/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.3.swift index 32e30a9343..b3b3554194 100644 --- a/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.3.swift +++ b/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.3.swift @@ -29,15 +29,15 @@ extension MIDIReceiver { @available( *, deprecated, - renamed: "eventsWithMetadata(options:_:)", + renamed: "events(options:_:)", message: "`translateMIDI1NoteOnZeroVelocityToNoteOff` property is now an OptionSet flag." ) @_disfavoredOverload public static func eventsWithMetadata( translateMIDI1NoteOnZeroVelocityToNoteOff: Bool, - _ handler: @escaping EventsWithMetadataHandler + _ handler: @escaping EventsHandler ) -> Self { - .eventsWithMetadata( + .events( options: translateMIDI1NoteOnZeroVelocityToNoteOff ? [.translateMIDI1NoteOnZeroVelocityToNoteOff] : [], handler ) diff --git a/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.5.swift b/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.5.swift new file mode 100644 index 0000000000..b21693cbf7 --- /dev/null +++ b/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.5.swift @@ -0,0 +1,43 @@ +// +// MIDIKit-0.9.5.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +#if !os(tvOS) && !os(watchOS) + +extension MIDIReceiver { + public typealias LegacyEventsHandler = ( + _ events: [MIDIEvent] + ) -> Void + + @available( + *, + deprecated, + renamed: "events", + message: "This receive handler's closure now takes 3 parameters instead of one. (events, timeStamp, source)" + ) + public static func events( + options: MIDIReceiverOptions = [], + _ handler: @escaping LegacyEventsHandler + ) -> Self { + .events(options: options) { events, _, _ in + handler(events) + } + } + + @available( + *, + deprecated, + renamed: "events", + message: "`eventsWithMetadata` is now renamed to `events`, and the old `events` receive handler without metadata has been removed." + ) + public static func eventsWithMetadata( + options: MIDIReceiverOptions = [], + _ handler: @escaping EventsHandler + ) -> Self { + .events(options: options, handler) + } +} + +#endif diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift index 86fc3842f3..a87d5e266a 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift @@ -7,13 +7,20 @@ #if !os(tvOS) && !os(watchOS) extension MIDIReceiver { + /// Handler for the ``events(options:_:)`` MIDI receiver. + /// Source endpoint is only available when used with ``MIDIInputConnection`` and will always be + /// `nil` when used with ``MIDIInput``. public typealias EventsHandler = ( - _ events: [MIDIEvent] + _ events: [MIDIEvent], + _ timeStamp: CoreMIDITimeStamp, + _ source: MIDIOutputEndpoint? ) -> Void } extension MIDIReceiveHandler { - /// MIDI Event receive handler. + /// MIDI Event receive handler including packet timestamp and source endpoint. + /// Source endpoint is only available when used with ``MIDIInputConnection`` and will always be + /// `nil` when used with ``MIDIInput``. final class Events: MIDIReceiveHandlerProtocol { public var handler: MIDIReceiver.EventsHandler @@ -22,15 +29,14 @@ extension MIDIReceiveHandler { var midi2Parser: MIDI2Parser? = nil var advancedMIDI2Parser: AdvancedMIDI2Parser? = nil - let options: MIDIReceiverOptions - public func packetListReceived( _ packets: [MIDIPacketData] ) { for midiPacket in packets { let events = midi1Parser.parsedEvents(in: midiPacket) guard !events.isEmpty else { continue } - handle(events: events) + + handler(events, midiPacket.timeStamp, midiPacket.source) } } @@ -42,7 +48,9 @@ extension MIDIReceiveHandler { if let parser = midi2Parser { for midiPacket in packets { let events = parser.parsedEvents(in: midiPacket) - handle(events: events) + guard !events.isEmpty else { continue } + + handler(events, midiPacket.timeStamp, midiPacket.source) } } else if let parser = advancedMIDI2Parser { for midiPacket in packets { @@ -55,13 +63,11 @@ extension MIDIReceiveHandler { options: MIDIReceiverOptions, handler: @escaping MIDIReceiver.EventsHandler ) { - self.options = options - if options.contains(.filterActiveSensingAndClock) { - self.handler = { events in + self.handler = { events, timeStamp, source in let filtered = events.filter(sysRealTime: .dropTypes([.activeSensing, .timingClock])) guard !filtered.isEmpty else { return } - handler(filtered) + handler(filtered, timeStamp, source) } } else { self.handler = handler @@ -77,25 +83,13 @@ extension MIDIReceiveHandler { // MIDI 2 if options.contains(.bundleRPNAndNRPNDataEntryLSB) { - advancedMIDI2Parser = AdvancedMIDI2Parser { [weak self] events, _, _ in - self?.handle(events: events) + advancedMIDI2Parser = AdvancedMIDI2Parser { [weak self] events, timeStamp, source in + self?.handler(events, timeStamp, source) } } else { midi2Parser = MIDI2Parser() } } - - func handle(events: [MIDIEvent]) { - guard !events.isEmpty else { return } - - if options.contains(.filterActiveSensingAndClock) { - let filtered = events.filter(sysRealTime: .dropTypes([.activeSensing, .timingClock])) - guard !filtered.isEmpty else { return } - handler(filtered) - } else { - handler(events) - } - } } } diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsLogging.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsLogging.swift index b45f2e1753..db21b40b9a 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsLogging.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsLogging.swift @@ -9,6 +9,7 @@ import os.log extension MIDIReceiver { + /// Handler for the ``eventsLogging(options:_:)`` MIDI receiver. public typealias EventsLoggingHandler = (_ eventString: String) -> Void } @@ -22,7 +23,7 @@ extension MIDIReceiveHandler { options: MIDIReceiverOptions, log: OSLog = .default, handler: MIDIReceiver.EventsLoggingHandler? - ) -> MIDIReceiveHandler.EventsWithMetadata { + ) -> MIDIReceiveHandler.Events { let stringLogHandler: MIDIReceiver.EventsLoggingHandler = handler ?? { packetBytesString in #if DEBUG @@ -35,7 +36,7 @@ extension MIDIReceiveHandler { #endif } - return MIDIReceiveHandler.EventsWithMetadata(options: options) { events, timeStamp, source in + return MIDIReceiveHandler.Events(options: options) { events, timeStamp, source in let logString = generateLogString( events: events, timeStamp: timeStamp, diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsWithMetadata.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsWithMetadata.swift deleted file mode 100644 index 8449503054..0000000000 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsWithMetadata.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// EventsWithMetadata.swift -// MIDIKit • https://github.com/orchetect/MIDIKit -// © 2021-2023 Steffan Andrews • Licensed under MIT License -// - -#if !os(tvOS) && !os(watchOS) - -extension MIDIReceiver { - public typealias EventsWithMetadataHandler = ( - _ events: [MIDIEvent], - _ timeStamp: CoreMIDITimeStamp, - _ source: MIDIOutputEndpoint? - ) -> Void -} - -extension MIDIReceiveHandler { - /// MIDI Event receive handler including packet timestamp and source endpoint metadata. - /// Source endpoint is only available when used with ``MIDIInputConnection`` and will always be - /// `nil` when used with ``MIDIInput``. - final class EventsWithMetadata: MIDIReceiveHandlerProtocol { - public var handler: MIDIReceiver.EventsWithMetadataHandler - - let midi1Parser: MIDI1Parser - - var midi2Parser: MIDI2Parser? = nil - var advancedMIDI2Parser: AdvancedMIDI2Parser? = nil - - public func packetListReceived( - _ packets: [MIDIPacketData] - ) { - for midiPacket in packets { - let events = midi1Parser.parsedEvents(in: midiPacket) - guard !events.isEmpty else { continue } - - handler(events, midiPacket.timeStamp, midiPacket.source) - } - } - - @available(macOS 11, iOS 14, macCatalyst 14, *) - public func eventListReceived( - _ packets: [UniversalMIDIPacketData], - protocol midiProtocol: MIDIProtocolVersion - ) { - if let parser = midi2Parser { - for midiPacket in packets { - let events = parser.parsedEvents(in: midiPacket) - guard !events.isEmpty else { continue } - - handler(events, midiPacket.timeStamp, midiPacket.source) - } - } else if let parser = advancedMIDI2Parser { - for midiPacket in packets { - parser.parseEvents(in: midiPacket) - } - } - } - - init( - options: MIDIReceiverOptions, - handler: @escaping MIDIReceiver.EventsWithMetadataHandler - ) { - if options.contains(.filterActiveSensingAndClock) { - self.handler = { events, timeStamp, source in - let filtered = events.filter(sysRealTime: .dropTypes([.activeSensing, .timingClock])) - guard !filtered.isEmpty else { return } - handler(filtered, timeStamp, source) - } - } else { - self.handler = handler - } - - // MIDI 1 - - midi1Parser = MIDI1Parser() - - midi1Parser.translateNoteOnZeroVelocityToNoteOff = options - .contains(.translateMIDI1NoteOnZeroVelocityToNoteOff) - - // MIDI 2 - - if options.contains(.bundleRPNAndNRPNDataEntryLSB) { - advancedMIDI2Parser = AdvancedMIDI2Parser { [weak self] events, timeStamp, source in - self?.handler(events, timeStamp, source) - } - } else { - midi2Parser = MIDI2Parser() - } - } - } -} - -#endif diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawData.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawData.swift index c8806150c8..0175a24c88 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawData.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawData.swift @@ -7,6 +7,7 @@ #if !os(tvOS) && !os(watchOS) extension MIDIReceiver { + /// Handler for the ``rawData(_:)`` MIDI receiver. public typealias RawDataHandler = (_ packet: AnyMIDIPacket) -> Void } diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawDataLogging.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawDataLogging.swift index 36d56218ea..3f704c117d 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawDataLogging.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawDataLogging.swift @@ -10,6 +10,7 @@ import Foundation import os.log extension MIDIReceiver { + /// Handler for the ``rawDataLogging(_:)`` MIDI receiver. public typealias RawDataLoggingHandler = (_ packetBytesString: String) -> Void } diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift index 8574079a33..f68ef7e1a7 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift @@ -7,7 +7,8 @@ #if !os(tvOS) && !os(watchOS) extension MIDIReceiveHandler { - /// MIDI Event receive handler that holds a weak reference to a receiver object. + /// MIDI Event receive handler that holds a weak reference to a receiver object that conforms + /// to the ``ReceivesMIDIEvents`` protocol. final class WeakEventsReceiver: MIDIReceiveHandlerProtocol { public weak var receiver: ReceivesMIDIEvents? diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiver.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiver.swift index 06cea9a9be..24f045ae77 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiver.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiver.swift @@ -13,19 +13,13 @@ public enum MIDIReceiver { /// One or more receivers in series. case group([MIDIReceiver]) - /// Provides a closure to handle strongly-typed MIDI events. (Recommended) - case events( - options: MIDIReceiverOptions = [], - _ handler: EventsHandler - ) - /// Provides a closure to handle strongly-typed MIDI events including packet timestamp and - /// source endpoint metadata. + /// source endpoint metadata. (Recommended) /// Source endpoint is only available when used with ``MIDIInputConnection`` and will always be /// `nil` when used with ``MIDIInput``. - case eventsWithMetadata( + case events( options: MIDIReceiverOptions = [], - _ handler: EventsWithMetadataHandler + _ handler: EventsHandler ) /// Provides a convenience to automatically log MIDI events to the console. @@ -92,9 +86,6 @@ extension MIDIReceiver { case let .events(options, handler): return MIDIReceiveHandler.Events(options: options, handler: handler) - case let .eventsWithMetadata(options, handler): - return MIDIReceiveHandler.EventsWithMetadata(options: options, handler: handler) - case let .eventsLogging(options, handler): return MIDIReceiveHandler._eventsLogging(options: options, handler: handler) diff --git a/Sources/MIDIKitIO/MIDIThruConnection/MIDIThruConnectionProxy.swift b/Sources/MIDIKitIO/MIDIThruConnection/MIDIThruConnectionProxy.swift index 63c0c90c92..72a9b5f726 100644 --- a/Sources/MIDIKitIO/MIDIThruConnection/MIDIThruConnectionProxy.swift +++ b/Sources/MIDIKitIO/MIDIThruConnection/MIDIThruConnectionProxy.swift @@ -33,7 +33,7 @@ final class MIDIThruConnectionProxy { inputConnection = MIDIInputConnection( mode: .outputs(matching: Set(outputs.asIdentities())), filter: .default(), - receiver: .events { [weak self] events in + receiver: .events { [weak self] events, _, _ in try? self?.outputConnection.send(events: events) }, midiManager: midiManager, diff --git a/Tests/MIDIKitIOTests/Integration Tests/RPN NRPN IO Tests.swift b/Tests/MIDIKitIOTests/Integration Tests/RPN NRPN IO Tests.swift index 0b883a89e9..4b83ba2a12 100644 --- a/Tests/MIDIKitIOTests/Integration Tests/RPN NRPN IO Tests.swift +++ b/Tests/MIDIKitIOTests/Integration Tests/RPN NRPN IO Tests.swift @@ -116,7 +116,7 @@ final class RPN_NRPN_IO_Tests: XCTestCase { try manager.addInputConnection( to: .outputs([endpoint]), tag: inputConnectionTag, - receiver: .events { [weak self] events in + receiver: .events { [weak self] events, _, _ in DispatchQueue.main.async { self?.receivedEvents.append(contentsOf: events) } diff --git a/Tests/MIDIKitIOTests/Integration Tests/Round Trip Tests.swift b/Tests/MIDIKitIOTests/Integration Tests/Round Trip Tests.swift index 41551e99b6..5aa4280a15 100644 --- a/Tests/MIDIKitIOTests/Integration Tests/Round Trip Tests.swift +++ b/Tests/MIDIKitIOTests/Integration Tests/Round Trip Tests.swift @@ -92,7 +92,7 @@ open class RoundTrip_Tests_Base: XCTestCase { try manager.addInputConnection( to: .outputs(matching: [.uniqueID(outputID)]), tag: inputConnectionTag, - receiver: .events { events in + receiver: .events { events, _, _ in DispatchQueue.main.async { self.receivedEvents.append(contentsOf: events) } diff --git a/Tests/MIDIKitIOTests/MIDIInputConnection/MIDIInputConnection Tests.swift b/Tests/MIDIKitIOTests/MIDIInputConnection/MIDIInputConnection Tests.swift index 39c1fb449c..cd153ff1ef 100644 --- a/Tests/MIDIKitIOTests/MIDIInputConnection/MIDIInputConnection Tests.swift +++ b/Tests/MIDIKitIOTests/MIDIInputConnection/MIDIInputConnection Tests.swift @@ -52,7 +52,7 @@ final class MIDIInputConnection_Tests: XCTestCase { try manager.addInputConnection( to: .outputs(matching: [.uniqueID(output1ID)]), tag: connTag, - receiver: .events { events in + receiver: .events { events, _, _ in DispatchQueue.main.async { print(events) self.connEvents.append(contentsOf: events) @@ -157,7 +157,7 @@ final class MIDIInputConnection_Tests: XCTestCase { to: .allOutputs, tag: connTag, filter: .init(owned: false, criteria: .currentOutputs()), - receiver: .events { events in + receiver: .events { events, _, _ in DispatchQueue.main.async { print(events) self.connEvents.append(contentsOf: events) @@ -216,7 +216,7 @@ final class MIDIInputConnection_Tests: XCTestCase { to: .allOutputs, tag: connTag, filter: .init(owned: true, criteria: .currentOutputs()), - receiver: .events { events in + receiver: .events { events, _, _ in DispatchQueue.main.async { print(events) self.connEvents.append(contentsOf: events) @@ -292,7 +292,7 @@ final class MIDIInputConnection_Tests: XCTestCase { owned: true, criteria: manager.endpoints.outputsUnowned ), - receiver: .events { events in + receiver: .events { events, _, _ in DispatchQueue.main.async { print(events) self.connEvents.append(contentsOf: events) @@ -360,7 +360,7 @@ final class MIDIInputConnection_Tests: XCTestCase { try manager.addInputConnection( to: .outputs([output1.endpoint]), tag: connTag, - receiver: .events { events in + receiver: .events { events, _, _ in DispatchQueue.main.async { print(events) self.connEvents.append(contentsOf: events) @@ -432,7 +432,7 @@ final class MIDIInputConnection_Tests: XCTestCase { owned: false, criteria: [.uniqueID(output1ID)] ), - receiver: .events { events in + receiver: .events { events, _, _ in DispatchQueue.main.async { print(events) self.connEvents.append(contentsOf: events) @@ -499,7 +499,7 @@ final class MIDIInputConnection_Tests: XCTestCase { try manager.addInputConnection( to: .outputs([output1.endpoint]), tag: connTag, - receiver: .events { events in + receiver: .events { events, _, _ in DispatchQueue.main.async { print(events) self.connEvents.append(contentsOf: events) diff --git a/Tests/MIDIKitIOTests/MIDIOutputConnection/MIDIOutputConnection Tests.swift b/Tests/MIDIKitIOTests/MIDIOutputConnection/MIDIOutputConnection Tests.swift index 202cf3ed74..ebc94ff3ed 100644 --- a/Tests/MIDIKitIOTests/MIDIOutputConnection/MIDIOutputConnection Tests.swift +++ b/Tests/MIDIKitIOTests/MIDIOutputConnection/MIDIOutputConnection Tests.swift @@ -45,7 +45,7 @@ final class MIDIOutputConnection_Tests: XCTestCase { name: "MIDIKit IO Tests Input 1", tag: input1Tag, uniqueID: .adHoc, // allow system to generate random ID each time, without persistence - receiver: .events { [weak self] events in + receiver: .events { [weak self] events, _, _ in DispatchQueue.main.async { self?.input1Events.append(contentsOf: events) } @@ -83,7 +83,7 @@ final class MIDIOutputConnection_Tests: XCTestCase { name: "MIDIKit IO Tests Input 2", tag: input2Tag, uniqueID: .adHoc, // allow system to generate random ID each time, without persistence - receiver: .events { [weak self] events in + receiver: .events { [weak self] events, _, _ in DispatchQueue.main.async { self?.input2Events.append(contentsOf: events) } @@ -176,7 +176,7 @@ final class MIDIOutputConnection_Tests: XCTestCase { name: "MIDIKit IO Tests Input 1", tag: input1Tag, uniqueID: .adHoc, // allow system to generate random ID each time, without persistence - receiver: .events { [weak self] events in + receiver: .events { [weak self] events, _, _ in DispatchQueue.main.async { self?.input1Events.append(contentsOf: events) } @@ -239,7 +239,7 @@ final class MIDIOutputConnection_Tests: XCTestCase { name: "MIDIKit IO Tests Input 1", tag: input1Tag, uniqueID: .adHoc, // allow system to generate random ID each time, without persistence - receiver: .events { [weak self] events in + receiver: .events { [weak self] events, _, _ in DispatchQueue.main.async { self?.input1Events.append(contentsOf: events) } @@ -287,7 +287,7 @@ final class MIDIOutputConnection_Tests: XCTestCase { name: "MIDIKit IO Tests Input 1", tag: input1Tag, uniqueID: .adHoc, // allow system to generate random ID each time, without persistence - receiver: .events { [weak self] events in + receiver: .events { [weak self] events, _, _ in DispatchQueue.main.async { self?.input1Events.append(contentsOf: events) } @@ -361,7 +361,7 @@ final class MIDIOutputConnection_Tests: XCTestCase { name: "MIDIKit IO Tests Input 1", tag: input1Tag, uniqueID: .adHoc, // allow system to generate random ID each time, without persistence - receiver: .events { [weak self] events in + receiver: .events { [weak self] events, _, _ in DispatchQueue.main.async { self?.input1Events.append(contentsOf: events) } @@ -434,7 +434,7 @@ final class MIDIOutputConnection_Tests: XCTestCase { name: "MIDIKit IO Tests Input 1", tag: input1Tag, uniqueID: .adHoc, // allow system to generate random ID each time, without persistence - receiver: .events { [weak self] events in + receiver: .events { [weak self] events, _, _ in DispatchQueue.main.async { self?.input1Events.append(contentsOf: events) } @@ -507,7 +507,7 @@ final class MIDIOutputConnection_Tests: XCTestCase { name: "MIDIKit IO Tests Input 1", tag: input1Tag, uniqueID: .adHoc, // allow system to generate random ID each time, without persistence - receiver: .events { [weak self] events in + receiver: .events { [weak self] events, _, _ in DispatchQueue.main.async { self?.input1Events.append(contentsOf: events) } diff --git a/Tests/MIDIKitIOTests/MIDIThruConnection/MIDIThruConnection Tests.swift b/Tests/MIDIKitIOTests/MIDIThruConnection/MIDIThruConnection Tests.swift index 7e293b8523..cef29415da 100644 --- a/Tests/MIDIKitIOTests/MIDIThruConnection/MIDIThruConnection Tests.swift +++ b/Tests/MIDIKitIOTests/MIDIThruConnection/MIDIThruConnection Tests.swift @@ -55,7 +55,7 @@ final class MIDIThruConnection_Tests: XCTestCase { tag: input1Tag, uniqueID: .adHoc, // allow system to generate random ID each time, without persistence - receiver: .events { events in + receiver: .events { events, _, _ in DispatchQueue.main.async { self.connEvents.append(contentsOf: events) } @@ -138,7 +138,7 @@ final class MIDIThruConnection_Tests: XCTestCase { tag: input1Tag, uniqueID: .adHoc, // allow system to generate random ID each time, without persistence - receiver: .events { events in + receiver: .events { events, _, _ in DispatchQueue.main.async { self.connEvents.append(contentsOf: events) } @@ -251,7 +251,7 @@ final class MIDIThruConnection_Tests: XCTestCase { tag: input1Tag, uniqueID: .adHoc, // allow system to generate random ID each time, without persistence - receiver: .events { events in + receiver: .events { events, _, _ in DispatchQueue.main.async { self.connEvents.append(contentsOf: events) } @@ -329,7 +329,7 @@ final class MIDIThruConnection_Tests: XCTestCase { tag: input1Tag, uniqueID: .adHoc, // allow system to generate random ID each time, without persistence - receiver: .events { events in + receiver: .events { events, _, _ in DispatchQueue.main.async { self.connEvents.append(contentsOf: events) } From 63d446d25b666ef04f4ec3b2512a68dcf3c4e6a8 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Tue, 26 Dec 2023 16:57:55 -0800 Subject: [PATCH 13/22] `MIDIReceiver`: Internal DRY refactoring --- .../MIDIReceiveHandler/Handlers/Events.swift | 62 +++------------ .../Handlers/EventsBase.swift | 79 +++++++++++++++++++ .../Handlers/StrongEventsReceiver.swift | 63 ++------------- .../Handlers/WeakEventsReceiver.swift | 61 ++------------ 4 files changed, 104 insertions(+), 161 deletions(-) create mode 100644 Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsBase.swift diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift index a87d5e266a..7784bd0035 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift @@ -21,44 +21,9 @@ extension MIDIReceiveHandler { /// MIDI Event receive handler including packet timestamp and source endpoint. /// Source endpoint is only available when used with ``MIDIInputConnection`` and will always be /// `nil` when used with ``MIDIInput``. - final class Events: MIDIReceiveHandlerProtocol { + final class Events: EventsBase { public var handler: MIDIReceiver.EventsHandler - let midi1Parser: MIDI1Parser - - var midi2Parser: MIDI2Parser? = nil - var advancedMIDI2Parser: AdvancedMIDI2Parser? = nil - - public func packetListReceived( - _ packets: [MIDIPacketData] - ) { - for midiPacket in packets { - let events = midi1Parser.parsedEvents(in: midiPacket) - guard !events.isEmpty else { continue } - - handler(events, midiPacket.timeStamp, midiPacket.source) - } - } - - @available(macOS 11, iOS 14, macCatalyst 14, *) - public func eventListReceived( - _ packets: [UniversalMIDIPacketData], - protocol midiProtocol: MIDIProtocolVersion - ) { - if let parser = midi2Parser { - for midiPacket in packets { - let events = parser.parsedEvents(in: midiPacket) - guard !events.isEmpty else { continue } - - handler(events, midiPacket.timeStamp, midiPacket.source) - } - } else if let parser = advancedMIDI2Parser { - for midiPacket in packets { - parser.parseEvents(in: midiPacket) - } - } - } - init( options: MIDIReceiverOptions, handler: @escaping MIDIReceiver.EventsHandler @@ -73,22 +38,15 @@ extension MIDIReceiveHandler { self.handler = handler } - // MIDI 1 - - midi1Parser = MIDI1Parser() - - midi1Parser.translateNoteOnZeroVelocityToNoteOff = options - .contains(.translateMIDI1NoteOnZeroVelocityToNoteOff) - - // MIDI 2 - - if options.contains(.bundleRPNAndNRPNDataEntryLSB) { - advancedMIDI2Parser = AdvancedMIDI2Parser { [weak self] events, timeStamp, source in - self?.handler(events, timeStamp, source) - } - } else { - midi2Parser = MIDI2Parser() - } + super.init(options: options) + } + + override func handle( + events: [MIDIEvent], + timeStamp: CoreMIDITimeStamp, + source: MIDIOutputEndpoint? + ) { + handler(events, timeStamp, source) } } } diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsBase.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsBase.swift new file mode 100644 index 0000000000..66aa2ae0a8 --- /dev/null +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsBase.swift @@ -0,0 +1,79 @@ +// +// EventsBase.swift +// MIDIKit • https://github.com/orchetect/MIDIKit +// © 2021-2023 Steffan Andrews • Licensed under MIT License +// + +#if !os(tvOS) && !os(watchOS) + +extension MIDIReceiveHandler { + /// Foundational receiver class which other events receivers subclass. + class EventsBase: MIDIReceiveHandlerProtocol { + let midi1Parser: MIDI1Parser + + var midi2Parser: MIDI2Parser? = nil + var advancedMIDI2Parser: AdvancedMIDI2Parser? = nil + + let options: MIDIReceiverOptions + + public func packetListReceived( + _ packets: [MIDIPacketData] + ) { + for midiPacket in packets { + let events = midi1Parser.parsedEvents(in: midiPacket) + guard !events.isEmpty else { continue } + + handle(events: events, timeStamp: midiPacket.timeStamp, source: midiPacket.source) + } + } + + @available(macOS 11, iOS 14, macCatalyst 14, *) + public func eventListReceived( + _ packets: [UniversalMIDIPacketData], + protocol midiProtocol: MIDIProtocolVersion + ) { + if let parser = midi2Parser { + for midiPacket in packets { + let events = parser.parsedEvents(in: midiPacket) + guard !events.isEmpty else { continue } + + handle(events: events, timeStamp: midiPacket.timeStamp, source: midiPacket.source) + } + } else if let parser = advancedMIDI2Parser { + for midiPacket in packets { + parser.parseEvents(in: midiPacket) + } + } + } + + init(options: MIDIReceiverOptions) { + // store options + self.options = options + + // MIDI 1 + midi1Parser = MIDI1Parser() + midi1Parser.translateNoteOnZeroVelocityToNoteOff = options + .contains(.translateMIDI1NoteOnZeroVelocityToNoteOff) + + // MIDI 2 + if options.contains(.bundleRPNAndNRPNDataEntryLSB) { + advancedMIDI2Parser = AdvancedMIDI2Parser { [weak self] events, timeStamp, source in + self?.handle(events: events, timeStamp: timeStamp, source: source) + } + } else { + midi2Parser = MIDI2Parser() + } + } + + func handle( + events: [MIDIEvent], + timeStamp: CoreMIDITimeStamp, + source: MIDIOutputEndpoint? + ) { + // method must be overridden by subclasses + assertionFailure("EventsBase subclasses must implement func handle(events:timeStamp:source:).") + } + } +} + +#endif diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/StrongEventsReceiver.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/StrongEventsReceiver.swift index 4fc3fa868f..3238cf6d88 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/StrongEventsReceiver.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/StrongEventsReceiver.swift @@ -9,70 +9,23 @@ extension MIDIReceiveHandler { /// MIDI Event receive handler that holds a strong reference to a receiver object that conforms /// to the ``ReceivesMIDIEvents`` protocol. - final class StrongEventsReceiver: MIDIReceiveHandlerProtocol { - public let receiver: ReceivesMIDIEvents - - let midi1Parser: MIDI1Parser - - var midi2Parser: MIDI2Parser? = nil - var advancedMIDI2Parser: AdvancedMIDI2Parser? = nil - - let options: MIDIReceiverOptions - - public func packetListReceived( - _ packets: [MIDIPacketData] - ) { - for midiPacket in packets { - let events = midi1Parser.parsedEvents(in: midiPacket) - guard !events.isEmpty else { continue } - handle(events: events) - } - } - - @available(macOS 11, iOS 14, macCatalyst 14, *) - public func eventListReceived( - _ packets: [UniversalMIDIPacketData], - protocol midiProtocol: MIDIProtocolVersion - ) { - if let parser = midi2Parser { - for midiPacket in packets { - let events = parser.parsedEvents(in: midiPacket) - guard !events.isEmpty else { continue } - handle(events: events) - } - } else if let parser = advancedMIDI2Parser { - for midiPacket in packets { - parser.parseEvents(in: midiPacket) - } - } - } + final class StrongEventsReceiver: EventsBase { + public var receiver: ReceivesMIDIEvents init( options: MIDIReceiverOptions, receiver: ReceivesMIDIEvents ) { - self.options = options self.receiver = receiver - // MIDI 1 - - midi1Parser = MIDI1Parser() - - midi1Parser.translateNoteOnZeroVelocityToNoteOff = options - .contains(.translateMIDI1NoteOnZeroVelocityToNoteOff) - - // MIDI 2 - - if options.contains(.bundleRPNAndNRPNDataEntryLSB) { - advancedMIDI2Parser = AdvancedMIDI2Parser { [weak self] events, _, _ in - self?.handle(events: events) - } - } else { - midi2Parser = MIDI2Parser() - } + super.init(options: options) } - func handle(events: [MIDIEvent]) { + override func handle( + events: [MIDIEvent], + timeStamp: CoreMIDITimeStamp, + source: MIDIOutputEndpoint? + ) { if options.contains(.filterActiveSensingAndClock) { let filtered = events.filter(sysRealTime: .dropTypes([.activeSensing, .timingClock])) guard !filtered.isEmpty else { return } diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift index f68ef7e1a7..f5d88868c3 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift @@ -9,70 +9,23 @@ extension MIDIReceiveHandler { /// MIDI Event receive handler that holds a weak reference to a receiver object that conforms /// to the ``ReceivesMIDIEvents`` protocol. - final class WeakEventsReceiver: MIDIReceiveHandlerProtocol { + final class WeakEventsReceiver: EventsBase { public weak var receiver: ReceivesMIDIEvents? - let midi1Parser: MIDI1Parser - - var midi2Parser: MIDI2Parser? = nil - var advancedMIDI2Parser: AdvancedMIDI2Parser? = nil - - let options: MIDIReceiverOptions - - public func packetListReceived( - _ packets: [MIDIPacketData] - ) { - for midiPacket in packets { - let events = midi1Parser.parsedEvents(in: midiPacket) - guard !events.isEmpty else { continue } - handle(events: events) - } - } - - @available(macOS 11, iOS 14, macCatalyst 14, *) - public func eventListReceived( - _ packets: [UniversalMIDIPacketData], - protocol midiProtocol: MIDIProtocolVersion - ) { - if let parser = midi2Parser { - for midiPacket in packets { - let events = parser.parsedEvents(in: midiPacket) - guard !events.isEmpty else { continue } - handle(events: events) - } - } else if let parser = advancedMIDI2Parser { - for midiPacket in packets { - parser.parseEvents(in: midiPacket) - } - } - } - init( options: MIDIReceiverOptions, receiver: ReceivesMIDIEvents ) { - self.options = options self.receiver = receiver - // MIDI 1 - - midi1Parser = MIDI1Parser() - - midi1Parser.translateNoteOnZeroVelocityToNoteOff = options - .contains(.translateMIDI1NoteOnZeroVelocityToNoteOff) - - // MIDI 2 - - if options.contains(.bundleRPNAndNRPNDataEntryLSB) { - advancedMIDI2Parser = AdvancedMIDI2Parser { [weak self] events, _, _ in - self?.handle(events: events) - } - } else { - midi2Parser = MIDI2Parser() - } + super.init(options: options) } - func handle(events: [MIDIEvent]) { + override func handle( + events: [MIDIEvent], + timeStamp: CoreMIDITimeStamp, + source: MIDIOutputEndpoint? + ) { if options.contains(.filterActiveSensingAndClock) { let filtered = events.filter(sysRealTime: .dropTypes([.activeSensing, .timingClock])) guard !filtered.isEmpty else { return } From 5dab85055d86ff3ee7745f2958ab0aab5e32929b Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Tue, 26 Dec 2023 17:06:02 -0800 Subject: [PATCH 14/22] `MIDIReceiver`: Access levels --- Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift | 2 +- .../MIDIKitIO/MIDIReceiveHandler/Handlers/EventsBase.swift | 4 ++-- Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Group.swift | 6 +++--- Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawData.swift | 6 +++--- .../MIDIReceiveHandler/Handlers/StrongEventsReceiver.swift | 2 +- .../MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift index 7784bd0035..5f4ba350a8 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift @@ -22,7 +22,7 @@ extension MIDIReceiveHandler { /// Source endpoint is only available when used with ``MIDIInputConnection`` and will always be /// `nil` when used with ``MIDIInput``. final class Events: EventsBase { - public var handler: MIDIReceiver.EventsHandler + var handler: MIDIReceiver.EventsHandler init( options: MIDIReceiverOptions, diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsBase.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsBase.swift index 66aa2ae0a8..785fd5f85d 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsBase.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsBase.swift @@ -16,7 +16,7 @@ extension MIDIReceiveHandler { let options: MIDIReceiverOptions - public func packetListReceived( + func packetListReceived( _ packets: [MIDIPacketData] ) { for midiPacket in packets { @@ -28,7 +28,7 @@ extension MIDIReceiveHandler { } @available(macOS 11, iOS 14, macCatalyst 14, *) - public func eventListReceived( + func eventListReceived( _ packets: [UniversalMIDIPacketData], protocol midiProtocol: MIDIProtocolVersion ) { diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Group.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Group.swift index dd25c4552a..1ec4d33aa2 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Group.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Group.swift @@ -10,9 +10,9 @@ extension MIDIReceiveHandler { /// ``MIDIReceiveHandler`` group. /// Can contain one or more ``MIDIReceiveHandler`` in series. final class Group: MIDIReceiveHandlerProtocol { - public var receiveHandlers: [MIDIReceiveHandler] = [] + var receiveHandlers: [MIDIReceiveHandler] = [] - public func packetListReceived( + func packetListReceived( _ packets: [MIDIPacketData] ) { for handler in receiveHandlers { @@ -21,7 +21,7 @@ extension MIDIReceiveHandler { } @available(macOS 11, iOS 14, macCatalyst 14, *) - public func eventListReceived( + func eventListReceived( _ packets: [UniversalMIDIPacketData], protocol midiProtocol: MIDIProtocolVersion ) { diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawData.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawData.swift index 0175a24c88..609cd66eeb 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawData.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawData.swift @@ -16,9 +16,9 @@ extension MIDIReceiveHandler { /// This handler is provided for debugging and data introspection but is discouraged for /// manually parsing MIDI packets. It is recommended to use a MIDI event handler instead. final class RawData: MIDIReceiveHandlerProtocol { - public var handler: MIDIReceiver.RawDataHandler + var handler: MIDIReceiver.RawDataHandler - public func packetListReceived( + func packetListReceived( _ packets: [MIDIPacketData] ) { for midiPacket in packets { @@ -28,7 +28,7 @@ extension MIDIReceiveHandler { } @available(macOS 11, iOS 14, macCatalyst 14, *) - public func eventListReceived( + func eventListReceived( _ packets: [UniversalMIDIPacketData], protocol midiProtocol: MIDIProtocolVersion ) { diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/StrongEventsReceiver.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/StrongEventsReceiver.swift index 3238cf6d88..7313b2d3b4 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/StrongEventsReceiver.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/StrongEventsReceiver.swift @@ -10,7 +10,7 @@ extension MIDIReceiveHandler { /// MIDI Event receive handler that holds a strong reference to a receiver object that conforms /// to the ``ReceivesMIDIEvents`` protocol. final class StrongEventsReceiver: EventsBase { - public var receiver: ReceivesMIDIEvents + var receiver: ReceivesMIDIEvents init( options: MIDIReceiverOptions, diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift index f5d88868c3..c6a1f84688 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift @@ -10,7 +10,7 @@ extension MIDIReceiveHandler { /// MIDI Event receive handler that holds a weak reference to a receiver object that conforms /// to the ``ReceivesMIDIEvents`` protocol. final class WeakEventsReceiver: EventsBase { - public weak var receiver: ReceivesMIDIEvents? + weak var receiver: ReceivesMIDIEvents? init( options: MIDIReceiverOptions, From 55da5874bf68babc723f56070745e1252c906cf9 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Tue, 26 Dec 2023 17:18:54 -0800 Subject: [PATCH 15/22] Removed redundant `MIDIReceiveHandler` class, renamed `MIDIReceiveHandlerProtocol` to `MIDIReceiverProtocol` and made it public --- .../API Evolution/MIDIKit-0.7.1.swift | 2 +- .../API Evolution/MIDIKit-0.9.5.swift | 8 ++++ Sources/MIDIKitIO/MIDIInput/MIDIInput.swift | 2 +- .../MIDIInputConnection.swift | 2 +- .../MIDIReceiveHandler/Handlers/Events.swift | 4 +- .../Handlers/EventsBase.swift | 4 +- .../Handlers/EventsLogging.swift | 12 +++--- .../MIDIReceiveHandler/Handlers/Group.swift | 10 ++--- .../MIDIReceiveHandler/Handlers/RawData.swift | 8 ++-- .../Handlers/RawDataLogging.swift | 12 +++--- .../Handlers/StrongEventsReceiver.swift | 2 +- .../Handlers/WeakEventsReceiver.swift | 2 +- .../MIDIReceiveHandler.swift | 40 ------------------- .../MIDIReceiveHandler/MIDIReceiver.swift | 20 ++++------ ...tocol.swift => MIDIReceiverProtocol.swift} | 4 +- 15 files changed, 44 insertions(+), 88 deletions(-) delete mode 100644 Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiveHandler.swift rename Sources/MIDIKitIO/MIDIReceiveHandler/{MIDIReceiveHandlerProtocol.swift => MIDIReceiverProtocol.swift} (90%) diff --git a/Sources/MIDIKitIO/API Evolution/MIDIKit-0.7.1.swift b/Sources/MIDIKitIO/API Evolution/MIDIKit-0.7.1.swift index ee3fb8596d..1f796a564b 100644 --- a/Sources/MIDIKitIO/API Evolution/MIDIKit-0.7.1.swift +++ b/Sources/MIDIKitIO/API Evolution/MIDIKit-0.7.1.swift @@ -316,7 +316,7 @@ public protocol MIDIIOEndpointsProtocol { } @available(*, unavailable, renamed: "MIDIEndpoint") public protocol MIDIIOEndpointProtocol { } -@available(*, unavailable, renamed: "MIDIReceiveHandlerProtocol") +@available(*, unavailable, renamed: "MIDIReceiverProtocol") public protocol MIDIIOReceiveHandlerProtocol { } #endif diff --git a/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.5.swift b/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.5.swift index b21693cbf7..27cd9cd47e 100644 --- a/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.5.swift +++ b/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.5.swift @@ -40,4 +40,12 @@ extension MIDIReceiver { } } +@available( + *, + unavailable, + renamed: "MIDIReceiverProtocol", + message: "`MIDIReceiveHandler` class has been removed as it was redundant. Please use the `MIDIReceiverProtocol` protocol instead or consider using a built-in receiver." +) +public typealias MIDIReceiveHandler = MIDIReceiverProtocol + #endif diff --git a/Sources/MIDIKitIO/MIDIInput/MIDIInput.swift b/Sources/MIDIKitIO/MIDIInput/MIDIInput.swift index 8703b08c53..ea15325693 100644 --- a/Sources/MIDIKitIO/MIDIInput/MIDIInput.swift +++ b/Sources/MIDIKitIO/MIDIInput/MIDIInput.swift @@ -52,7 +52,7 @@ public final class MIDIInput: _MIDIManaged { public private(set) var coreMIDIInputPortRef: CoreMIDIPortRef? /// Receive handler for inbound MIDI events. - var receiveHandler: MIDIReceiveHandler + var receiveHandler: MIDIReceiverProtocol // init diff --git a/Sources/MIDIKitIO/MIDIInputConnection/MIDIInputConnection.swift b/Sources/MIDIKitIO/MIDIInputConnection/MIDIInputConnection.swift index bbb0fc1074..3e08156ef6 100644 --- a/Sources/MIDIKitIO/MIDIInputConnection/MIDIInputConnection.swift +++ b/Sources/MIDIKitIO/MIDIInputConnection/MIDIInputConnection.swift @@ -108,7 +108,7 @@ public final class MIDIInputConnection: _MIDIManaged { } /// Receive handler for inbound MIDI events. - var receiveHandler: MIDIReceiveHandler + var receiveHandler: MIDIReceiverProtocol // init diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift index 5f4ba350a8..b1db1b6464 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift @@ -15,9 +15,7 @@ extension MIDIReceiver { _ timeStamp: CoreMIDITimeStamp, _ source: MIDIOutputEndpoint? ) -> Void -} - -extension MIDIReceiveHandler { + /// MIDI Event receive handler including packet timestamp and source endpoint. /// Source endpoint is only available when used with ``MIDIInputConnection`` and will always be /// `nil` when used with ``MIDIInput``. diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsBase.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsBase.swift index 785fd5f85d..dbbcba36b2 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsBase.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsBase.swift @@ -6,9 +6,9 @@ #if !os(tvOS) && !os(watchOS) -extension MIDIReceiveHandler { +extension MIDIReceiver { /// Foundational receiver class which other events receivers subclass. - class EventsBase: MIDIReceiveHandlerProtocol { + class EventsBase: MIDIReceiverProtocol { let midi1Parser: MIDI1Parser var midi2Parser: MIDI2Parser? = nil diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsLogging.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsLogging.swift index db21b40b9a..1832eaf46f 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsLogging.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/EventsLogging.swift @@ -11,9 +11,7 @@ import os.log extension MIDIReceiver { /// Handler for the ``eventsLogging(options:_:)`` MIDI receiver. public typealias EventsLoggingHandler = (_ eventString: String) -> Void -} - -extension MIDIReceiveHandler { + /// MIDI Event logging handler (event description strings). /// If `handler` is nil, all events are logged to the console (but only in `DEBUG` preprocessor /// flag builds). @@ -22,9 +20,9 @@ extension MIDIReceiveHandler { static func _eventsLogging( options: MIDIReceiverOptions, log: OSLog = .default, - handler: MIDIReceiver.EventsLoggingHandler? - ) -> MIDIReceiveHandler.Events { - let stringLogHandler: MIDIReceiver.EventsLoggingHandler = handler + handler: EventsLoggingHandler? + ) -> Events { + let stringLogHandler: EventsLoggingHandler = handler ?? { packetBytesString in #if DEBUG os_log( @@ -36,7 +34,7 @@ extension MIDIReceiveHandler { #endif } - return MIDIReceiveHandler.Events(options: options) { events, timeStamp, source in + return Events(options: options) { events, timeStamp, source in let logString = generateLogString( events: events, timeStamp: timeStamp, diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Group.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Group.swift index 1ec4d33aa2..b75140e12d 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Group.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Group.swift @@ -6,11 +6,11 @@ #if !os(tvOS) && !os(watchOS) -extension MIDIReceiveHandler { +extension MIDIReceiver { /// ``MIDIReceiveHandler`` group. - /// Can contain one or more ``MIDIReceiveHandler`` in series. - final class Group: MIDIReceiveHandlerProtocol { - var receiveHandlers: [MIDIReceiveHandler] = [] + /// Can contain one or more ``MIDIReceiver`` in series. + final class Group: MIDIReceiverProtocol { + var receiveHandlers: [MIDIReceiverProtocol] = [] func packetListReceived( _ packets: [MIDIPacketData] @@ -30,7 +30,7 @@ extension MIDIReceiveHandler { } } - init(_ receiveHandlers: [MIDIReceiveHandler]) { + init(_ receiveHandlers: [MIDIReceiverProtocol]) { self.receiveHandlers = receiveHandlers } } diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawData.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawData.swift index 609cd66eeb..9740adb765 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawData.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawData.swift @@ -9,14 +9,12 @@ extension MIDIReceiver { /// Handler for the ``rawData(_:)`` MIDI receiver. public typealias RawDataHandler = (_ packet: AnyMIDIPacket) -> Void -} - -extension MIDIReceiveHandler { + /// Raw packet data receive handler. /// This handler is provided for debugging and data introspection but is discouraged for /// manually parsing MIDI packets. It is recommended to use a MIDI event handler instead. - final class RawData: MIDIReceiveHandlerProtocol { - var handler: MIDIReceiver.RawDataHandler + final class RawData: MIDIReceiverProtocol { + var handler: RawDataHandler func packetListReceived( _ packets: [MIDIPacketData] diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawDataLogging.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawDataLogging.swift index 3f704c117d..7826af147a 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawDataLogging.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/RawDataLogging.swift @@ -12,9 +12,7 @@ import os.log extension MIDIReceiver { /// Handler for the ``rawDataLogging(_:)`` MIDI receiver. public typealias RawDataLoggingHandler = (_ packetBytesString: String) -> Void -} - -extension MIDIReceiveHandler { + /// Raw data logging handler (hex byte strings). /// On systems that use legacy MIDI 1.0 packets, their raw bytes will be logged. /// On systems that support UMP and MIDI 2.0, the raw UMP packet data is logged. @@ -25,9 +23,9 @@ extension MIDIReceiveHandler { /// automatically logged. static func _rawDataLogging( log: OSLog = .default, - handler: MIDIReceiver.RawDataLoggingHandler? - ) -> MIDIReceiveHandler.RawData { - let stringLogHandler: MIDIReceiver.RawDataLoggingHandler = handler ?? { packetBytesString in + handler: RawDataLoggingHandler? + ) -> RawData { + let stringLogHandler: RawDataLoggingHandler = handler ?? { packetBytesString in #if DEBUG os_log( "%{public}@", @@ -38,7 +36,7 @@ extension MIDIReceiveHandler { #endif } - return MIDIReceiveHandler.RawData { packet in + return RawData { packet in let logString = generateLogString( bytes: packet.bytes, timeStamp: packet.timeStamp, diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/StrongEventsReceiver.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/StrongEventsReceiver.swift index 7313b2d3b4..d83dc53eee 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/StrongEventsReceiver.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/StrongEventsReceiver.swift @@ -6,7 +6,7 @@ #if !os(tvOS) && !os(watchOS) -extension MIDIReceiveHandler { +extension MIDIReceiver { /// MIDI Event receive handler that holds a strong reference to a receiver object that conforms /// to the ``ReceivesMIDIEvents`` protocol. final class StrongEventsReceiver: EventsBase { diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift index c6a1f84688..2bac68b761 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/WeakEventsReceiver.swift @@ -6,7 +6,7 @@ #if !os(tvOS) && !os(watchOS) -extension MIDIReceiveHandler { +extension MIDIReceiver { /// MIDI Event receive handler that holds a weak reference to a receiver object that conforms /// to the ``ReceivesMIDIEvents`` protocol. final class WeakEventsReceiver: EventsBase { diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiveHandler.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiveHandler.swift deleted file mode 100644 index dee19a3c35..0000000000 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiveHandler.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// MIDIReceiveHandler.swift -// MIDIKit • https://github.com/orchetect/MIDIKit -// © 2021-2023 Steffan Andrews • Licensed under MIT License -// - -#if !os(tvOS) && !os(watchOS) - -import Foundation - -// MARK: - ReceiveHandler - -/// Wrapper for MIDI receive handlers. -public class MIDIReceiveHandler: MIDIReceiveHandlerProtocol { - /// The specialized MIDI receiver instance. - var handler: MIDIReceiveHandlerProtocol - - /// Parses a MIDI Packet (MIDI 1.0, legacy Core MIDI API) and passes parsed data to the handler. - func packetListReceived( - _ packets: [MIDIPacketData] - ) { - handler.packetListReceived(packets) - } - - /// Parses a Universal MIDI Packet (UMP; MIDI 2.0, new Core MIDI API) and passes parsed data to - /// the handler. - @available(macOS 11, iOS 14, macCatalyst 14, *) - func eventListReceived( - _ packets: [UniversalMIDIPacketData], - protocol midiProtocol: MIDIProtocolVersion - ) { - handler.eventListReceived(packets, protocol: midiProtocol) - } - - init(_ handler: MIDIReceiveHandlerProtocol) { - self.handler = handler - } -} - -#endif diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiver.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiver.swift index 24f045ae77..483491e4c0 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiver.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiver.swift @@ -73,35 +73,31 @@ extension MIDIReceiver { /// /// This is only useful for custom implementations. Do not call this method when supplying a /// ``MIDIReceiver`` to the ``MIDIManager``. - public func create() -> MIDIReceiveHandler { - MIDIReceiveHandler(createInternalHandler()) - } - - private func createInternalHandler() -> MIDIReceiveHandlerProtocol { + public func create() -> MIDIReceiverProtocol { switch self { case let .group(definitions): let handlers = definitions.map { $0.create() } - return MIDIReceiveHandler.Group(handlers) + return Group(handlers) case let .events(options, handler): - return MIDIReceiveHandler.Events(options: options, handler: handler) + return Events(options: options, handler: handler) case let .eventsLogging(options, handler): - return MIDIReceiveHandler._eventsLogging(options: options, handler: handler) + return Self._eventsLogging(options: options, handler: handler) case let .rawData(handler): - return MIDIReceiveHandler(MIDIReceiveHandler.RawData(handler: handler)) + return RawData(handler: handler) case let .rawDataLogging(handler): - return MIDIReceiveHandler._rawDataLogging(handler: handler) + return Self._rawDataLogging(handler: handler) case let .object(object, storageType, options): switch storageType { case .strongly: - return MIDIReceiveHandler.StrongEventsReceiver(options: options, receiver: object) + return StrongEventsReceiver(options: options, receiver: object) case .weakly: - return MIDIReceiveHandler.WeakEventsReceiver(options: options, receiver: object) + return WeakEventsReceiver(options: options, receiver: object) } } } diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiveHandlerProtocol.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiverProtocol.swift similarity index 90% rename from Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiveHandlerProtocol.swift rename to Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiverProtocol.swift index 622f0d0df2..7c53b93660 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiveHandlerProtocol.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiverProtocol.swift @@ -1,5 +1,5 @@ // -// MIDIReceiveHandlerProtocol.swift +// MIDIReceiverProtocol.swift // MIDIKit • https://github.com/orchetect/MIDIKit // © 2021-2023 Steffan Andrews • Licensed under MIT License // @@ -11,7 +11,7 @@ /// For backwards compatibility with older operating systems, /// both `MIDIReadBlock` (old Core MIDI API) /// and `MIDIReceiveBlock` (new Core MIDI API) must be handled. -protocol MIDIReceiveHandlerProtocol { +public protocol MIDIReceiverProtocol: AnyObject { /// CoreMIDI `MIDIReadBlock` /// (deprecated after macOS 11 / iOS 14) func packetListReceived( From a715deae1715e01141adc4c0d9407ecfce10e0fe Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Tue, 26 Dec 2023 17:32:01 -0800 Subject: [PATCH 16/22] Updated docs --- .../MIDIKit/MIDIKit.docc/MIDIKitCore/MIDIKitCore-Internals.md | 1 + Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Internals.md | 3 +++ .../MIDIKit.docc/MIDIKitIO/MIDIKitIO-Receiving-MIDI-Events.md | 4 ++-- Sources/MIDIKitCore/MIDIKitCore.docc/MIDIKitCore-Internals.md | 1 + Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Internals.md | 2 ++ .../MIDIKitIO.docc/MIDIKitIO-Receiving-MIDI-Events.md | 4 ++-- Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift | 2 +- 7 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Sources/MIDIKit/MIDIKit.docc/MIDIKitCore/MIDIKitCore-Internals.md b/Sources/MIDIKit/MIDIKit.docc/MIDIKitCore/MIDIKitCore-Internals.md index 91bfe660d8..7546a235fc 100644 --- a/Sources/MIDIKit/MIDIKit.docc/MIDIKitCore/MIDIKitCore-Internals.md +++ b/Sources/MIDIKit/MIDIKit.docc/MIDIKitCore/MIDIKitCore-Internals.md @@ -10,6 +10,7 @@ - ``MIDIUMPUtilityStatusField`` - ``MIDIUMPMixedDataSetStatusField`` - ``MIDIParameterNumber`` +- ``MIDIParameterNumberEvent`` ### Value Type Protocols diff --git a/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Internals.md b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Internals.md index 7fd2bf70de..950c99e986 100644 --- a/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Internals.md +++ b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Internals.md @@ -19,6 +19,7 @@ - ``MIDIInputConnectionMode`` - ``MIDIOutputConnectionMode`` +- ``MIDIConnectionMode`` - ``MIDIEndpointType`` ### MIDI Packets and Parsing @@ -32,6 +33,8 @@ - ``AnyMIDIPacket`` - ``MIDI1Parser`` - ``MIDI2Parser`` +- ``AdvancedMIDI2Parser`` +- ``ParameterNumberEventBundler`` - ``MIDIProtocolVersion`` ### Core MIDI Related diff --git a/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Receiving-MIDI-Events.md b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Receiving-MIDI-Events.md index f2df145575..2966b6794a 100644 --- a/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Receiving-MIDI-Events.md +++ b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIKitIO-Receiving-MIDI-Events.md @@ -18,6 +18,6 @@ In order to begin receiving MIDI events, there are two primary mechanisms: - ``ReceivesMIDIEvents`` -### Internals +### Internal Protocols -- ``MIDIReceiveHandler`` +- ``MIDIReceiverProtocol`` diff --git a/Sources/MIDIKitCore/MIDIKitCore.docc/MIDIKitCore-Internals.md b/Sources/MIDIKitCore/MIDIKitCore.docc/MIDIKitCore-Internals.md index 91bfe660d8..7546a235fc 100644 --- a/Sources/MIDIKitCore/MIDIKitCore.docc/MIDIKitCore-Internals.md +++ b/Sources/MIDIKitCore/MIDIKitCore.docc/MIDIKitCore-Internals.md @@ -10,6 +10,7 @@ - ``MIDIUMPUtilityStatusField`` - ``MIDIUMPMixedDataSetStatusField`` - ``MIDIParameterNumber`` +- ``MIDIParameterNumberEvent`` ### Value Type Protocols diff --git a/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Internals.md b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Internals.md index 7fd2bf70de..2473736ddb 100644 --- a/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Internals.md +++ b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Internals.md @@ -32,6 +32,8 @@ - ``AnyMIDIPacket`` - ``MIDI1Parser`` - ``MIDI2Parser`` +- ``AdvancedMIDI2Parser`` +- ``ParameterNumberEventBundler`` - ``MIDIProtocolVersion`` ### Core MIDI Related diff --git a/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Receiving-MIDI-Events.md b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Receiving-MIDI-Events.md index f2df145575..2966b6794a 100644 --- a/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Receiving-MIDI-Events.md +++ b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Receiving-MIDI-Events.md @@ -18,6 +18,6 @@ In order to begin receiving MIDI events, there are two primary mechanisms: - ``ReceivesMIDIEvents`` -### Internals +### Internal Protocols -- ``MIDIReceiveHandler`` +- ``MIDIReceiverProtocol`` diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift index b1db1b6464..4e42324829 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/Handlers/Events.swift @@ -7,7 +7,7 @@ #if !os(tvOS) && !os(watchOS) extension MIDIReceiver { - /// Handler for the ``events(options:_:)`` MIDI receiver. + /// Handler for events-based MIDI receivers. /// Source endpoint is only available when used with ``MIDIInputConnection`` and will always be /// `nil` when used with ``MIDIInput``. public typealias EventsHandler = ( From 1ae7766cf65f67652d489a21e57ea8c8b116b2d6 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Tue, 26 Dec 2023 17:53:31 -0800 Subject: [PATCH 17/22] Updated docs --- .../MIDIKitControlSurfaces.docc/Internals-From-MIDIKitCore.md | 1 + Sources/MIDIKitIO/MIDIKitIO.docc/Internals-From-MIDIKitCore.md | 1 + Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Internals.md | 1 + Sources/MIDIKitSMF/MIDIKitSMF.docc/Internals-From-MIDIKitCore.md | 1 + .../MIDIKitSync/MIDIKitSync.docc/Internals-From-MIDIKitCore.md | 1 + Sources/MIDIKitUI/MIDIKitUI.docc/Internals-From-MIDIKitCore.md | 1 + 6 files changed, 6 insertions(+) diff --git a/Sources/MIDIKitControlSurfaces/MIDIKitControlSurfaces.docc/Internals-From-MIDIKitCore.md b/Sources/MIDIKitControlSurfaces/MIDIKitControlSurfaces.docc/Internals-From-MIDIKitCore.md index 152d5493c5..b94f35df2d 100644 --- a/Sources/MIDIKitControlSurfaces/MIDIKitControlSurfaces.docc/Internals-From-MIDIKitCore.md +++ b/Sources/MIDIKitControlSurfaces/MIDIKitControlSurfaces.docc/Internals-From-MIDIKitCore.md @@ -51,6 +51,7 @@ For more detailed documentation on these types, see MIDIKitCore documentation. - ``MIDIUMPUtilityStatusField`` - ``MIDIUMPMixedDataSetStatusField`` - ``MIDIParameterNumber`` +- ``MIDIParameterNumberEvent`` ### Value Type Protocols diff --git a/Sources/MIDIKitIO/MIDIKitIO.docc/Internals-From-MIDIKitCore.md b/Sources/MIDIKitIO/MIDIKitIO.docc/Internals-From-MIDIKitCore.md index 152d5493c5..b94f35df2d 100644 --- a/Sources/MIDIKitIO/MIDIKitIO.docc/Internals-From-MIDIKitCore.md +++ b/Sources/MIDIKitIO/MIDIKitIO.docc/Internals-From-MIDIKitCore.md @@ -51,6 +51,7 @@ For more detailed documentation on these types, see MIDIKitCore documentation. - ``MIDIUMPUtilityStatusField`` - ``MIDIUMPMixedDataSetStatusField`` - ``MIDIParameterNumber`` +- ``MIDIParameterNumberEvent`` ### Value Type Protocols diff --git a/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Internals.md b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Internals.md index 2473736ddb..950c99e986 100644 --- a/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Internals.md +++ b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIKitIO-Internals.md @@ -19,6 +19,7 @@ - ``MIDIInputConnectionMode`` - ``MIDIOutputConnectionMode`` +- ``MIDIConnectionMode`` - ``MIDIEndpointType`` ### MIDI Packets and Parsing diff --git a/Sources/MIDIKitSMF/MIDIKitSMF.docc/Internals-From-MIDIKitCore.md b/Sources/MIDIKitSMF/MIDIKitSMF.docc/Internals-From-MIDIKitCore.md index 152d5493c5..b94f35df2d 100644 --- a/Sources/MIDIKitSMF/MIDIKitSMF.docc/Internals-From-MIDIKitCore.md +++ b/Sources/MIDIKitSMF/MIDIKitSMF.docc/Internals-From-MIDIKitCore.md @@ -51,6 +51,7 @@ For more detailed documentation on these types, see MIDIKitCore documentation. - ``MIDIUMPUtilityStatusField`` - ``MIDIUMPMixedDataSetStatusField`` - ``MIDIParameterNumber`` +- ``MIDIParameterNumberEvent`` ### Value Type Protocols diff --git a/Sources/MIDIKitSync/MIDIKitSync.docc/Internals-From-MIDIKitCore.md b/Sources/MIDIKitSync/MIDIKitSync.docc/Internals-From-MIDIKitCore.md index 152d5493c5..b94f35df2d 100644 --- a/Sources/MIDIKitSync/MIDIKitSync.docc/Internals-From-MIDIKitCore.md +++ b/Sources/MIDIKitSync/MIDIKitSync.docc/Internals-From-MIDIKitCore.md @@ -51,6 +51,7 @@ For more detailed documentation on these types, see MIDIKitCore documentation. - ``MIDIUMPUtilityStatusField`` - ``MIDIUMPMixedDataSetStatusField`` - ``MIDIParameterNumber`` +- ``MIDIParameterNumberEvent`` ### Value Type Protocols diff --git a/Sources/MIDIKitUI/MIDIKitUI.docc/Internals-From-MIDIKitCore.md b/Sources/MIDIKitUI/MIDIKitUI.docc/Internals-From-MIDIKitCore.md index 152d5493c5..b94f35df2d 100644 --- a/Sources/MIDIKitUI/MIDIKitUI.docc/Internals-From-MIDIKitCore.md +++ b/Sources/MIDIKitUI/MIDIKitUI.docc/Internals-From-MIDIKitCore.md @@ -51,6 +51,7 @@ For more detailed documentation on these types, see MIDIKitCore documentation. - ``MIDIUMPUtilityStatusField`` - ``MIDIUMPMixedDataSetStatusField`` - ``MIDIParameterNumber`` +- ``MIDIParameterNumberEvent`` ### Value Type Protocols From 00838f2c6ea52abfdcc2aa030c16bc48c7c71d25 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Tue, 26 Dec 2023 17:56:35 -0800 Subject: [PATCH 18/22] Updated Example projects --- .../HUITest/HUITest/HUIHostView/HUIHostHelper.swift | 5 +---- .../HUITest/HUITest/HUISurfaceView/HUIClientView.swift | 2 +- .../MIDIEventLogger/MIDIEventLogger/MIDIHelper.swift | 5 ++++- .../MTCExample/MTCExample.xcodeproj/project.pbxproj | 2 +- .../EndpointMenus/MIDIEndpointsMenusHelper.swift | 5 ++++- Examples/AppKit/EventParsing/EventParsing/AppDelegate.swift | 2 +- Examples/AppKit/VirtualInput/VirtualInput/AppDelegate.swift | 5 ++++- .../EndpointPickers/EndpointPickers/MIDIHelper.swift | 6 +++--- .../EventParsing/EventParsing/MIDIHelper.swift | 2 +- .../SystemNotifications/MIDIHelper.swift | 2 +- .../VirtualInput/VirtualInput/MIDIHelper.swift | 5 ++++- .../BluetoothMIDI/BluetoothMIDI/MIDIHelper.swift | 5 ++++- .../USB iOS to Mac/USB iOS to Mac/MIDIHelper.swift | 5 ++++- Examples/UIKit/EventParsing/EventParsing/AppDelegate.swift | 2 +- Examples/UIKit/VirtualInput/VirtualInput/AppDelegate.swift | 5 ++++- 15 files changed, 38 insertions(+), 20 deletions(-) diff --git a/Examples/Advanced/HUITest/HUITest/HUIHostView/HUIHostHelper.swift b/Examples/Advanced/HUITest/HUITest/HUIHostView/HUIHostHelper.swift index 5631bdb7e9..7b4afa6b2c 100644 --- a/Examples/Advanced/HUITest/HUITest/HUIHostView/HUIHostHelper.swift +++ b/Examples/Advanced/HUITest/HUITest/HUIHostView/HUIHostHelper.swift @@ -34,10 +34,7 @@ class HUIHostHelper: ObservableObject { try midiManager.addInputConnection( to: .outputs(matching: [.name(HUIClientView.kHUIOutputName)]), tag: Self.kHUIInputConnectionTag, - receiver: .object( - huiHost.banks[0], - held: .weakly - ) + receiver: .object(huiHost.banks[0], held: .weakly) ) try midiManager.addOutputConnection( diff --git a/Examples/Advanced/HUITest/HUITest/HUISurfaceView/HUIClientView.swift b/Examples/Advanced/HUITest/HUITest/HUISurfaceView/HUIClientView.swift index 41c6764d6d..0458ed9ce3 100644 --- a/Examples/Advanced/HUITest/HUITest/HUISurfaceView/HUIClientView.swift +++ b/Examples/Advanced/HUITest/HUITest/HUISurfaceView/HUIClientView.swift @@ -53,7 +53,7 @@ struct HUIClientView: View { name: Self.kHUIInputName, tag: Self.kHUIInputName, uniqueID: .userDefaultsManaged(key: Self.kHUIInputName), - receiver: .events { [weak huiSurface] events in + receiver: .events { [weak huiSurface] events, timeStamp, source in // since handler callbacks from MIDI are on a CoreMIDI thread, // parse the MIDI on the main thread because SwiftUI state in // this app will be updated as a result diff --git a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/MIDIHelper.swift b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/MIDIHelper.swift index f1f556e268..9e042996a0 100644 --- a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/MIDIHelper.swift +++ b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/MIDIHelper.swift @@ -49,7 +49,10 @@ final class MIDIHelper: ObservableObject { try midiManager?.addInputConnection( to: .none, tag: ConnectionTags.inputConnectionTag, - receiver: .eventsLogging() + receiver: .eventsLogging(options: [ + .bundleRPNAndNRPNDataEntryLSB, + .filterActiveSensingAndClock + ]) ) } } catch { diff --git a/Examples/Advanced/MTCExample/MTCExample.xcodeproj/project.pbxproj b/Examples/Advanced/MTCExample/MTCExample.xcodeproj/project.pbxproj index 03f0c20ab5..97231feff1 100644 --- a/Examples/Advanced/MTCExample/MTCExample.xcodeproj/project.pbxproj +++ b/Examples/Advanced/MTCExample/MTCExample.xcodeproj/project.pbxproj @@ -432,7 +432,7 @@ repositoryURL = "https://github.com/orchetect/OTCore.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.4.13; + minimumVersion = 1.5.1; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/AppKit/EndpointMenus/EndpointMenus/MIDIEndpointsMenusHelper.swift b/Examples/AppKit/EndpointMenus/EndpointMenus/MIDIEndpointsMenusHelper.swift index 6e72565992..7665ba9a37 100644 --- a/Examples/AppKit/EndpointMenus/EndpointMenus/MIDIEndpointsMenusHelper.swift +++ b/Examples/AppKit/EndpointMenus/EndpointMenus/MIDIEndpointsMenusHelper.swift @@ -51,7 +51,10 @@ extension MIDIEndpointsMenusHelper { try midiManager.addInputConnection( to: .none, tag: ConnectionTags.midiIn, - receiver: .eventsLogging() + receiver: .eventsLogging(options: [ + .bundleRPNAndNRPNDataEntryLSB, + .filterActiveSensingAndClock + ]) ) // set up output connection diff --git a/Examples/AppKit/EventParsing/EventParsing/AppDelegate.swift b/Examples/AppKit/EventParsing/EventParsing/AppDelegate.swift index 6692ff4c57..43eb6df63d 100644 --- a/Examples/AppKit/EventParsing/EventParsing/AppDelegate.swift +++ b/Examples/AppKit/EventParsing/EventParsing/AppDelegate.swift @@ -32,7 +32,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { name: virtualInputName, tag: virtualInputName, uniqueID: .userDefaultsManaged(key: virtualInputName), - receiver: .events { [weak self] events in + receiver: .events { [weak self] events, timeStamp, source in DispatchQueue.main.async { events.forEach { self?.handleMIDI(event: $0) } } diff --git a/Examples/AppKit/VirtualInput/VirtualInput/AppDelegate.swift b/Examples/AppKit/VirtualInput/VirtualInput/AppDelegate.swift index f010d051b7..3f447c0b0b 100644 --- a/Examples/AppKit/VirtualInput/VirtualInput/AppDelegate.swift +++ b/Examples/AppKit/VirtualInput/VirtualInput/AppDelegate.swift @@ -31,7 +31,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { name: virtualInputName, tag: virtualInputName, uniqueID: .userDefaultsManaged(key: virtualInputName), - receiver: .eventsLogging(options: [.filterActiveSensingAndClock]) + receiver: .eventsLogging(options: [ + .bundleRPNAndNRPNDataEntryLSB, + .filterActiveSensingAndClock + ]) ) } catch { print("Error creating virtual MIDI input:", error.localizedDescription) diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/MIDIHelper.swift b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/MIDIHelper.swift index 5c091ac369..dc735efc58 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/MIDIHelper.swift +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers/MIDIHelper.swift @@ -46,7 +46,7 @@ final class MIDIHelper: ObservableObject { try midiManager.addInputConnection( to: .none, tag: Tags.midiIn, - receiver: .events { [weak self] events in + receiver: .events { [weak self] events, timeStamp, source in self?.received(events: events) } ) @@ -109,7 +109,7 @@ final class MIDIHelper: ObservableObject { name: "Test In 1", tag: Tags.midiTestIn1, uniqueID: .userDefaultsManaged(key: Tags.midiTestIn1), - receiver: .events { [weak self] events in + receiver: .events { [weak self] events, timeStamp, source in self?.received(events: events) } ) @@ -118,7 +118,7 @@ final class MIDIHelper: ObservableObject { name: "Test In 2", tag: Tags.midiTestIn2, uniqueID: .userDefaultsManaged(key: Tags.midiTestIn2), - receiver: .events { [weak self] events in + receiver: .events { [weak self] events, timeStamp, source in self?.received(events: events) } ) diff --git a/Examples/SwiftUI Multiplatform/EventParsing/EventParsing/MIDIHelper.swift b/Examples/SwiftUI Multiplatform/EventParsing/EventParsing/MIDIHelper.swift index 1784d0ecb6..640d322e26 100644 --- a/Examples/SwiftUI Multiplatform/EventParsing/EventParsing/MIDIHelper.swift +++ b/Examples/SwiftUI Multiplatform/EventParsing/EventParsing/MIDIHelper.swift @@ -34,7 +34,7 @@ final class MIDIHelper: ObservableObject { name: virtualInputName, tag: virtualInputName, uniqueID: .userDefaultsManaged(key: virtualInputName), - receiver: .events { [weak self] events in + receiver: .events { [weak self] events, timeStamp, source in events.forEach { self?.received(event: $0) } } ) diff --git a/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications/MIDIHelper.swift b/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications/MIDIHelper.swift index de7d05d85e..44b9b3aedc 100644 --- a/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications/MIDIHelper.swift +++ b/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications/MIDIHelper.swift @@ -40,7 +40,7 @@ final class MIDIHelper: ObservableObject { name: name, tag: name, uniqueID: .adHoc, - receiver: .events { _ in } + receiver: .events { _, _, _ in } ) } diff --git a/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput/MIDIHelper.swift b/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput/MIDIHelper.swift index 0eefae0005..9ab134ad55 100644 --- a/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput/MIDIHelper.swift +++ b/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput/MIDIHelper.swift @@ -40,7 +40,10 @@ final class MIDIHelper: ObservableObject { name: Self.virtualInputName, tag: Self.virtualInputName, uniqueID: .userDefaultsManaged(key: Self.virtualInputName), - receiver: .eventsLogging(options: [.filterActiveSensingAndClock]) + receiver: .eventsLogging(options: [ + .bundleRPNAndNRPNDataEntryLSB, + .filterActiveSensingAndClock + ]) ) } catch { print("Error creating virtual MIDI input:", error.localizedDescription) diff --git a/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI/MIDIHelper.swift b/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI/MIDIHelper.swift index 8a08c2b726..c12054ab6d 100644 --- a/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI/MIDIHelper.swift +++ b/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI/MIDIHelper.swift @@ -41,7 +41,10 @@ final class MIDIHelper: ObservableObject { to: .allOutputs, // auto-connect to all outputs that may appear tag: "Listener", filter: .owned(), // don't allow self-created virtual endpoints - receiver: .eventsLogging() + receiver: .eventsLogging(options: [ + .bundleRPNAndNRPNDataEntryLSB, + .filterActiveSensingAndClock + ]) ) } catch { print( diff --git a/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac/MIDIHelper.swift b/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac/MIDIHelper.swift index 1b07085b24..c358d9992d 100644 --- a/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac/MIDIHelper.swift +++ b/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac/MIDIHelper.swift @@ -45,7 +45,10 @@ final class MIDIHelper: ObservableObject { try midiManager.addInputConnection( to: .outputs(matching: [.name("IDAM MIDI Host")]), tag: Self.inputConnectionName, - receiver: .eventsLogging() + receiver: .eventsLogging(options: [ + .bundleRPNAndNRPNDataEntryLSB, + .filterActiveSensingAndClock + ]) ) print("Creating MIDI output connection.") diff --git a/Examples/UIKit/EventParsing/EventParsing/AppDelegate.swift b/Examples/UIKit/EventParsing/EventParsing/AppDelegate.swift index 4bfcb1614e..b6cb6009f0 100644 --- a/Examples/UIKit/EventParsing/EventParsing/AppDelegate.swift +++ b/Examples/UIKit/EventParsing/EventParsing/AppDelegate.swift @@ -37,7 +37,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { name: virtualInputName, tag: virtualInputName, uniqueID: .userDefaultsManaged(key: virtualInputName), - receiver: .events { [weak self] events in + receiver: .events { [weak self] events, timeStamp, source in events.forEach { self?.handleMIDI(event: $0) } } ) diff --git a/Examples/UIKit/VirtualInput/VirtualInput/AppDelegate.swift b/Examples/UIKit/VirtualInput/VirtualInput/AppDelegate.swift index 8eadc00f9f..b270db084c 100644 --- a/Examples/UIKit/VirtualInput/VirtualInput/AppDelegate.swift +++ b/Examples/UIKit/VirtualInput/VirtualInput/AppDelegate.swift @@ -36,7 +36,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { name: virtualInputName, tag: virtualInputName, uniqueID: .userDefaultsManaged(key: virtualInputName), - receiver: .eventsLogging(options: [.filterActiveSensingAndClock]) + receiver: .eventsLogging(options: [ + .bundleRPNAndNRPNDataEntryLSB, + .filterActiveSensingAndClock + ]) ) } catch { print("Error creating virtual MIDI input:", error.localizedDescription) From b64ee3bb45157746c724ee827e982a29ea78668c Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Tue, 26 Dec 2023 19:12:30 -0800 Subject: [PATCH 19/22] `MIDIReceiver`: `object(_:held:options:)` replaced by `strong(_:options:)` or `weak(_:options:)` --- .../HUITest/HUIHostView/HUIHostHelper.swift | 2 +- .../MTCExample/MTCRecContentView.swift | 4 +-- .../API Evolution/MIDIKit-0.9.3.swift | 2 +- .../API Evolution/MIDIKit-0.9.5.swift | 19 ++++++++++++ .../MIDIReceiveHandler/MIDIReceiver.swift | 31 +++++++++++-------- 5 files changed, 41 insertions(+), 17 deletions(-) diff --git a/Examples/Advanced/HUITest/HUITest/HUIHostView/HUIHostHelper.swift b/Examples/Advanced/HUITest/HUITest/HUIHostView/HUIHostHelper.swift index 7b4afa6b2c..62cade6f82 100644 --- a/Examples/Advanced/HUITest/HUITest/HUIHostView/HUIHostHelper.swift +++ b/Examples/Advanced/HUITest/HUITest/HUIHostView/HUIHostHelper.swift @@ -34,7 +34,7 @@ class HUIHostHelper: ObservableObject { try midiManager.addInputConnection( to: .outputs(matching: [.name(HUIClientView.kHUIOutputName)]), tag: Self.kHUIInputConnectionTag, - receiver: .object(huiHost.banks[0], held: .weakly) + receiver: .weak(huiHost.banks[0]) ) try midiManager.addOutputConnection( diff --git a/Examples/Advanced/MTCExample/MTCExample/MTCRecContentView.swift b/Examples/Advanced/MTCExample/MTCExample/MTCRecContentView.swift index 7856331dd3..9eb970e9a3 100644 --- a/Examples/Advanced/MTCExample/MTCExample/MTCRecContentView.swift +++ b/Examples/Advanced/MTCExample/MTCExample/MTCRecContentView.swift @@ -124,7 +124,7 @@ struct MTCRecContentView: View { name: kMIDIPorts.MTCRec.name, tag: kMIDIPorts.MTCRec.tag, uniqueID: .userDefaultsManaged(key: udKey), - receiver: .object(mtcRec, held: .weakly) + receiver: .weak(mtcRec) ) } catch { logger.error(error) @@ -291,7 +291,7 @@ struct MTCRecContentView: View { try? midiManager.addInputConnection( to: .outputs(matching: [.name(kMIDIPorts.MTCGen.name)]), tag: tag, - receiver: .object(mtcRec, held: .weakly) + receiver: .weak(mtcRec) ) case false: midiManager.remove(.inputConnection, .withTag(tag)) diff --git a/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.3.swift b/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.3.swift index b3b3554194..6b85972275 100644 --- a/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.3.swift +++ b/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.3.swift @@ -78,7 +78,7 @@ extension MIDIReceiver { *, deprecated, renamed: "object(_:held:options:)", - message: "`translateMIDI1NoteOnZeroVelocityToNoteOff` has been removed from rawDataLogging." + message: "`translateMIDI1NoteOnZeroVelocityToNoteOff` property is now an OptionSet flag. `object(_:held:translateMIDI1NoteOnZeroVelocityToNoteOff:)` is also now replaced by `strong(_:options:)` or `weak(_:options:)` and as such, no longer carries a `held` property." ) @_disfavoredOverload public static func object( diff --git a/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.5.swift b/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.5.swift index 27cd9cd47e..e1d1a1df1a 100644 --- a/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.5.swift +++ b/Sources/MIDIKitIO/API Evolution/MIDIKit-0.9.5.swift @@ -38,6 +38,25 @@ extension MIDIReceiver { ) -> Self { .events(options: options, handler) } + + @available( + *, + deprecated, + renamed: "events", + message: "`object(_:held:options:)` is now replaced with `strong(_:options:)` or `weak(_:options:)` and as such, no longer carries a `held` property." + ) + public static func object( + _ object: ReceivesMIDIEvents, + held: ReceiverRefStorage, + options: MIDIReceiverOptions = [] + ) -> Self { + switch held { + case .weakly: + return .weak(object, options: options) + case .strongly: + return .strong(object, options: options) + } + } } @available( diff --git a/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiver.swift b/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiver.swift index 483491e4c0..4b24aaff62 100644 --- a/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiver.swift +++ b/Sources/MIDIKitIO/MIDIReceiveHandler/MIDIReceiver.swift @@ -50,18 +50,26 @@ public enum MIDIReceiver { ) /// Pass to a receiver object instance. - /// MIDI Event receive handler that holds a reference to a receiver object that conforms to the + /// MIDI event receive handler that holds a reference to a receiver object that conforms to the /// ``ReceivesMIDIEvents`` protocol. - /// The object reference may be held strongly or weakly. - case object( + /// The object is stored as a strong reference. + case strong( + _ object: ReceivesMIDIEvents, + options: MIDIReceiverOptions = [] + ) + + /// Pass to a receiver object instance. + /// MIDI event receive handler that holds a reference to a receiver object that conforms to the + /// ``ReceivesMIDIEvents`` protocol. + /// The object is stored as a weak reference. + case weak( _ object: ReceivesMIDIEvents, - held: ReceiverRefStorage, options: MIDIReceiverOptions = [] ) } extension MIDIReceiver { - /// Class instance storage semantics. + /// Class reference storage semantics. public enum ReceiverRefStorage { case weakly case strongly @@ -91,14 +99,11 @@ extension MIDIReceiver { case let .rawDataLogging(handler): return Self._rawDataLogging(handler: handler) - case let .object(object, storageType, options): - switch storageType { - case .strongly: - return StrongEventsReceiver(options: options, receiver: object) - - case .weakly: - return WeakEventsReceiver(options: options, receiver: object) - } + case let .strong(object, options): + return StrongEventsReceiver(options: options, receiver: object) + + case let .weak(object, options): + return WeakEventsReceiver(options: options, receiver: object) } } } From e64a00bfecc8c559f74c6b52880d28a026b6c166 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Tue, 26 Dec 2023 19:22:37 -0800 Subject: [PATCH 20/22] Updated Example projects --- .../MIDIEventLogger/MIDIEventLogger/MIDIHelper.swift | 5 ++++- .../MIDIKitUIExample/MIDIKitUIExample/MIDIHelper.swift | 5 ++++- Examples/UIKit/BluetoothMIDI/BluetoothMIDI/AppDelegate.swift | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/MIDIHelper.swift b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/MIDIHelper.swift index 9e042996a0..1edcbc7d76 100644 --- a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/MIDIHelper.swift +++ b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger/MIDIHelper.swift @@ -92,7 +92,10 @@ final class MIDIHelper: ObservableObject { name: ConnectionTags.inputName, tag: ConnectionTags.inputTag, uniqueID: .userDefaultsManaged(key: ConnectionTags.inputTag), - receiver: .eventsLogging() + receiver: .eventsLogging(options: [ + .bundleRPNAndNRPNDataEntryLSB, + .filterActiveSensingAndClock + ]) ) } } catch { diff --git a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/MIDIHelper.swift b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/MIDIHelper.swift index 6d806eb61c..e95ae6a7e4 100644 --- a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/MIDIHelper.swift +++ b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample/MIDIHelper.swift @@ -54,7 +54,10 @@ final class MIDIHelper: ObservableObject { name: tag, tag: tag, uniqueID: .userDefaultsManaged(key: tag), - receiver: .eventsLogging { eventString in + receiver: .eventsLogging(options: [ + .bundleRPNAndNRPNDataEntryLSB, + .filterActiveSensingAndClock + ]) { eventString in print("Received on \(tag): \(eventString)") } ) diff --git a/Examples/UIKit/BluetoothMIDI/BluetoothMIDI/AppDelegate.swift b/Examples/UIKit/BluetoothMIDI/BluetoothMIDI/AppDelegate.swift index 83e9bfef27..ac32da5843 100644 --- a/Examples/UIKit/BluetoothMIDI/BluetoothMIDI/AppDelegate.swift +++ b/Examples/UIKit/BluetoothMIDI/BluetoothMIDI/AppDelegate.swift @@ -36,7 +36,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { to: .allOutputs, // auto-connect to all outputs that may appear tag: "Listener", filter: .owned(), // don't allow self-created virtual endpoints - receiver: .eventsLogging() + receiver: .eventsLogging(options: [ + .bundleRPNAndNRPNDataEntryLSB, + .filterActiveSensingAndClock + ]) ) } catch { print( From a7013ac0953a43d2e9bbd422c1e9ac7aa35bd3ce Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Tue, 26 Dec 2023 19:23:03 -0800 Subject: [PATCH 21/22] Updated docs --- .../MIDIKit.docc/MIDIKitIO/MIDIManager-Creating-Ports.md | 2 +- .../MIDIKitIO/Simple-MIDI-Listener-Class-Example.md | 7 ++++--- .../MIDIKitIO/MIDIKitIO.docc/MIDIManager-Creating-Ports.md | 2 +- .../MIDIKitIO.docc/Simple-MIDI-Listener-Class-Example.md | 7 ++++--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIManager-Creating-Ports.md b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIManager-Creating-Ports.md index 8098b08d4f..4cf545d5a4 100644 --- a/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIManager-Creating-Ports.md +++ b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/MIDIManager-Creating-Ports.md @@ -27,7 +27,7 @@ try midiManager.addInput( name: "MyApp MIDI In", tag: inputTag, uniqueID: .userDefaultsManaged(key: inputTag), - receiver: .events { [weak self] events in + receiver: .events { [weak self] events, timeStamp, source in // Note: this handler will be called on a background thread so be // sure to call anything that may result in UI updates on the main thread DispatchQueue.main.async { diff --git a/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/Simple-MIDI-Listener-Class-Example.md b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/Simple-MIDI-Listener-Class-Example.md index aaf902c914..f60187fa4f 100644 --- a/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/Simple-MIDI-Listener-Class-Example.md +++ b/Sources/MIDIKit/MIDIKit.docc/MIDIKitIO/Simple-MIDI-Listener-Class-Example.md @@ -6,11 +6,12 @@ A barebones example of how to set up MIDIKit to receive MIDI events on a created import Foundation import MIDIKitIO -public class MIDIModule { +public class MIDIHelper { private let midiManager = MIDIManager( clientName: "MyAppMIDIModule", model: "MyApp", - manufacturer: "MyCompany") + manufacturer: "MyCompany" + ) let inputTag = "Virtual_MIDI_In" @@ -22,7 +23,7 @@ public class MIDIModule { name: "MyApp MIDI In", tag: inputTag, uniqueID: .userDefaultsManaged(key: inputTag), - receiver: .events { [weak self] events in + receiver: .events { [weak self] events, timeStamp, source in // Note: this handler will be called on a background thread so be // sure to call anything that may result in UI updates on the main thread DispatchQueue.main.async { diff --git a/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIManager-Creating-Ports.md b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIManager-Creating-Ports.md index 8098b08d4f..4cf545d5a4 100644 --- a/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIManager-Creating-Ports.md +++ b/Sources/MIDIKitIO/MIDIKitIO.docc/MIDIManager-Creating-Ports.md @@ -27,7 +27,7 @@ try midiManager.addInput( name: "MyApp MIDI In", tag: inputTag, uniqueID: .userDefaultsManaged(key: inputTag), - receiver: .events { [weak self] events in + receiver: .events { [weak self] events, timeStamp, source in // Note: this handler will be called on a background thread so be // sure to call anything that may result in UI updates on the main thread DispatchQueue.main.async { diff --git a/Sources/MIDIKitIO/MIDIKitIO.docc/Simple-MIDI-Listener-Class-Example.md b/Sources/MIDIKitIO/MIDIKitIO.docc/Simple-MIDI-Listener-Class-Example.md index aaf902c914..f60187fa4f 100644 --- a/Sources/MIDIKitIO/MIDIKitIO.docc/Simple-MIDI-Listener-Class-Example.md +++ b/Sources/MIDIKitIO/MIDIKitIO.docc/Simple-MIDI-Listener-Class-Example.md @@ -6,11 +6,12 @@ A barebones example of how to set up MIDIKit to receive MIDI events on a created import Foundation import MIDIKitIO -public class MIDIModule { +public class MIDIHelper { private let midiManager = MIDIManager( clientName: "MyAppMIDIModule", model: "MyApp", - manufacturer: "MyCompany") + manufacturer: "MyCompany" + ) let inputTag = "Virtual_MIDI_In" @@ -22,7 +23,7 @@ public class MIDIModule { name: "MyApp MIDI In", tag: inputTag, uniqueID: .userDefaultsManaged(key: inputTag), - receiver: .events { [weak self] events in + receiver: .events { [weak self] events, timeStamp, source in // Note: this handler will be called on a background thread so be // sure to call anything that may result in UI updates on the main thread DispatchQueue.main.async { From 3252c7e8ffb98bd269bc893b58a044180c56f8a5 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Tue, 26 Dec 2023 19:24:25 -0800 Subject: [PATCH 22/22] Set Example projects to MIDIKit 0.9.5 --- Examples/Advanced/HUITest/HUITest.xcodeproj/project.pbxproj | 2 +- .../MIDIEventLogger/MIDIEventLogger.xcodeproj/project.pbxproj | 2 +- .../Advanced/MTCExample/MTCExample.xcodeproj/project.pbxproj | 2 +- .../EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj | 2 +- .../AppKit/EventParsing/EventParsing.xcodeproj/project.pbxproj | 2 +- .../AppKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj | 2 +- .../VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj | 2 +- .../EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj | 2 +- .../EventParsing/EventParsing.xcodeproj/project.pbxproj | 2 +- .../MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj | 2 +- .../MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj | 2 +- .../SystemNotifications.xcodeproj/project.pbxproj | 2 +- .../VirtualInput/VirtualInput.xcodeproj/project.pbxproj | 2 +- .../VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj | 2 +- .../BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj | 2 +- .../USB iOS to Mac/USB iOS to Mac.xcodeproj/project.pbxproj | 2 +- .../UIKit/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj | 2 +- .../UIKit/EventParsing/EventParsing.xcodeproj/project.pbxproj | 2 +- .../UIKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj | 2 +- .../UIKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Examples/Advanced/HUITest/HUITest.xcodeproj/project.pbxproj b/Examples/Advanced/HUITest/HUITest.xcodeproj/project.pbxproj index c0220aaae5..db5bda565b 100644 --- a/Examples/Advanced/HUITest/HUITest.xcodeproj/project.pbxproj +++ b/Examples/Advanced/HUITest/HUITest.xcodeproj/project.pbxproj @@ -554,7 +554,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.4; + minimumVersion = 0.9.5; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/project.pbxproj b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/project.pbxproj index 58985e77df..57df67ec2e 100644 --- a/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/project.pbxproj +++ b/Examples/Advanced/MIDIEventLogger/MIDIEventLogger.xcodeproj/project.pbxproj @@ -447,7 +447,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.4; + minimumVersion = 0.9.5; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/Advanced/MTCExample/MTCExample.xcodeproj/project.pbxproj b/Examples/Advanced/MTCExample/MTCExample.xcodeproj/project.pbxproj index 97231feff1..0fd5e43694 100644 --- a/Examples/Advanced/MTCExample/MTCExample.xcodeproj/project.pbxproj +++ b/Examples/Advanced/MTCExample/MTCExample.xcodeproj/project.pbxproj @@ -408,7 +408,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.4; + minimumVersion = 0.9.5; }; }; E29EF02D267BF47F00282F94 /* XCRemoteSwiftPackageReference "SwiftRadix" */ = { diff --git a/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj b/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj index 90fc0e6875..336df902ed 100644 --- a/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj +++ b/Examples/AppKit/EndpointMenus/EndpointMenus.xcodeproj/project.pbxproj @@ -368,7 +368,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.4; + minimumVersion = 0.9.5; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/AppKit/EventParsing/EventParsing.xcodeproj/project.pbxproj b/Examples/AppKit/EventParsing/EventParsing.xcodeproj/project.pbxproj index bec29e55f4..c730841be2 100644 --- a/Examples/AppKit/EventParsing/EventParsing.xcodeproj/project.pbxproj +++ b/Examples/AppKit/EventParsing/EventParsing.xcodeproj/project.pbxproj @@ -371,7 +371,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.4; + minimumVersion = 0.9.5; }; }; E2D7FF2029754A93003212AF /* XCRemoteSwiftPackageReference "SwiftRadix" */ = { diff --git a/Examples/AppKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj b/Examples/AppKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj index beebd5237c..188ed6dc55 100644 --- a/Examples/AppKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj +++ b/Examples/AppKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj @@ -367,7 +367,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.4; + minimumVersion = 0.9.5; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/AppKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj b/Examples/AppKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj index 6054dc4538..afe62fb797 100644 --- a/Examples/AppKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj +++ b/Examples/AppKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj @@ -367,7 +367,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.4; + minimumVersion = 0.9.5; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj index c6955b52c9..2279e3af9b 100644 --- a/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/EndpointPickers/EndpointPickers.xcodeproj/project.pbxproj @@ -400,7 +400,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.4; + minimumVersion = 0.9.5; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/SwiftUI Multiplatform/EventParsing/EventParsing.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/EventParsing/EventParsing.xcodeproj/project.pbxproj index ab2d65b6a5..860f26047a 100644 --- a/Examples/SwiftUI Multiplatform/EventParsing/EventParsing.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/EventParsing/EventParsing.xcodeproj/project.pbxproj @@ -377,7 +377,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.4; + minimumVersion = 0.9.5; }; }; E2D7FF1A29754911003212AF /* XCRemoteSwiftPackageReference "SwiftRadix" */ = { diff --git a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj index f7d83daf5a..9196d60fcd 100644 --- a/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/MIDIKitUIExample/MIDIKitUIExample.xcodeproj/project.pbxproj @@ -407,7 +407,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.4; + minimumVersion = 0.9.5; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj index 5032a8f1f5..ad420066b4 100644 --- a/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/MIDISystemInfo/MIDISystemInfo.xcodeproj/project.pbxproj @@ -620,7 +620,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.4; + minimumVersion = 0.9.5; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications.xcodeproj/project.pbxproj index d999493e80..e83d47a763 100644 --- a/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/SystemNotifications/SystemNotifications.xcodeproj/project.pbxproj @@ -377,7 +377,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.4; + minimumVersion = 0.9.5; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput.xcodeproj/project.pbxproj index c8a729eba7..0bb055d726 100644 --- a/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/VirtualInput/VirtualInput.xcodeproj/project.pbxproj @@ -373,7 +373,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.4; + minimumVersion = 0.9.5; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj b/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj index 84f49f370b..7d9739ef35 100644 --- a/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI Multiplatform/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj @@ -375,7 +375,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.4; + minimumVersion = 0.9.5; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj b/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj index 4fe3f8fdfe..57c7180d52 100644 --- a/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI iOS/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj @@ -368,7 +368,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.4; + minimumVersion = 0.9.5; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac.xcodeproj/project.pbxproj b/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac.xcodeproj/project.pbxproj index 51b954d456..a69e3cffd0 100644 --- a/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac.xcodeproj/project.pbxproj +++ b/Examples/SwiftUI iOS/USB iOS to Mac/USB iOS to Mac.xcodeproj/project.pbxproj @@ -361,7 +361,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.4; + minimumVersion = 0.9.5; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/UIKit/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj b/Examples/UIKit/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj index 0e405e28a2..80f45914ca 100644 --- a/Examples/UIKit/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj +++ b/Examples/UIKit/BluetoothMIDI/BluetoothMIDI.xcodeproj/project.pbxproj @@ -393,7 +393,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.4; + minimumVersion = 0.9.5; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/UIKit/EventParsing/EventParsing.xcodeproj/project.pbxproj b/Examples/UIKit/EventParsing/EventParsing.xcodeproj/project.pbxproj index c95da5773e..142c2f61f0 100644 --- a/Examples/UIKit/EventParsing/EventParsing.xcodeproj/project.pbxproj +++ b/Examples/UIKit/EventParsing/EventParsing.xcodeproj/project.pbxproj @@ -380,7 +380,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.4; + minimumVersion = 0.9.5; }; }; E2D7FF1D29754A1C003212AF /* XCRemoteSwiftPackageReference "SwiftRadix" */ = { diff --git a/Examples/UIKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj b/Examples/UIKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj index 6c94aad4fa..085069409b 100644 --- a/Examples/UIKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj +++ b/Examples/UIKit/VirtualInput/VirtualInput.xcodeproj/project.pbxproj @@ -376,7 +376,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.4; + minimumVersion = 0.9.5; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Examples/UIKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj b/Examples/UIKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj index 3d1ae9bdd2..b640f48d0e 100644 --- a/Examples/UIKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj +++ b/Examples/UIKit/VirtualOutput/VirtualOutput.xcodeproj/project.pbxproj @@ -376,7 +376,7 @@ repositoryURL = "https://github.com/orchetect/MIDIKit"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.9.4; + minimumVersion = 0.9.5; }; }; /* End XCRemoteSwiftPackageReference section */