From d54895766d7826ed1f1fe974b8997d0f2ed6976f Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Mon, 12 Dec 2022 15:24:44 -0800 Subject: [PATCH 1/3] `Timecode.FrameRate` was renamed to `TimecodeFrameRate` --- .../MTCExample/MTCExample/MTCGenContentView.swift | 4 ++-- .../MTCExample/MTCExample/MTCRecContentView.swift | 8 ++++---- Package.swift | 2 +- .../MIDIFileEvent/Events/Event SMPTEOffset.swift | 6 +++--- .../MIDIKitSync/MTC/MTCFrameRate Translation.swift | 14 +++++++------- Sources/MIDIKitSync/MTC/MTCFrameRate.swift | 2 +- .../MIDIKitSync/MTC/MTCGenerator/MTCEncoder.swift | 6 +++--- .../MTC/MTCGenerator/MTCGenerator.swift | 14 +++++++------- .../MIDIKitSync/MTC/MTCReceiver/MTCDecoder.swift | 8 ++++---- .../MTC/MTCReceiver/MTCReceiver SyncPolicy.swift | 4 ++-- .../MIDIKitSync/MTC/MTCReceiver/MTCReceiver.swift | 6 +++--- .../MTC/Generator/MTC Encoder Tests.swift | 10 +++++----- .../MTC/Integration/MTC Integration Tests.swift | 2 +- .../MTC/MTC MTCFrameRate ScaledFrames Tests.swift | 8 ++++---- .../MTC/MTC MTCFrameRate Tests.swift | 2 +- 15 files changed, 48 insertions(+), 48 deletions(-) diff --git a/Examples/Advanced/MTCExample/MTCExample/MTCGenContentView.swift b/Examples/Advanced/MTCExample/MTCExample/MTCGenContentView.swift index 8ef7cad397..5aba542dd5 100644 --- a/Examples/Advanced/MTCExample/MTCExample/MTCGenContentView.swift +++ b/Examples/Advanced/MTCExample/MTCExample/MTCGenContentView.swift @@ -20,7 +20,7 @@ struct MTCGenContentView: View { @State var mtcGen: MTCGenerator = .init() @AppStorage("mtcGen-localFrameRate") - var localFrameRate: Timecode.FrameRate = ._24 + var localFrameRate: TimecodeFrameRate = ._24 @AppStorage("mtcGen-locateBehavior") var locateBehavior: MTCEncoder.FullFrameBehavior = .ifDifferent @@ -217,7 +217,7 @@ struct MTCGenContentView: View { .frame(height: 15) Picker("Local Frame Rate", selection: $localFrameRate) { - ForEach(Timecode.FrameRate.allCases) { fRate in + ForEach(TimecodeFrameRate.allCases) { fRate in Text(fRate.stringValue) .tag(fRate) } diff --git a/Examples/Advanced/MTCExample/MTCExample/MTCRecContentView.swift b/Examples/Advanced/MTCExample/MTCExample/MTCRecContentView.swift index aab399c0ae..bfb1bbd811 100644 --- a/Examples/Advanced/MTCExample/MTCExample/MTCRecContentView.swift +++ b/Examples/Advanced/MTCExample/MTCExample/MTCRecContentView.swift @@ -26,7 +26,7 @@ struct MTCRecContentView: View { @State var receiverTC = "--:--:--:--" @State var receiverFR: MTCFrameRate? = nil @State var receiverState: MTCReceiver.State = .idle - @State var localFrameRate: Timecode.FrameRate? = nil + @State var localFrameRate: TimecodeFrameRate? = nil // MARK: - Internal State @@ -203,15 +203,15 @@ struct MTCRecContentView: View { Picker(selection: $localFrameRate, label: Text("Local Frame Rate")) { Text("None") - .tag(Timecode.FrameRate?.none) + .tag(TimecodeFrameRate?.none) Rectangle() .frame(maxWidth: .infinity) .frame(height: 3) - ForEach(Timecode.FrameRate.allCases) { fRate in + ForEach(TimecodeFrameRate.allCases) { fRate in Text(fRate.stringValue) - .tag(Timecode.FrameRate?.some(fRate)) + .tag(TimecodeFrameRate?.some(fRate)) } } .frame(width: 250) diff --git a/Package.swift b/Package.swift index db1f1bf0d1..fc762acb2f 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,7 @@ let package = Package( ], dependencies: [ - .package(url: "https://github.com/orchetect/TimecodeKit", from: "1.3.1"), + .package(url: "https://github.com/orchetect/TimecodeKit", branch: "fcpxml-framerate"), // testing-only: .package(url: "https://github.com/orchetect/XCTestUtils", from: "1.0.1") diff --git a/Sources/MIDIKitSMF/MIDIFileEvent/Events/Event SMPTEOffset.swift b/Sources/MIDIKitSMF/MIDIFileEvent/Events/Event SMPTEOffset.swift index 8db67dfe19..95cf3bec6b 100644 --- a/Sources/MIDIKitSMF/MIDIFileEvent/Events/Event SMPTEOffset.swift +++ b/Sources/MIDIKitSMF/MIDIFileEvent/Events/Event SMPTEOffset.swift @@ -401,9 +401,9 @@ extension Timecode { } } -extension Timecode.FrameRate { +extension TimecodeFrameRate { /// Returns the best corresponding MIDI File SMPTE Offset frame rate to represent the timecode - /// framerate. + /// frame rate. public var midiFileSMPTEOffsetRate: MIDIFile.SMPTEOffsetFrameRate { switch self { case ._23_976: return ._24fps // as output from Pro Tools @@ -432,7 +432,7 @@ extension Timecode.FrameRate { extension MIDIFile.SMPTEOffsetFrameRate { /// Returns exact `Timecode` frame rate that matches the MIDI File SMPTE Offset frame rate. - public var timecodeRate: Timecode.FrameRate { + public var timecodeRate: TimecodeFrameRate { switch self { case ._24fps: return ._24 case ._25fps: return ._25 diff --git a/Sources/MIDIKitSync/MTC/MTCFrameRate Translation.swift b/Sources/MIDIKitSync/MTC/MTCFrameRate Translation.swift index ce8e24911e..dd4a4fdacf 100644 --- a/Sources/MIDIKitSync/MTC/MTCFrameRate Translation.swift +++ b/Sources/MIDIKitSync/MTC/MTCFrameRate Translation.swift @@ -11,10 +11,10 @@ import TimecodeKit extension MTCFrameRate { /// Returns all timecode frame rates derived from the MTC base frame rate. - public var derivedFrameRates: [Timecode.FrameRate] { + public var derivedFrameRates: [TimecodeFrameRate] { // could hard-code these, but keeping it functional ensures compiler safety checking - Timecode.FrameRate + TimecodeFrameRate .allCases .filter { $0.transmitsMTC(using: self) } } @@ -24,7 +24,7 @@ extension MTCFrameRate { /// Useful for internal calculations. /// /// To get all timecode frame rates that are compatible, use ``derivedFrameRates`` instead. - public var directEquivalentFrameRate: Timecode.FrameRate { + public var directEquivalentFrameRate: TimecodeFrameRate { switch self { case .mtc24: return ._24 case .mtc25: return ._25 @@ -34,7 +34,7 @@ extension MTCFrameRate { } } -extension Timecode.FrameRate { +extension TimecodeFrameRate { /// Returns the base MTC frame rate that DAWs use to transmit timecode (scaling frame number if /// necessary) public var mtcFrameRate: MTCFrameRate { @@ -90,7 +90,7 @@ extension MTCFrameRate { internal func scaledFrames( fromRawMTCFrames: Int, quarterFrames: UInt8, - to timecodeRate: Timecode.FrameRate + to timecodeRate: TimecodeFrameRate ) -> Double? { // if real timecode frame rates are not compatible (H:MM:SS stable), frame value scaling is // not possible @@ -131,7 +131,7 @@ extension MTCFrameRate { } } -extension Timecode.FrameRate { +extension TimecodeFrameRate { /// Scales frames at other timecode frame rate to MTC frames at `self` MTC base rate. /// /// - Note: This is a specialized calculation, and is intended to produce raw MTC frames and @@ -168,7 +168,7 @@ extension Timecode.FrameRate { } } -extension Timecode.FrameRate { +extension TimecodeFrameRate { /// Internal: scale factor used when scaling timecode frame rate to/from MTC SMPTE frame rates internal var mtcScaleFactor: Double { // calculated from: diff --git a/Sources/MIDIKitSync/MTC/MTCFrameRate.swift b/Sources/MIDIKitSync/MTC/MTCFrameRate.swift index 496f075fe5..4b79d84281 100644 --- a/Sources/MIDIKitSync/MTC/MTCFrameRate.swift +++ b/Sources/MIDIKitSync/MTC/MTCFrameRate.swift @@ -80,7 +80,7 @@ public enum MTCFrameRate: Hashable, CaseIterable { // MARK: - Init /// Construct based on the corresponding real timecode frame rate - init(_ timecodeFrameRate: Timecode.FrameRate) { + init(_ timecodeFrameRate: TimecodeFrameRate) { self = timecodeFrameRate.mtcFrameRate } diff --git a/Sources/MIDIKitSync/MTC/MTCGenerator/MTCEncoder.swift b/Sources/MIDIKitSync/MTC/MTCGenerator/MTCEncoder.swift index f61fbe923a..9587d3e103 100644 --- a/Sources/MIDIKitSync/MTC/MTCGenerator/MTCEncoder.swift +++ b/Sources/MIDIKitSync/MTC/MTCGenerator/MTCEncoder.swift @@ -51,10 +51,10 @@ public final class MTCEncoder: SendsMIDIEvents { } /// Local frame rate (desired rate, not internal MTC SMPTE frame rate). - public internal(set) var localFrameRate: Timecode.FrameRate = ._30 + public internal(set) var localFrameRate: TimecodeFrameRate = ._30 /// Set local frame rate (desired rate, not internal MTC SMPTE frame rate). - internal func setLocalFrameRate(_ newFrameRate: Timecode.FrameRate) { + internal func setLocalFrameRate(_ newFrameRate: TimecodeFrameRate) { localFrameRate = newFrameRate mtcFrameRate = newFrameRate.mtcFrameRate } @@ -120,7 +120,7 @@ public final class MTCEncoder: SendsMIDIEvents { /// - triggerFullFrame: Triggers the MIDI handler to send a full-frame message. public func locate( to components: Timecode.Components, - frameRate: Timecode.FrameRate? = nil, + frameRate: TimecodeFrameRate? = nil, transmitFullFrame: FullFrameBehavior = .ifDifferent ) { if let unwrappedFrameRate = frameRate { diff --git a/Sources/MIDIKitSync/MTC/MTCGenerator/MTCGenerator.swift b/Sources/MIDIKitSync/MTC/MTCGenerator/MTCGenerator.swift index f5fb540192..1537c04de3 100644 --- a/Sources/MIDIKitSync/MTC/MTCGenerator/MTCGenerator.swift +++ b/Sources/MIDIKitSync/MTC/MTCGenerator/MTCGenerator.swift @@ -49,8 +49,8 @@ public final class MTCGenerator: SendsMIDIEvents { return getTimecode } - public var localFrameRate: Timecode.FrameRate { - var getFrameRate: Timecode.FrameRate! + public var localFrameRate: TimecodeFrameRate { + var getFrameRate: TimecodeFrameRate! queue.sync { getFrameRate = encoder.localFrameRate @@ -142,7 +142,7 @@ public final class MTCGenerator: SendsMIDIEvents { } /// Sets timer rate to corresponding MTC quarter-frame duration in Hz. - internal func setTimerRate(from frameRate: Timecode.FrameRate) { + internal func setTimerRate(from frameRate: TimecodeFrameRate) { // const values generated from: // TCC(f: 1).toTimecode(at: frameRate)!.realTimeValue @@ -247,7 +247,7 @@ public final class MTCGenerator: SendsMIDIEvents { /// Call ``stop()`` to stop generating events. public func start( now components: Timecode.Components, - frameRate: Timecode.FrameRate, + frameRate: TimecodeFrameRate, base: Timecode.SubFramesBase ) { queue.sync { @@ -272,7 +272,7 @@ public final class MTCGenerator: SendsMIDIEvents { /// Call ``stop()`` to stop generating events. public func start( now realTime: TimeInterval, - frameRate: Timecode.FrameRate + frameRate: TimecodeFrameRate ) { // since realTime can be between frames, // we need to ensure that MTC quarter-frames begin generating @@ -288,7 +288,7 @@ public final class MTCGenerator: SendsMIDIEvents { // convert real time to timecode at the given frame rate guard let inRTtoTimecode = try? Timecode( - realTimeValue: realTime, + realTime: realTime, at: frameRate, limit: ._24hours, base: ._100SubFrames // base doesn't matter, just for calculation @@ -337,7 +337,7 @@ public final class MTCGenerator: SendsMIDIEvents { /// - Important: This must be called on `self.queue`. internal func locateAndStart( now components: Timecode.Components, - frameRate: Timecode.FrameRate + frameRate: TimecodeFrameRate ) { encoder.locate( to: components, diff --git a/Sources/MIDIKitSync/MTC/MTCReceiver/MTCDecoder.swift b/Sources/MIDIKitSync/MTC/MTCReceiver/MTCDecoder.swift index 48cbf2908f..6de1b5ed19 100644 --- a/Sources/MIDIKitSync/MTC/MTCReceiver/MTCDecoder.swift +++ b/Sources/MIDIKitSync/MTC/MTCReceiver/MTCDecoder.swift @@ -72,7 +72,7 @@ public final class MTCDecoder { /// /// Remember to also set this any time the local frame rate changes so the receiver can /// interpret the incoming MTC accordingly. - public var localFrameRate: Timecode.FrameRate? + public var localFrameRate: TimecodeFrameRate? /// Status of the direction of MTC quarter-frames received public internal(set) var direction: MTCDirection = .forwards @@ -163,7 +163,7 @@ public final class MTCDecoder { // MARK: - init public init( - initialLocalFrameRate: Timecode.FrameRate? = nil, + initialLocalFrameRate: TimecodeFrameRate? = nil, timecodeChanged: (( _ timecode: Timecode, _ event: MTCMessageType, @@ -248,7 +248,7 @@ extension MTCDecoder: ReceivesMIDIEvents { ) // set up a variable to store the actual output frame rate - let outputFrameRate: Timecode.FrameRate + let outputFrameRate: TimecodeFrameRate // scale frames if local frame rate is set // scaling will return nil if frame rates are not compatible @@ -476,7 +476,7 @@ extension MTCDecoder: ReceivesMIDIEvents { } // set up a variable to store the actual output frame rate - let outputFrameRate: Timecode.FrameRate + let outputFrameRate: TimecodeFrameRate // scale or interpolate based on if local frame rate is set // scaling will return nil if frame rates are not compatible diff --git a/Sources/MIDIKitSync/MTC/MTCReceiver/MTCReceiver SyncPolicy.swift b/Sources/MIDIKitSync/MTC/MTCReceiver/MTCReceiver SyncPolicy.swift index d011369895..462956e364 100644 --- a/Sources/MIDIKitSync/MTC/MTCReceiver/MTCReceiver SyncPolicy.swift +++ b/Sources/MIDIKitSync/MTC/MTCReceiver/MTCReceiver SyncPolicy.swift @@ -41,13 +41,13 @@ extension MTCReceiver { // MARK: - Public Methods /// Returns real time duration in seconds of the ``lockFrames`` property. - public func lockDuration(at rate: Timecode.FrameRate) -> TimeInterval { + public func lockDuration(at rate: TimecodeFrameRate) -> TimeInterval { let tc = Timecode(wrapping: TCC(f: lockFrames), at: rate) return tc.realTimeValue } /// Returns real time duration in seconds of the ``dropOutFrames`` property. - public func dropOutDuration(at rate: Timecode.FrameRate) -> TimeInterval { + public func dropOutDuration(at rate: TimecodeFrameRate) -> TimeInterval { let tc = Timecode(wrapping: TCC(f: dropOutFrames), at: rate) return tc.realTimeValue } diff --git a/Sources/MIDIKitSync/MTC/MTCReceiver/MTCReceiver.swift b/Sources/MIDIKitSync/MTC/MTCReceiver/MTCReceiver.swift index 430ab18e6a..b0609078e4 100644 --- a/Sources/MIDIKitSync/MTC/MTCReceiver/MTCReceiver.swift +++ b/Sources/MIDIKitSync/MTC/MTCReceiver/MTCReceiver.swift @@ -47,9 +47,9 @@ public final class MTCReceiver { /// The frame rate the local system is using. /// Remember to also set this any time the local frame rate changes so the receiver can /// interpret the incoming MTC accordingly. - public var localFrameRate: Timecode.FrameRate? { + public var localFrameRate: TimecodeFrameRate? { get { - var getMTCFrameRate: Timecode.FrameRate? + var getMTCFrameRate: TimecodeFrameRate? queue.sync { getMTCFrameRate = decoder.localFrameRate @@ -125,7 +125,7 @@ public final class MTCReceiver { /// - stateChanged: handle receiver state change callbacks, pass `nil` if not needed public init( name: String? = nil, - initialLocalFrameRate: Timecode.FrameRate? = nil, + initialLocalFrameRate: TimecodeFrameRate? = nil, syncPolicy: SyncPolicy? = nil, timecodeChanged: (( _ timecode: Timecode, diff --git a/Tests/MIDIKitSyncTests/MTC/Generator/MTC Encoder Tests.swift b/Tests/MIDIKitSyncTests/MTC/Generator/MTC Encoder Tests.swift index f03f6ea241..6bbc1e39b8 100644 --- a/Tests/MIDIKitSyncTests/MTC/Generator/MTC Encoder Tests.swift +++ b/Tests/MIDIKitSyncTests/MTC/Generator/MTC Encoder Tests.swift @@ -29,7 +29,7 @@ final class MTC_Generator_Encoder_Tests: XCTestCase { var mtcEnc: MTCEncoder! - Timecode.FrameRate.allCases.forEach { + TimecodeFrameRate.allCases.forEach { mtcEnc = MTCEncoder() mtcEnc.setLocalFrameRate($0) @@ -249,7 +249,7 @@ final class MTC_Generator_Encoder_Tests: XCTestCase { var mtcEnc: MTCEncoder! - Timecode.FrameRate.allCases.forEach { + TimecodeFrameRate.allCases.forEach { mtcEnc = MTCEncoder() mtcEnc.setLocalFrameRate($0) mtcEnc.setMTCComponents(mtc: TCC(h: 1, m: 20, s: 32, f: 10)) @@ -382,7 +382,7 @@ final class MTC_Generator_Encoder_Tests: XCTestCase { var mtcEnc: MTCEncoder! - Timecode.FrameRate.allDrop.forEach { + TimecodeFrameRate.allDrop.forEach { mtcEnc = MTCEncoder() mtcEnc.setLocalFrameRate($0) @@ -753,7 +753,7 @@ final class MTC_Generator_Encoder_Tests: XCTestCase { var mtcEnc: MTCEncoder! - Timecode.FrameRate.allCases.forEach { + TimecodeFrameRate.allCases.forEach { mtcEnc = MTCEncoder() mtcEnc.setLocalFrameRate($0) mtcEnc.setMTCComponents(mtc: TCC(h: 1, m: 20, s: 32, f: 10)) @@ -1323,7 +1323,7 @@ final class MTC_Generator_Encoder_Tests: XCTestCase { // test an arbitrary timecode in all timecode frame rates // while covering all four MTC SMPTE base frame rates - Timecode.FrameRate.allCases.forEach { + TimecodeFrameRate.allCases.forEach { mtcEnc.locate(to: TCC(h: 2, m: 4, s: 6, f: 0).toTimecode(rawValuesAt: $0)) mtcEnc.mtcComponents.f = 8 diff --git a/Tests/MIDIKitSyncTests/MTC/Integration/MTC Integration Tests.swift b/Tests/MIDIKitSyncTests/MTC/Integration/MTC Integration Tests.swift index bb70d301d2..736ac09903 100644 --- a/Tests/MIDIKitSyncTests/MTC/Integration/MTC Integration Tests.swift +++ b/Tests/MIDIKitSyncTests/MTC/Integration/MTC Integration Tests.swift @@ -604,7 +604,7 @@ final class MTC_Integration_Integration_Tests: XCTestCase { // iterate: each timecode frame rate // omit 24.98fps because it does not conform to the nature of this test - Timecode.FrameRate.allCases + TimecodeFrameRate.allCases .filter { $0 != ._24_98 } .forEach { frameRate in diff --git a/Tests/MIDIKitSyncTests/MTC/MTC MTCFrameRate ScaledFrames Tests.swift b/Tests/MIDIKitSyncTests/MTC/MTC MTCFrameRate ScaledFrames Tests.swift index 18a688f508..10e3936475 100644 --- a/Tests/MIDIKitSyncTests/MTC/MTC MTCFrameRate ScaledFrames Tests.swift +++ b/Tests/MIDIKitSyncTests/MTC/MTC MTCFrameRate ScaledFrames Tests.swift @@ -26,7 +26,7 @@ final class MTC_MTCFrameRate_ScaledFrames_Tests: XCTestCase { // (reminder: MTC SMPTE frame numbers should only ever be even numbers, // as this is how the MTC spec functions) - for realRate in Timecode.FrameRate.allCases { + for realRate in TimecodeFrameRate.allCases { let mtcRate = realRate.mtcFrameRate // avoid testing incompatible frame rates which will fail any way @@ -334,7 +334,7 @@ final class MTC_MTCFrameRate_ScaledFrames_Tests: XCTestCase { func testMTC_TimecodeFrameRate_ScaledFrames() { // zero - for realRate in Timecode.FrameRate.allCases { + for realRate in TimecodeFrameRate.allCases { let scaled = realRate.scaledFrames(fromTimecodeFrames: 0.0) XCTAssertEqual( @@ -351,7 +351,7 @@ final class MTC_MTCFrameRate_ScaledFrames_Tests: XCTestCase { // spot-check - for realRate in Timecode.FrameRate.allCases { + for realRate in TimecodeFrameRate.allCases { switch realRate { case ._23_976, ._24: for qf in UInt8(0) ... 7 { @@ -471,7 +471,7 @@ final class MTC_MTCFrameRate_ScaledFrames_Tests: XCTestCase { } func testMTC_RoundTrip_ScaledFrames() { - for realRate in Timecode.FrameRate.allCases { + for realRate in TimecodeFrameRate.allCases { // zero do { let scaledToMTC = realRate.scaledFrames(fromTimecodeFrames: 0.0) diff --git a/Tests/MIDIKitSyncTests/MTC/MTC MTCFrameRate Tests.swift b/Tests/MIDIKitSyncTests/MTC/MTC MTCFrameRate Tests.swift index 13f3bae3f2..0ce29454e0 100644 --- a/Tests/MIDIKitSyncTests/MTC/MTC MTCFrameRate Tests.swift +++ b/Tests/MIDIKitSyncTests/MTC/MTC MTCFrameRate Tests.swift @@ -14,7 +14,7 @@ final class MTC_MTCFrameRate_Tests: XCTestCase { func testMTC_MTCFrameRate_Init_TimecodeFrameRate() { // test is pedantic, but worth having - Timecode.FrameRate.allCases.forEach { + TimecodeFrameRate.allCases.forEach { XCTAssertEqual(MTCFrameRate($0), $0.mtcFrameRate) } } From 17707daa550d5bc8ede652d62f0274b80bcd5cd0 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Wed, 14 Dec 2022 22:26:44 -0800 Subject: [PATCH 2/3] Updated .swiftformat --- .swiftformat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.swiftformat b/.swiftformat index 139b3cc9ec..2a2cf75e67 100644 --- a/.swiftformat +++ b/.swiftformat @@ -9,7 +9,7 @@ --closurevoid remove --commas inline --conflictmarkers reject ---decimalgrouping 3,6 +--decimalgrouping ignore --elseposition same-line --emptybraces spaced --enumthreshold 0 From 03870c32c7cfb3b6eb5da0c098dbdf2f3dbb2b83 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Wed, 14 Dec 2022 22:26:50 -0800 Subject: [PATCH 3/3] Bumped TimecodeKit dependency to min 1.6.0 --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index fc762acb2f..7d7593a47b 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,7 @@ let package = Package( ], dependencies: [ - .package(url: "https://github.com/orchetect/TimecodeKit", branch: "fcpxml-framerate"), + .package(url: "https://github.com/orchetect/TimecodeKit", from: "1.6.0"), // testing-only: .package(url: "https://github.com/orchetect/XCTestUtils", from: "1.0.1")