diff --git a/.swiftformat b/.swiftformat index fbba382c..c0878d85 100644 --- a/.swiftformat +++ b/.swiftformat @@ -23,7 +23,7 @@ --funcattributes prev-line --groupedextension "MARK: %c" --guardelse auto ---header "\n {file}\n TimecodeKit • https://github.com/orchetect/TimecodeKit\n © {year} Steffan Andrews • Licensed under MIT License\n" +--header "\n {file}\n TimecodeKit • https://github.com/orchetect/TimecodeKit\n © 2020-{year} Steffan Andrews • Licensed under MIT License\n" --hexgrouping 4,8 --hexliteralcase uppercase --ifdef no-indent @@ -37,7 +37,7 @@ --markcategories true --markextensions always --marktypes always ---maxwidth 100 +--maxwidth 140 --modifierorder --nevertrailing --nospaceoperators diff --git a/Images/timecode-init.png b/Images/timecode-init.png new file mode 100644 index 00000000..01111fb4 Binary files /dev/null and b/Images/timecode-init.png differ diff --git a/Package.swift b/Package.swift index 39066e3f..8dc13472 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.5 +// swift-tools-version: 5.5 // (be sure to update the .swift-version file when this Swift version changes) import PackageDescription @@ -11,7 +11,16 @@ let package = Package( // certain features of the library are marked @available only on newer versions of OSes, // but a platforms spec here determines what base platforms // the library is currently supported on - platforms: [.macOS(.v10_12), .iOS(.v9), .tvOS(.v9), .watchOS(.v2)], + + // Add visionOS platform in supported Swift toolchain / Xcode versions + // TODO: Not yet implemented in Xcode 15.0 but can be added later + platforms: { + // #if swift(>=5.9.1) + // [.macOS(.v10_12), .iOS(.v9), .tvOS(.v9), .watchOS(.v2), .visionOS(.v1)] + // #else + [.macOS(.v10_12), .iOS(.v9), .tvOS(.v9), .watchOS(.v2)] + // #endif + }(), products: [ .library( @@ -28,7 +37,7 @@ let package = Package( ], dependencies: [ - // used only for Dev tests, not part of regular unit test + // used only for Dev tests, not part of regular unit tests // .package(url: "https://github.com/orchetect/XCTestUtils", from: "1.0.3") ], @@ -44,10 +53,20 @@ let package = Package( name: "TimecodeKitUI", dependencies: ["TimecodeKit"], linkerSettings: [ - .linkedFramework("SwiftUI", .when(platforms: [.macOS, .iOS, .tvOS, .watchOS])) + .linkedFramework( + "SwiftUI", + .when(platforms: { + // Xcode 15 beta 8 (Swift 5.9) introduced visionOS + #if swift(>=5.9) + [.macOS, .iOS, .tvOS, .watchOS, .visionOS] + #else + [.macOS, .iOS, .tvOS, .watchOS] + #endif + }()) + ) ] ), - + // unit tests .testTarget( name: "TimecodeKit-Unit-Tests", @@ -56,14 +75,17 @@ let package = Package( ), // dev tests - // (not meant to be run as unit tests, but only to verify library's computational integrity when making major changes to the library, as these tests require modification to be meaningful) + // (not meant to be run as unit tests, but only to verify library's computational integrity when making major changes to the + // library, as these tests require modification to be meaningful) .testTarget( name: "TimecodeKit-Dev-Tests", - dependencies: ["TimecodeKit"] // , "SegmentedProgress" + dependencies: ["TimecodeKit"] // , "XCTestUtils" ) ] ) +// MARK: - Conditional Unit Testing + func addShouldTestFlag() { package.targets.filter { $0.isTest }.forEach { target in if target.swiftSettings == nil { target.swiftSettings = [] } diff --git a/README.md b/README.md index 4cdd5524..fd6f527f 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,17 @@ # TimecodeKit -[![CI Build Status](https://github.com/orchetect/TimecodeKit/actions/workflows/build.yml/badge.svg)](https://github.com/orchetect/TimecodeKit/actions/workflows/build.yml) [![Platforms - macOS 10.12 | iOS 9 | tvOS 9 | watchOS 2 | visionOS 1](https://img.shields.io/badge/platforms-macOS%2010.12%20|%20iOS%209%20|%20tvOS%209%20|%20watchOS%202%20|%20visionOS%201-lightgrey.svg?style=flat)](https://developer.apple.com/swift) ![Swift 5.5-5.9](https://img.shields.io/badge/Swift-5.5–5.9-orange.svg?style=flat) [![Xcode 13-15](https://img.shields.io/badge/Xcode-13–15-blue.svg?style=flat)](https://developer.apple.com/swift) [![License: MIT](http://img.shields.io/badge/license-MIT-lightgrey.svg?style=flat)](https://github.com/orchetect/TimecodeKit/blob/main/LICENSE) +[![CI Build Status](https://github.com/orchetect/TimecodeKit/actions/workflows/build.yml/badge.svg)](https://github.com/orchetect/TimecodeKit/actions/workflows/build.yml) [![Platforms - macOS 10.12 | iOS 9 | tvOS 9 | watchOS 2 | visionOS 1](https://img.shields.io/badge/Platforms-macOS%2010.12%20|%20iOS%209%20|%20tvOS%209%20|%20watchOS%202%20|%20visionOS%201-lightgrey.svg?style=flat)](https://developer.apple.com/swift) ![Swift 5.5-5.9](https://img.shields.io/badge/Swift-5.5–5.9-orange.svg?style=flat) [![Xcode 13-15](https://img.shields.io/badge/Xcode-13–15-blue.svg?style=flat)](https://developer.apple.com/swift) [![License: MIT](http://img.shields.io/badge/License-MIT-lightgrey.svg?style=flat)](https://github.com/orchetect/TimecodeKit/blob/main/LICENSE) -The most robust, precise and complete Swift library for working with SMPTE timecode. Supports 22 industry timecode frame rates, including conversions to/from timecode strings and offering timecode-based calculations. +The most robust, precise and complete Swift library for working with SMPTE/EBU timecode. Supports 22 industry timecode frame rates, with a suite of conversions, calculations and integrations with Apple AV frameworks. -Timecode is a standard for representing video frames and used for video burn-in timecode (BITC), or display in a DAW (Digital Audio Workstation) or video playback/NLE applications. +Timecode is a broadcast and post-production standard for addressing video frames. It is used for video burn-in timecode (BITC), and display in a DAW (Digital Audio Workstation) or video playback/editing applications. + +> **Note**: See the TimecodeKit 1.x → 2.x [Migration Guide](https://orchetect.github.io/TimecodeKit/documentation/timecodekit/timecodekit-2-migration-guide) if you are moving from TimecodeKit 1.x. ## Supported Timecode Frame Rates -The following timecode frame rates are supported. These are display rates. +The following timecode rates and formats are supported. | Film / ATSC / HD | PAL / SECAM / DVB / ATSC | NTSC / ATSC / PAL-M | NTSC Non-Standard | ATSC | | ---------------- | ------------------------ | ------------------- | ----------------- | ---- | @@ -24,7 +26,7 @@ The following timecode frame rates are supported. These are display rates. ## Supported Video Frame Rates -The following video frame rates are supported. These are actual video rates. +The following video frame rates are supported. (Video rates) | Film / HD | PAL | NTSC | | --------- | --------- | --------------- | @@ -37,17 +39,22 @@ The following video frame rates are supported. These are actual video rates. ## Core Features -- Convert timecode values to timecode display string, and vice-versa -- Convert timecode values to real wall-clock time, and vice-versa -- Convert timecode to # of samples at any audio sample-rate, and vice-versa +- Convert timecode between: + - timecode display string + - total elapsed frame count + - real wall-clock time + - elapsed audio samples at any audio sample rate + - rational time notation (such as `CMTime` or Final Cut Pro XML and AAF encoding) + - feet + frames - Convert timecode and/or frame rate to a rational fraction, and vice-versa (including `CMTime`) -- Support for Subframes - Support for Days as a timecode component (some DAWs including Cubase support > 24 hour timecode) -- Common math operations between timecodes: add, subtract, multiply, divide +- Support for Subframes +- Math operations: add, subtract, multiply, divide - Granular timecode validation - Form a `Range` or `Stride` between two timecodes - Conforms to `Codable` -- A `Formatter` object that can format timecode and also provide an `NSAttributedString` showing invalid timecode components using alternate attributes (such as red text color) +- A `Formatter` object that can format timecode +- A `NSAttributedString` showing invalid timecode components using alternate attributes (such as red text color) - A SwiftUI `Text` object showing invalid timecode components using alternate attributes (such as red text color) - `AVAsset` video file utilities to easily read/write timecode tracks and locate `AVPlayer` to timecode locations - Exhaustive unit tests ensuring accuracy @@ -57,703 +64,26 @@ The following video frame rates are supported. These are actual video rates. ### Swift Package Manager (SPM) 1. Add TimecodeKit as a dependency using Swift Package Manager. - - In an app project or framework, in Xcode: - - Select the menu: **File → Swift Packages → Add Package Dependency...** - Enter this URL: `https://github.com/orchetect/TimecodeKit` - - In a Swift Package, add it to the Package.swift dependencies: ```swift - .package(url: "https://github.com/orchetect/TimecodeKit", from: "1.6.0") + .package(url: "https://github.com/orchetect/TimecodeKit", from: "2.0.0") ``` - 2. Import the library: - ```swift import TimecodeKit ``` ## Documentation -Note: This documentation does not cover every property and initializer available but covers most typical use cases. - -### Table of Contents - -- [Initialization](#Initialization) -- [Properties](#Properties) -- [Components](#Components) -- [Timecode Display String](#Timecode-Display-String) -- [Math](#Math) -- [Conversions](#Conversions) - - [To another frame rate](#to-another-frame-rate) - - [Real Time](#Real-Time) - - [Audio Samples](#Audio-Samples) -- [Validation](#Validation) - - [Timecode Component Validation](#Timecode-Component-Validation) - - [NSAttributedString](#Timecode-Validation-NSAttributedString) - - [SwiftUI Text](#Timecode-Validation-SwiftUI-Text) - - [NSFormatter](#Timecode-Validation-NSFormatter) -- [Advanced](#Advanced) - - [Days Component](#Days-Component) - - [Subframes Component](#Subframes-Component) - - [Comparable](#Comparable) - - [Range, Strideable](#Range-Strideable) - - [Rational Number Expression](#Rational-Number-Expression) - - [CMTime Conversion](#CMTime-Conversion) - - [Timecode Intervals](#Timecode-Intervals) - - [Timecode Transformer](#Timecode-Transformer) - - [Feet+Frames](#Feet-Frames) - - [AVAsset Timecode Track Read/Write](#AVAsset-Timecode-Track-ReadWrite) - -### Initialization - -Using `(_ exactly:)` by default: - -```swift -// from Int timecode component values -try Timecode(TCC(h: 01, m: 00, s: 00, f: 00), at: ._23_976) -try TCC(h: 01, m: 00, s: 00, f: 00).toTimecode(at: ._23_976) // alternate method - -// from frame number (total elapsed frames) -try Timecode(.frames(40000), at: ._23_976) - -// from timecode string -try Timecode("01:00:00:00", at: ._23_976) -try "01:00:00:00".toTimecode(at: ._23_976) // alternate method - -// from real time (wall clock) elapsed in seconds -try Timecode(realTime: 4723.241579, at: ._23_976) -try (4723.241579).toTimecode(at: ._23_976) // alternate method on TimeInterval - -// from elapsed number of audio samples at a given sample rate -try Timecode(samples: 123456789, sampleRate: 48000, at: ._23_976) -``` - -Using `(clamping:, ...)` and `(clampingEach:, ...)`: - -```swift -// clamp full timecode to valid range -try Timecode(clamping: "26:00:00:00", at: ._24) - .stringValue // == "23:59:59:23" - -// clamp individual timecode component values to valid values if they are out-of-bounds -try Timecode(clampingEach: "01:00:85:50", at: ._24) - .stringValue // == "01:00:59:23" -``` - -Using `(wrapping:, ...)`: - -```swift -// wrap around clock continuously if entire timecode overflows or underflows - -try Timecode(wrapping: "26:00:00:00", at: ._24) - .stringValue // == "02:00:00:00" - -try Timecode(wrapping: "23:59:59:24", at: ._24) - .stringValue // == "00:00:00:00" -``` - -### Properties - -Timecode components can be get or set directly as instance properties. - -```swift -let tc = try "01:12:20:05".toTimecode(at: ._23_976) - -// get -tc.days // == 0 -tc.hours // == 1 -tc.minutes // == 12 -tc.seconds // == 20 -tc.frames // == 5 -tc.subFrames // == 0 - -// set -tc.hours = 5 -tc.stringValue // == "05:12:20:05" -``` - -### Components - -In order to help facilitate defining a set of timecode component values, a simple `Components` struct is implemented. This struct can be passed into many methods and initializers. - -```swift -// a global typealias is exposed to shorten the syntax when constructing -public typealias TCC = Timecode.Components - -// ie: -Timecode(TCC(h: 1), at: ._23_976) -// is the same as: -Timecode(Timecode.Components(h: 1), at: ._23_976) -``` - -```swift -let cmp = try "01:12:20:05" - .toTimecode(at: ._23_976) - .components // Timecode.Components(), aka TCC() - -cmp.d // == 0 (days) -cmp.h // == 1 (hours) -cmp.m // == 12 (minutes) -cmp.s // == 20 (seconds) -cmp.f // == 5 (frames) -cmp.sf // == 0 (subframes) -``` - -### Timecode Display String - -```swift -try TCC(h: 01, m: 00, s: 00, f: 00) - .toTimecode(at: ._29_97_drop) - .stringValue // == "01:00:00;00" -``` - -### Math - -Using operators (which use `wrapping:` internally if the result underflows or overflows timecode bounds): - -```swift -let tc1 = try "01:00:00:00".toTimecode(at: ._23_976) -let tc2 = try "00:00:02:00".toTimecode(at: ._23_976) - -(tc1 + tc2).stringValue // == "01:00:02:00" -(tc1 - tc2).stringValue // == "00:00:58:00" -(tc1 * 2.0).stringValue // == "02:00:00:00" -(tc1 / 2.0).stringValue // == "00:30:00:00" -``` - -Methods also exist to achieve the same results. - -Mutating methods: - -- `.add()` -- `.subtract()` -- `.multiply()` -- `.divide()` -- `.offset()` - -Non-mutating methods that produce a new `Timecode` instance: - -- `.adding()` -- `.subtracting()` -- `.multiplying()` -- `.dividing()` -- `.offsetting()` - -### Conversions - -#### To another frame rate - -```swift -// convert between frame rates -try "01:00:00;00" - .toTimecode(at: ._29_97_drop) - .converted(to: ._29_97) - .stringValue // == "00:59:56:12" -``` - -#### Real Time - -```swift -// timecode to real-world time in seconds -let tc = try "01:00:00:00" - .toTimecode(at: ._23_976) - .realTimeValue // == 3603.6 as TimeInterval (Double) - -// real-world time to timecode -try (3603.6) // TimeInterval, aka Double - .toTimecode(at: ._23_976) - .stringValue // == "01:00:00:00" -``` - -#### Audio Samples - -```swift -// timecode to elapsed audio samples -let tc = try "01:00:00:00" - .toTimecode(at: ._24) - .samplesValue(sampleRate: 48000) // == 172800000 - -// elapsed audio samples to timecode -try Timecode(samples: 172800000, sampleRate: 48000, at: ._24) - .stringValue // == "01:00:00:00" -``` - -### Validation - -#### Timecode Component Validation - -Timecode validation can be helpful and powerful, for example, when parsing timecode strings read from an external data file or received as user-input in a text field. - -Timecode can be tested as: - -- valid or invalid as a whole, by testing for `nil` when using the default `exactly:` failable initializers or instance `set...` methods, or -- granularly to test validity of individual timecode components - -```swift -// example: -// 1 hour and 20 minutes ARE valid at 23.976 fps, -// but 75 seconds and 60 frames are NOT valid - -// non-granular validation -try TCC(h: 1, m: 20, s: 75, f: 60) - .toTimecode(at: ._23_976) // == throws error; cannot form a valid timecode - -// granular validation -// rawValues allow invalid values; does not throw errors so 'try' is not needed -TCC(h: 1, m: 20, s: 75, f: 60) - .toTimecode(rawValuesAt: ._23_976) - .invalidComponents // == [.seconds, .frames] -``` - -#### Timecode Validation: NSAttributedString - -This method can produce an `NSAttributedString` highlighting individual invalid timecode components with a specified set of attributes. - -```swift -TCC(h: 1, m: 20, s: 75, f: 60) - .toTimecode(rawValuesAt: ._23_976) - .stringValueValidated -``` - -The invalid formatting attributes defaults to applying `[.foregroundColor: NSColor.red]` to invalid components. You can alternatively supply your own invalid attributes by setting the `invalidAttributes` argument. - -You can also supply a set of default attributes to set as the baseline attributes for the entire string. - -```swift -// set text's background color to red instead of its foreground color -let invalidAttr: [NSAttributedString.Key: Any] = - [.backgroundColor: NSColor.red] - -// set custom font and font size for the entire string -let defaultAttr: [NSAttributedString.Key: Any] = - [.font: NSFont.systemFont(ofSize: 16)] - -TCC(h: 1, m: 20, s: 75, f: 60) - .toTimecode(rawValuesAt: ._23_976) - .stringValueValidated(invalidAttributes: invalidAttr, - withDefaultAttributes: defaultAttr) -``` - -#### Timecode Validation: SwiftUI Text - -This method can produce a SwiftUI `Text` view highlighting individual invalid timecode components with a specified set of modifiers. - -```swift -TCC(h: 1, m: 20, s: 75, f: 60) - .toTimecode(rawValuesAt: ._23_976) - .stringValueValidatedText() -``` - -The invalid formatting attributes defaults to applying `.foregroundColor(Color.red)` to invalid components. You can alternatively supply your own invalid modifiers by setting the `invalidModifiers` argument. - -```swift -TCC(h: 1, m: 20, s: 75, f: 60) - .toTimecode(rawValuesAt: ._23_976) - .stringValueValidatedText( - invalidModifiers: { - $0.foregroundColor(.blue) - }, withDefaultModifiers: { - $0.foregroundColor(.black) - } - ) -``` - -#### Timecode Validation: NSFormatter - -A special string `Formatter` (`NSFormatter`) subclass can - -- process user-entered timecode strings and format them in realtime in a TextField -- optionally highlight individual invalid timecode components with a specified set of attributes (defaults to red foreground color) - -The invalid formatting attributes defaults to applying `[.foregroundColor: NSColor.red]` to invalid components. You can alternatively supply your own invalid attributes by setting the `validationAttributes` property on the formatter. - -```swift -// set up formatter -let tcFormatter = - Timecode.TextFormatter(frameRate: ._23_976, - limit: ._24hours, - stringFormat: [.showSubFrames], - subFramesBase: ._80SubFrames, - showsValidation: true, // enable invalid component highlighting - validationAttributes: nil) // if nil, defaults to red foreground color - -// assign formatter to a TextField UI object, for example -let textField = NSTextField() -textField.formatter = tcFormatter -``` - -### Advanced - -#### Days Component - -Some DAWs (digital audio workstation) such as Cubase supports the use of the Days timecode component when deemed appropriate. - -By default, `Timecode` is constructed with an `upperLimit` of 24-hour maximum expression (`._24hours`) which suppresses the ability to use Days. To enable Days, set the limit to `._100days`. - -The limit setting naturally affects internal timecode validation routines, as well as clamping and wrapping. - -```swift -// valid timecode range at 24 fps, ._24hours -"00:00:00:00" ... "23:59:59:23" - -// valid timecode range at 24 fps, ._100days -"00:00:00:00" ... "99 23:59:59:23" -``` - -#### Subframes Component - -Subframes represent a fraction (subdivision) of a single frame. - -Subframes are only used by some software and hardware, and since there are no industry standards, each manufacturer can decide how they want to implement subframes. Subframes are frame rate agnostic, meaning the subframe base (divisor) is mutually exclusive of frame rate. - -For example: - -- *Cubase/Nuendo* and *Logic Pro* globally use 80 subframes per frame (0...79) regardless of frame rate -- *Pro Tools* uses 100 subframes (0...99) globally regardless of frame rate - -Timecode supports subframes throughout. However, by default subframes are not displayed in `stringValue`. You can enable them: - -```swift -var tc = try "01:12:20:05.62" - .toTimecode(at: ._24, base: ._80SubFrames) - -tc.stringValue // == "01:12:20:05" -tc.subFrames // == 62 (subframes are preserved even though not displayed in stringValue) - -tc.stringFormat.showSubFrames = true // default: false - -tc.stringValue // == "01:12:20:05.62" -``` - -Subframes are always calculated when performing operations on the `Timecode` instance, regardless whether `displaySubFrames` set or not. - -```swift -var tc = try "00:00:00:00.40" - .toTimecode(at: ._24, base: ._80SubFrames) - -tc.stringValue // == "00:00:00:00" - -tc.stringFormat.showSubFrames = true // default: false -tc.stringValue // == "00:00:00:00.40" - -// multiply timecode by 2. 40 subframes is half of a frame at 80 subframes per frame -(tc * 2).stringValue // == "00:00:00:01.00" -``` - -It is also possible to set this flag during construction. - -```swift -var tc = try "01:12:20:05.62" - .toTimecode(at: ._24, base: ._80SubFrames, format: [.showSubFrames]) - -tc.stringValue // == "01:12:20:05.62" -``` - -#### Comparable - -Two `Timecode` instances can be compared linearly using common comparison operators. - -```swift -try "01:00:00:00".toTimecode(at: ._24) - == try "01:00:00:00".toTimecode(at: ._24) // == true - -try "00:59:50:00".toTimecode(at: ._24) - < "01:00:00:00".toTimecode(at: ._24) // == true - -try "00:59:50:00".toTimecode(at: ._24) - > "01:00:00:00".toTimecode(at: ._24) // == false -``` - -#### Compare using Timeline Context - -Sometimes a timeline does not have a zero start time (00:00:00:00). For example, many DAW applications such as Pro Tools allow a project start time to be set to any timecode. Its timeline then extends for 24 hours from that timecode, wrapping over 00:00:00:00 at some point along the timeline. - -For example, given a 24 hour limit: - -- A timeline start of 00:00:00:00 @ 24fps: - - 24 hours elapses from 00:00:00:00 → 23:59:59:23 - -- A timeline start of 20:00:00:00 @ 24fps: - - 24 hours elapses from 20:00:00:00 → 00:00:00:00 → 19:59:59:23 - - This would mean for example, that 21:00:00:00 is < 00:00:00:00 since it is earlier in the wrapping timeline, and 18:00:00:00 is > 21:00:00:00 since it is later in the wrapping timeline. - -Methods to sort and test sort order of `Timecode` collections are provided. - -Note that passing `timelineStart` of `nil` or zero (00:00:00:00) is the same as using the standard `<`, `==`, or `>` operators as a sort comparator. - -```swift -let timecode1: Timecode -let timecode2: Timecode -let start: Timecode -let result = timecode1.compare(to: timecode2, timelineStart: start) -// result is a ComparisonResult of orderedAscending, orderedSame, or orderedDescending -``` - -#### Sorting - -Collections of `Timecode` can be sorted ascending or descending. - -```swift -let timeline: [Timecode] = [ ... ] -let sorted: [Timecode] = timeline.sorted() // ascending -let sorted: [Timecode] = timeline.sorted(ascending: false) // descending -``` - -These collections can also be tested for sort order: - -```swift -let timeline: [Timecode] = [ ... ] -let isSorted: Bool = timeline.isSorted() // ascending -let isSorted: Bool = timeline.isSorted(ascending: false) // descending -``` - -On newer systems, a `SortComparator` called `TimecodeSortComparator` is available as well. - -```swift -let comparator = TimecodeSortComparator() // ascending -let comparator = TimecodeSortComparator(order: .reverse) // descending - -let timeline: [Timecode] = [ ... ] -let sorted: [Timecode] = timeline.sorted(using: comparator) -``` - -#### Sorting using Timeline Context - -For an explanation of timeline context, see the [Compare using Timeline Context](#Compare-using-Timeline-Context) section above. - -Collections of `Timecode` can be sorted ascending or descending. - -```swift -let timeline: [Timecode] = [ ... ] -let start = try "01:00:00:00".toTimecode(at: ._24) -let sorted: [Timecode] = timeline.sorted(timelineStart: start) // ascending -let sorted: [Timecode] = timeline.sorted(order: .reverse, timelineStart: start) // descending -``` - -These collections can also be tested for sort order: - -```swift -let timeline: [Timecode] = [ ... ] -let start = try "01:00:00:00".toTimecode(at: ._24) -let isSorted: Bool = timeline.isSorted(timelineStart: start) // ascending -let isSorted: Bool = timeline.isSorted(order: .reverse, timelineStart: start) // descending -``` - -On newer systems, a `SortComparator` called `TimecodeSortComparator` is available as well. - -```swift -let start = try "01:00:00:00".toTimecode(at: ._24) -let comparator = TimecodeSortComparator(timelineStart: start) // ascending -let comparator = TimecodeSortComparator(order: .reverse, timelineStart: start) // descending - -let timeline: [Timecode] = [ ... ] -let sorted: [Timecode] = timeline.sorted(using: comparator) -``` - -#### Range, Strideable - -A `Stride` or `Range` can be formed between two `Timecode` instances. - -```swift -let startTC = try "01:00:00:00".toTimecode(at: ._24) -let endTC = try "01:00:00:10".toTimecode(at: ._24) -``` - -Range: - -```swift -// check if a timecode is contained within the range - -(startTC...endTC).contains(try "01:00:00:05".toTimecode(at: ._24)) // == true -(startTC...endTC).contains(try "01:05:00:00".toTimecode(at: ._24)) // == false -``` - -```swift -// iterate on each frame of the range - -for tc in startTC...endTC { - print(tc) -} - -// prints: -01:00:00:00 -01:00:00:01 -01:00:00:02 -01:00:00:03 -01:00:00:04 -01:00:00:05 -01:00:00:06 -01:00:00:07 -01:00:00:08 -01:00:00:09 -01:00:00:10 -``` - -Stride: - -```swift -// iterate on every 5 frames of the range by using a stride - -for tc in stride(from: startTC, to: endTC, by: 5) { - print(tc) -} - -// prints: -01:00:00:00 -01:00:00:05 -01:00:00:10 -``` - -#### Rational Number Expression - -Video file metadata and timeline interchange files (AAF, Final Cut Pro XML) encode frame rate and timecode as rational numbers (a fraction consisting of two integers - a numerator and a denominator). - -`Timecode` is capable of initializing from an elapsed time expressed as a rational fraction using the `init?(rational:)` initializer. The `rationalValue` property returns the `Timecode`'s elapsed time expressed as a rational fraction. - -```swift -try Timecode(Fraction(1920919, 30000), at: ._29_97) - .stringValue // == "00:01:03;29" - -try Timecode(TCC(h: 00, m: 01, s: 03, f: 29), at: ._29_97) - .rationalValue // == Fraction(1920919, 30000) -``` - -`TimecodeFrameRate` and `VideoFrameRate` are both capable of initializing from a rational fraction, and also provide a `rationalRate` and `rationalFrameDuration` property that provides this fraction. - -Since drop-frame (timecode) or interlaced (video) attributes are not encodable in a rational fraction, they must be imperatively supplied. - -```swift -// fraction representing the duration of 1 frame -TimecodeFrameRate(frameDuration: Fraction(1001, 30000), drop: false) // == ._29_97 -// fraction representing the fps -TimecodeFrameRate(rate: Fraction(30000, 1001), drop: false) // == ._29_97 - -// fraction representing the duration of 1 frame -VideoFrameRate(frameDuration: Fraction(1001, 30000), interlaced: false) // == ._29_97p -// fraction representing the fps -VideoFrameRate(rate: Fraction(30000, 1001), interlaced: false) // == ._29_97p -``` - -#### CMTime Conversion - -`CMTime` is a type exported by the Core Media framework (and used pervasively in AVFoundation). It represents time as a rational fraction of a `value` in a `timescale`. - -`Timecode` and `TimecodeInterval`, as well as `TimecodeFrameRate` and `VideoFrameRate` can convert to/from `CMTime` using the respective inits and properties. - -`CMTime` and `Fraction` can convert between themselves as well with respective inits and properties. - -#### Timecode Intervals - -The `TimecodeInterval` struct wraps a `Timecode` instance and adds a sign (positive of negative). - -It serves to represent an absolute interval of timecode accompanied by a sign (+ / -) to establish the intent of the interval being *additive* or *subtractive* when passed into methods that accept a `TimecodeInterval` instance. - -`TimecodeInterval` also accepts intervals larger than 24 hours and works well with raw timecode values. - -On the whole, timecode itself is the expression of an absolute video timestamp, or used as a duration of video frames. The concept of a 'negative' timecode is antithetical; timecode is not meant to be expressed or displayed on-screen to the user using a negative sign. In practise, timecode wraps around the clock forwards and backwards: typically around a 24 hour clock but `Timecode` can be set to 100 day wrapping for unique cases. This means that, at 24 fps: - -- `00:00:00:00` minus 1 frame is `23:59:59:23` (and not `-00:00:00:01`) -- `23:59:59:23` plus 1 frame is `00:00:00:00` - -However, to meet the demand of some timecode calculations (such as offset transforms, theoretical calculations involving raw timecode values, or aggregate operations that may have otherwise resulted in wrapping the clock one or more times) `TimecodeInterval` is provided. - -```swift -// construct directly: -let tc = try Timecode(TCC(h: 1), at: ._24) -let interval = TimecodeInterval(tc, .negative) - -// construct with Timecode method: -let tc = try Timecode(TCC(h: 1), at: ._24) -let interval = tc.interval(.negative) - -// construct with - or + unary operator: -let interval = try -Timecode(TCC(h: 1), at: ._24) // negative -let interval = try +Timecode(TCC(h: 1), at: ._24) // positive - -// construct between two Timecode instances -let interval = timecode1.interval(to: timecode2) -``` - -The absolute interval can be returned. - -```swift -let tc = try Timecode(TCC(h: 1), at: ._24) - -let interval = TimecodeInterval(tc, .positive) // 01:00:00:00 -interval.absoluteInterval // 01:00:00:00 - -let interval = TimecodeInterval(tc, .negative) // -01:00:00:00 -interval.absoluteInterval // 01:00:00:00 -``` - -The interval can be flattened by wrapping it around the upper limit if necessary, which is 24 hours in timecode by default. - -```swift -let tc = try Timecode(TCC(h: 1), at: ._24) - -let interval = TimecodeInterval(tc, .positive) // 01:00:00:00 -interval.flattened() // 01:00:00:00 - -let interval = TimecodeInterval(tc, .negative) // -01:00:00:00 -interval.flattened() // 23:00:00:00 -``` - -#### Timecode Transformer - -`TimecodeTransformer` is a mechanism that can define one or more timecode transforms in series. It can then be used to transform a `Timecode` instance. - -#### Feet+Frames - -`FeetAndFrames` is a type used to convert feet+frames. Initializers and properties on `Timecode` are also available. - -#### AVAsset Timecode Track Read/Write - -##### Read Timecode from QuickTime Movie - -Simple methods to read start timecode and duration from `AVAsset` and its subclasses (`AVMovie`) as well as `AVAssetTrack` are provided by TimecodeKit. The methods are throwing since timecode information is not guaranteed to be present inside movie files. - -```swift -let asset = AVAsset( ... ) - -// auto-detect frame rate if it's embedded in the file -let frameRate = try asset.timecodeFrameRate() // ie: ._29_97 - -// read start timecode, auto-detecting frame rate -let startTimecode = try asset.startTimecode() -// read start timecode, forcing a known frame rate -let startTimecode = try asset.startTimecode(at: ._29_97) - -// read video duration expressed as timecode -let durationTimecode = try asset.durationTimecode() -// read video duration expressed as timecode, forcing a known frame rate -let durationTimecode = try asset.durationTimecode(at: ._29_97) - -// read end timecode, auto-detecting frame rate -let endTimecode = try asset.endTimecode() -// read end timecode, forcing a known frame rate -let endTimecode = try asset.endTimecode(at: ._29_97) -``` - -##### Add or Replace Timecode Track in a QuickTime Movie - -Currently timecode tracks can be modified on `AVMutableMovie`. +See the [online documentation](https://orchetect.github.io/TimecodeKit) for library usage, getting started info, and 1.x → 2.x migration guide. -```swift -let movie = AVMutableMovie( ... ) +## Known Issues -// replace existing timecode track if it exists, otherwise add a new timecode track -try movie.replaceTimecodeTrack( - startTimecode: Timecode(TCC(h: 0, m: 59, s: 58, f: 00), at: ._29_97), - duration: Timecode(TCC(h: 1), at: ._29_97), - fileType: .mov -) -``` +- As of iOS 17, Apple appears to have introduced a regression when using `AVAssetExportSession` to save a QuickTime movie file when using a physical iOS device (simulator works fine). A radar has been filed with Apple (FB12986599). This is not an issue with TimecodeKit itself. However, until Apple fixes this bug it will affect saving a movie file after performing timecode track modifications. See [this thread](https://github.com/orchetect/TimecodeKit/discussions/63) for details. ## References @@ -780,4 +110,4 @@ Please do not email maintainers for technical support. Several options are avail ## Contributions -Contributions are welcome. Feel free to post an Issue to discuss. +Contributions are welcome. Feel free to post in Discussions first before submitting a PR. diff --git a/Sources/TimecodeKit/API Evolution/API-1.3.0.swift b/Sources/TimecodeKit/API Evolution/API-1.3.0.swift deleted file mode 100644 index 675cc563..00000000 --- a/Sources/TimecodeKit/API Evolution/API-1.3.0.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// API-1.3.0.swift -// TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License -// - -import Foundation - -// MARK: API Changes in TimecodeKit 1.3.0 - -// MARK: TimecodeInterval - -extension Timecode { - @available(*, deprecated, renamed: "TimecodeInterval") - public typealias Delta = TimecodeInterval - - @available(*, deprecated, renamed: "TimecodeTransformer") - public typealias Transformer = TimecodeTransformer - - /// Returns a ``TimecodeInterval`` distance between the current timecode and another timecode. - @available(*, deprecated, renamed: "interval(to:)") - public func delta(to other: Timecode) -> TimecodeInterval { - interval(to: other) - } -} - -extension TimecodeInterval { - /// The interval's absolute distance, stripping sign negation if present. - /// The ``isNegative`` property determines the delta direction of the interval. - @available(*, deprecated, renamed: "absoluteInterval") - public var delta: Timecode { absoluteInterval } - - /// Flattens the interval and returns it expressed as valid timecode, wrapping as necessary based on the ``Timecode/upperLimit-swift.property`` of the interval. - /// - /// If the interval is already valid timecode and the sign is positive, the interval is returned as-is. - @available(*, deprecated, renamed: "flattened()") - public var timecode: Timecode { - flattened() - } -} diff --git a/Sources/TimecodeKit/API Evolution/API-1.5.0.swift b/Sources/TimecodeKit/API Evolution/API-1.5.0.swift deleted file mode 100644 index a7126fd9..00000000 --- a/Sources/TimecodeKit/API Evolution/API-1.5.0.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// API-1.5.0.swift -// TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License -// - -import Foundation - -// MARK: API Changes in TimecodeKit 1.5.0 - -// MARK: Timecode FrameCount - -extension Timecode { - @available(*, deprecated, renamed: "components(of:at:)") - public static func components( - from frameCount: FrameCount, - at frameRate: TimecodeFrameRate - ) -> Components { - components(of: frameCount, at: frameRate) - } -} - - -// MARK: Real Time - -extension Timecode { - @available(*, deprecated, renamed: "init(realTime:at:limit:base:format:)") - public init( - realTimeValue exactly: TimeInterval, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) throws { - try self.init( - realTime: exactly, - at: rate, - limit: limit, - base: base, - format: format - ) - } - - @available(*, deprecated, renamed: "setTimecode(realTime:)") - public mutating func setTimecode(fromRealTimeValue: TimeInterval) throws { - try setTimecode(realTime: fromRealTimeValue) - } -} - -// MARK: Samples - -extension Timecode { - @available(*, deprecated, renamed: "samplesDoubleValue(sampleRate:)") - public func samplesValue(atSampleRate: Int) -> Double { - samplesDoubleValue(sampleRate: atSampleRate) - } - - @available(*, deprecated, renamed: "setTimecode(samples:sampleRate:)") - public mutating func setTimecode( - fromSamplesValue: Double, - atSampleRate: Int - ) throws { - try setTimecode( - samples: fromSamplesValue, - sampleRate: atSampleRate - ) - } -} diff --git a/Sources/TimecodeKit/API Evolution/API-1.6.0.swift b/Sources/TimecodeKit/API Evolution/API-1.6.0.swift deleted file mode 100644 index 436de709..00000000 --- a/Sources/TimecodeKit/API Evolution/API-1.6.0.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// API-1.6.0.swift -// TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License -// - -import Foundation - -// MARK: API Changes in TimecodeKit 1.6.0 - -// MARK: Timecode.FrameRate - -extension Timecode { - @available(*, deprecated, renamed: "TimecodeFrameRate") - public enum FrameRate: String, CaseIterable, Codable { - case _23_976 = "23.976" - case _24 = "24" - case _24_98 = "24.98" - case _25 = "25" - case _29_97 = "29.97" - case _29_97_drop = "29.97d" - case _30 = "30" - case _30_drop = "30d" - case _47_952 = "47.952" - case _48 = "48" - case _50 = "50" - case _59_94 = "59.94" - case _59_94_drop = "59.94d" - case _60 = "60" - case _60_drop = "60d" - case _100 = "100" - case _119_88 = "119.88" - case _119_88_drop = "119.88d" - case _120 = "120" - case _120_drop = "120d" - - // Identifiable - public var id: String { - rawValue - } - } -} - -// MARK: Timecode.FrameRate - -extension TimecodeFrameRate { - @available(*, unavailable, renamed: "VideoFrameRate(fps:)") - @_disfavoredOverload - public init?( - raw fps: Float, - favorDropFrame: Bool = false - ) { fatalError() } - - @available(*, unavailable, renamed: "VideoFrameRate(fps:)") - public init?( - raw fps: Double, - favorDropFrame: Bool = false - ) { fatalError() } - - @available(*, deprecated, renamed: "rate") - public var fraction: (numerator: Int, denominator: Int) { - ( - numerator: rate.numerator, - denominator: rate.denominator - ) - } -} - -extension String { - @_disfavoredOverload - @available(*, deprecated, renamed: "toTimecodeFrameRate") - public var toFrameRate: TimecodeFrameRate? { toTimecodeFrameRate } -} diff --git a/Sources/TimecodeKit/API Evolution/API-1.6.7.swift b/Sources/TimecodeKit/API Evolution/API-1.6.7.swift deleted file mode 100644 index 26f6ca14..00000000 --- a/Sources/TimecodeKit/API Evolution/API-1.6.7.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// API-1.6.7.swift -// TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License -// - -import Foundation - -// MARK: API Changes in TimecodeKit 1.6.7 - -// MARK: Timecode.FrameRate - -extension VideoFrameRate { - @available(*, deprecated, renamed: "_48p") - public static let _48 = Self._48p -} diff --git a/Sources/TimecodeKit/API Evolution/API-2.0.0.swift b/Sources/TimecodeKit/API Evolution/API-2.0.0.swift new file mode 100644 index 00000000..da93fb91 --- /dev/null +++ b/Sources/TimecodeKit/API Evolution/API-2.0.0.swift @@ -0,0 +1,1377 @@ +// +// API-2.0.0.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + +import Foundation + +// MARK: API Changes in TimecodeKit 2.0.0 + +// MARK: - UpperLimit.swift + +extension Timecode.UpperLimit { + @available(*, renamed: "_24Hours") + public static let _24hours: Self = .max24Hours + + @available(*, renamed: "_100Days") + public static let _100days: Self = .max100Days +} + +// MARK: - Additional Deprecations + +// NOTE: +// These are disabled because the API changes from 1.x to 2.x were too extensive to fully/properly +// implement using @available() attributes and was actually causing issues with Xcode's autocomplete +// in the IDE's code editor. +// So instead, a 1.x -> 2.x Migration Guide was written and included in TimecodeKit 2's documentation. + +#if ENABLE_EXTENDED_API_DEPRECATIONS + +#if os(macOS) +import AppKit +#elseif os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) +import UIKit +#endif + +// MARK: - TimecodeFrameRate CompatibleGroup.swift + +extension TimecodeFrameRate.CompatibleGroup { + @available(*, deprecated, renamed: "ntscColor", message: "Renamed to lower camel case.") + public static let NTSC: Self = .ntscColor + + @available(*, deprecated, renamed: "ntscDrop", message: "Renamed to lower camel case.") + public static let NTSC_drop: Self = .ntscDrop + + @available(*, deprecated, renamed: "whole", message: "Renamed to lower camel case.") + public static let ATSC: Self = .whole + + @available(*, deprecated, renamed: "ntscColorWallTime", message: "Renamed to lower camel case.") + public static let ATSC_drop: Self = .ntscColorWallTime +} + +// MARK: - Timecode String.swift + +extension Timecode { + @_disfavoredOverload + @available( + *, + deprecated, + renamed: "stringValueValidated(format:invalidAttributes:defaultAttributes:)", + message: "`withDefaultAttributes` parameter has been renamed to `defaultAttributes`." + ) + public func stringValueValidated( + format: StringFormat = .default(), + invalidAttributes: [NSAttributedString.Key: Any]? = nil, + withDefaultAttributes attrs: [NSAttributedString.Key: Any]? = nil + ) -> NSAttributedString { + stringValueValidated( + format: format, + invalidAttributes: invalidAttributes, + defaultAttributes: attrs + ) + } +} + +// MARK: - TCC + +@available( + *, + deprecated, + message: "TCC() is removed in TimecodeKit 2.x. Use Timecode(.components(), at:) instead." +) +public typealias TCC = Timecode.Components + +// MARK: - Inits + +extension Timecode { + @available(*, deprecated, message: "Renamed to Timecode(.zero, using:)") + public init( + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.zero, using: properties) + } + + @available(*, deprecated, message: "Renamed to Timecode(.interval(flattening:))") + public init( + flattening interval: TimecodeInterval + ) { + self.init(.interval(flattening: interval)) + } +} + +// MARK: - Math + +extension Timecode { + // MARK: Add + + @available(*, deprecated, message: "Renamed to add(_:, by: .clamping)") + public mutating func add(clamping values: Components) { + add(values, by: .clamping) + } + + @available(*, deprecated, message: "Renamed to add(_:, by: .wrapping)") + public mutating func add(wrapping values: Components) { + add(values, by: .wrapping) + } + + @available(*, deprecated, message: "Renamed to adding(_:, by: .clamping)") + public func adding(clamping values: Components) -> Timecode { + adding(values, by: .clamping) + } + + @available(*, deprecated, message: "Renamed to adding(_:, by: .wrapping)") + public func adding(wrapping values: Components) -> Timecode { + adding(values, by: .wrapping) + } + + // MARK: Subtract + + @available(*, deprecated, message: "Renamed to subtract(_:, by: .clamping)") + public mutating func subtract(clamping values: Components) { + subtract(values, by: .clamping) + } + + @available(*, deprecated, message: "Renamed to subtract(_:, by: .wrapping)") + public mutating func subtract(wrapping values: Components) { + subtract(values, by: .wrapping) + } + + @available(*, deprecated, message: "Renamed to subtracting(_:, by: .clamping)") + public func subtracting(clamping values: Components) -> Timecode { + subtracting(values, by: .clamping) + } + + @available(*, deprecated, message: "Renamed to subtracting(_:, by: .wrapping)") + public func subtracting(wrapping values: Components) -> Timecode { + subtracting(values, by: .wrapping) + } + + // MARK: Multiply + + @available(*, deprecated, message: "Renamed to multiply(_:, by: .clamping)") + public mutating func multiply(clamping value: Double) { + multiply(value, by: .clamping) + } + + @available(*, deprecated, message: "Renamed to multiply(_:, by: .wrapping)") + public mutating func multiply(wrapping value: Double) { + multiply(value, by: .wrapping) + } + + @available(*, deprecated, message: "Renamed to multiplying(_:, by: .clamping)") + public func multiplying(clamping value: Double) -> Timecode { + multiplying(value, by: .clamping) + } + + @available(*, deprecated, message: "Renamed to multiplying(_:, by: .wrapping)") + public func multiplying(wrapping value: Double) -> Timecode { + multiplying(value, by: .wrapping) + } + + // MARK: Divide + + @available(*, deprecated, message: "Renamed to divide(_:, by: .clamping)") + public mutating func divide(clamping value: Double) { + divide(value, by: .clamping) + } + + @available(*, deprecated, message: "Renamed to divide(_:, by: .wrapping)") + public mutating func divide(wrapping value: Double) { + divide(value, by: .wrapping) + } + + @available(*, deprecated, message: "Renamed to dividing(_:, by: .clamping)") + public func dividing(clamping value: Double) -> Timecode { + dividing(value, by: .clamping) + } + + @available(*, deprecated, message: "Renamed to dividing(_:, by: .wrapping)") + public func dividing(wrapping value: Double) -> Timecode { + dividing(value, by: .wrapping) + } +} + +// MARK: - AVAsset + +// AVAssetReader is unavailable on watchOS so we can't support any AVAsset operations +#if canImport(AVFoundation) && !os(watchOS) + +import AVFoundation +import Foundation + +extension AVAsset { + @available(*, deprecated, renamed: "startTimecode(at:base:limit:)") + @_disfavoredOverload + public func startTimecode( + at frameRate: TimecodeFrameRate? = nil, + base: Timecode.SubFramesBase = .default(), + limit: Timecode.UpperLimit = .max24Hours, + format: Timecode.StringFormat + ) throws -> Timecode? { + try startTimecode(at: frameRate, base: base, limit: limit) + } + + @available(*, deprecated, renamed: "endTimecode(at:base:limit:)") + @_disfavoredOverload + public func endTimecode( + at frameRate: TimecodeFrameRate? = nil, + base: Timecode.SubFramesBase = .default(), + limit: Timecode.UpperLimit = .max24Hours, + format: Timecode.StringFormat + ) throws -> Timecode? { + try endTimecode(at: frameRate, base: base, limit: limit) + } + + @available(*, deprecated, renamed: "durationTimecode(at:base:limit:)") + @_disfavoredOverload + public func durationTimecode( + at frameRate: TimecodeFrameRate? = nil, + limit: Timecode.UpperLimit = .max24Hours, + base: Timecode.SubFramesBase = .default(), + format: Timecode.StringFormat = .default() + ) throws -> Timecode { + try durationTimecode(at: frameRate, base: base, limit: limit) + } +} + +extension Timecode { + @available(*, deprecated, message: "Renamed to Timecode(.avAsset(asset, .start))") + public init( + startOf asset: AVAsset, + at rate: TimecodeFrameRate? = nil, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) throws { + if let rate = rate { + let properties = Properties(rate: rate, base: base, limit: limit) + try self.init(.avAsset(asset, .start), using: properties) + } else { + try self.init(.avAsset(asset, .start)) + } + } + + @available(*, deprecated, message: "Renamed to Timecode(.avAsset(asset, .end))") + public init( + endOf asset: AVAsset, + at rate: TimecodeFrameRate? = nil, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) throws { + if let rate = rate { + let properties = Properties(rate: rate, base: base, limit: limit) + try self.init(.avAsset(asset, .start), using: properties) + } else { + try self.init(.avAsset(asset, .start)) + } + } + + @available(*, deprecated, message: "Renamed to Timecode(.avAsset(asset, .duration))") + public init( + durationOf asset: AVAsset, + at rate: TimecodeFrameRate? = nil, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) throws { + if let rate = rate { + let properties = Properties(rate: rate, base: base, limit: limit) + try self.init(.avAsset(asset, .duration), using: properties) + } else { + try self.init(.avAsset(asset, .duration)) + } + } +} + +#endif + +// MARK: - AVAssetTrack + +// AVAssetReader is unavailable on watchOS so we can't support any AVAsset operations +#if canImport(AVFoundation) && !os(watchOS) + +import AVFoundation +import Foundation + +extension AVAssetTrack { + @available(*, deprecated, renamed: "durationTimecode(at:limit:base:)") + @_disfavoredOverload + public func durationTimecode( + at frameRate: TimecodeFrameRate? = nil, + limit: Timecode.UpperLimit = .max24Hours, + base: Timecode.SubFramesBase = .default(), + format: Timecode.StringFormat = .default() + ) throws -> Timecode { + try durationTimecode(at: frameRate, limit: limit, base: base) + } +} + +#endif + +// MARK: - Components + +extension Timecode { + @available(*, deprecated, message: "Renamed to Timecode(.components(), at:, base:, limit:)") + @_disfavoredOverload + public init( + _ exactly: Components, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) throws { + let properties = Properties(rate: rate, base: base, limit: limit) + try self.init(.components(exactly), using: properties) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.components(), at:, base:, limit:, by: .clamping)" + ) + public init( + clamping rawValues: Components, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.components(rawValues), using: properties, by: .clamping) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.components(), at:, base:, limit:, by: .clampingComponents)" + ) + public init( + clampingEach rawValues: Components, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.components(rawValues), using: properties, by: .clampingComponents) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.components(), at:, base:, limit:, by: .wrapping)" + ) + public init( + wrapping rawValues: Components, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.components(rawValues), using: properties, by: .wrapping) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.components(), at:, base:, limit:, by: .allowingInvalid)" + ) + public init( + rawValues: Components, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.components(rawValues), using: properties, by: .allowingInvalid) + } +} + +extension Timecode { + @available(*, deprecated, message: "Renamed to set(.components())") + public mutating func setTimecode(exactly values: Components) throws { + try set(values) + } + + @available(*, deprecated, message: "Renamed to set(.components(), by: .clamping)") + public mutating func setTimecode(clamping values: Components) { + set(values, by: .clamping) + } + + @available(*, deprecated, message: "Renamed to set(.components(), by: .clampingComponents)") + public mutating func setTimecode(clampingEach values: Components) { + set(values, by: .clampingComponents) + } + + @available(*, deprecated, message: "Renamed to set(.components(), by: .wrapping)") + public mutating func setTimecode(wrapping values: Components) { + set(values, by: .wrapping) + } + + @available(*, deprecated, message: "Renamed to set(.components(), by: .allowingInvalid)") + public mutating func setTimecode(rawValues values: Components) { + set(values, by: .allowingInvalid) + } +} + +extension Timecode.Components { + @available(*, deprecated, renamed: "timecode(at:base:limit:)") + public func toTimecode( + at rate: TimecodeFrameRate, + limit: Timecode.UpperLimit = .max24Hours, + base: Timecode.SubFramesBase = .default(), + format: Timecode.StringFormat = .default() + ) throws -> Timecode { + let properties = Timecode.Properties(rate: rate, base: base, limit: limit) + return try timecode(using: properties) + } + + @available(*, deprecated, message: "Renamed to timecode(at:base:limit: by: .allowingInvalid)") + public func toTimecode( + rawValuesAt rate: TimecodeFrameRate, + limit: Timecode.UpperLimit = .max24Hours, + base: Timecode.SubFramesBase = .default(), + format: Timecode.StringFormat = .default() + ) -> Timecode { + let properties = Timecode.Properties(rate: rate, base: base, limit: limit) + return timecode(using: properties, by: .allowingInvalid) + } + + @available(*, deprecated, renamed: "invalidComponents(at:base:limit:)") + public func invalidComponents( + at rate: TimecodeFrameRate, + limit: Timecode.UpperLimit, + base: Timecode.SubFramesBase + ) -> Set { + let properties = Timecode.Properties(rate: rate, base: base, limit: limit) + return invalidComponents(using: properties) + } +} + +// MARK: - FeetAndFrames + +extension Timecode { + @available(*, deprecated, message: "Renamed to Timecode(.feetAndFrames(), at:, base:, limit:)") + @_disfavoredOverload + public init( + _ exactly: FeetAndFrames, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) throws { + let properties = Properties(rate: rate, base: base, limit: limit) + try self.init(.feetAndFrames(exactly), using: properties) + } +} + +extension Timecode { + @available(*, deprecated, message: "Renamed to set(.feetAndFrames())") + public mutating func setTimecode(exactly feetAndFrames: FeetAndFrames) throws { + try set(feetAndFrames) + } +} + +// MARK: - FrameCount Value + +extension Timecode { + @available(*, deprecated, message: "Renamed to Timecode(.frames(), at:, base:, limit:)") + @_disfavoredOverload + public init( + _ exactly: FrameCount.Value, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) throws { + let properties = Properties(rate: rate, base: base, limit: limit) + try self.init(.frames(exactly), using: properties) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.frames(), at:, base:, limit:, by: .clamping)" + ) + public init( + clamping source: FrameCount.Value, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.frames(source), using: properties, by: .clamping) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.frames(), at:, base:, limit:, by: .wrapping)" + ) + public init( + wrapping source: FrameCount.Value, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.frames(source), using: properties, by: .wrapping) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.frames(), at:, base:, limit:, by: .allowingInvalid)" + ) + public init( + rawValues source: FrameCount.Value, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.frames(source), using: properties, by: .allowingInvalid) + } +} + +extension Timecode { + @available(*, deprecated, message: "Renamed to set(.frames())") + public mutating func setTimecode(exactly frameCountValue: FrameCount.Value) throws { + try set(frameCountValue) + } + + @available(*, deprecated, message: "Renamed to set(.frames(), by: .clamping)") + public mutating func setTimecode(clamping source: FrameCount.Value) { + set(source, by: .clamping) + } + + @available(*, deprecated, message: "Renamed to set(.frames(), by: .wrapping)") + public mutating func setTimecode(wrapping source: FrameCount.Value) { + set(source, by: .wrapping) + } + + @available(*, deprecated, message: "Renamed to set(.frames(), by: .allowingInvalid)") + public mutating func setTimecode(rawValues source: FrameCount.Value) { + set(source, by: .allowingInvalid) + } +} + +// MARK: - FrameCount + +extension Timecode { + @available(*, deprecated, message: "Renamed to Timecode(.frames(), at:, base:, limit:)") + @_disfavoredOverload + public init( + _ exactly: FrameCount, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + format: StringFormat = .default() + ) throws { + let properties = Properties(rate: rate, base: .default(), limit: limit) + try self.init(.frames(exactly), using: properties) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.frames(), at:, base:, limit:, by: .clamping)" + ) + public init( + clamping source: FrameCount, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: .default(), limit: limit) + self.init(.frames(source), using: properties, by: .clamping) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.frames(), at:, base:, limit:, by: .wrapping)" + ) + public init( + wrapping source: FrameCount, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: .default(), limit: limit) + self.init(.frames(source), using: properties, by: .wrapping) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.frames(), at:, base:, limit:, by: .allowingInvalid)" + ) + public init( + rawValues source: FrameCount, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: .default(), limit: limit) + self.init(.frames(source), using: properties, by: .allowingInvalid) + } +} + +extension Timecode { + @available(*, deprecated, message: "Renamed to set(.frames(count:))") + public mutating func setTimecode(exactly source: FrameCount) throws { + try set(source) + } + + @available(*, deprecated, message: "Renamed to set(.frames(count:), by: .clamping)") + public mutating func setTimecode(clamping source: FrameCount) { + set(source, by: .clamping) + } + + @available(*, deprecated, message: "Renamed to set(.frames(count:), by: .clamping)") + public mutating func setTimecode(wrapping source: FrameCount) { + set(source, by: .wrapping) + } + + @available(*, deprecated, message: "Renamed to set(.frames(count:), by: .clamping)") + public mutating func setTimecode(rawValues source: FrameCount) { + set(source, by: .allowingInvalid) + } +} + +// MARK: - CMTime + +#if canImport(CoreMedia) + +import CoreMedia +import Foundation + +@available(macOS 10.7, iOS 4.0, tvOS 9.0, watchOS 6.0, *) +extension Timecode { + @available(*, deprecated, message: "Renamed to Timecode(.cmTime(), at:, base:, limit:)") + @_disfavoredOverload + public init( + _ source: CMTime, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) throws { + let properties = Properties(rate: rate, base: base, limit: limit) + try self.init(.cmTime(source), using: properties) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.cmTime(), at:, base:, limit:, by: .clamping)" + ) + public init( + clamping source: CMTime, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.cmTime(source), using: properties, by: .clamping) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.cmTime(), at:, base:, limit:, by: .wrapping)" + ) + public init( + wrapping source: CMTime, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.cmTime(source), using: properties, by: .wrapping) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.cmTime(), at:, base:, limit:, by: .allowingInvalid)" + ) + public init( + rawValues source: CMTime, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.cmTime(source), using: properties, by: .allowingInvalid) + } +} + +@available(macOS 10.7, iOS 4.0, tvOS 9.0, watchOS 6.0, *) +extension Timecode { + @available(*, deprecated, message: "Renamed to set(.cmTime())") + public mutating func setTimecode(_ exactly: CMTime) throws { + try set(exactly) + } + + @available(*, deprecated, message: "Renamed to set(.cmTime(), by: .clamping)") + public mutating func setTimecode(clamping source: CMTime) { + set(source, by: .clamping) + } + + @available(*, deprecated, message: "Renamed to set(.cmTime(), by: .wrapping)") + public mutating func setTimecode(wrapping source: CMTime) { + set(source, by: .wrapping) + } + + @available(*, deprecated, message: "Renamed to set(.cmTime(), by: .allowingInvalid)") + public mutating func setTimecode(rawValues source: CMTime) { + set(source, by: .allowingInvalid) + } +} + +@available(macOS 10.7, iOS 4.0, tvOS 9.0, watchOS 6.0, *) +extension CMTime { + @available(*, deprecated, renamed: "timecode(at:base:limit:)") + public func toTimecode( + at rate: TimecodeFrameRate, + limit: Timecode.UpperLimit = .max24Hours, + base: Timecode.SubFramesBase = .default(), + format: Timecode.StringFormat = .default() + ) throws -> Timecode { + let properties = Timecode.Properties(rate: rate, base: base, limit: limit) + return try timecode(using: properties) + } +} + +#endif + +// MARK: - Fraction + +extension Timecode { + @available(*, deprecated, message: "Renamed to Timecode(.rational(), at:, base:, limit:)") + @_disfavoredOverload + public init( + _ exactly: Fraction, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) throws { + let properties = Properties(rate: rate, base: base, limit: limit) + try self.init(.rational(exactly), using: properties) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.rational(), at:, base:, limit:, by: .clamping)" + ) + public init( + clamping source: Fraction, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.rational(source), using: properties, by: .clamping) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.rational(), at:, base:, limit:, by: .wrapping)" + ) + /// fractions.) + public init( + wrapping source: Fraction, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.rational(source), using: properties, by: .wrapping) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.rational(), at:, base:, limit::, by: .allowingInvalid)" + ) + public init( + rawValues source: Fraction, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.rational(source), using: properties, by: .allowingInvalid) + } +} + +extension Timecode { + @available(*, deprecated, message: "Renamed to set(.rational())") + public mutating func setTimecode(_ exactly: Fraction) throws { + try set(exactly) + } + + @available(*, deprecated, message: "Renamed to set(.rational(), by: .clamping)") + public mutating func setTimecode(clamping source: Fraction) { + set(source, by: .clamping) + } + + @available(*, deprecated, message: "Renamed to set(.rational(), by: .wrapping)") + public mutating func setTimecode(wrapping source: Fraction) { + set(source, by: .wrapping) + } + + @available(*, deprecated, message: "Renamed to set(.rational(), by: .allowingInvalid)") + public mutating func setTimecode(rawValues source: Fraction) { + set(source, by: .allowingInvalid) + } +} + +extension Fraction { + @available(*, deprecated, renamed: "timecode(at:base:limit:)") + public func toTimecode( + at rate: TimecodeFrameRate, + limit: Timecode.UpperLimit = .max24Hours, + base: Timecode.SubFramesBase = .default(), + format: Timecode.StringFormat = .default() + ) throws -> Timecode { + let properties = Timecode.Properties(rate: rate, base: base, limit: limit) + return try timecode(using: properties) + } + + @available(*, deprecated, renamed: "timecodeInterval(at:base:limit:)") + public func toTimecodeInterval( + at rate: TimecodeFrameRate, + limit: Timecode.UpperLimit = .max24Hours, + base: Timecode.SubFramesBase = .default(), + format: Timecode.StringFormat = .default() + ) throws -> TimecodeInterval { + let properties = Timecode.Properties(rate: rate, base: base, limit: limit) + return try timecodeInterval(using: properties) + } +} + +// MARK: - Real Time + +extension Timecode { + @available(*, deprecated, message: "Renamed to Timecode(.realTime(), at:, base:, limit:)") + public init( + realTime exactly: TimeInterval, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) throws { + let properties = Properties(rate: rate, base: base, limit: limit) + try self.init(.realTime(seconds: exactly), using: properties) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.realTime(), at:, base:, limit:, by: .clamping)" + ) + public init( + clampingRealTime source: TimeInterval, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.realTime(seconds: source), using: properties, by: .clamping) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.realTime(), at:, base:, limit:, by: .wrapping)" + ) + public init( + wrappingRealTime source: TimeInterval, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.realTime(seconds: source), using: properties, by: .wrapping) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.realTime(), at:, base:, limit:, by: .allowingInvalid)" + ) + public init( + rawValuesRealTime source: TimeInterval, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.realTime(seconds: source), using: properties, by: .allowingInvalid) + } +} + +extension Timecode { + @available(*, deprecated, message: "Renamed to set(.realTime())") + public mutating func setTimecode(realTime: TimeInterval) throws { + try set(realTime) + } + + @available(*, deprecated, message: "Renamed to set(.realTime(), by: .clamping)") + public mutating func setTimecode(clampingRealTime source: TimeInterval) { + set(source, by: .clamping) + } + + @available(*, deprecated, message: "Renamed to set(.realTime(), by: .wrapping)") + public mutating func setTimecode(wrappingRealTime source: TimeInterval) { + set(source, by: .wrapping) + } + + @available(*, deprecated, message: "Renamed to set(.realTime(), by: .allowingInvalid)") + public mutating func setTimecode(rawValuesRealTime source: TimeInterval) { + set(source, by: .allowingInvalid) + } +} + +extension TimeInterval { + @available(*, deprecated, renamed: "timecode(at:base:limit:)") + public func toTimecode( + at rate: TimecodeFrameRate, + limit: Timecode.UpperLimit = .max24Hours, + base: Timecode.SubFramesBase = .default(), + format: Timecode.StringFormat = .default() + ) throws -> Timecode { + let properties = Timecode.Properties(rate: rate, base: base, limit: limit) + return try timecode(using: properties) + } +} + +// MARK: - Samples + +extension Timecode { + // MARK: Int + + @available( + *, + deprecated, + message: "Renamed to Timecode(.samples(_:sampleRate:), at:, base:, limit:)" + ) + public init( + samples exactly: Int, + sampleRate: Int, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) throws { + let properties = Properties(rate: rate, base: base, limit: limit) + try self.init(.samples(exactly, sampleRate: sampleRate), using: properties) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.samples(_:sampleRate:), at:, base:, limit:, by: .clamping)" + ) + public init( + clampingSamples source: Int, + sampleRate: Int, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.samples(source, sampleRate: sampleRate), using: properties, by: .clamping) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.samples(_:sampleRate:), at:, base:, limit:, by: .wrapping)" + ) + public init( + wrappingSamples source: Int, + sampleRate: Int, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.samples(source, sampleRate: sampleRate), using: properties, by: .wrapping) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.samples(_:sampleRate:), at:, base:, limit:, by: .allowingInvalid)" + ) + public init( + rawValuesSamples source: Int, + sampleRate: Int, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.samples(source, sampleRate: sampleRate), using: properties, by: .allowingInvalid) + } + + // MARK: Double + + @available( + *, + deprecated, + message: "Renamed to Timecode(.samples(_:sampleRate:), at:, base:, limit:)" + ) + public init( + samples exactly: Double, + sampleRate: Int, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) throws { + let properties = Properties(rate: rate, base: base, limit: limit) + try self.init(.samples(exactly, sampleRate: sampleRate), using: properties) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.samples(_:sampleRate:), at:, base:, limit:, by: .clamping)" + ) + public init( + clampingSamples source: Double, + sampleRate: Int, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.samples(source, sampleRate: sampleRate), using: properties, by: .clamping) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.samples(_:sampleRate:), at:, base:, limit:, by: .wrapping)" + ) + public init( + wrappingSamples source: Double, + sampleRate: Int, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.samples(source, sampleRate: sampleRate), using: properties, by: .wrapping) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.samples(_:sampleRate:), at:, base:, limit:, by: .allowingInvalid)" + ) + public init( + rawValuesSamples source: Double, + sampleRate: Int, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) { + let properties = Properties(rate: rate, base: base, limit: limit) + self.init(.samples(source, sampleRate: sampleRate), using: properties, by: .allowingInvalid) + } +} + +extension Timecode { + // MARK: Int + + @available(*, deprecated, message: "Renamed to set(.samples(_:sampleRate:))") + public mutating func setTimecode(samples: Int, sampleRate: Int) throws { + try set(.samples(samples, sampleRate: sampleRate)) + } + + @available(*, deprecated, message: "Renamed to set(.samples(_:sampleRate:), by: .clamping)") + public mutating func setTimecode(clampingSamples: Int, sampleRate: Int) { + set(.samples(clampingSamples, sampleRate: sampleRate), by: .clamping) + } + + @available(*, deprecated, message: "Renamed to set(.samples(_:sampleRate:), by: .wrapping)") + public mutating func setTimecode(wrappingSamples: Int, sampleRate: Int) { + set(.samples(wrappingSamples, sampleRate: sampleRate), by: .wrapping) + } + + @available( + *, + deprecated, + message: "Renamed to set(.samples(_:sampleRate:), by: .allowingInvalid)" + ) + public mutating func setTimecode(rawValuesSamples: Int, sampleRate: Int) { + set(.samples(rawValuesSamples, sampleRate: sampleRate), by: .allowingInvalid) + } + + // MARK: Double + + @available(*, deprecated, message: "Renamed to set(.samples(_:sampleRate:))") + public mutating func setTimecode(samples: Double, sampleRate: Int) throws { + try set(.samples(samples, sampleRate: sampleRate)) + } + + @available(*, deprecated, message: "Renamed to set(.samples(_:sampleRate:), by: .clamping)") + public mutating func setTimecode(clampingSamples: Double, sampleRate: Int) { + set(.samples(clampingSamples, sampleRate: sampleRate), by: .clamping) + } + + @available(*, deprecated, message: "Renamed to set(.samples(_:sampleRate:), by: .wrapping)") + public mutating func setTimecode(wrappingSamples: Double, sampleRate: Int) { + set(.samples(wrappingSamples, sampleRate: sampleRate), by: .wrapping) + } + + @available( + *, + deprecated, + message: "Renamed to set(.samples(_:sampleRate:), by: .allowingInvalid)" + ) + public mutating func setTimecode(rawValuesSamples: Double, sampleRate: Int) { + set(.samples(rawValuesSamples, sampleRate: sampleRate), by: .allowingInvalid) + } +} + +// MARK: - String + +extension Timecode { + @available(*, deprecated, message: "Renamed to Timecode(.string(), at:, base:, limit:)") + @_disfavoredOverload + public init( + _ exactlyTimecodeString: String, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) throws { + let properties = Properties(rate: rate, base: base, limit: limit) + try self.init(.string(exactlyTimecodeString), using: properties) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.string(), at:, base:, limit:, by: .clamping)" + ) + public init( + clamping timecodeString: String, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) throws { + let properties = Properties(rate: rate, base: base, limit: limit) + try self.init(.string(timecodeString), using: properties, by: .clamping) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.string(), at:, base:, limit:, by: .clampingComponents)" + ) + public init( + clampingEach timecodeString: String, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) throws { + let properties = Properties(rate: rate, base: base, limit: limit) + try self.init(.string(timecodeString), using: properties, by: .clampingComponents) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.string(), at:, base:, limit:, by: .wrapping)" + ) + public init( + wrapping timecodeString: String, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) throws { + let properties = Properties(rate: rate, base: base, limit: limit) + try self.init(.string(timecodeString), using: properties, by: .wrapping) + } + + @available( + *, + deprecated, + message: "Renamed to Timecode(.string(), at:, base:, limit:, by: .allowingInvalid)" + ) + public init( + rawValues timecodeString: String, + at rate: TimecodeFrameRate, + limit: UpperLimit = .max24Hours, + base: SubFramesBase = .default(), + format: StringFormat = .default() + ) throws { + let properties = Properties(rate: rate, base: base, limit: limit) + try self.init(.string(timecodeString), using: properties, by: .allowingInvalid) + } +} + +extension Timecode { + @available(*, deprecated, renamed: "stringValue()") + public var stringValue: String { stringValue() } + + @available(*, deprecated, message: "Renamed to stringValue(format: [.filenameCompatible])") + public var stringValueFileNameCompatible: String { + stringValue(format: [.filenameCompatible]) + } +} + +extension Timecode { + @available(*, deprecated, message: "Renamed to set(.string())") + public mutating func setTimecode(exactly string: String) throws { + try set(string) + } + + @available(*, deprecated, message: "Renamed to set(.string(), by: .clamping)") + public mutating func setTimecode(clamping string: String) throws { + try set(string, by: .clamping) + } + + @available(*, deprecated, message: "Renamed to set(.string(), by: .clampingComponents)") + public mutating func setTimecode(clampingEach string: String) throws { + try set(string, by: .clampingComponents) + } + + @available(*, deprecated, message: "Renamed to set(.string(), by: .wrapping)") + public mutating func setTimecode(wrapping string: String) throws { + try set(string, by: .wrapping) + } + + @available(*, deprecated, message: "Renamed to set(.string(), by: .allowingInvalid)") + public mutating func setTimecode(rawValues string: String) throws { + try set(string, by: .allowingInvalid) + } +} + +extension String { + @available(*, deprecated, renamed: "timecode(at:base:limit:)") + public func toTimecode( + at rate: TimecodeFrameRate, + limit: Timecode.UpperLimit = .max24Hours, + base: Timecode.SubFramesBase = .default(), + format: Timecode.StringFormat = .default() + ) throws -> Timecode { + let properties = Timecode.Properties(rate: rate, base: base, limit: limit) + return try timecode(using: properties) + } + + @available( + *, + deprecated, + message: "Renamed to timecode(at:, base:, limit:, by: .allowingInvalid)" + ) + public func toTimecode( + rawValuesAt rate: TimecodeFrameRate, + limit: Timecode.UpperLimit = .max24Hours, + base: Timecode.SubFramesBase = .default(), + format: Timecode.StringFormat = .default() + ) throws -> Timecode { + let properties = Timecode.Properties(rate: rate, base: base, limit: limit) + return try timecode(using: properties, by: .allowingInvalid) + } +} + +// MARK: - TimecodeFrameRate + +extension String { + @available(*, deprecated, renamed: "timecodeFrameRate()") + @_disfavoredOverload + public var toTimecodeFrameRate: TimecodeFrameRate? { + timecodeFrameRate() + } +} + +// MARK: - VideoFrameRate + +extension String { + @available(*, deprecated, renamed: "videoFrameRate()") + @_disfavoredOverload + public var toVideoFrameRate: VideoFrameRate? { + VideoFrameRate(stringValue: self) + } +} + +// MARK: - TimecodeInterval + +#if canImport(CoreMedia) + +import CoreMedia +import Foundation + +@available(macOS 10.7, iOS 4.0, tvOS 9.0, watchOS 6.0, *) +extension TimecodeInterval { + @available(*, deprecated, renamed: "init(_:at:base:limit:)") + @_disfavoredOverload + public init( + _ cmTime: CMTime, + at rate: TimecodeFrameRate, + limit: Timecode.UpperLimit = .max24Hours, + base: Timecode.SubFramesBase = .default(), + format: Timecode.StringFormat = .default() + ) throws { + let properties = Timecode.Properties(rate: rate, base: base, limit: limit) + try self.init(cmTime, using: properties) + } +} + +@available(macOS 10.7, iOS 4.0, tvOS 9.0, watchOS 6.0, *) +extension CMTime { + @available(*, deprecated, renamed: "timecodeInterval(at:base:limit:)") + public func toTimecodeInterval( + at rate: TimecodeFrameRate, + limit: Timecode.UpperLimit = .max24Hours, + base: Timecode.SubFramesBase = .default(), + format: Timecode.StringFormat = .default() + ) throws -> TimecodeInterval { + let properties = Timecode.Properties(rate: rate, base: base, limit: limit) + return try timecodeInterval(using: properties) + } +} + +#endif + +#endif diff --git a/Sources/TimecodeKit/AVFoundation Extensions/AVAsset Frame Rate Read.swift b/Sources/TimecodeKit/AVFoundation Extensions/AVAsset Frame Rate Read.swift index aec3bd75..3ca3ccef 100644 --- a/Sources/TimecodeKit/AVFoundation Extensions/AVAsset Frame Rate Read.swift +++ b/Sources/TimecodeKit/AVFoundation Extensions/AVAsset Frame Rate Read.swift @@ -1,14 +1,14 @@ // // AVAsset Frame Rate Read.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // // AVAssetReader is unavailable on watchOS so we can't support any AVAsset operations #if canImport(AVFoundation) && !os(watchOS) && !os(visionOS) -import Foundation import AVFoundation +import Foundation // MARK: - Timecode Frame Rate @@ -71,7 +71,7 @@ extension AVAsset { /// If drop-frame status is embedded, returns `true` (drop) or `false` (non-drop). /// Returns `nil` if drop-frame status is unknown. /// Best practise is to default to `false` if `nil` is returned. - internal var isTimecodeFrameRateDropFrame: Bool? { + var isTimecodeFrameRateDropFrame: Bool? { guard #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) else { return nil } @@ -94,7 +94,8 @@ extension AVAsset { public func videoFrameRate(interlaced: Bool? = nil) throws -> VideoFrameRate { // only video tracks contain interlaced (field) info - // use supplied interlaced status, otherwise auto-detect and default to non-interlaced (progressive) + // use supplied interlaced status, otherwise auto-detect and default to non-interlaced + // (progressive) let interlaced = interlaced ?? isVideoInterlaced // first, frame rate can be determined from minimum frame duration @@ -161,7 +162,7 @@ extension AVAsset { // MARK: - Helpers /// Returns the nominal frame rate as `Float` for each video track. - internal func readNominalVideoFrameRates() -> [Float] { + func readNominalVideoFrameRates() -> [Float] { tracks(withMediaType: .video) .map(\.nominalFrameRate) } @@ -170,7 +171,7 @@ extension AVAsset { extension AVAssetTrack { /// Returns `true` if the video track is interlaced. /// Not applicable for non-video tracks. - internal var isVideoInterlaced: Bool { + var isVideoInterlaced: Bool { // progressive is 1 field, interlaced is 2 fields formatDescriptionsTyped .map(\.extensionsDictionary) diff --git a/Sources/TimecodeKit/AVFoundation Extensions/AVAsset Timecode Read.swift b/Sources/TimecodeKit/AVFoundation Extensions/AVAsset Timecode Read.swift index 3148cf81..6359953f 100644 --- a/Sources/TimecodeKit/AVFoundation Extensions/AVAsset Timecode Read.swift +++ b/Sources/TimecodeKit/AVFoundation Extensions/AVAsset Timecode Read.swift @@ -1,14 +1,14 @@ // // AVAsset Timecode Read.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // // AVAssetReader is unavailable on watchOS so we can't support any AVAsset operations #if canImport(AVFoundation) && !os(watchOS) && !os(visionOS) -import Foundation import AVFoundation +import Foundation // MARK: - Start Timecode @@ -24,15 +24,13 @@ extension AVAsset { @_disfavoredOverload public func startTimecode( at frameRate: TimecodeFrameRate? = nil, - limit: Timecode.UpperLimit = ._24hours, base: Timecode.SubFramesBase = .default(), - format: Timecode.StringFormat = .default() + limit: Timecode.UpperLimit = .max24Hours ) throws -> Timecode? { try timecodes( at: frameRate, - limit: limit, base: base, - format: format + limit: limit ) .compactMap(\.first) // first element of each track .first // first track @@ -49,23 +47,20 @@ extension AVAsset { @_disfavoredOverload public func endTimecode( at frameRate: TimecodeFrameRate? = nil, - limit: Timecode.UpperLimit = ._24hours, base: Timecode.SubFramesBase = .default(), - format: Timecode.StringFormat = .default() + limit: Timecode.UpperLimit = .max24Hours ) throws -> Timecode? { - let frameRate = try frameRate ?? self.timecodeFrameRate() + let frameRate = try frameRate ?? timecodeFrameRate() guard let start = try startTimecode( at: frameRate, - limit: limit, base: base, - format: format + limit: limit ) else { return nil } return try start + durationTimecode( at: frameRate, - limit: limit, base: base, - format: format + limit: limit ) } @@ -80,17 +75,15 @@ extension AVAsset { @_disfavoredOverload public func durationTimecode( at frameRate: TimecodeFrameRate? = nil, - limit: Timecode.UpperLimit = ._24hours, base: Timecode.SubFramesBase = .default(), - format: Timecode.StringFormat = .default() + limit: Timecode.UpperLimit = .max24Hours ) throws -> Timecode { - let frameRate = try frameRate ?? self.timecodeFrameRate() + let frameRate = try frameRate ?? timecodeFrameRate() return try Timecode( - duration, + .cmTime(duration), at: frameRate, - limit: limit, base: base, - format: format + limit: limit ) } @@ -104,11 +97,10 @@ extension AVAsset { @_disfavoredOverload public func timecodes( at frameRate: TimecodeFrameRate? = nil, - limit: Timecode.UpperLimit = ._24hours, base: Timecode.SubFramesBase = .default(), - format: Timecode.StringFormat = .default() + limit: Timecode.UpperLimit = .max24Hours ) throws -> [[Timecode]] { - let frameRate = try frameRate ?? self.timecodeFrameRate() + let frameRate = try frameRate ?? timecodeFrameRate() let samples = try tracks(withMediaType: .timecode) .map { try $0.readTimecodeSamples(context: self) } @@ -116,9 +108,8 @@ extension AVAsset { let timecodes = try samples.map { try $0.mapToTimecode( at: frameRate, - limit: limit, base: base, - format: format + limit: limit ) } @@ -128,7 +119,7 @@ extension AVAsset { // MARK: - Helpers @_disfavoredOverload - internal func readTimecodeSamples() throws -> [[CMTimeCode]] { + func readTimecodeSamples() throws -> [[CMTimeCode]] { try tracks(withMediaType: .timecode) .map { try $0.readTimecodeSamples(context: self) diff --git a/Sources/TimecodeKit/AVFoundation Extensions/AVAsset Timecode Write.swift b/Sources/TimecodeKit/AVFoundation Extensions/AVAsset Timecode Write.swift index 2a071d84..056d045b 100644 --- a/Sources/TimecodeKit/AVFoundation Extensions/AVAsset Timecode Write.swift +++ b/Sources/TimecodeKit/AVFoundation Extensions/AVAsset Timecode Write.swift @@ -1,14 +1,14 @@ // // AVAsset Timecode Write.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // // AVAssetReader is unavailable on watchOS so we can't support any AVAsset operations #if canImport(AVFoundation) && !os(watchOS) && !os(visionOS) -import Foundation import AVFoundation +import Foundation #if !os(tvOS) // AVMutableMovie not available on tvOS @@ -31,7 +31,7 @@ extension AVMutableMovie { // of a timecode track. let newAsset = try AVMutableMovie( timecodeTrackStart: startTimecode, - duration: duration ?? (try durationTimecode()), + duration: duration ?? durationTimecode(), extensions: extensions, fileType: outputFileType ) @@ -78,7 +78,7 @@ extension AVMutableMovie { return try addTimecodeTrack( startTimecode: startTimecode, - duration: duration ?? (try durationTimecode()), + duration: duration ?? durationTimecode(), extensions: extensions, fileType: outputFileType ) @@ -95,7 +95,7 @@ extension AVMutableMovie { /// Internal helper: /// Creates a new asset with a timecode track containing one sample (start timecode). - internal convenience init( + convenience init( timecodeTrackStart: Timecode, duration: Timecode, extensions: CMFormatDescription.Extensions? = nil, @@ -119,7 +119,7 @@ extension AVMutableMovie { // otherwise we have to use append(buffer:) which is all kinds of scary try blockBuffer.fillDataBytes(with: 0x00) try withUnsafeBytes(of: &frames) { framesPtr in - //try blockBuffer.append(buffer: framesPtr) // dealloc crash + // try blockBuffer.append(buffer: framesPtr) // dealloc crash try blockBuffer.replaceDataBytes(with: framesPtr) } @@ -127,7 +127,7 @@ extension AVMutableMovie { dataBuffer: blockBuffer, formatDescription: timecodeTrackStart.cmFormatDescription(extensions: extensions), numSamples: 1, - sampleTimings: [CMSampleTimingInfo(duration: duration.cmTime, presentationTimeStamp: .zero, decodeTimeStamp: .invalid)], + sampleTimings: [CMSampleTimingInfo(duration: duration.cmTimeValue, presentationTimeStamp: .zero, decodeTimeStamp: .invalid)], sampleSizes: [4] ) try sampleBuffer.makeDataReady() // needed? doesn't seem to hurt @@ -135,7 +135,7 @@ extension AVMutableMovie { input.markAsFinished() // finish - writer.endSession(atSourceTime: duration.cmTime) + writer.endSession(atSourceTime: duration.cmTimeValue) let g = DispatchGroup() g.enter() writer.finishWriting { @@ -152,7 +152,7 @@ extension AVMutableMovie { @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) extension Timecode { - /// Returns a new CoreMedia format description based on the timecode `frameRate`. + /// Returns a new Core Media format description based on the timecode `frameRate`. /// /// - Throws: Core Media error. public func cmFormatDescription( @@ -161,7 +161,7 @@ extension Timecode { // this method essentially wraps CMTimeCodeFormatDescriptionCreate, // an old crusty Obj-C method. it returned OSStatus so we assume the new // 'throws' method might return something related if an error occurs. - return try CMTimeCodeFormatDescription( // typealias of CMFormatDescription + try CMTimeCodeFormatDescription( // typealias of CMFormatDescription timeCodeFormatType: .timeCode32, // one that exists in CMTimeCodeFormatType frameDuration: frameRate.frameDurationCMTime, frameQuanta: frameRate.maxFrames, @@ -174,7 +174,7 @@ extension Timecode { /// Assembles timecode flags for use in `CMFormatDescription` private var cmFormatDescriptionTimeCodeFlags: CMFormatDescription.TimeCode.Flag { var flags: CMFormatDescription.TimeCode.Flag = [] - if upperLimit == ._24hours { flags.insert(.twentyFourHourMax) } + if upperLimit == .max24Hours { flags.insert(.twentyFourHourMax) } if frameRate.isDrop { flags.insert(.dropFrame) } return flags } diff --git a/Sources/TimecodeKit/AVFoundation Extensions/AVAssetTrack Timecode Read.swift b/Sources/TimecodeKit/AVFoundation Extensions/AVAssetTrack Timecode Read.swift index 217f8b9e..7a3b4ae7 100644 --- a/Sources/TimecodeKit/AVFoundation Extensions/AVAssetTrack Timecode Read.swift +++ b/Sources/TimecodeKit/AVFoundation Extensions/AVAssetTrack Timecode Read.swift @@ -1,14 +1,14 @@ // // AVAssetTrack Timecode Read.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // // AVAssetReader is unavailable on watchOS so we can't support any AVAsset operations #if canImport(AVFoundation) && !os(watchOS) && !os(visionOS) -import Foundation import AVFoundation +import Foundation // MARK: - Helper methods @@ -22,11 +22,10 @@ extension AVAssetTrack { @_disfavoredOverload public func durationTimecode( at frameRate: TimecodeFrameRate? = nil, - limit: Timecode.UpperLimit = ._24hours, - base: Timecode.SubFramesBase = .default(), - format: Timecode.StringFormat = .default() + limit: Timecode.UpperLimit = .max24Hours, + base: Timecode.SubFramesBase = .default() ) throws -> Timecode { - guard let frameRate = try frameRate ?? self.asset?.timecodeFrameRate() + guard let frameRate = try frameRate ?? asset?.timecodeFrameRate() else { throw Timecode.MediaParseError.missingOrNonStandardFrameRate } @@ -34,8 +33,7 @@ extension AVAssetTrack { let range = try timecodeRange( at: frameRate, limit: limit, - base: base, - format: format + base: base ) return range.upperBound - range.lowerBound @@ -56,28 +54,26 @@ extension AVAssetTrack { /// /// - Throws: ``Timecode/MediaParseError`` @_disfavoredOverload - internal func timecodeRange( + func timecodeRange( at frameRate: TimecodeFrameRate? = nil, - limit: Timecode.UpperLimit = ._24hours, - base: Timecode.SubFramesBase = .default(), - format: Timecode.StringFormat = .default() + limit: Timecode.UpperLimit = .max24Hours, + base: Timecode.SubFramesBase = .default() ) throws -> ClosedRange { - guard let frameRate = try frameRate ?? self.asset?.timecodeFrameRate() + guard let frameRate = try frameRate ?? asset?.timecodeFrameRate() else { throw Timecode.MediaParseError.missingOrNonStandardFrameRate } return try timeRange.timecodeRange( at: frameRate, - limit: limit, base: base, - format: format + limit: limit ) } /// Returns the start frame number from a timecode track. /// Returns `nil` if the track is not a timecode track. - internal func readTimecodeSamples( + func readTimecodeSamples( context: AVAsset ) throws -> [CMTimeCode] { let assetReader = try AVAssetReader(asset: context) @@ -119,7 +115,7 @@ extension AVAssetTrack { // formatDescription.mediaSubType instead of CMFormatDescriptionGetMediaSubType let type = CMFormatDescriptionGetMediaSubType(formatDescription) - var offset: Int = 0 + var offset = 0 switch type { case kCMTimeCodeFormatType_TimeCode32: @@ -150,7 +146,7 @@ extension AVAssetTrack { offset: Int ) -> CMTimeCode32? { var rawData: UnsafeMutablePointer? // CChar == Int8 - var length: Int = 0 + var length = 0 let status = CMBlockBufferGetDataPointer( blockBuffer, @@ -164,9 +160,9 @@ extension AVAssetTrack { guard length >= MemoryLayout.size, let frame = rawData?.withMemoryRebound( - to: UInt32.self, - capacity: 1, - { CFSwapInt32BigToHost($0.pointee) } + to: UInt32.self, + capacity: 1, + { CFSwapInt32BigToHost($0.pointee) } ) else { return nil } @@ -179,7 +175,7 @@ extension AVAssetTrack { offset: Int ) -> CMTimeCode64? { var rawData: UnsafeMutablePointer? // CChar == Int8 - var length: Int = 0 + var length = 0 let status = CMBlockBufferGetDataPointer( blockBuffer, @@ -193,9 +189,9 @@ extension AVAssetTrack { guard length >= MemoryLayout.size, let rawValue = rawData?.withMemoryRebound( - to: UInt64.self, - capacity: 1, - { CFSwapInt64BigToHost($0.pointee) } + to: UInt64.self, + capacity: 1, + { CFSwapInt64BigToHost($0.pointee) } ) else { return nil } diff --git a/Sources/TimecodeKit/AVFoundation Extensions/AVFoundation Utils.swift b/Sources/TimecodeKit/AVFoundation Extensions/AVFoundation Utils.swift index 9af917f8..3eb93a85 100644 --- a/Sources/TimecodeKit/AVFoundation Extensions/AVFoundation Utils.swift +++ b/Sources/TimecodeKit/AVFoundation Extensions/AVFoundation Utils.swift @@ -1,23 +1,24 @@ // // AVFoundation Utils.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // // AVAssetReader is unavailable on watchOS so we can't support any AVAsset operations -#if canImport(AVFoundation) && !os(watchOS) +#if canImport(AVFoundation) && !os(watchOS) && !os(visionOS) -import Foundation import AVFoundation +import Foundation extension CMFormatDescription { /// Returns extensions as a dictionary. /// /// Extensions dictionaries are valid property list objects. - /// This means that dictionary keys are all CFStrings, and the values are all either CFNumber, CFString, CFBoolean, CFArray, CFDictionary, CFDate, or CFData + /// This means that dictionary keys are all CFStrings, and the values are all either CFNumber, CFString, CFBoolean, CFArray, + /// CFDictionary, CFDate, or CFData /// /// You can subscript the dictionary using global AVFoundation key constants beginning with `kCMFormatDescriptionExtension_` - internal var extensionsDictionary: [CFString: Any] { + var extensionsDictionary: [CFString: Any] { let nsDict = CMFormatDescriptionGetExtensions(self) as NSDictionary? let dict = nsDict as? [CFString: Any] return dict ?? [:] @@ -26,9 +27,9 @@ extension CMFormatDescription { extension AVAssetTrack { /// Returns `formatDescriptions` cast as `[CMFormatDescription]` - internal var formatDescriptionsTyped: [CMFormatDescription] { + var formatDescriptionsTyped: [CMFormatDescription] { formatDescriptions as? [CMFormatDescription] - ?? [] + ?? [] } } @@ -41,9 +42,21 @@ extension CMTimeRange { /// - Throws: ``Timecode/MediaParseError`` public func timecodeRange( at frameRate: TimecodeFrameRate, - limit: Timecode.UpperLimit = ._24hours, base: Timecode.SubFramesBase = .default(), - format: Timecode.StringFormat = .default() + limit: Timecode.UpperLimit = .max24Hours + ) throws -> ClosedRange { + let properties = Timecode.Properties(rate: frameRate, base: base, limit: limit) + return try timecodeRange(using: properties) + } + + /// Returns the time range as a timecode range. + /// + /// Throws an error if the range is invalid or if one or both of the times cannot be converted + /// to valid timecode. + /// + /// - Throws: ``Timecode/MediaParseError`` + public func timecodeRange( + using properties: Timecode.Properties ) throws -> ClosedRange { guard isValid, start <= end else { throw Timecode.MediaParseError.unknownTimecode @@ -51,7 +64,7 @@ extension CMTimeRange { let timecodes = try [start, end] .map { - try Timecode($0, at: frameRate, limit: limit, base: base, format: format) + try Timecode(.cmTime($0), using: properties) } return timecodes[0] ... timecodes[1] @@ -65,7 +78,7 @@ extension CMTimeRange { extension AVMediaDataStorage { /// Initializes by writing data to a temporary file. @_disfavoredOverload - convenience init(data: Data, options: [String : Any]? = nil) throws { + convenience init(data: Data, options: [String: Any]? = nil) throws { let url = try URL(temporaryFileWithData: data) self.init(url: url, options: options) } diff --git a/Sources/TimecodeKit/AVFoundation Extensions/CMTimeCode.swift b/Sources/TimecodeKit/AVFoundation Extensions/CMTimeCode.swift index b2abf837..98b36bf4 100644 --- a/Sources/TimecodeKit/AVFoundation Extensions/CMTimeCode.swift +++ b/Sources/TimecodeKit/AVFoundation Extensions/CMTimeCode.swift @@ -1,14 +1,14 @@ // // CMTimeCode.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // // AVAssetReader is unavailable on watchOS so we can't support any AVAsset operations -#if canImport(AVFoundation) && !os(watchOS) +#if canImport(AVFoundation) && !os(watchOS) && !os(visionOS) -import Foundation import AVFoundation +import Foundation protocol CMTimeCode { static var byteLength: Int { get } @@ -17,33 +17,33 @@ protocol CMTimeCode { extension Collection where Element == CMTimeCode { func mapToTimecode( at frameRate: TimecodeFrameRate, - limit: Timecode.UpperLimit = ._24hours, base: Timecode.SubFramesBase = .default(), - format: Timecode.StringFormat = .default() + limit: Timecode.UpperLimit = .max24Hours + ) throws -> [Timecode] { + let properties = Timecode.Properties(rate: frameRate, base: base, limit: limit) + return try mapToTimecode(using: properties) + } + + func mapToTimecode( + using properties: Timecode.Properties ) throws -> [Timecode] { try compactMap { sample in switch sample { case let timecode32 as CMTimeCode32: return try Timecode( .frames(Int(timecode32.frameNumber)), - at: frameRate, - limit: limit, - base: base, - format: format + using: properties ) case let timecode64 as CMTimeCode64: - let tcc = TCC( + let tcc = Timecode.Components( h: Int(timecode64.h), m: Int(timecode64.m), s: Int(timecode64.s), f: Int(timecode64.f) ) return try Timecode( - tcc, - at: frameRate, - limit: limit, - base: base, - format: format + .components(tcc), + using: properties ) default: return nil @@ -79,7 +79,7 @@ struct CMTimeCode32: CMTimeCode, Equatable, Hashable { /// > frame number that is typically converted to and from SMPTE timecodes representing hours, /// > minutes, seconds, and frames, according to information carried in the format description. /// > -/// > Converting to and from the frame number stored as media sample data and a CVSMPTETime +/// > Converting to and from the frame number stored as media sample data and a `CVSMPTETime` /// > structure is performed using simple modular arithmetic with the expected adjustments for drop /// > frame timecode performed using information in the format description such as the frame quanta /// > and the drop frame flag. @@ -109,9 +109,9 @@ struct CMTimeCode64: CMTimeCode, Equatable, Hashable { init(h: UInt16, m: UInt16, s: UInt16, f: UInt16) { uInt64 = ((UInt64(h) & 0xFFFF) << 48) - + ((UInt64(m) & 0xFFFF) << 32) - + ((UInt64(s) & 0xFFFF) << 16) - + (UInt64(f) & 0xFFFF) + + ((UInt64(m) & 0xFFFF) << 32) + + ((UInt64(s) & 0xFFFF) << 16) + + (UInt64(f) & 0xFFFF) } var h: UInt16 { UInt16((uInt64 >> 48) & 0xFFFF) } diff --git a/Sources/TimecodeKit/Documentation.docc/AVAsset-Timecode-Track.md b/Sources/TimecodeKit/Documentation.docc/AVAsset-Timecode-Track.md new file mode 100644 index 00000000..561d6c5b --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/AVAsset-Timecode-Track.md @@ -0,0 +1,106 @@ +# AVAsset Timecode Track + +Manipulating timecode track(s) for movie assets in AVFoundation. + +TimecodeKit provides a full suite of methods to read and write timecode track(s) in a QuickTime movie file when working with AVFoundation. + +## Read Timecode from QuickTime Movie + +Simple methods to read start timecode and duration from `AVAsset` and its subclasses (`AVMovie`) as well as `AVAssetTrack` are provided by TimecodeKit. +The methods are throwing since timecode information is not guaranteed to be present inside movie files. + +```swift +let asset = AVAsset( ... ) +``` + +Auto-detect the movie's frame rate (if it's embedded in the file) and return it if desired. +If frame rate information is not available in the video file, this method will throw an error. + +```swift +let frameRate = try asset.timecodeFrameRate() // ie: .fps29_97 +``` + +Read the start timecode, duration expressed as elapsed timecode, and end timecode. + +If a known frame rate is not passed to the methods, the frame rate will be auto-detected. +If frame rate information is not available in the video file, these methods will throw an error. + +```swift +// read start timecode, auto-detecting frame rate +let startTimecode = try asset.startTimecode() +// read start timecode, forcing a known frame rate +let startTimecode = try asset.startTimecode(at: .fps29_97) + +// read video duration expressed as timecode +let durationTimecode = try asset.durationTimecode() +// read video duration expressed as timecode, forcing a known frame rate +let durationTimecode = try asset.durationTimecode(at: .fps29_97) + +// read end timecode, auto-detecting frame rate +let endTimecode = try asset.endTimecode() +// read end timecode, forcing a known frame rate +let endTimecode = try asset.endTimecode(at: .fps29_97) +``` + +## Add or Replace Timecode Track in a QuickTime Movie + +Currently timecode tracks can be modified on `AVMutableMovie`. + +This is one way to make an `AVMovie` into a mutable `AVMutableMovie` if needed. + +```swift +let movie = AVMovie( ... ) +guard let mutableMovie = movie.mutableCopy() as? AVMutableMovie else { ... } +``` + +Then add/replace the timecode track. + +```swift +// replace existing timecode track if it exists, otherwise add a new timecode track +try mutableMovie.replaceTimecodeTrack( + startTimecode: Timecode(.components(h: 0, m: 59, s: 58, f: 00), at: .fps29_97), + fileType: .mov +) +``` + +Finally, the new file can be saved back to disk using `AVAssetExportSession`. There are other ways of course but this is the vanilla method. + +```swift +let export = AVAssetExportSession( + asset: mutableMovie, + presetName: AVAssetExportPresetPassthrough +) +export.outputFileType = .mov +export.outputURL = // new file URL on disk +export.exportAsynchronously { + // completion handler +} +``` + +> Warning: +> +> As of iOS 17, Apple appears to have introduced a regression when using `AVAssetExportSession` to save a QuickTime movie file when using a physical iOS device (simulator works fine). +> A radar has been filed with Apple (FB12986599). This is not an issue with TimecodeKit itself. +> However, until Apple fixes this bug it will affect saving a movie file after performing timecode track modifications. +> See [this thread](https://github.com/orchetect/TimecodeKit/discussions/63) for details. + +## Topics + +### AVAsset Extensions + +- ``AVFoundation/AVAsset/startTimecode(at:base:limit:)`` +- ``AVFoundation/AVAsset/durationTimecode(at:base:limit:)`` +- ``AVFoundation/AVAsset/endTimecode(at:base:limit:)`` +- ``AVFoundation/AVAsset/timecodes(at:base:limit:)`` +- ``AVFoundation/AVAsset/timecodeFrameRate(drop:)`` +- ``AVFoundation/AVAsset/videoFrameRate(interlaced:)`` +- ``AVFoundation/AVAsset/isVideoInterlaced`` + +### AVAssetTrack Extensions + +- ``AVFoundation/AVAssetTrack/durationTimecode(at:limit:base:)`` + +### AVMutableMovie Extensions + +- ``AVFoundation/AVMutableMovie/addTimecodeTrack(startTimecode:duration:extensions:fileType:)`` +- ``AVFoundation/AVMutableMovie/replaceTimecodeTrack(startTimecode:duration:extensions:fileType:)`` diff --git a/Sources/TimecodeKit/Documentation.docc/Documentation.md b/Sources/TimecodeKit/Documentation.docc/Documentation.md new file mode 100644 index 00000000..1c6f33df --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/Documentation.md @@ -0,0 +1,55 @@ +# ``TimecodeKit`` + +Value types for representing and working with SMPTE/EBU timecode. + +![TimecodeKit](timecodekit-banner.png) + +- A variety of initializers and methods are available for string and numeric representation, validation, and conversion +- Mathematical operators are available between two instances: `+`, `-`, `*`, `\` +- Comparison operators are available between two instances: `==`, `!=`, `<`, `>` +- `Range` and `Stride` can be formed between two instances +- Many more features are detailed in the documentation + +## Topics + +### Getting Started + +- +- + +### Timecode + +- ``Timecode`` +- ``Timecode/Properties-swift.struct`` +- ``Timecode/Components-swift.struct`` +- +- +- +- + +### Frame Rate + +- ``TimecodeFrameRate`` +- ``VideoFrameRate`` + +### Math & Conversions + +- +- +- +- +- + +### Encoding Formats + +- +- +- + +### Additional Value Types + +- ``FeetAndFrames`` + +### Internals + +- diff --git a/Sources/TimecodeKit/Documentation.docc/Getting-Started.md b/Sources/TimecodeKit/Documentation.docc/Getting-Started.md new file mode 100644 index 00000000..e8aaf991 --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/Getting-Started.md @@ -0,0 +1,21 @@ +# Getting Started + +Documentation and guides to get the most out of TimecodeKit. + +Add the library to your app project or Swift package, and import it. + +```swift +import TimecodeKit +``` + +The library provides UI components that may also be imported if desired. + +```swift +import TimecodeKitUI +``` + +The documentation page for ``Timecode`` is a good starting point to jump in. It provides a quick overview of how to form timecode and convert to/from various other time values. + +The topics list in the sidebar give overviews of specific areas of concern. + +If upgrading from TimecodeKit 1.x, see . diff --git a/Sources/TimecodeKit/Documentation.docc/Internals.md b/Sources/TimecodeKit/Documentation.docc/Internals.md new file mode 100644 index 00000000..5d585945 --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/Internals.md @@ -0,0 +1,21 @@ +# Internals + +Internal types and protocols. + +## Topics + +### Timecode Source Values + +- ``TimecodeSourceValue`` +- ``RichTimecodeSourceValue`` +- ``FormattedTimecodeSourceValue`` +- ``GuaranteedTimecodeSourceValue`` +- ``GuaranteedRichTimecodeSourceValue`` + +### Supporting Types + +- ``RangeAttribute`` + +### Frame Rate + +- ``FrameRateProtocol`` diff --git a/Sources/TimecodeKit/Documentation.docc/LTC-Linear-Timecode.md b/Sources/TimecodeKit/Documentation.docc/LTC-Linear-Timecode.md new file mode 100644 index 00000000..581eb353 --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/LTC-Linear-Timecode.md @@ -0,0 +1,13 @@ +# LTC (Linear Timecode) + +Information on LTC (Linear/Longitudinal Timecode). + +LTC is an encoding of SMPTE timecode data in an audio signal, as defined in SMPTE 12M specification. + +TimecodeKit does not implement LTC encoding or decoding directly. + +Support may be added in future versions of TimecodeKit, or possibly in an audio library such as AudioKit. + +## References + +- [Linear Timecode](https://en.wikipedia.org/wiki/Linear_timecode) on Wikipedia diff --git a/Sources/TimecodeKit/Documentation.docc/MTC-MIDI-Timecode.md b/Sources/TimecodeKit/Documentation.docc/MTC-MIDI-Timecode.md new file mode 100644 index 00000000..10b33787 --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/MTC-MIDI-Timecode.md @@ -0,0 +1,15 @@ +# MTC (MIDI Timecode) + +Information on MIDI Timecode (part of the MIDI Specification). + +MIDI Timecode is a device synchronization protocol that encodes SPMTE timecode using MIDI 1.0 (or MIDI 2.0) as transport. + +TimecodeKit does not implement MTC encoding or decoding directly. + +Instead, [MIDIKit](https://github.com/orchetect/MIDIKit) (an open-source Swift MIDI I/O package for all Apple platforms) implements MTC encoding/decoding. +It imports TimecodeKit as a dependency and uses ``Timecode`` and ``TimecodeFrameRate`` as data structures. + +## References + +- [MIDI Timecode Specification on midi.org](https://www.midi.org/specifications/midi1-specifications/midi-time-code) (requires a free account to access) +- [MIDI Timecode](https://en.wikipedia.org/wiki/MIDI_timecode) on Wikipedia diff --git a/Sources/TimecodeKit/Documentation.docc/Math.md b/Sources/TimecodeKit/Documentation.docc/Math.md new file mode 100644 index 00000000..abf32dd3 --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/Math.md @@ -0,0 +1,58 @@ +# Math + +Performing mathematical calculations between timecodes. + +Math operations are possible by either methods or operators. + +Addition and subtraction may be performed using two timecode operands to produce a timecode result. + +- `Timecode` + `Timecode` = `Timecode` +- `Timecode` - `Timecode` = `Timecode` + +Multiplication and division may be performed using one timecode operand and one floating-point number operand. This forms a calculation of timecode (position or duration) against a number of iterations or subdivisions. + +Multiplying timecode against timecode in order to produce a timecode result is not possible since it is ambiguous and considered undefined behavior. + +- `Timecode` * `Double` = `Timecode` +- `Timecode` * `Timecode` is undefined and therefore not implemented +- `Timecode` / `Double` = `Timecode` +- `Timecode` / `Timecode` = `Double` + +## Arithmetic Operators + +Arithmetic operators are provided for convenience. These operators employ the ``Timecode/ValidationRule/wrapping`` validation rule in the event of underflows or overflows. + +```swift +let tc1 = try "01:00:00:00".timecode(at: .fps23_976) +let tc2 = try "00:02:00:00".timecode(at: .fps23_976) + +(tc1 + tc2).stringValue() // == "01:02:00:00" +(tc1 - tc2).stringValue() // == "00:58:00:00" +(tc1 * 2.0).stringValue() // == "02:00:00:00" +(tc1 / 2.0).stringValue() // == "00:30:00:00" +tc1 / tc2 // == 30.0 +``` + +## Arithmetic Methods + +Arithmetic methods follow the same behavior as ``Timecode`` initializers whereby the operation can be completed either using validation with a throwing call, or by using validation rules to constrain the result (See ``Timecode/ValidationRule``). + +The right-hand operand may be a ``Timecode`` instance, or any time source value. + +- `add()` / `adding()` +- `subtract()` / `subtracting()` +- `multiply()` / `multiplying()` +- `divide()` / `dividing()` + +```swift +var tc1 = try "01:00:00:00".timecode(at: .fps23_976) +var tc2 = try "00:00:02:00".timecode(at: .fps23_976) + +// in-place mutation +try tc1.add(tc2) +try tc1.add(tc2, by: wrapping) // using result validation rule + +// return a new instance +let tc3 = try tc1.adding(tc2) +let tc3 = try tc1.adding(tc2, by: wrapping) // using result validation rule +``` diff --git a/Sources/TimecodeKit/Documentation.docc/Rational-Numbers-and-CMTime.md b/Sources/TimecodeKit/Documentation.docc/Rational-Numbers-and-CMTime.md new file mode 100644 index 00000000..0aa6608d --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/Rational-Numbers-and-CMTime.md @@ -0,0 +1,50 @@ +# Rational Numbers & CMTime + +Using rational (fractional) time values and `CMTime`. + +## Rational Numbers + +Video file metadata and timeline interchange files (AAF, Final Cut Pro XML) encode frame rate and timecode as rational numbers (a fraction consisting of two integers - a numerator and a denominator). + +``Timecode`` is capable of initializing from an elapsed time expressed as a rational fraction using a ``TimecodeSourceValue/rational(_:)`` value. The ``Timecode/rationalValue`` property returns the elapsed time expressed as a rational fraction. + +```swift +try Timecode(.rational(Fraction(1920919, 30000)), at: .fps29_97) + .stringValue() // == "00:01:03;29" + +try Timecode(.components(h: 00, m: 01, s: 03, f: 29), at: .fps29_97) + .rationalValue // == Fraction(1920919, 30000) +``` + +``TimecodeFrameRate`` and ``VideoFrameRate`` are both capable of initializing from a rational fraction, and also provide a `rationalRate` and `rationalFrameDuration` property that provides this fraction. + +Since drop-frame (timecode) or interlaced (video) attributes are not encodable in a rational fraction, they must be imperatively supplied. + +```swift +// fraction representing the duration of 1 frame +TimecodeFrameRate(frameDuration: Fraction(1001, 30000), drop: false) // == .fps29_97 +// fraction representing the fps +TimecodeFrameRate(rate: Fraction(30000, 1001), drop: false) // == .fps29_97 + +// fraction representing the duration of 1 frame +VideoFrameRate(frameDuration: Fraction(1001, 30000), interlaced: false) // == .fps29_97p +// fraction representing the fps +VideoFrameRate(rate: Fraction(30000, 1001), interlaced: false) // == .fps29_97p +``` + +## CMTime Conversion + +`CMTime` is a type exported by the Core Media framework (and used pervasively in AVFoundation). It represents time as a rational fraction of a `value` in a `timescale`. + +``Timecode`` and ``TimecodeInterval``, as well as ``TimecodeFrameRate`` and ``VideoFrameRate`` can convert to/from `CMTime` using the respective inits and properties. + +`CMTime` and ``Fraction`` can convert between themselves as well with respective inits and properties. + +## Topics + +- ``Fraction`` + +### CMTime Extensions + +- ``CoreMedia/CMTime/init(_:)`` +- ``CoreMedia/CMTime/fractionValue`` diff --git a/Sources/TimecodeKit/Documentation.docc/Resources/timecode-init.png b/Sources/TimecodeKit/Documentation.docc/Resources/timecode-init.png new file mode 100644 index 00000000..01111fb4 Binary files /dev/null and b/Sources/TimecodeKit/Documentation.docc/Resources/timecode-init.png differ diff --git a/Sources/TimecodeKit/Documentation.docc/Resources/timecodekit-banner.png b/Sources/TimecodeKit/Documentation.docc/Resources/timecodekit-banner.png new file mode 100644 index 00000000..06456995 Binary files /dev/null and b/Sources/TimecodeKit/Documentation.docc/Resources/timecodekit-banner.png differ diff --git a/Sources/TimecodeKit/Documentation.docc/Timecode-Comparison-and-Sort.md b/Sources/TimecodeKit/Documentation.docc/Timecode-Comparison-and-Sort.md new file mode 100644 index 00000000..b6d6fff9 --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/Timecode-Comparison-and-Sort.md @@ -0,0 +1,128 @@ +# Comparison & Sort + +Comparing and sorting the ordering of timecodes. + +## Comparable Conformance + +Comparable protocol conformance. + +Two ``Timecode`` instances can be compared linearly using common comparison operators. + +```swift +try "01:00:00:00".timecode(at: .fps24) + == try "01:00:00:00".timecode(at: .fps24) // == true + +try "00:59:50:00".timecode(at: .fps24) + < "01:00:00:00".timecode(at: .fps24) // == true + +try "00:59:50:00".timecode(at: .fps24) + > "01:00:00:00".timecode(at: .fps24) // == false +``` + +## Comparing using Timeline Context + +Special comparison methods based on non-zero timeline origin timecode. + +Sometimes a timeline does not have a zero start time (00:00:00:00). For example, many DAW applications such as Pro Tools allow a project start time to be set to any timecode. Its timeline then extends for 24 hours from that timecode, wrapping over 00:00:00:00 at some point along the timeline. + +For example, given a 24 hour limit: + +- A timeline start of 00:00:00:00 @ 24fps: + + 24 hours elapses from 00:00:00:00 → 23:59:59:23 + +- A timeline start of 20:00:00:00 @ 24fps: + + 24 hours elapses from 20:00:00:00 → 00:00:00:00 → 19:59:59:23 + + This would mean for example, that 21:00:00:00 is < 00:00:00:00 since it is earlier in the wrapping timeline, and 18:00:00:00 is > 21:00:00:00 since it is later in the wrapping timeline. + +Methods to sort and test sort order of ``Timecode`` collections are provided. + +Note that passing `timelineStart` of `nil` or zero (00:00:00:00) is the same as using the standard `<`, `==`, or `>` operators as a sort comparator. + +```swift +let timecode1: Timecode +let timecode2: Timecode +let start: Timecode +let result = timecode1.compare(to: timecode2, timelineStart: start) +// result is a ComparisonResult of orderedAscending, orderedSame, or orderedDescending +``` + +## Sorting + +Collections of `Timecode` can be sorted ascending or descending. + +```swift +let timeline: [Timecode] = [ ... ] +let sorted = timeline.sorted() // ascending +let sorted = timeline.sorted(ascending: false) // descending +``` + +These collections can also be tested for sort order: + +```swift +let timeline: [Timecode] = [ ... ] +let isSorted: Bool = timeline.isSorted() // ascending +let isSorted: Bool = timeline.isSorted(ascending: false) // descending +``` + +On newer systems, a `SortComparator` called ``TimecodeSortComparator`` is available as well. + +```swift +let comparator = TimecodeSortComparator() // ascending +let comparator = TimecodeSortComparator(order: .reverse) // descending + +let timeline: [Timecode] = [ ... ] +let sorted = timeline.sorted(using: comparator) +``` + +## Sorting using Timeline Context + +For an explanation of timeline context, see [Comparing using Timeline Context](#Comparing-using-Timeline-Context>). + +Collections of ``Timecode`` can be sorted ascending or descending. + +```swift +let timeline: [Timecode] = [ ... ] +let start = try "01:00:00:00".timecode(at: .fps24) +let sorted = timeline.sorted(timelineStart: start) // ascending +let sorted = timeline.sorted(order: .reverse, timelineStart: start) // descending +``` + +These collections can also be tested for sort order: + +```swift +let timeline: [Timecode] = [ ... ] +let start = try "01:00:00:00".timecode(at: .fps24) +let isSorted: Bool = timeline.isSorted(timelineStart: start) // ascending +let isSorted: Bool = timeline.isSorted(order: .reverse, timelineStart: start) // descending +``` + +On newer systems, a `SortComparator` called ``TimecodeSortComparator`` is available as well. + +```swift +let start = try "01:00:00:00".timecode(at: .fps24) +let comparator = TimecodeSortComparator(timelineStart: start) // ascending +let comparator = TimecodeSortComparator(order: .reverse, timelineStart: start) // descending + +let timeline: [Timecode] = [ ... ] +let sorted = timeline.sorted(using: comparator) +``` + +## Topics + +### Instance Comparison + +- ``Timecode/compare(to:timelineStart:)`` + +### Collection Sorting + +- ``Swift/Collection/isSorted(ascending:timelineStart:)`` +- ``Swift/MutableCollection/sort(ascending:timelineStart:)`` +- ``Swift/Collection/sorted()`` +- ``Swift/Collection/sorted(ascending:timelineStart:)`` + +### SortComparator + +- ``TimecodeSortComparator`` diff --git a/Sources/TimecodeKit/Documentation.docc/Timecode-Constructors.md b/Sources/TimecodeKit/Documentation.docc/Timecode-Constructors.md new file mode 100644 index 00000000..19f64de8 --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/Timecode-Constructors.md @@ -0,0 +1,20 @@ +# Constructors + +## Topics + +- ``Timecode/init(_:using:)-91fix`` +- ``Timecode/init(_:using:by:)-62c6g`` +- ``Timecode/init(_:at:base:limit:)-5c83i`` +- ``Timecode/init(_:at:base:limit:by:)-7ini4`` + +- ``Timecode/init(_:)-3i5yx`` + +- ``Timecode/init(_:using:)-41kgh`` +- ``Timecode/init(_:using:by:)-vi3i`` +- ``Timecode/init(_:at:base:limit:)-w372`` +- ``Timecode/init(_:at:base:limit:by:)-1mmhx`` + +- ``Timecode/init(_:using:)-7lle2`` +- ``Timecode/init(_:at:base:limit:)-6c3tu`` + +- ``Timecode/init(_:)-6vdaz`` diff --git a/Sources/TimecodeKit/Documentation.docc/Timecode-Conversions.md b/Sources/TimecodeKit/Documentation.docc/Timecode-Conversions.md new file mode 100644 index 00000000..dda2845a --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/Timecode-Conversions.md @@ -0,0 +1,96 @@ +# Conversions + +Converting various time values to/from timecode. + +## Convert to Another Frame Rate + +```swift +// convert between frame rates +let tc = try "01:00:00;00" + .timecode(at: .fps29_97d) + .converted(to: .fps29_97) // == 00:59:56:12 +``` + +## Total Frame Count + +```swift +// timecode → frame count +try Timecode(.components(h: 1), at: .fps23_976) + .frameCount // == 86400 +``` + +Useful `.frameCount` properties are also available. See ``Timecode/FrameCount-swift.struct`` for more details. + +```swift +// frame number → timecode +try Timecode(.frames(86400), at: .fps23_976) +// frame number + subframes → timecode +try Timecode(.frames(86400, subFrames: 25), at: .fps23_976) +// frame number + subframes unit interval as Double → timecode +try Timecode(.frames(86400.25), at: .fps23_976) +// frame number + subframes unit interval → timecode +try Timecode(.frames(86400, subFramesUnitInterval: 0.25), at: .fps23_976) +``` + +## String + +```swift +// timecode → string +try Timecode(.components(h: 1), at: .fps23_976) + .stringValue() // == "01:00:00:00" +``` + +```swift +// string → timecode +try Timecode(.string("01:00:00:00"), at: .fps23_976) +``` + +See for more details. + +## Real Time + +```swift +// timecode → elapsed real-world wall time in seconds +try "01:00:00:00" + .timecode(at: .fps23_976) + .realTimeValue // == 3603.6 as TimeInterval (Double) +``` + +```swift +// elapsed real-world wall time → timecode +try Timecode(.realTime(seconds: 3603.6), at: .fps23_976) +``` + +## Audio Samples + +```swift +// timecode → elapsed audio samples +try "01:00:00:00" + .timecode(at: .fps24) + .samplesValue(sampleRate: 48000) // == 172800000 +``` + +```swift +// elapsed audio samples → timecode +try Timecode(.samples(172800000, sampleRate: 48000), at: .fps24) +``` + +## Rational Fraction / CMTime + +See for more details. + +## Feet+Frames + +```swift +// timecode → feet+frames +try "01:00:00:00" + .timecode(at: .fps23_976) + .feetAndFramesValue // 5400+00 +``` + +```swift +// feet+frames components → timecode +try Timecode(.feetAndFrames(feet: 5400, frames: 0), at: .fps23_976) +// feet+frames string → timecode +try Timecode(.feetAndFrames("5400+00"), at: .fps23_976) +``` diff --git a/Sources/TimecodeKit/Documentation.docc/Timecode-Internals.md b/Sources/TimecodeKit/Documentation.docc/Timecode-Internals.md new file mode 100644 index 00000000..38993b44 --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/Timecode-Internals.md @@ -0,0 +1,14 @@ +# Timecode Internals + +## Topics + +### Utilities + +- ``Timecode/components(of:at:)`` +- ``Timecode/frameCount(of:at:base:)`` +- ``Timecode/invalidComponents(in:using:)`` +- ``Timecode/invalidComponents(in:at:base:limit:)`` + +### AVFoundation Utilities + +- ``Timecode/cmFormatDescription(extensions:)`` diff --git a/Sources/TimecodeKit/Documentation.docc/Timecode-Interval.md b/Sources/TimecodeKit/Documentation.docc/Timecode-Interval.md new file mode 100644 index 00000000..b5478817 --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/Timecode-Interval.md @@ -0,0 +1,66 @@ +# Timecode Interval + +Working with intervals between two timecodes. + +The ``TimecodeInterval`` struct wraps a ``Timecode`` instance and adds a sign (positive of negative). + +It serves to represent an absolute interval of timecode accompanied by a sign (+ / -) to establish the intent of the interval being *additive* or *subtractive* when passed into methods that accept a ``TimecodeInterval`` instance. + +``TimecodeInterval`` also accepts intervals larger than 24 hours and works well with raw timecode values. + +On the whole, timecode itself is the expression of an absolute video timestamp, or used as a duration of video frames. The concept of a 'negative' timecode is antithetical; timecode is not meant to be expressed or displayed on-screen to the user using a negative sign. In practise, timecode wraps around the clock forwards and backwards: typically around a 24 hour clock but ``Timecode`` can be set to 100 day wrapping for unique cases. This means that, at 24 fps: + +- `00:00:00:00` minus 1 frame is `23:59:59:23` (not `-00:00:00:01`) +- `23:59:59:23` plus 1 frame is `00:00:00:00` + +However, to meet the demand of some timecode calculations (such as offset transforms, theoretical calculations involving raw timecode values, or aggregate operations that may have otherwise resulted in wrapping the clock one or more times) ``TimecodeInterval`` is provided. + +```swift +// construct directly: +let tc = try Timecode(.components(h: 1), at: .fps24) +let interval = TimecodeInterval(tc, .negative) + +// construct with Timecode instance method: +let tc = try Timecode(.components(h: 1), at: .fps24) +let interval = tc.interval(.negative) + +// construct with - or + unary operator: +let interval = try -Timecode(.components(h: 1), at: .fps24) // negative +let interval = try +Timecode(.components(h: 1), at: .fps24) // positive + +// construct between two Timecode instances +let interval = timecode1.interval(to: timecode2) +``` + +The absolute interval can be returned. + +```swift +let tc = try Timecode(.components(h: 1), at: .fps24) + +let interval = TimecodeInterval(tc, .positive) // 01:00:00:00 +interval.absoluteInterval // 01:00:00:00 + +let interval = TimecodeInterval(tc, .negative) // -01:00:00:00 +interval.absoluteInterval // 01:00:00:00 +``` + +The interval can be flattened by wrapping it around the upper limit if necessary, which is 24 hours in timecode by default. + +```swift +let tc = try Timecode(.components(h: 1), at: .fps24) + +let interval = TimecodeInterval(tc, .positive) // 01:00:00:00 +interval.flattened() // 01:00:00:00 + +let interval = TimecodeInterval(tc, .negative) // -01:00:00:00 +interval.flattened() // 23:00:00:00 +``` + +## Topics + +- ``TimecodeInterval`` + +### CMTime Extensions + +- ``CoreMedia/CMTime/timecodeInterval(at:base:limit:)`` +- ``CoreMedia/CMTime/timecodeInterval(using:)`` diff --git a/Sources/TimecodeKit/Documentation.docc/Timecode-Math.md b/Sources/TimecodeKit/Documentation.docc/Timecode-Math.md new file mode 100644 index 00000000..e430bbd7 --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/Timecode-Math.md @@ -0,0 +1,113 @@ +# Math + +Math operations between two timecodes. + +See in the Getting Started guide. + +## Topics + +### Addition + +- ``Timecode/add(_:)-9m8xq`` +- ``Timecode/add(_:by:)-42lw`` + +- ``Timecode/add(_:)-88aua`` +- ``Timecode/add(_:by:)-4lww7`` + +- ``Timecode/add(_:)-857gw`` +- ``Timecode/add(_:by:)-1yniv`` + +- ``Timecode/add(_:)-14tde`` +- ``Timecode/add(_:by:)-2njan`` + +- ``Timecode/add(_:)-71xxo`` +- ``Timecode/add(_:by:)-14dcw`` + +- ``Timecode/add(_:)-3240f`` +- ``Timecode/add(_:by:)-4nkq8`` + +- ``Timecode/add(_:)-55w29`` +- ``Timecode/add(_:by:)-6fwhs`` + +- ``Timecode/adding(_:)-904hr`` +- ``Timecode/adding(_:by:)-8pf7p`` + +- ``Timecode/adding(_:)-5bv1`` +- ``Timecode/adding(_:by:)-7fcyg`` + +- ``Timecode/adding(_:)-1o83k`` +- ``Timecode/adding(_:by:)-74tzs`` + +- ``Timecode/adding(_:)-z5j7`` +- ``Timecode/adding(_:by:)-6awjq`` + +- ``Timecode/adding(_:)-qlqv`` +- ``Timecode/adding(_:by:)-19lr1`` + +- ``Timecode/adding(_:)-7b8yj`` +- ``Timecode/adding(_:by:)-9o39d`` + +- ``Timecode/adding(_:)-8rxca`` +- ``Timecode/adding(_:by:)-91bfw`` + +### Subtraction + +- ``Timecode/subtract(_:)-5gp46`` +- ``Timecode/subtract(_:by:)-4l96x`` + +- ``Timecode/subtract(_:)-7furw`` +- ``Timecode/subtract(_:by:)-9n8ue`` + +- ``Timecode/subtract(_:)-9oxpu`` +- ``Timecode/subtract(_:by:)-508dt`` + +- ``Timecode/subtract(_:)-ocmj`` +- ``Timecode/subtract(_:by:)-3onj2`` + +- ``Timecode/subtract(_:)-1rf5l`` +- ``Timecode/subtract(_:by:)-857ne`` + +- ``Timecode/subtract(_:)-9js9a`` +- ``Timecode/subtract(_:by:)-33g0h`` + +- ``Timecode/subtract(_:)-3j57f`` +- ``Timecode/subtract(_:by:)-8cjf8`` + +- ``Timecode/subtracting(_:)-9gs31`` +- ``Timecode/subtracting(_:by:)-4i08g`` + +- ``Timecode/subtracting(_:)-4k13k`` +- ``Timecode/subtracting(_:by:)-17ava`` + +- ``Timecode/subtracting(_:)-3ovst`` +- ``Timecode/subtracting(_:by:)-p5la`` + +- ``Timecode/subtracting(_:)-49fu`` +- ``Timecode/subtracting(_:by:)-2gt9c`` + +- ``Timecode/subtracting(_:)-1o05d`` +- ``Timecode/subtracting(_:by:)-42lpp`` + +- ``Timecode/subtracting(_:)-8mzxx`` +- ``Timecode/subtracting(_:by:)-92c0s`` + +- ``Timecode/subtracting(_:)-29zr6`` +- ``Timecode/subtracting(_:by:)-3pd2x`` + +### Multiplication + +- ``Timecode/multiply(_:)`` +- ``Timecode/multiply(_:by:)`` + +- ``Timecode/multiplying(_:)`` +- ``Timecode/multiplying(_:by:)`` + +### Division + +- ``Timecode/divide(_:)`` +- ``Timecode/divide(_:by:)`` + +- ``Timecode/dividing(_:)-9pfge`` +- ``Timecode/dividing(_:)-96rdz`` +- ``Timecode/dividing(_:)-4bgmz`` +- ``Timecode/dividing(_:by:)`` diff --git a/Sources/TimecodeKit/Documentation.docc/Timecode-Range-and-Strideable.md b/Sources/TimecodeKit/Documentation.docc/Timecode-Range-and-Strideable.md new file mode 100644 index 00000000..4f3972a1 --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/Timecode-Range-and-Strideable.md @@ -0,0 +1,68 @@ +# Range & Strideable + +Forming a `Range` or `Stride` between two ``Timecode`` instances. + +For simplicity, we will define the start and end timecodes beforehand. + +```swift +let startTC = try "01:00:00:00".timecode(at: .fps24) +let endTC = try "01:00:00:10".timecode(at: .fps24) +``` + +## Range + +Check if a timecode is contained within the range: + +```swift +(startTC...endTC).contains(try "01:00:00:05".timecode(at: .fps24)) // == true +(startTC...endTC).contains(try "01:05:00:00".timecode(at: .fps24)) // == false +``` + +Iterate on each frame of the range: + +```swift +for tc in startTC...endTC { + print(tc) +} +``` + +Prints: + +``` +01:00:00:00 +01:00:00:01 +01:00:00:02 +01:00:00:03 +01:00:00:04 +01:00:00:05 +01:00:00:06 +01:00:00:07 +01:00:00:08 +01:00:00:09 +01:00:00:10 +``` + +## Stride + +Iterate on every `n` frames of the range by using a stride: + +```swift +for tc in stride(from: startTC, to: endTC, by: 5) { + print(tc) +} +``` + +Prints: + +``` +01:00:00:00 +01:00:00:05 +01:00:00:10 +``` + +## Topics + +### CMTimeRange Extensions + +- ``CoreMedia/CMTimeRange/timecodeRange(at:base:limit:)`` +- ``CoreMedia/CMTimeRange/timecodeRange(using:)`` diff --git a/Sources/TimecodeKit/Documentation.docc/Timecode-Set.md b/Sources/TimecodeKit/Documentation.docc/Timecode-Set.md new file mode 100644 index 00000000..97c780e9 --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/Timecode-Set.md @@ -0,0 +1,30 @@ +# Setting Timecode + +Set an existing ``Timecode`` instance to a new timecode. + +## Topics + +- ``Timecode/set(_:)-5y7su`` +- ``Timecode/set(_:by:)-92yoi`` + +- ``Timecode/set(_:)-2rc5m`` +- ``Timecode/set(_:)-2rc5m`` + +- ``Timecode/set(_:)-22ndo`` +- ``Timecode/set(_:by:)-1goc0`` + +- ``Timecode/set(_:)-3p8ei`` + +- ``Timecode/set(_:)-89p8h`` + +- ``Timecode/setting(_:)-3akbp`` +- ``Timecode/setting(_:by:)-42wj3`` + +- ``Timecode/setting(_:)-1dhd5`` + +- ``Timecode/setting(_:)-95f3c`` +- ``Timecode/setting(_:by:)-2fpos`` + +- ``Timecode/setting(_:)-81hub`` + +- ``Timecode/setting(_:)-5q0hp`` diff --git a/Sources/TimecodeKit/Documentation.docc/Timecode-String.md b/Sources/TimecodeKit/Documentation.docc/Timecode-String.md new file mode 100644 index 00000000..95f2eb44 --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/Timecode-String.md @@ -0,0 +1,52 @@ +# Display String + +Working with timecode strings for GUI display or data output. + +To get the timecode string from a ``Timecode`` instance using default formatting options: + +```swift +try Timecode(.components(h: 01, m: 00, s: 00, f: 05), at: .fps29_97d) + .stringValue() // == "01:00:00;00" +``` + +## Formatting Options + +Additionally, formatting options may be provided. These options may by combined. + +### Show Subframes + +By default, subframes are not expressed in the string value. They can be enabled by passing the ``Timecode/StringFormatOption/showSubFrames`` option. + +```swift +try Timecode(.components(h: 01, m: 00, s: 00, f: 05), at: .fps29_97d) + .stringValue(format: [.showSubFrames]) // == "01:00:00;00.05" +``` + +### Filename Compatible + +The string value can be formatted to be filename-friendly by passing the ``Timecode/StringFormatOption/filenameCompatible`` option. + +```swift +try Timecode(.components(h: 01, m: 05, s: 20, f: 10), at: .fps29_97d) + .stringValue(format: [.filenameCompatible]) // == "01-05-20-10" +``` + +## Topics + +- ``Timecode/stringValue(format:)`` +- ``Timecode/stringValueValidated(format:invalidAttributes:defaultAttributes:)`` + +### StringFormat Options + +- ``Timecode/StringFormatOption/showSubFrames`` +- ``Timecode/StringFormatOption/filenameCompatible`` + +### StringFormat Static Constructors + +- ``Swift/Set/default()`` +- ``Swift/Set/showSubFrames-type.property`` + +### StringFormat Extensions + +- ``Swift/Set/showSubFrames-property`` +- ``Swift/Set/filenameCompatible-property`` diff --git a/Sources/TimecodeKit/Documentation.docc/Timecode-Transformer.md b/Sources/TimecodeKit/Documentation.docc/Timecode-Transformer.md new file mode 100644 index 00000000..e9143fc8 --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/Timecode-Transformer.md @@ -0,0 +1,11 @@ +# Timecode Transformer + +Using the timecode transformer facility. + +``TimecodeTransformer`` is a mechanism that can define one or more timecode transforms in series. + +It can then be used to transform a ``Timecode`` instance. + +## Topics + +- ``TimecodeTransformer`` diff --git a/Sources/TimecodeKit/Documentation.docc/Timecode-Validation.md b/Sources/TimecodeKit/Documentation.docc/Timecode-Validation.md new file mode 100644 index 00000000..eaf6f8db --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/Timecode-Validation.md @@ -0,0 +1,125 @@ +# Validation + +Timecode validation based on frame rate and upper limit. + +## Timecode Component Validation + +Timecode validation can be helpful and powerful, for example, when parsing timecode strings read from an external data file or received as user-input in a text field. + +Timecode can be tested as: + +- valid or invalid as a whole, by catching a throwing error when using the default throwing initializers or `set()` methods, or +- granularly to test validity of individual timecode components + +```swift +// example: +// 1 hour and 20 minutes ARE valid at 23.976 fps, +// but 75 seconds and 60 frames are NOT valid + +// non-granular validation +try Timecode(.components(h: 1, m: 20, s: 75, f: 60), at: .fps23_976) +// == throws error; cannot form a valid timecode + +// granular validation +// allowingInvalid allows invalid values; does not throw errors so 'try' is not needed +Timecode(.components(h: 1, m: 20, s: 75, f: 60), at: .fps23_976, by: .allowingInvalid) + .invalidComponents // == [.seconds, .frames] +``` + +## Formatted NSAttributedString + +This method can produce an `NSAttributedString` highlighting individual invalid timecode components with a specified set of attributes. + +```swift +Timecode(.components(h: 1, m: 20, s: 75, f: 60), at: .fps23_976, by: .allowingInvalid) + .stringValueValidated() +``` + +The invalid formatting attributes defaults to applying `[.foregroundColor: NSColor.red]` to invalid components. You can alternatively supply your own invalid attributes by setting the `invalidAttributes` argument. + +You can also supply a set of default attributes to set as the baseline attributes for the entire string. + +```swift +// set text's background color to red instead of its foreground color +let invalidAttr: [NSAttributedString.Key: Any] = [ + .backgroundColor: NSColor.red +] + +// set custom font and font size for the entire string +let defaultAttr: [NSAttributedString.Key: Any] = [ + .font: NSFont.systemFont(ofSize: 16) +] + +Timecode(.components(h: 1, m: 20, s: 75, f: 60), at: .fps23_976, by: .allowingInvalid) + .stringValueValidated(invalidAttributes: invalidAttr, + defaultAttributes: defaultAttr) +``` + +## NSFormatter + +A special string `Formatter` (`NSFormatter`) subclass can + +- process user-entered timecode strings and format them in realtime in a TextField +- optionally highlight individual invalid timecode components with a specified set of attributes (defaults to red foreground color) + +The invalid formatting attributes defaults to applying `[.foregroundColor: NSColor.red]` to invalid components. You can alternatively supply your own invalid attributes by setting the `validationAttributes` property on the formatter. + +```swift +// set up formatter +let formatter = Timecode.TextFormatter( + using: .init( + rate: .fps23_976, + base: .max80SubFrames, + limit: .max24Hours + ) + stringFormat: [.showSubFrames], + showsValidation: true, // enable invalid component highlighting + validationAttributes: nil // if nil, defaults to red foreground color +) + +// assign formatter to a TextField UI object, for example +let textField = NSTextField() +textField.formatter = formatter +``` + +## Formatted SwiftUI Text + +When importing `TimecodeKitUI`, a SwiftUI `Text` view is available which highlights individual invalid timecode components with a specified set of modifiers. + +The invalid formatting attributes defaults to applying `.foregroundColor(Color.red)` to invalid components. + +```swift +Timecode(.components(h: 1, m: 20, s: 75, f: 60), at: .fps23_976, by: .allowingInvalid) + .stringValueValidatedText() +``` + +You can alternatively supply your own invalid modifiers by setting the `invalidModifiers` argument. + +```swift +import TimecodeKitUI + +Timecode(.components(h: 1, m: 20, s: 75, f: 60), at: .fps23_976, by: .allowingInvalid) + .stringValueValidatedText( + invalidModifiers: { + $0.foregroundColor(.blue) + }, defaultModifiers: { + $0.foregroundColor(.black) + } + ) +``` + +## Topics + +### Invalid Components + +- ``Timecode/invalidComponents`` +- ``Timecode/invalidComponents(in:at:base:limit:)`` +- ``Timecode/invalidComponents(in:using:)`` + +### Formatted Attributed String + +- ``Timecode/stringValueValidated(format:invalidAttributes:defaultAttributes:)`` + +### Formatter + +- ``Timecode/TextFormatter`` diff --git a/Sources/TimecodeKit/Documentation.docc/Timecode.md b/Sources/TimecodeKit/Documentation.docc/Timecode.md new file mode 100644 index 00000000..71e39d53 --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/Timecode.md @@ -0,0 +1,211 @@ +# ``TimecodeKit/Timecode`` + +TimecodeKit is designed to work with Xcode's autocomplete fluidly. Beginning a ``Timecode`` initializer with a period will produce a list of all available source time value types. + +![Timecode init](timecode-init.png) + +Timecode can be formed by providing discrete values. + +```swift +// zero timecode (00:00:00:00) +.zero + +// timecode component values +.components(h: 01, m: 00, s: 00, f: 00) +.components(d: 00, h: 01, m: 00, s: 00, f: 00, sf: 00) // days and subframes allowed +.components(Timecode.Components(h: 1)) // also accepts struct instance +``` + +Timecode can be formed by converting from a variety of common time values. + +```swift +// frame number (total elapsed frames) +.frames(10000) // whole frames +.frames(10000, subFrames: 20) // whole frames + subframes +.frames(10000.25) // (Double) whole frames + float subframes +.frames(10000, subFramesUnitInterval: 0.25) // whole frames + float subframes +.frames(Timecode.FrameCount(...)) // also accepts struct instance + +// timecode string +.string("01:00:00:00") +.string("2 01:00:00:00.00") // days and subframes allowed + +// real time (wall clock) elapsed in seconds +.realTime(seconds: 4723.241579) + +// elapsed audio samples at a given sample rate in Hz +.samples(123456789, sampleRate: 48000) + +// AVFoundation AVAsset: read .start, .end or .duration timecode of a movie +.avAsset(AVAsset(...), .start) + +// AVFoundation CMTime +.cmTime(CMTime(value: 1920919, timescale: 30000)) + +// rational time fraction (ie: Final Cut Pro XML, or AAF) +.rational(1920919, 30000) +.rational(Fraction(1920919, 30000)) // also accepts struct instance + +// traditional Feet+Frames reference +.feetAndFrames(feet: 60, frames: 10) +.feetAndFrames("60+10") +.feetAndFrames(FeetAndFrames(feet: 60, frames: 10)) // also accepts struct instance +``` + +The frame rate must also be supplied. This can be done easily with the `at:` overload. + +```swift +let tc = try Timecode(.string("01:00:00:00"), at: .fps23_976) +``` + +If additional properties need to be specified, supply a ``Timecode/Properties`` struct with the `using:` overload. + +```swift +let properties = Timecode.Properties( + rate: .fps23_976, + base: .max100SubFrames, + limit: .max24Hours +) +let tc = try Timecode(.string("01:00:00:00"), using: properties) +``` + +It is possible to clamp to valid timecode using a non-throwing init. + +```swift +// clamp full timecode to valid range +Timecode(.components(h: 26, m: 00, s: 00, f: 00), at: .fps24, by: .clamping) + .stringValue() // == "23:59:59:23" + +// clamp individual timecode component values to valid values if they are out-of-bounds +Timecode(.components(h: 01, m: 00, s: 85, f: 50), at: .fps24, by: .clampingEach) + .stringValue() // == "01:00:59:23" +``` + +It is also possible to wrap to valid timecode using a non-throwing init. + +```swift +// wrap around clock continuously if entire timecode overflows or underflows + +Timecode(.components(h: 26, m: 00, s: 00, f: 00), at: .fps24, by: .wrapping) + .stringValue() // == "02:00:00:00" + +Timecode(.components(h: 23, m: 59, s: 59, f: 24), at: .fps24, by: .wrapping) + .stringValue() // == "00:00:00:00" +``` + +## Topics + +### Constructors + +- + +### Components + +- ``Components-swift.struct`` +- ``components-swift.property`` +- ``days`` +- ``hours`` +- ``minutes`` +- ``seconds`` +- ``frames`` +- ``subFrames`` + +### Properties + +- ``Properties-swift.struct`` +- ``properties-swift.property`` + +### Frame Rate + +- ``frameRate`` + +### Subframes Base + +- ``SubFramesBase-swift.enum`` +- ``subFramesBase-swift.property`` + +### Upper Limit + +- ``UpperLimit-swift.enum`` +- ``upperLimit-swift.property`` + +### Frame Count + +- ``FrameCount-swift.struct`` +- ``frameCount-swift.property`` +- ``maxFrameCountExpressible`` +- ``maxSubFramesExpressible`` +- ``maxSubFrameCountExpressible`` + +### Timecode String + +- ``StringFormat`` +- ``StringFormatOption`` +- ``stringValue(format:)`` +- ``stringValueValidated(format:invalidAttributes:defaultAttributes:)`` + +### Conversion + +- ``converted(to:preservingValues:)`` +- ``cmTimeValue`` +- ``feetAndFramesValue`` +- ``rationalValue`` +- ``realTimeValue`` +- ``samplesValue(sampleRate:)`` +- ``samplesDoubleValue(sampleRate:)`` + +### Setting Timecode + +- + +### Math + +- +- ``isZero`` + +### Rounding + +- ``clampComponents()`` +- ``roundUp(toNearest:)`` +- ``roundedUp(toNearest:)`` +- ``roundDown(toNearest:)`` +- ``roundedDown(toNearest:)`` + +### Comparison + +- ``compare(to:timelineStart:)`` + +### Intervals + +- ``asInterval(_:)`` +- ``interval(to:)`` +- ``offset(by:)`` +- ``offsetting(by:)`` + +### Validation + +- ``ValidationRule`` +- ``ValidationError`` +- ``invalidComponents`` +- ``stringValueValidated(format:invalidAttributes:defaultAttributes:)`` +- ``Component`` +- ``validRange(of:)`` + +### Transformer + +- ``transform(using:)`` +- ``transformed(using:)`` + +### Formatter + +- ``TextFormatter`` + +### Errors + +- ``StringParseError`` +- ``MediaParseError`` +- ``MediaWriteError`` + +### Internals + +- diff --git a/Sources/TimecodeKit/Documentation.docc/TimecodeKit-2-Migration-Guide.md b/Sources/TimecodeKit/Documentation.docc/TimecodeKit-2-Migration-Guide.md new file mode 100644 index 00000000..47c40edc --- /dev/null +++ b/Sources/TimecodeKit/Documentation.docc/TimecodeKit-2-Migration-Guide.md @@ -0,0 +1,184 @@ +# TimecodeKit 2 Migration Guide + +API changes from TimecodeKit version 1 to version 2. + +This guide is designed to assist in migrating projects currently using TimecodeKit 1.x to version 2.x. While not exhaustive, this guide covers the major API and workflow changes. + +## Time Value Types + +In order to simplify initialization API and make time value types more easily discoverable, time values are now passed in as static wrappers to Timecode inits. + +![Timecode init](timecode-init.png) + +For example: + +```swift +// 1.x API +Timecode("01:00:00:00", at: ._24) +// 2.x API +Timecode(.string("01:00:00:00"), at: .fps24) +``` + +Full list of corresponding time value enum cases: + +| 1.x API | 2.x API | +| --------------------------------------------------------- | ------------------------------------------------------------ | +| `Timecode(TCC(), at: ._24)` | `Timecode(.zero, at: .fps24)` | +| `Timecode(TCC(h: 1, m: 0, s: 0, f: 0), at: ._24)` | `Timecode(.components(h: 1, m: 0, s: 0, f: 0), at: .fps24)` | +| `Timecode(.frames(1234), at: ._24)` | `Timecode(.frames(1234), at: .fps24)` | +| `Timecode(.combined(frames: 123.5), at: ._24)` | `Timecode(.frames(123.5), at: .fps24)` | +| `Timecode(.split(frames: 123, subFrames: 50), at: ._24)` | `Timecode(.frames(123, subFrames: 50), at: .fps24)` | +| `Timecode(.splitUnitInterval(frames: 123, subFramesUnitInterval: 0.5), at: ._24)` | `Timecode(.frames(123, subFramesUnitInterval: 0.5), at: .fps24)` | +| `Timecode("01:00:00:00", at: ._24)` | `Timecode(.string("01:00:00:00"), at: .fps24)` | +| `Timecode(realTime: 123.0, at: ._24)` | `Timecode(.realTime(seconds: 123.0), at: .fps24)` | +| `Timecode(samples: 123.0, sampleRate: 48000, at: ._24)` | `Timecode(.samples(123.0, sampleRate: 48000), at: .fps24)` | +| `Timecode(startOf: AVAsset(), at: ._24)` | `Timecode(.avAsset(AVAsset(), .start), at: .fps24)` | +| `Timecode(durationOf: AVAsset(), at: ._24)` | `Timecode(.avAsset(AVAsset(), .duration), at: .fps24)` | +| `Timecode(endOf: AVAsset(), at: ._24)` | `Timecode(.avAsset(AVAsset(), .end), at: .fps24)` | +| `Timecode(CMTime(), at: ._24)` | `Timecode(.cmTime(CMTime()), at: .fps24)` | +| `Timecode(Fraction(60, 1), at: ._24)` | `Timecode(.rational(60, 1), at: .fps24)` | +| `Timecode(FeetAndFrames(feet: 60, frames: 10), at: ._24)` | `Timecode(.feetAndFrames(feet: 60, frames: 10), at: .fps24)` | +| `Timecode(flattening: TimecodeInterval)` | `Timecode(.interval(flattening: TimecodeInterval)` | + +## Timecode Validation + +Time value validation has changed from parameter labels to a new `by:` parameter. + +```swift +// "exactly" was denoted by an empty parameter label, and is a throwing init +// 1.x API +try Timecode("01:00:00:00", at: ._24) +// 2.x API +try Timecode(.string("01:00:00:00"), at: .fps24) + +// "clamping" +// 1.x API +Timecode(clamping: "01:00:00:00", at: ._24) +// 2.x API +Timecode(.string("01:00:00:00"), at: .fps24, by: .clamping) + +// "wrapping" +// 1.x API +Timecode(wrapping: "01:00:00:00", at: ._24) +// 2.x API +Timecode(.string("01:00:00:00"), at: .fps24, by: .wrapping) + +// "rawValues" +// 1.x API +Timecode(rawValues: "01:00:00:00", at: ._24) +// 2.x API +Timecode(.string("01:00:00:00"), at: .fps24, by: .allowingInvalid) +``` + +## Timecode String Value + +- The `stringValue` property is now the ``Timecode/stringValue(format:)`` method. +- The Timecode struct no longer stores string formatting properties. Instead, formatting options are now optionally passed when calling ``Timecode/stringValue(format:)``. + +```swift +// 1.x API +let timecode = try Timecode(TCC(h: 1, m: 0, s: 0, f: 0, sf: 50), at: ._24) +timecode.stringValue // "01:00:00:00" +timecode.stringFormat = [.showSubFrames] +timecode.stringValue // "01:00:00:00.50" +// 2.x API +let timecode = try Timecode(.components(h: 1, m: 0, s: 0, f: 0, sf: 50), at: .fps24) +timecode.stringValue() // "01:00:00:00" +timecode.stringValue(format: [.showSubFrames]) // "01:00:00:00.50" +``` + +- The `stringValueFileNameCompatible` property has been removed and is now a format option. + +```swift +timecode.stringValue(format: [.filenameCompatible]) +``` + +## Timecode Properties + +As in TimecodeKit 1.x, it is still possible to pass properties directly to the initializer as parameters: + +```swift +let timecode = try Timecode( + .components(h: 1, m: 0, s: 0, f: 0), + at: .fps24, + base: .max80SubFrames, + limit: .max24Hours +) +``` + +Timecode metadata can now also be constructed and passed using a new ``Timecode/Properties-swift.struct`` struct. It contains: + +- `frameRate` +- `subFramesBase` +- `upperLimit` + +```swift +// construct a Timecode.Properties instance +// and pass it to a new Timecode instance +let properties = Timecode.Properties( + rate: .fps24, + base: .max80SubFrames, + limit: .max24Hours +) +let timecode = Timecode( + .components(h: 1, m: 0, s: 0, f: 0), + using: properties +) + +// it can also be fetched using the `properties` property and used to +// construct a new Timecode with the same properties +let newTimecode = Timecode( + .components(h: 2, m: 0, s: 0, f: 0), + using: timecode.properties +) +``` + +## Set Timecode on an Existing Timecode Instance + +Previous `Timecode` `setTimecode()` methods have been refactored to use a more consistent `set()` methods, with overloads similar to the new `Timecode` initializers. +This allows set methods to take the same value sources and validation rules by using the same API as the initializers. + +For example: + +```swift +var timecode = Timecode(.zero, at: .fps24) +try timecode.set(.realTime(seconds: 123.0)) +timecode.set(.frames(1234), by: .wrapping) +``` + +For value type reference, see the [Time Value Types](#Time-Value-Types) section above. + +For timecode validation rules reference, see the [Timecode Validation](#Timecode-Validation) section above. + +## Functional Shorthand + +The time value category method `toTimecode(...)` has been renamed to `timecode(...)`. + +For example: + +```swift +// 1.x API +try "01:00:00:00".toTimecode(at: ._24) +// 2.x API +try "01:00:00:00".timecode(at: .fps24) +``` + +## Enum Case Respellings + +Some enum cases have been renamed to conform to lowerCamelCase and replace underscore prefixes. + +- ``TimecodeFrameRate`` cases have been renamed. + - `._24` is now `.fps24` and so on +- ``VideoFrameRate`` cases have been renamed. + - `._24p` is now `.fps24p` and so on +- ``Timecode/UpperLimit-swift.enum`` cases have been renamed. + - `._24hours` is now `.max24Hours` + - `._100days` is now `.max100Days` +- ``Timecode/SubFramesBase-swift.enum`` cases have been renamed. + - `._80SubFrames` is now `.max80SubFrames` + - `._100SubFrames` is now `.max100SubFrames` +- ``TimecodeFrameRate/CompatibleGroup-swift.enum`` cases have been renamed. + - `.NTSC` is now `.ntscColor` + - `.NTSC_drop` is now `.ntscDrop` + - `.ATSC` is now `.whole` + - `.ATSC_drop` is now `.ntscColorWallTime` diff --git a/Sources/TimecodeKit/FeetAndFrames/FeetAndFrames.swift b/Sources/TimecodeKit/FeetAndFrames/FeetAndFrames.swift index 3b0fc0f1..117a86aa 100644 --- a/Sources/TimecodeKit/FeetAndFrames/FeetAndFrames.swift +++ b/Sources/TimecodeKit/FeetAndFrames/FeetAndFrames.swift @@ -1,17 +1,22 @@ // // FeetAndFrames.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // import Foundation -/// Feet+Frames value. +/// Feet+Frames is a time reference traditionally used when measuring and splicing physical film, but its use in modern day is uncommon. +/// Some digital video editors and DAWs support this time format. +/// +/// Initializers and properties on ``Timecode`` are available to convert to or from Feet+Frames. /// /// When used as a counter in the audio-world the footage count refers to 35mm 4-perf. Detailed /// discussion can be found [in this thread.]( /// https://gearspace.com/board/post-production-forum/898755-timecode-feet-frames.html /// ) +/// +/// For added precision, ``subFrames`` are an optional additional component. public struct FeetAndFrames: Equatable, Hashable { public var feet: Int public var frames: Int @@ -29,6 +34,22 @@ public struct FeetAndFrames: Equatable, Hashable { self.subFrames = subFrames self.subFramesBase = subFramesBase } + + /// Initialize from a Feet+Frames string value. + /// Throws an error if the string is not formatted correctly. + /// + /// - Throws: ``Timecode/StringParseError`` + public init( + _ string: S, + subFramesBase: Timecode.SubFramesBase = .default() + ) throws { + let decoded = try Self.decode(feetAndFrames: string) + + feet = decoded.feet + frames = decoded.frames + subFrames = decoded.subFrames + self.subFramesBase = subFramesBase + } } extension FeetAndFrames: CustomStringConvertible { diff --git a/Sources/TimecodeKit/FrameRateProtocol/FrameRateProtocol Properties.swift b/Sources/TimecodeKit/FrameRateProtocol/FrameRateProtocol Properties.swift index 742bf060..17a9406f 100644 --- a/Sources/TimecodeKit/FrameRateProtocol/FrameRateProtocol Properties.swift +++ b/Sources/TimecodeKit/FrameRateProtocol/FrameRateProtocol Properties.swift @@ -1,7 +1,7 @@ // // FrameRateProtocol Properties.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // // MARK: Sorted @@ -25,14 +25,14 @@ extension Collection where Element: FrameRateProtocol { extension Collection where Element: FrameRateProtocol { /// Internal: /// Filters collection to rates that match the given rational rate fraction. - internal func filter( + func filter( rate: Fraction ) -> [Element] { filter { let lhsFrac = $0.rate let isLiteralMatch = lhsFrac.numerator == rate.numerator - && lhsFrac.denominator == rate.denominator + && lhsFrac.denominator == rate.denominator let lhsFPS = Double(lhsFrac.numerator) / Double(lhsFrac.denominator) let rhsFPS = Double(rate.numerator) / Double(rate.denominator) @@ -44,19 +44,19 @@ extension Collection where Element: FrameRateProtocol { /// Internal: /// Filters collection to rates that match the given rational frame duration fraction. - internal func filter( + func filter( frameDuration: Fraction ) -> [Element] { filter { func compare(lhsFrac: Fraction, result: inout Bool) { let isLiteralMatch = lhsFrac.numerator == frameDuration.numerator - && lhsFrac.denominator == frameDuration.denominator - if isLiteralMatch { result = true ; return } + && lhsFrac.denominator == frameDuration.denominator + if isLiteralMatch { result = true; return } let lhsFPS = Double(lhsFrac.numerator) / Double(lhsFrac.denominator) let rhsFPS = Double(frameDuration.numerator) / Double(frameDuration.denominator) let isFPSMatch = lhsFPS == rhsFPS - if isFPSMatch { result = true ; return } + if isFPSMatch { result = true; return } } var result: Bool = false @@ -73,3 +73,49 @@ extension Collection where Element: FrameRateProtocol { } } } + +// MARK: - CMTime + +#if canImport(CoreMedia) +import CoreMedia + +@available(macOS 10.7, iOS 4.0, tvOS 9.0, watchOS 6.0, *) +extension FrameRateProtocol { + // NOTE: Initializers that take CMTime rate/frameDuration are implemented on each concrete type that conforms to `FrameRateProtocol`. + + /// Returns the frame rate (fps) as a rational number (fraction) + /// as a Core Media `CMTime` instance. + /// + /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to represent + /// times and durations. + /// + /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in + /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate + /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as + /// fractions.) + public var rateCMTime: CMTime { + CMTime( + value: CMTimeValue(rate.numerator), + timescale: CMTimeScale(rate.denominator) + ) + } + + /// Returns the duration of 1 frame as a rational number (fraction) + /// as a Core Media `CMTime` instance. + /// + /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to represent + /// times and durations. + /// + /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in + /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate + /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as + /// fractions.) + public var frameDurationCMTime: CMTime { + CMTime( + value: CMTimeValue(frameDuration.numerator), + timescale: CMTimeScale(frameDuration.denominator) + ) + } +} + +#endif diff --git a/Sources/TimecodeKit/FrameRateProtocol/FrameRateProtocol.swift b/Sources/TimecodeKit/FrameRateProtocol/FrameRateProtocol.swift index 1e4487f5..dab86be6 100644 --- a/Sources/TimecodeKit/FrameRateProtocol/FrameRateProtocol.swift +++ b/Sources/TimecodeKit/FrameRateProtocol/FrameRateProtocol.swift @@ -1,22 +1,17 @@ // // FrameRateProtocol.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // import Foundation -#if canImport(CoreMedia) -import CoreMedia -#endif - +/// Protocol used in TimecodeKit to provide shared properties and methods for frame rate types. public protocol FrameRateProtocol where Self: CaseIterable, AllCases.Index == Int, Self: Equatable { - // MARK: Properties - /// Returns human-readable frame rate string. var stringValue: String { get } @@ -43,9 +38,4 @@ public protocol FrameRateProtocol where /// rate (and not a video rate), its status as a drop or non-drop rate must be stored /// independently and recalled. var alternateFrameDuration: Fraction? { get } - - #if canImport(CoreMedia) - @available(macOS 10.7, iOS 4.0, tvOS 9.0, watchOS 6.0, *) - var frameDurationCMTime: CMTime { get } - #endif } diff --git a/Sources/TimecodeKit/Timecode/Components/Component.swift b/Sources/TimecodeKit/Timecode/Components/Component.swift index f5e35519..dba313fc 100644 --- a/Sources/TimecodeKit/Timecode/Components/Component.swift +++ b/Sources/TimecodeKit/Timecode/Components/Component.swift @@ -1,7 +1,7 @@ // // Component.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // extension Timecode { diff --git a/Sources/TimecodeKit/Timecode/Components/Components.swift b/Sources/TimecodeKit/Timecode/Components/Components.swift index 5dcbb710..185d2805 100644 --- a/Sources/TimecodeKit/Timecode/Components/Components.swift +++ b/Sources/TimecodeKit/Timecode/Components/Components.swift @@ -1,37 +1,144 @@ // // Components.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // -/// Convenience typealias for cleaner code. -public typealias TCC = Timecode.Components - extension Timecode { /// Primitive struct describing timecode values, agnostic of frame rate. - /// (The global typealias `TCC()` is also available for convenience.) /// - /// Raw values are stored and are not implicitly validated or clamped. + /// In order to help facilitate defining a set of timecode component values, a simple ``Timecode/Components`` struct is implemented. This struct can be passed into many methods and initializers. + /// + /// ```swift + /// let tcc = Timecode.Components(h: 1) + /// Timecode(.components(tcc), at: .fps23_976) + /// + /// // is the same as using the shorthand: + /// Timecode(.components(h: 1), at: .fps23_976) + /// ``` + /// + /// ```swift + /// let cmp = try "01:12:20:05" + /// .timecode(at: .fps23_976) + /// .components // Timecode.Components + /// + /// cmp.days // == 0 + /// cmp.hours // == 1 + /// cmp.minutes // == 12 + /// cmp.seconds // == 20 + /// cmp.frames // == 5 + /// cmp.subFrames // == 0 + /// ``` + /// + /// Raw values are stored verbatim and are not implicitly validated or clamped. + /// + /// ## Days Component + /// + /// Using the Days timecode component. + /// + /// Although not covered by SMPTE spec, some DAWs (digital audio workstation) such as Cubase support the use of the Days timecode component for timelines longer than (or outside of) 24 hours. + /// + /// By default, ``Timecode`` is constructed with an ``Timecode/upperLimit`` of 24-hour maximum expression (`.max24Hours`) which suppresses the ability to use Days. To enable Days, set the limit to `.max100Days`. + /// + /// The limit setting naturally affects internal timecode validation routines, as well as clamping and wrapping. + /// + /// ```swift + /// // valid timecode range at 24 fps, with 24 hours limit + /// "00:00:00:00" ... "23:59:59:23" + /// + /// // valid timecode range at 24 fps, with 100 days limit + /// "00:00:00:00" ... "99 23:59:59:23" + /// ``` + /// + /// ## SubFrames Component + /// + /// Using the SubFrames timecode component. + /// + /// Subframes represent a fraction (subdivision) of a single frame. + /// + /// Subframes are only used by some software and hardware, and since there are no industry standards, each manufacturer can decide how they want to implement subframes. Subframes are frame rate agnostic, meaning the subframe base (divisor) is mutually exclusive of frame rate. + /// + /// For example: + /// + /// - *Cubase/Nuendo* and *Logic Pro* globally use 80 subframes per frame (0 ... 79) regardless of frame rate + /// - *Pro Tools* uses 100 subframes (0 ... 99) globally regardless of frame rate + /// + /// Timecode supports subframes throughout. However, by default subframes are not displayed in ``Timecode/stringValue(format:)``. You can enable them: + /// + /// ```swift + /// var tc = try "01:12:20:05.62" + /// .timecode(at: .fps24, base: .max80SubFrames) + /// + /// // string with default formatting + /// tc.stringValue() // == "01:12:20:05" + /// tc.subFrames // == 62 (subframes are preserved even though not displayed in stringValue) + /// + /// // string with subframes shown + /// tc.stringValue(format: .showSubFrames) // == "01:12:20:05.62" + /// ``` + /// + /// Subframes are always calculated when performing operations on the ``Timecode`` instance, even if they are not expressed in the timecode string when not displaying subframes. + /// + /// ```swift + /// var tc = try "00:00:00:00.40" + /// .timecode(at: .fps24, base: .max80SubFrames) + /// + /// tc.stringValue() // == "00:00:00:00" + /// tc.stringValue(format: .showSubFrames) // == "00:00:00:00.40" + /// + /// // multiply timecode by 2. + /// // 40 subframes is half of a frame at 80 subframes per frame + /// (tc * 2).stringValue(format: .showSubFrames) // == "00:00:00:01.00" + /// ``` public struct Components { // MARK: Contents - /// Days - public var d: Int + /// Timecode days component. + /// + /// Valid only if ``Timecode/upperLimit-swift.property`` is set to `.max100Days`. + /// + /// Setting this value directly does not trigger any validation. + public var days: Int - /// Hours - public var h: Int + /// Timecode hours component. + /// + /// Valid range: 0 ... 23. + /// + /// Setting this value directly does not trigger any validation. + public var hours: Int - /// Minutes - public var m: Int + /// Timecode minutes component. + /// + /// Valid range: 0 ... 59. + /// + /// Setting this value directly does not trigger any validation. + public var minutes: Int - /// Seconds - public var s: Int + /// Timecode seconds component. + /// + /// Valid range: 0 ... 59. + /// + /// Setting this value directly does not trigger any validation. + public var seconds: Int - /// Frames - public var f: Int + /// Timecode frames component. + /// + /// Valid range is dependent on the `frameRate` property. + /// + /// Setting this value directly does not trigger any validation. + public var frames: Int - /// Subframe component (expressed as unit interval 0.0...1.0) - public var sf: Int + /// Timecode subframes component. + /// Represents a subdivision of the current frame. + /// + /// Some implementations refer to these as SMPTE frame "bits". + /// + /// Industry standards vary regarding subframe divisors depending on manufacturers and formats, + /// and not all manufacturers support the usage of subframes. + /// - DAWs such as Cubase, Nuendo, Logic Pro, and Final Cut Pro use 80 subframes per frame (0 ... 79). + /// - DAWs such as Pro Tools use 100 subframes per frame (0 ... 99). + /// - MIDI Timecode (MTC) uses 4 subframes per frame, also known as quarter-frames (0 ... 3). + public var subFrames: Int // MARK: init @@ -43,23 +150,26 @@ extension Timecode { f: Int = 0, sf: Int = 0 ) { - self.d = d - self.h = h - self.m = m - self.s = s - self.f = f - self.sf = sf + days = d + hours = h + minutes = m + seconds = s + frames = f + subFrames = sf } } } -extension Timecode.Components: Equatable { - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.d == rhs.d && - lhs.h == rhs.h && - lhs.m == rhs.m && - lhs.s == rhs.s && - lhs.f == rhs.f && - lhs.sf == rhs.sf - } +extension Timecode.Components: Equatable { } + +extension Timecode.Components: Hashable { } + +extension Timecode.Components: Codable { } + +// MARK: - Static Constructors + +extension Timecode.Components { + /// Components value of zero (00:00:00:00) + @_disfavoredOverload + public static let zero: Self = .init(h: 0, m: 0, s: 0, f: 0) } diff --git a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode AVAsset.swift b/Sources/TimecodeKit/Timecode/Data Interchange/Timecode AVAsset.swift deleted file mode 100644 index 24b10e86..00000000 --- a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode AVAsset.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// Timecode Rational.swift -// TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License -// - -// AVAssetReader is unavailable on watchOS so we can't support any AVAsset operations -#if canImport(AVFoundation) && !os(watchOS) && !os(visionOS) - -import Foundation -import AVFoundation - -extension Timecode { - /// Instance from embedded start timecode of an `AVAsset`. - /// - /// - Note: Passing a value to `frameRate` will override frame rate detection. - /// Passing `nil` will detect frame rate from the asset's contents if possible. - /// - /// - Throws: ``MediaParseError`` - public init( - startOf asset: AVAsset, - at rate: TimecodeFrameRate? = nil, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) throws { - guard let tc = try asset.startTimecode( - at: rate, - limit: limit, - base: base, - format: format - ) else { - throw MediaParseError.unknownTimecode - } - - self = tc - } - - /// Instance from embedded end timecode (start + duration) of an `AVAsset`. - /// - /// - Note: Passing a value to `frameRate` will override frame rate detection. - /// Passing `nil` will detect frame rate from the asset's contents if possible. - /// - /// - Throws: ``MediaParseError`` or ``ValidationError`` - public init( - endOf asset: AVAsset, - at rate: TimecodeFrameRate? = nil, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) throws { - guard let tc = try asset.endTimecode( - at: rate, - limit: limit, - base: base, - format: format - ) else { - throw MediaParseError.unknownTimecode - } - - self = tc - } - - /// Instance from embedded duration of an `AVAsset`. - /// - /// - Note: Passing a value to `frameRate` will override frame rate detection. - /// Passing `nil` will detect frame rate from the asset's contents if possible. - /// - /// - Throws: ``MediaParseError`` or ``ValidationError`` - public init( - durationOf asset: AVAsset, - at rate: TimecodeFrameRate? = nil, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) throws { - self = try asset.durationTimecode( - at: rate, - limit: limit, - base: base, - format: format - ) - } -} - -#endif diff --git a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode Components.swift b/Sources/TimecodeKit/Timecode/Data Interchange/Timecode Components.swift deleted file mode 100644 index 35a3c422..00000000 --- a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode Components.swift +++ /dev/null @@ -1,247 +0,0 @@ -// -// Timecode Components.swift -// TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License -// - -// MARK: - Init - -extension Timecode { - /// Instance exactly from timecode values and frame rate. - /// - /// If any values are out-of-bounds an error will be thrown, indicating an invalid timecode. - /// - /// Validation is based on the `upperLimit` and `subFramesBase` properties. - /// - /// - Throws: ``ValidationError`` - public init( - _ exactly: Components, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) throws { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - try setTimecode(exactly: exactly) - } - - /// Instance from timecode values and frame rate, clamping to valid timecode if necessary. - /// - /// Clamping is based on the `upperLimit` and `subFramesBase` properties. - public init( - clamping rawValues: Components, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode(clamping: rawValues) - } - - /// Instance from timecode values and frame rate, clamping individual values if necessary. - /// - /// Individual components which are out-of-bounds will be clamped to minimum or maximum possible - /// values. - /// - /// Clamping is based on the `upperLimit` and `subFramesBase` properties. - public init( - clampingEach rawValues: Components, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode(clampingEach: rawValues) - } - - /// Instance from timecode values and frame rate, wrapping timecode if necessary. - /// - /// Timecode will be wrapped around the timecode clock if out-of-bounds. - /// - /// Wrapping is based on the `upperLimit` and `subFramesBase` properties. - public init( - wrapping rawValues: Components, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode(wrapping: rawValues) - } - - /// Instance from raw timecode values and frame rate. - /// - /// Timecode values will not be validated or rejected if they overflow. - /// - /// This is useful, for example, when intending on running timecode validation methods against timecode values that are unknown to be valid or not at the time of initializing. - public init( - rawValues: Components, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode(rawValues: rawValues) - } -} - -// MARK: - Get and Set - -extension Timecode { - /// Timecode component values (day, hour, minute, second, frame, subframe). - /// - /// When setting, raw values are accepted and are not validated prior to setting. - /// - /// (Validation is based on the frame rate and `upperLimit` property.) - public var components: Components { - get { - Components( - d: days, - h: hours, - m: minutes, - s: seconds, - f: frames, - sf: subFrames - ) - } - set { - setTimecode(rawValues: newValue) - } - } - - /// Set timecode from tuple values. - /// - /// Returns true/false depending on whether the string values are valid or not. - /// - /// Values which are out-of-bounds will return false. - /// - /// (Validation is based on the frame rate and `upperLimit` property.) - /// - /// - Throws: ``ValidationError`` - public mutating func setTimecode(exactly values: Components) throws { - guard values - .invalidComponents( - at: frameRate, - limit: upperLimit, - base: subFramesBase - ) - .isEmpty - else { throw ValidationError.outOfBounds } - - days = values.d - hours = values.h - minutes = values.m - seconds = values.s - frames = values.f - subFrames = values.sf - } - - /// Set timecode from components. - /// Clamps to valid timecode as set by the `upperLimit` property. - /// - /// (Validation is based on the frame rate and `upperLimit` property.) - public mutating func setTimecode(clamping source: Components) { - let result = __add(clamping: source, to: TCC()) - - setTimecode(rawValues: result) - } - - /// Set timecode from components, clamping individual values if necessary. - /// - /// (Validation is based on the frame rate and `upperLimit` property.) - public mutating func setTimecode(clampingEach values: Components) { - days = values.d - hours = values.h - minutes = values.m - seconds = values.s - frames = values.f - subFrames = values.sf - - clampComponents() - } - - /// Set timecode from tuple values. - /// - /// Timecode will wrap if out-of-bounds. Will handle negative values and wrap accordingly. - /// - /// (Wrapping is based on the frame rate and `upperLimit` property.) - public mutating func setTimecode(wrapping values: Components) { - setTimecode(rawValues: __add( - wrapping: values, - to: Components(f: 0) - )) - } - - /// Set timecode from tuple values. - /// Timecode values will not be validated or rejected if they overflow. - public mutating func setTimecode(rawValues values: Components) { - days = values.d - hours = values.h - minutes = values.m - seconds = values.s - frames = values.f - subFrames = values.sf - } -} - -// MARK: - .toTimecode - -extension Timecode.Components { - /// Returns an instance of `Timecode(exactly:)`. - /// - /// - Throws: ``Timecode/ValidationError`` - public func toTimecode( - at rate: TimecodeFrameRate, - limit: Timecode.UpperLimit = ._24hours, - base: Timecode.SubFramesBase = .default(), - format: Timecode.StringFormat = .default() - ) throws -> Timecode { - try Timecode( - self, - at: rate, - limit: limit, - base: base, - format: format - ) - } - - /// Returns an instance of `Timecode(rawValues:)`. - public func toTimecode( - rawValuesAt rate: TimecodeFrameRate, - limit: Timecode.UpperLimit = ._24hours, - base: Timecode.SubFramesBase = .default(), - format: Timecode.StringFormat = .default() - ) -> Timecode { - Timecode( - rawValues: self, - at: rate, - limit: limit, - base: base, - format: format - ) - } -} diff --git a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode FeetAndFrames.swift b/Sources/TimecodeKit/Timecode/Data Interchange/Timecode FeetAndFrames.swift deleted file mode 100644 index a4c8149c..00000000 --- a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode FeetAndFrames.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// Timecode Feet+Frames.swift -// TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License -// - -import Foundation - -// MARK: - Init - -extension Timecode { - /// Instance exactly from Feet+Frames. - /// - /// If any values are out-of-bounds an error will be thrown, indicating an invalid timecode. - /// - /// Validation is based on the `upperLimit` and `subFramesBase` properties. - /// - /// When used as a counter in the audio-world the footage count refers to 35mm 4-perf. Detailed - /// discussion can be found [in this thread.]( - /// https://gearspace.com/board/post-production-forum/898755-timecode-feet-frames.html - /// ) - /// - /// - Throws: ``ValidationError`` - public init( - _ exactly: FeetAndFrames, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) throws { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - try setTimecode(exactly: exactly) - } -} - -// MARK: - Get and Set - -extension Timecode { - /// Returns the timecode expressed as Feet+Frames. - /// - /// When used as a counter in the audio-world the footage count refers to 35mm 4-perf. Detailed - /// discussion can be found [in this thread.]( - /// https://gearspace.com/board/post-production-forum/898755-timecode-feet-frames.html - /// ) - public var feetAndFramesValue: FeetAndFrames { - let fc = frameCount.wholeFrames - let feet = fc / 16 - let frames = fc % 16 - - return FeetAndFrames( - feet: feet, - frames: frames, - subFrames: subFrames, - subFramesBase: subFramesBase - ) - } - - /// Set timecode from Feet+Frames. - /// - /// Returns true/false depending on whether the string values are valid or not. - /// - /// Values which are out-of-bounds will return false. - /// - /// (Validation is based on the frame rate and `upperLimit` property.) - /// - /// When used as a counter in the audio-world the footage count refers to 35mm 4-perf. Detailed - /// discussion can be found [in this thread.]( - /// https://gearspace.com/board/post-production-forum/898755-timecode-feet-frames.html - /// ) - /// - /// - Throws: ``ValidationError`` - public mutating func setTimecode(exactly feetAndFrames: FeetAndFrames) throws { - try setTimecode(exactly: feetAndFrames.frameCount) - } -} diff --git a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode FrameCount Value.swift b/Sources/TimecodeKit/Timecode/Data Interchange/Timecode FrameCount Value.swift deleted file mode 100644 index de71b476..00000000 --- a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode FrameCount Value.swift +++ /dev/null @@ -1,170 +0,0 @@ -// -// Timecode FrameCount Value.swift -// TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License -// - -import Foundation - -// MARK: - Init - -extension Timecode { - /// Instance exactly from total elapsed frames ("frame number") at a given frame rate. - /// - /// Validation is based on the `upperLimit` and `subFramesBase` properties. - /// - /// - Throws: ``ValidationError`` - public init( - _ exactly: FrameCount.Value, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) throws { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - try setTimecode(exactly: exactly) - } - - /// Instance exactly from total elapsed frames ("frame number"), clamping to valid timecode if - /// necessary. - /// - /// Clamping is based on the `upperLimit` and `subFramesBase` properties. - public init( - clamping source: FrameCount.Value, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode(clamping: source) - } - - /// Instance exactly from total elapsed frames ("frame number"), wrapping timecode if necessary. - /// - /// Timecode will be wrapped around the timecode clock if out-of-bounds. - public init( - wrapping source: FrameCount.Value, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode(wrapping: source) - } - - /// Instance exactly from total elapsed frames ("frame number"). - /// - /// Allows for invalid raw values (in this case, unbounded Days component). - public init( - rawValues source: FrameCount.Value, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode(rawValues: source) - } -} - -// MARK: - Get and Set - -extension Timecode { - /// Set timecode from total elapsed frames ("frame number"). - /// - /// Subframes are represented by the fractional portion of the number. - /// Timecode is updated as long as the value passed is in valid range. - /// (Validation is based on the frame rate and `upperLimit` property.) - /// - /// - Throws: ``ValidationError`` - public mutating func setTimecode(exactly frameCountValue: FrameCount.Value) throws { - let convertedComponents = try components(exactly: frameCountValue) - - days = convertedComponents.d - hours = convertedComponents.h - minutes = convertedComponents.m - seconds = convertedComponents.s - frames = convertedComponents.f - subFrames = convertedComponents.sf - } - - /// Set timecode from total elapsed frames ("frame number"). - /// - /// Clamps to valid timecode. - /// - /// Subframes are represented by the fractional portion of the number. - public mutating func setTimecode(clamping source: FrameCount.Value) { - let convertedComponents = components(rawValues: source) - setTimecode(clamping: convertedComponents) - } - - /// Set timecode from total elapsed frames ("frame number"). - /// - /// Timecode will be wrapped around the timecode clock if out-of-bounds. - /// - /// Subframes are represented by the fractional portion of the number. - public mutating func setTimecode(wrapping source: FrameCount.Value) { - let convertedComponents = components(rawValues: source) - setTimecode(wrapping: convertedComponents) - } - - /// Set timecode from total elapsed frames ("frame number"). - /// - /// Allows for invalid raw values (in this case, unbounded Days component). - /// - /// Subframes are represented by the fractional portion of the number. - public mutating func setTimecode(rawValues source: FrameCount.Value) { - let convertedComponents = components(rawValues: source) - setTimecode(rawValues: convertedComponents) - } - - // MARK: Internal Methods - - /// Internal: - /// Returns frame count value converted to components using the instance's - /// frame rate and subframes base. - /// - /// - Throws: ``ValidationError`` - internal func components(exactly source: FrameCount.Value) throws -> Components { - let fc = FrameCount(source, base: subFramesBase) - - guard fc.subFrameCount >= 0, - fc <= maxFrameCountExpressible - else { throw ValidationError.outOfBounds } - - return Self.components( - of: fc, - at: frameRate - ) - } - - /// Internal: - /// Returns frame count value converted to components using the instance's - /// frame rate and subframes base. - internal func components(rawValues source: FrameCount.Value) -> Components { - let fc = FrameCount(source, base: subFramesBase) - - return Self.components( - of: fc, - at: frameRate - ) - } -} diff --git a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode Rational CMTime.swift b/Sources/TimecodeKit/Timecode/Data Interchange/Timecode Rational CMTime.swift deleted file mode 100644 index 9766d35f..00000000 --- a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode Rational CMTime.swift +++ /dev/null @@ -1,184 +0,0 @@ -// -// Timecode Rational CMTime.swift -// TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License -// - -#if canImport(CoreMedia) - -import Foundation -import CoreMedia - -@available(macOS 10.7, iOS 4.0, tvOS 9.0, watchOS 6.0, *) -extension Timecode { - /// Instance from elapsed time `CMTime`. - /// - /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to communicate - /// times and durations. - /// - /// - Throws: ``ValidationError`` - public init( - _ cmTime: CMTime, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) throws { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - try setTimecode(cmTime) - } - - /// Instance from elapsed time `CMTime`, clamping to valid timecode if necessary. - /// - /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to communicate - /// times and durations. - /// - /// - Throws: ``ValidationError`` - public init( - clamping cmTime: CMTime, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode(clamping: cmTime) - } - - /// Instance from elapsed time `CMTime`, wrapping timecode if necessary. - /// - /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to communicate - /// times and durations. - /// - /// - Throws: ``ValidationError`` - public init( - wrapping cmTime: CMTime, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode(wrapping: cmTime) - } - - /// Instance from elapsed time `CMTime`. - /// - /// Allows for invalid raw values (in this case, unbounded Days component). - /// - /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to communicate - /// times and durations. - /// - /// - Throws: ``ValidationError`` - public init( - rawValues cmTime: CMTime, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode(rawValues: cmTime) - } -} - -// MARK: - Get and Set - -@available(macOS 10.7, iOS 4.0, tvOS 9.0, watchOS 6.0, *) -extension Timecode { - /// Returns the time location as a `CMTime` instance. - /// - /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to communicate - /// times and durations. - public var cmTime: CMTime { - let fraction = rationalValue - return CMTime( - value: CMTimeValue(fraction.numerator), - timescale: CMTimeScale(fraction.denominator) - ) - } - - /// Instance from elapsed time `CMTime`. - /// - /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to communicate - /// times and durations. - /// - /// - Throws: ``ValidationError`` - public mutating func setTimecode(_ exactly: CMTime) throws { - let fraction = Fraction(Int(exactly.value), Int(exactly.timescale)) - try setTimecode(fraction) - } - - /// Instance from elapsed time `CMTime`. - /// - /// Clamps to valid timecode. - /// - /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to communicate - /// times and durations. - public mutating func setTimecode(clamping cmTime: CMTime) { - let fraction = Fraction(Int(cmTime.value), Int(cmTime.timescale)) - setTimecode(clamping: fraction) - } - - /// Instance from elapsed time `CMTime`. - /// - /// Wraps timecode if necessary. - /// - /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to communicate - /// times and durations. - public mutating func setTimecode(wrapping cmTime: CMTime) { - let fraction = Fraction(Int(cmTime.value), Int(cmTime.timescale)) - setTimecode(wrapping: fraction) - } - - /// Instance from elapsed time `CMTime`. - /// - /// Allows for invalid raw values (in this case, unbounded Days component). - /// - /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to communicate - /// times and durations. - public mutating func setTimecode(rawValues cmTime: CMTime) { - let fraction = Fraction(Int(cmTime.value), Int(cmTime.timescale)) - setTimecode(rawValues: fraction) - } -} - -@available(macOS 10.7, iOS 4.0, tvOS 9.0, watchOS 6.0, *) -extension CMTime { - /// Convenience method to create an `Timecode` struct using the default - /// `(_ exactly:)` initializer. - /// - /// - Throws: ``ValidationError`` - public func toTimecode( - at rate: TimecodeFrameRate, - limit: Timecode.UpperLimit = ._24hours, - base: Timecode.SubFramesBase = .default(), - format: Timecode.StringFormat = .default() - ) throws -> Timecode { - try Timecode( - self, - at: rate, - limit: limit, - base: base, - format: format - ) - } -} - -#endif diff --git a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode Rational.swift b/Sources/TimecodeKit/Timecode/Data Interchange/Timecode Rational.swift deleted file mode 100644 index 0ad65326..00000000 --- a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode Rational.swift +++ /dev/null @@ -1,220 +0,0 @@ -// -// Timecode Rational.swift -// TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License -// - -import Foundation - -extension Timecode { - /// Instance from elapsed time expressed as a rational fraction. - /// - /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in - /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate - /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as - /// fractions.) - /// - /// - Note: A negative fraction will throw an error. Use ``TimecodeInterval`` init instead. - /// - /// - Throws: ``ValidationError`` - public init( - _ rational: Fraction, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) throws { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - try setTimecode(rational) - } - - /// Instance from elapsed time expressed as a rational fraction, clamping to valid timecode if - /// necessary. - /// - /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in - /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate - /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as - /// fractions.) - public init( - clamping rational: Fraction, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode(clamping: rational) - } - - /// Instance from elapsed time expressed as a rational fraction, wrapping timecode if necessary. - /// - /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in - /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate - /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as - /// fractions.) - public init( - wrapping rational: Fraction, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode(wrapping: rational) - } - - /// Instance from elapsed time expressed as a rational fraction. - /// - /// Allows for invalid raw values (in this case, unbounded Days component). - /// - /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in - /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate - /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as - /// fractions.) - public init( - rawValues rational: Fraction, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode(rawValues: rational) - } -} - -// MARK: - Get and Set - -extension Timecode { - /// Returns the time location as a rational fraction. - /// - /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in - /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate - /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as - /// fractions.) - public var rationalValue: Fraction { - let frFrac = frameRate.frameDuration - let n = frFrac.numerator * frameCount.subFrameCount - let d = frFrac.denominator * subFramesBase.rawValue - - return Fraction(n, d).reduced() - } - - /// Sets the timecode from elapsed time expressed as a rational fraction. - /// - /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in - /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate - /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as - /// fractions.) - /// - /// - Note: A negative fraction will throw an error. Use ``TimecodeInterval`` init instead. - /// - /// - Throws: ``ValidationError`` - public mutating func setTimecode(_ rational: Fraction) throws { - let frameCount = floatingFrameCount(of: rational) - try setTimecode(exactly: .combined(frames: frameCount)) - } - - /// Sets the timecode from elapsed time expressed as a rational fraction. - /// - /// Clamps to valid timecode. - /// - /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in - /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate - /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as - /// fractions.) - public mutating func setTimecode(clamping rational: Fraction) { - let frameCount = frameCount(of: rational) - setTimecode(clamping: .frames(frameCount)) - } - - /// Sets the timecode from elapsed time expressed as a rational fraction. - /// - /// Wraps timecode if necessary. - /// - /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in - /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate - /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as - /// fractions.) - public mutating func setTimecode(wrapping rational: Fraction) { - let frameCount = frameCount(of: rational) - setTimecode(wrapping: .frames(frameCount)) - } - - /// Sets the timecode from elapsed time expressed as a rational fraction. - /// - /// Allows for invalid raw values (in this case, unbounded Days component). - /// - /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in - /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate - /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as - /// fractions.) - public mutating func setTimecode(rawValues rational: Fraction) { - let frameCount = frameCount(of: rational) - setTimecode(rawValues: .frames(frameCount)) - } - - /// Internal: - /// Returns frame count of the rational fraction at current frame rate. - /// Truncates subframes if present. - internal func frameCount(of rational: Fraction) -> Int { - let frFrac = frameRate.frameDuration - let frameCount = (rational.numerator * frFrac.denominator) / - (rational.denominator * frFrac.numerator) - return frameCount - } - - /// Internal: - /// Returns frame count of the rational fraction at current frame rate. - /// Preserves subframes as floating-point potion of a frame. - internal func floatingFrameCount(of rational: Fraction) -> Double { - let frFrac = frameRate.frameDuration - let frameCount = (Double(rational.numerator) * Double(frFrac.denominator)) / - (Double(rational.denominator) * Double(frFrac.numerator)) - return frameCount - } -} - -extension Fraction { - /// Convenience method to create an `Timecode` struct using the default - /// `(_ exactly:)` initializer. - /// - /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in - /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate - /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as - /// fractions.) - /// - /// - Note: A negative fraction will throw an error. Use ``TimecodeInterval`` init instead. - /// - /// - Throws: ``Timecode/ValidationError`` - public func toTimecode( - at rate: TimecodeFrameRate, - limit: Timecode.UpperLimit = ._24hours, - base: Timecode.SubFramesBase = .default(), - format: Timecode.StringFormat = .default() - ) throws -> Timecode { - try Timecode( - self, - at: rate, - limit: limit, - base: base, - format: format - ) - } -} diff --git a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode Real Time.swift b/Sources/TimecodeKit/Timecode/Data Interchange/Timecode Real Time.swift deleted file mode 100644 index 19190529..00000000 --- a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode Real Time.swift +++ /dev/null @@ -1,197 +0,0 @@ -// -// Timecode Real Time.swift -// TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License -// - -import Foundation - -// MARK: - Init - -extension Timecode { - /// Instance from total elapsed real time and frame rate. - /// - /// - Note: This may be lossy. - /// - /// - Throws: ``ValidationError`` - public init( - realTime exactly: TimeInterval, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) throws { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - try setTimecode(realTime: exactly) - } - - /// Instance from total elapsed real time and frame rate, clamping to valid timecode if - /// necessary. - /// - /// Clamping is based on the `upperLimit` and `subFramesBase` properties. - /// - /// - Note: This may be lossy. - public init( - clampingRealTime source: TimeInterval, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode(clampingRealTime: source) - } - - /// Instance from total elapsed real time and frame rate, wrapping timecode if necessary. - /// - /// Timecode will be wrapped around the timecode clock if out-of-bounds. - /// - /// - Note: This may be lossy. - public init( - wrappingRealTime source: TimeInterval, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode(wrappingRealTime: source) - } - - /// Instance from total elapsed real time and frame rate. - /// - /// Allows for invalid raw values (in this case, unbounded Days component). - /// - /// - Note: This may be lossy. - public init( - rawValuesRealTime source: TimeInterval, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode(rawValuesRealTime: source) - } -} - -// MARK: - Get and Set - -extension Timecode { - /// (Lossy) Returns the current timecode converted to a duration in - /// real-time (wall-clock time), based on the frame rate. - public var realTimeValue: TimeInterval { - frameCount.doubleValue * (1.0 / frameRate.frameRateForRealTimeCalculation) - } - - /// Sets the timecode to the nearest frame at the current frame rate - /// from real-time (wall-clock time). - /// - /// Throws an error if it underflows or overflows valid timecode range. - /// (Validation is based on the frame rate and `upperLimit` property.) - /// - /// - Throws: ``ValidationError`` - public mutating func setTimecode(realTime: TimeInterval) throws { - let convertedComponents = components(realTime: realTime) - try setTimecode(exactly: convertedComponents) - } - - /// Sets the timecode to the nearest frame at the current frame rate - /// from real-time (wall-clock time). - /// - /// Clamps to valid timecode. - public mutating func setTimecode(clampingRealTime: TimeInterval) { - let convertedComponents = components(realTime: clampingRealTime) - setTimecode(clamping: convertedComponents) - } - - /// Sets the timecode to the nearest frame at the current frame rate - /// from real-time (wall-clock time). - /// - /// Wraps timecode if necessary. - public mutating func setTimecode(wrappingRealTime: TimeInterval) { - let convertedComponents = components(realTime: wrappingRealTime) - setTimecode(wrapping: convertedComponents) - } - - /// Sets the timecode to the nearest frame at the current frame rate - /// from real-time (wall-clock time). - /// - /// Allows for invalid raw values (in this case, unbounded Days component). - public mutating func setTimecode(rawValuesRealTime: TimeInterval) { - let convertedComponents = components(realTime: rawValuesRealTime) - setTimecode(rawValues: convertedComponents) - } - - // MARK: Internal Methods - - /// Internal: - /// Converts a real-time value (wall-clock time) to components using the instance's - /// frame rate and subframes base. - internal func components(realTime: TimeInterval) -> Components { - let elapsedFrames = elapsedFrames(realTime: realTime) - - return Self.components( - of: .init(.combined(frames: elapsedFrames), base: subFramesBase), - at: frameRate - ) - } - - /// Internal: - /// Calculates elapsed frames at current frame rate from real-time (wall-clock time). - internal func elapsedFrames(realTime: TimeInterval) -> Double { - var calc = realTime / (1.0 / frameRate.frameRateForRealTimeCalculation) - - // over-estimate so real time is just past the equivalent timecode - // since raw time values in practise can be a hair under the actual elapsed real time that would trigger the equivalent timecode (due to precision and rounding behaviors that may not be in our control, depending on where the passed real time value originated) - - let magicNumber = 0.000_010 // 10 microseconds - - switch calc.sign { - case .plus: - calc += magicNumber - case .minus: - calc -= magicNumber - } - - return calc - } -} - -// a.k.a: extension Double -extension TimeInterval { - /// Convenience method to create an `Timecode` struct using the default - /// `(_ exactly:)` initializer. - /// - /// - Throws: ``ValidationError`` - public func toTimecode( - at rate: TimecodeFrameRate, - limit: Timecode.UpperLimit = ._24hours, - base: Timecode.SubFramesBase = .default(), - format: Timecode.StringFormat = .default() - ) throws -> Timecode { - try Timecode( - realTime: self, - at: rate, - limit: limit, - base: base, - format: format - ) - } -} diff --git a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode Samples Double.swift b/Sources/TimecodeKit/Timecode/Data Interchange/Timecode Samples Double.swift deleted file mode 100644 index 48789436..00000000 --- a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode Samples Double.swift +++ /dev/null @@ -1,221 +0,0 @@ -// -// Timecode Samples Double.swift -// TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License -// - -// MARK: - Init - -extension Timecode { - /// Instance from total elapsed audio samples at a given sample rate. - /// - /// - Note: This may be lossy. - /// - /// - Throws: ``ValidationError`` - public init( - samples exactly: Double, - sampleRate: Int, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) throws { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - try setTimecode( - samples: exactly, - sampleRate: sampleRate - ) - } - - /// Instance from total elapsed audio samples at a given sample rate, clamping to valid timecode - /// if necessary. - /// - /// Clamping is based on the `upperLimit` and `subFramesBase` properties. - /// - /// - Note: This may be lossy. - public init( - clampingSamples source: Double, - sampleRate: Int, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode( - clampingSamples: source, - sampleRate: sampleRate - ) - } - - /// Instance from total elapsed audio samples at a given sample rate, clamping to valid timecode - /// if necessary. - /// - /// Timecode will be wrapped around the timecode clock if out-of-bounds. - /// - /// - Note: This may be lossy. - public init( - wrappingSamples source: Double, - sampleRate: Int, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode( - wrappingSamples: source, - sampleRate: sampleRate - ) - } - - /// Instance from total elapsed audio samples at a given sample rate, clamping to valid timecode - /// if necessary. - /// - /// Allows for invalid raw values (in this case, unbounded Days component). - /// - /// - Note: This may be lossy. - public init( - rawValuesSamples source: Double, - sampleRate: Int, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode( - wrappingSamples: source, - sampleRate: sampleRate - ) - } -} - -// MARK: - Get and Set - -extension Timecode { - /// (Lossy) - /// Returns the current timecode converted to a duration in audio samples - /// at the given sample rate, with floating-point sub-sample duration. - /// Sample rate is expressed in Hz. (ie: 48KHz would be passed as 48000) - public func samplesDoubleValue(sampleRate: Int) -> Double { - realTimeValue * Double(sampleRate) - } - - /// (Lossy) - /// Sets the timecode to the nearest elapsed frame at the current frame rate - /// from elapsed audio samples, with floating-point sub-sample duration. - /// Throws an error if it underflows or overflows valid timecode range. - /// Sample rate is expressed in Hz. (ie: 48KHz would be passed as 48000) - /// - /// - Throws: ``ValidationError`` - public mutating func setTimecode( - samples: Double, - sampleRate: Int - ) throws { - let convertedComponents = components( - fromSamples: samples, - sampleRate: sampleRate - ) - try setTimecode(exactly: convertedComponents) - } - - /// (Lossy) - /// Sets the timecode to the nearest elapsed frame at the current frame rate - /// from elapsed audio samples, with floating-point sub-sample duration. - /// Clamps to valid timecode. - /// Sample rate is expressed in Hz. (ie: 48KHz would be passed as 48000) - /// - /// - Throws: ``ValidationError`` - public mutating func setTimecode( - clampingSamples: Double, - sampleRate: Int - ) { - let convertedComponents = components( - fromSamples: clampingSamples, - sampleRate: sampleRate - ) - setTimecode(clamping: convertedComponents) - } - - /// (Lossy) - /// Sets the timecode to the nearest elapsed frame at the current frame rate - /// from elapsed audio samples, with floating-point sub-sample duration. - /// Wraps timecode if necessary. - /// Sample rate is expressed in Hz. (ie: 48KHz would be passed as 48000) - /// - /// - Throws: ``ValidationError`` - public mutating func setTimecode( - wrappingSamples: Double, - sampleRate: Int - ) { - let convertedComponents = components( - fromSamples: wrappingSamples, - sampleRate: sampleRate - ) - setTimecode(wrapping: convertedComponents) - } - - /// (Lossy) - /// Sets the timecode to the nearest elapsed frame at the current frame rate - /// from elapsed audio samples, with floating-point sub-sample duration. - /// Allows for invalid raw values (in this case, unbounded Days component). - /// Sample rate is expressed in Hz. (ie: 48KHz would be passed as 48000) - /// - /// - Throws: ``ValidationError`` - public mutating func setTimecode( - rawValuesSamples: Double, - sampleRate: Int - ) { - let convertedComponents = components( - fromSamples: rawValuesSamples, - sampleRate: sampleRate - ) - setTimecode(rawValues: convertedComponents) - } - - // MARK: Internal Methods - - internal func components( - fromSamples: Double, - sampleRate: Int - ) -> Components { - let rtv = fromSamples / Double(sampleRate) - var base = elapsedFrames(realTime: rtv) - - // over-estimate so samples are just past the equivalent timecode - // so calculations of samples back into timecode work reliably - // otherwise, this math produces a samples value that can be a hair under - // the actual elapsed samples that would convert back to equivalent timecode - - let magicNumber = 0.0001 - - if rtv < 0 { - base -= magicNumber - } else { - base += magicNumber - } - - // then derive components - return Self.components( - of: .init(.combined(frames: base), base: subFramesBase), - at: frameRate - ) - } -} diff --git a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode Samples Int.swift b/Sources/TimecodeKit/Timecode/Data Interchange/Timecode Samples Int.swift deleted file mode 100644 index 72abeba3..00000000 --- a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode Samples Int.swift +++ /dev/null @@ -1,180 +0,0 @@ -// -// Timecode Samples Int.swift -// TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License -// - -// MARK: - Init - -extension Timecode { - /// Instance from total elapsed audio samples at a given sample rate. - /// - /// - Note: This may be lossy. - /// - /// - Throws: ``ValidationError`` - public init( - samples exactly: Int, - sampleRate: Int, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) throws { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - try setTimecode( - samples: exactly, - sampleRate: sampleRate - ) - } - - /// Instance from total elapsed audio samples at a given sample rate, clamping to valid timecode - /// if necessary. - /// - /// Clamping is based on the `upperLimit` and `subFramesBase` properties. - /// - /// - Note: This may be lossy. - public init( - clampingSamples source: Int, - sampleRate: Int, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode( - clampingSamples: source, - sampleRate: sampleRate - ) - } - - /// Instance from total elapsed audio samples at a given sample rate, clamping to valid timecode - /// if necessary. - /// - /// Timecode will be wrapped around the timecode clock if out-of-bounds. - /// - /// - Note: This may be lossy. - public init( - wrappingSamples source: Int, - sampleRate: Int, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode( - wrappingSamples: source, - sampleRate: sampleRate - ) - } - - /// Instance from total elapsed audio samples at a given sample rate, clamping to valid timecode - /// if necessary. - /// - /// Allows for invalid raw values (in this case, unbounded Days component). - /// - /// - Note: This may be lossy. - public init( - rawValuesSamples source: Int, - sampleRate: Int, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - setTimecode( - rawValuesSamples: source, - sampleRate: sampleRate - ) - } -} - -// MARK: - Get and Set - -extension Timecode { - /// (Lossy) - /// Returns the current timecode converted to a duration in audio samples - /// at the given sample rate, rounded to the nearest sample. - /// Sample rate is expressed in Hz. (ie: 48KHz would be passed as 48000) - public func samplesValue(sampleRate: Int) -> Int { - Int(samplesDoubleValue(sampleRate: sampleRate).rounded()) - } - - /// (Lossy) - /// Sets the timecode to the nearest elapsed frame at the current frame rate - /// from elapsed audio samples. - /// Throws an error if it underflows or overflows valid timecode range. - /// Sample rate must be expressed as an Integer of Hz (ie: 48KHz would be passed as 48000) - /// - /// - Throws: ``ValidationError`` - public mutating func setTimecode( - samples: Int, - sampleRate: Int - ) throws { - try setTimecode(samples: Double(samples), - sampleRate: sampleRate) - } - - /// (Lossy) - /// Sets the timecode to the nearest elapsed frame at the current frame rate - /// from elapsed audio samples. - /// Clamps to valid timecode. - /// Sample rate must be expressed as an Integer of Hz (ie: 48KHz would be passed as 48000) - /// - /// - Throws: ``ValidationError`` - public mutating func setTimecode( - clampingSamples: Int, - sampleRate: Int - ) { - setTimecode(clampingSamples: Double(clampingSamples), - sampleRate: sampleRate) - } - - /// (Lossy) - /// Sets the timecode to the nearest elapsed frame at the current frame rate - /// from elapsed audio samples. - /// Wraps timecode if necessary. - /// Sample rate must be expressed as an Integer of Hz (ie: 48KHz would be passed as 48000) - /// - /// - Throws: ``ValidationError`` - public mutating func setTimecode( - wrappingSamples: Int, - sampleRate: Int - ) { - setTimecode(wrappingSamples: Double(wrappingSamples), - sampleRate: sampleRate) - } - - /// (Lossy) - /// Sets the timecode to the nearest elapsed frame at the current frame rate - /// from elapsed audio samples. - /// Allows for invalid raw values (in this case, unbounded Days component). - /// Sample rate must be expressed as an Integer of Hz (ie: 48KHz would be passed as 48000) - /// - /// - Throws: ``ValidationError`` - public mutating func setTimecode( - rawValuesSamples: Int, - sampleRate: Int - ) { - setTimecode(rawValuesSamples: Double(rawValuesSamples), - sampleRate: sampleRate) - } -} diff --git a/Sources/TimecodeKit/Timecode/Errors/Errors.swift b/Sources/TimecodeKit/Timecode/Errors/Errors.swift index 60588b7e..d6c0b990 100644 --- a/Sources/TimecodeKit/Timecode/Errors/Errors.swift +++ b/Sources/TimecodeKit/Timecode/Errors/Errors.swift @@ -1,7 +1,7 @@ // // Errors.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // import Foundation diff --git a/Sources/TimecodeKit/Timecode/Formatter/TextFormatter.swift b/Sources/TimecodeKit/Timecode/Formatter/TextFormatter.swift index 6609da65..459a4e88 100644 --- a/Sources/TimecodeKit/Timecode/Formatter/TextFormatter.swift +++ b/Sources/TimecodeKit/Timecode/Formatter/TextFormatter.swift @@ -1,7 +1,7 @@ // // TextFormatter.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if os(macOS) @@ -23,8 +23,8 @@ extension Timecode { public var frameRate: TimecodeFrameRate? public var upperLimit: Timecode.UpperLimit? - public var stringFormat: Timecode.StringFormat? public var subFramesBase: SubFramesBase? + public var stringFormat: Timecode.StringFormat = .default() /// The formatter's `attributedString(...) -> NSAttributedString` output will override a control's alignment (ie: `NSTextField`). /// Setting alignment here will add the appropriate paragraph alignment attribute to the output `NSAttributedString`. @@ -54,19 +54,17 @@ extension Timecode { } public init( - frameRate: TimecodeFrameRate? = nil, - limit: Timecode.UpperLimit? = nil, + using properties: Timecode.Properties? = nil, stringFormat: StringFormat? = nil, - subFramesBase: SubFramesBase? = nil, showsValidation: Bool = false, validationAttributes: [NSAttributedString.Key: Any]? = nil ) { super.init() - self.frameRate = frameRate - upperLimit = limit - self.subFramesBase = subFramesBase - self.stringFormat = stringFormat + frameRate = properties?.frameRate + upperLimit = properties?.upperLimit + subFramesBase = properties?.subFramesBase + self.stringFormat = stringFormat ?? .default() self.showsValidation = showsValidation @@ -78,14 +76,13 @@ extension Timecode { /// Initializes with properties from an `Timecode` object. public convenience init( using timecode: Timecode, + stringFormat: StringFormat? = nil, showsValidation: Bool = false, validationAttributes: [NSAttributedString.Key: Any]? = nil ) { self.init( - frameRate: timecode.frameRate, - limit: timecode.upperLimit, - stringFormat: timecode.stringFormat, - subFramesBase: timecode.subFramesBase, + using: timecode.properties, + stringFormat: stringFormat, showsValidation: showsValidation, validationAttributes: validationAttributes ) @@ -110,7 +107,7 @@ extension Timecode { guard let string = obj as? String else { return nil } - guard var tc = timecodeTemplate + guard let tcProps = timecodeProperties else { return string } // form timecode components without validating @@ -118,9 +115,9 @@ extension Timecode { else { return string } // set values without validating - tc.setTimecode(rawValues: tcc) + let tc = Timecode(.components(tcc), using: tcProps, by: .allowingInvalid) - return tc.stringValue + return tc.stringValue(format: stringFormat) } // MARK: attributedString @@ -133,35 +130,36 @@ extension Timecode { else { return nil } func entirelyInvalid() -> NSAttributedString { - showsValidation - ? NSAttributedString( - string: stringForObj, - attributes: validationAttributes - .merging( - attrs ?? [:], - uniquingKeysWith: { current, _ in current } - ) - ) - .addingAttribute(alignment: alignment) - : NSAttributedString(string: stringForObj, attributes: attrs) - .addingAttribute(alignment: alignment) + ( + showsValidation + ? NSAttributedString( + string: stringForObj, + attributes: validationAttributes + .merging( + attrs ?? [:], + uniquingKeysWith: { current, _ in current } + ) + ) + : NSAttributedString(string: stringForObj, attributes: attrs) + ) + .addingAttribute(alignment: alignment) } // grab properties from the formatter - guard var tc = timecodeTemplate else { return entirelyInvalid() } + guard let tcProps = timecodeProperties else { return entirelyInvalid() } // form timecode components without validating guard let tcc = try? Timecode.decode(timecode: stringForObj) else { return entirelyInvalid() } // set values without validating - tc.setTimecode(rawValues: tcc) + let tc = Timecode(.components(tcc), using: tcProps, by: .allowingInvalid) return ( showsValidation ? tc.stringValueValidated( invalidAttributes: validationAttributes, - withDefaultAttributes: attrs + defaultAttributes: attrs ) : NSAttributedString(string: stringForObj, attributes: attrs) ) @@ -187,9 +185,9 @@ extension Timecode { errorDescription error: AutoreleasingUnsafeMutablePointer? ) -> Bool { guard let unwrappedFrameRate = frameRate, - let unwrappedUpperLimit = upperLimit, - // let unwrappedSubFramesBase = subFramesBase, - let unwrappedStringFormat = stringFormat else { return true } + let unwrappedUpperLimit = upperLimit + // let unwrappedSubFramesBase = subFramesBase, + else { return true } let partialString = partialStringPtr.pointee as String @@ -261,7 +259,7 @@ extension Timecode { } if char == " " { - if unwrappedUpperLimit == ._24hours + if unwrappedUpperLimit == .max24Hours { return false } if !( @@ -291,7 +289,7 @@ extension Timecode { if char == ".", periodCount > 1 { return false } - if char == ".", !unwrappedStringFormat.showSubFrames + if char == ".", !stringFormat.showSubFrames { return false } // number validation (?) @@ -329,20 +327,18 @@ extension Timecode { // MARK: timecodeTemplate extension Timecode.TextFormatter { - public var timecodeTemplate: Timecode? { + public var timecodeProperties: Timecode.Properties? { guard let unwrappedFrameRate = frameRate, let unwrappedUpperLimit = upperLimit, - let unwrappedSubFramesBase = subFramesBase, - let unwrappedStringFormat = stringFormat + let unwrappedSubFramesBase = subFramesBase else { return nil } - return Timecode( - at: unwrappedFrameRate, - limit: unwrappedUpperLimit, + return Timecode.Properties( + rate: unwrappedFrameRate, base: unwrappedSubFramesBase, - format: unwrappedStringFormat + limit: unwrappedUpperLimit ) } } diff --git a/Sources/TimecodeKit/Timecode/FrameCount/FrameCount Value.swift b/Sources/TimecodeKit/Timecode/FrameCount/FrameCount Value.swift index b5582802..0962b75d 100644 --- a/Sources/TimecodeKit/Timecode/FrameCount/FrameCount Value.swift +++ b/Sources/TimecodeKit/Timecode/FrameCount/FrameCount Value.swift @@ -1,7 +1,7 @@ // // FrameCount Value.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // extension Timecode.FrameCount { @@ -13,10 +13,11 @@ extension Timecode.FrameCount { /// Total elapsed whole frames, and subframes. case split(frames: Int, subFrames: Int) - /// Total elapsed frames, expressed as a `Double` where the integer portion is whole frames and the fractional portion is the subframes unit interval. + /// Total elapsed frames, expressed as a `Double` where the integer portion is whole frames and the fractional portion is the + /// subframes unit interval. case combined(frames: Double) - /// Total elapsed whole frames, and subframes expressed as a floating-point unit interval (`0.0..<1.0`). + /// Total elapsed whole frames, and subframes expressed as a floating-point unit interval (`0.0 ..< 1.0`). case splitUnitInterval(frames: Int, subFramesUnitInterval: Double) } } diff --git a/Sources/TimecodeKit/Timecode/FrameCount/FrameCount.swift b/Sources/TimecodeKit/Timecode/FrameCount/FrameCount.swift index 03e68924..74587445 100644 --- a/Sources/TimecodeKit/Timecode/FrameCount/FrameCount.swift +++ b/Sources/TimecodeKit/Timecode/FrameCount/FrameCount.swift @@ -1,7 +1,7 @@ // // FrameCount.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // import Foundation @@ -63,7 +63,8 @@ extension Timecode { } } - /// Total elapsed frame count expressed as a `Double` where the integer portion is whole frames and the fractional portion is the subframes unit interval. + /// Total elapsed frame count expressed as a `Double` where the integer portion is whole frames and the fractional portion is the + /// subframes unit interval. public var doubleValue: Double { switch value { case let .frames(frames): @@ -80,7 +81,8 @@ extension Timecode { } } - /// Total elapsed frame count expressed as a `Decimal` where the integer portion is whole frames and the fractional portion is the subframes unit interval. + /// Total elapsed frame count expressed as a `Decimal` where the integer portion is whole frames and the fractional portion is the + /// subframes unit interval. public var decimalValue: Decimal { switch value { case let .frames(frames): @@ -102,7 +104,7 @@ extension Timecode { // MARK: - Internal inits extension Timecode.FrameCount { - internal init( + init( subFrameCount: Int, base: Timecode.SubFramesBase ) { @@ -258,7 +260,7 @@ extension Timecode.FrameCount { } extension Timecode.FrameCount { - internal var subFrameCount: Int { + var subFrameCount: Int { Timecode.framesToSubFrames( frames: wholeFrames, subFrames: subFrames, @@ -271,7 +273,7 @@ extension Timecode.FrameCount { extension Timecode { /// Internal utility - internal static func framesToSubFrames( + static func framesToSubFrames( frames: Int, subFrames: Int, base: SubFramesBase @@ -280,7 +282,7 @@ extension Timecode { } /// Internal utility - internal static func subFramesToFrames( + static func subFramesToFrames( _ subFrames: Int, base: SubFramesBase ) diff --git a/Sources/TimecodeKit/Timecode/Math/Timecode Math Internal.swift b/Sources/TimecodeKit/Timecode/Math/Timecode Math Internal.swift index 5ea7e5e4..09b90443 100644 --- a/Sources/TimecodeKit/Timecode/Math/Timecode Math Internal.swift +++ b/Sources/TimecodeKit/Timecode/Math/Timecode Math Internal.swift @@ -1,15 +1,15 @@ // // Timecode Math Internal.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // extension Timecode { // MARK: - Add /// Utility function to add a duration to a base timecode. - /// Returns nil if it overflows possible timecode values. - internal func __add( + /// Returns `nil` if it overflows possible timecode values. + func _add( exactly duration: Components, to base: Components ) -> Components? { @@ -42,7 +42,7 @@ extension Timecode { /// Utility function to add a duration to a base timecode. /// Clamps to maximum timecode expressible. - internal func __add( + func _add( clamping duration: Components, to base: Components ) -> Components { @@ -74,7 +74,7 @@ extension Timecode { /// Utility function to add a duration to a base timecode. /// Wraps around the clock as set by the `upperLimit` property. - internal func __add( + func _add( wrapping duration: Components, to base: Components ) -> Components { @@ -118,11 +118,41 @@ extension Timecode { ) } + /// Utility function to add a duration to a base timecode. + /// Invalid values are retained without validation. + func _add( + rawValues duration: Components, + to base: Components + ) -> Components { + let fcOrigin = Self.frameCount( + of: base, + at: frameRate, + base: subFramesBase + ) + let fcAdd = Self.frameCount( + of: duration, + at: frameRate, + base: subFramesBase + ) + + let sfcNew = fcOrigin.subFrameCount + fcAdd.subFrameCount + + let fcNew = FrameCount( + subFrameCount: sfcNew, + base: subFramesBase + ) + + return Self.components( + of: fcNew, + at: frameRate + ) + } + // MARK: - Subtract /// Utility function to add a duration to a base timecode. - /// Returns nil if overflows possible timecode values. - internal func __subtract( + /// Returns `nil` if overflows possible timecode values. + func _subtract( exactly duration: Components, from base: Components ) -> Components? { @@ -155,7 +185,7 @@ extension Timecode { /// Utility function to add a duration to a base timecode. /// Clamps to valid timecode as set by the `upperLimit` property. - internal func __subtract( + func _subtract( clamping duration: Components, from base: Components ) -> Components { @@ -187,7 +217,7 @@ extension Timecode { /// Utility function to add a duration to a base timecode. /// Wraps around the clock as set by the `upperLimit` property. - internal func __subtract( + func _subtract( wrapping duration: Components, from base: Components ) -> Components { @@ -231,11 +261,41 @@ extension Timecode { ) } - // MARK: - Multiply + /// Utility function to add a duration to a base timecode. + /// Invalid values are retained without validation. + func _subtract( + rawValues duration: Components, + from base: Components + ) -> Components { + let fcOrigin = Self.frameCount( + of: base, + at: frameRate, + base: subFramesBase + ) + let tcSubtract = Self.frameCount( + of: duration, + at: frameRate, + base: subFramesBase + ) + + let sfcNew = fcOrigin.subFrameCount - tcSubtract.subFrameCount + + let fcNew = FrameCount( + subFrameCount: sfcNew, + base: subFramesBase + ) + + return Self.components( + of: fcNew, + at: frameRate + ) + } - /// Utility function to multiply a base timecode by a duration. - /// Returns nil if it overflows possible timecode values. - internal func __multiply( + // MARK: - Multiply Double + + /// Utility function to multiply a base timecode by a float. + /// Returns `nil` if it overflows possible timecode values. + func _multiply( exactly factor: Double, with: Components ) -> Components? { @@ -261,9 +321,9 @@ extension Timecode { ) } - /// Utility function to multiply a base timecode by a duration. + /// Utility function to multiply a base timecode by a float. /// Clamps to maximum timecode expressible. - internal func __multiply( + func _multiply( clamping factor: Double, with: Components ) -> Components { @@ -288,9 +348,9 @@ extension Timecode { ) } - /// Utility function to multiply a base timecode by a duration. + /// Utility function to multiply a base timecode by a float. /// Wraps around the clock as set by the `upperLimit` property. - internal func __multiply( + func _multiply( wrapping factor: Double, with: Components ) -> Components { @@ -329,11 +389,36 @@ extension Timecode { ) } - // MARK: - Divide + /// Utility function to multiply a base timecode by a float. + /// Invalid values are retained without validation. + func _multiply( + rawValues factor: Double, + with: Components + ) -> Components { + let fcOrigin = Self.frameCount( + of: with, + at: frameRate, + base: subFramesBase + ) + + let sfcNew = Int(Double(fcOrigin.subFrameCount) * factor) + + let fcNew = FrameCount( + subFrameCount: sfcNew, + base: subFramesBase + ) + + return Self.components( + of: fcNew, + at: frameRate + ) + } + + // MARK: - Divide Double - /// Utility function to divide a base timecode by a duration. + /// Utility function to divide a base timecode by a float. /// Returns `nil` if it overflows possible timecode values. - internal func __divide( + func _divide( exactly divisor: Double, into: Components ) -> Components? { @@ -359,9 +444,9 @@ extension Timecode { ) } - /// Utility function to divide a base timecode by a duration. + /// Utility function to divide a base timecode by a float. /// Clamps to valid timecode between 0 and `upperLimit`. - internal func __divide( + func _divide( clamping divisor: Double, into: Components ) -> Components { @@ -370,6 +455,7 @@ extension Timecode { at: frameRate, base: subFramesBase ) + var sfcNew = Int(Double(fcOrigin.subFrameCount) / divisor) sfcNew = sfcNew.clamped(to: 0 ... maxSubFrameCountExpressible) @@ -385,9 +471,9 @@ extension Timecode { ) } - /// Utility function to divide a base timecode by a duration. + /// Utility function to divide a base timecode by a float. /// Wraps around the clock as set by the `upperLimit` property. - internal func __divide( + func _divide( wrapping divisor: Double, into: Components ) -> Components { @@ -426,41 +512,92 @@ extension Timecode { ) } + /// Utility function to divide a base timecode by a float. + /// Invalid values are retained without validation. + func _divide( + rawValues divisor: Double, + into: Components + ) -> Components { + let fcOrigin = Self.frameCount( + of: into, + at: frameRate, + base: subFramesBase + ) + + let sfcNew = Int(Double(fcOrigin.subFrameCount) / divisor) + + let fcNew = FrameCount( + subFrameCount: sfcNew, + base: subFramesBase + ) + + return Self.components( + of: fcNew, + at: frameRate + ) + } + + // MARK: - Divide Components + + /// Utility function to divide a base timecode by a duration. + /// Returns `nil` if it overflows possible timecode values. + func _divide( + exactly divisor: Components, + into: Components + ) -> Double? { + let fcDivisor = Self.frameCount( + of: divisor, + at: frameRate, + base: subFramesBase + ) + + let fcOrigin = Self.frameCount( + of: into, + at: frameRate, + base: subFramesBase + ) + + let sfcNew = Double(fcOrigin.subFrameCount) / Double(fcDivisor.subFrameCount) + + if sfcNew < 0.0 { return nil } + if sfcNew > Double(maxSubFrameCountExpressible) { return nil } + + return sfcNew + } + // MARK: - Offset / TimecodeInterval /// Utility function to return a `TimecodeInterval` interval. - internal func __offset(to other: Components) -> TimecodeInterval { + func _offset(to other: Components) -> TimecodeInterval { if components == other { return TimecodeInterval( - TCC().toTimecode( - rawValuesAt: frameRate, - limit: upperLimit, + Timecode.Components.zero.timecode( + at: frameRate, base: subFramesBase, - format: stringFormat + limit: upperLimit, + by: .allowingInvalid ), .plus ) } let otherTimecode = Timecode( - rawValues: other, + .components(other), at: frameRate, - limit: ._100days, base: subFramesBase, - format: stringFormat + limit: .max100Days, + by: .allowingInvalid ) if otherTimecode > self { - let diff = otherTimecode.__subtract( + let diff = otherTimecode._subtract( wrapping: components, from: otherTimecode.components ) - let deltaTC = diff.toTimecode( - rawValuesAt: frameRate, - limit: upperLimit, - base: subFramesBase, - format: stringFormat + let deltaTC = diff.timecode( + using: properties, + by: .allowingInvalid ) let delta = TimecodeInterval(deltaTC, .plus) @@ -468,16 +605,14 @@ extension Timecode { return delta } else /* other < self */ { - let diff = otherTimecode.__subtract( + let diff = otherTimecode._subtract( wrapping: other, from: components ) - let deltaTC = diff.toTimecode( - rawValuesAt: frameRate, - limit: upperLimit, - base: subFramesBase, - format: stringFormat + let deltaTC = diff.timecode( + using: properties, + by: .allowingInvalid ) let delta = TimecodeInterval(deltaTC, .minus) diff --git a/Sources/TimecodeKit/Timecode/Math/Timecode Math Public.swift b/Sources/TimecodeKit/Timecode/Math/Timecode Math Public.swift index 7d25b3a2..4d970e6c 100644 --- a/Sources/TimecodeKit/Timecode/Math/Timecode Math Public.swift +++ b/Sources/TimecodeKit/Timecode/Math/Timecode Math Public.swift @@ -1,278 +1,664 @@ // // Timecode Math Public.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // extension Timecode { - // MARK: - Add + // MARK: - Add Timecode /// Add a duration to the current timecode. - /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0) /// /// - Throws: ``ValidationError`` - public mutating func add(_ exactly: Components) throws { - guard let newTC = __add(exactly: exactly, to: components) - else { throw ValidationError.outOfBounds } - - try setTimecode(exactly: newTC) + public mutating func add(_ other: Timecode) throws { + try add( + frameRate == other.frameRate + ? other.components + : converted(to: other.frameRate).components + ) } /// Add a duration to the current timecode. - /// Clamps to valid timecode as set by the `upperLimit` property. - /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0) - public mutating func add(clamping values: Components) { - let newTC = __add(clamping: values, to: components) - - setTimecode(rawValues: newTC) + /// + /// - Throws: ``ValidationError`` + public mutating func add(_ other: Timecode, by validation: ValidationRule) throws { + try add( + frameRate == other.frameRate + ? other.components + : converted(to: other.frameRate).components, + by: validation + ) } + // MARK: - Add Time Source Value + /// Add a duration to the current timecode. - /// Wraps around the clock as set by the `upperLimit` property. - /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0) - public mutating func add(wrapping values: Components) { - let newTC = __add(wrapping: values, to: components) - - setTimecode(rawValues: newTC) + /// + /// - Throws: ``ValidationError`` + public mutating func add(_ other: TimecodeSourceValue) throws { + let otherTC = try Timecode(other, using: properties) + try add(otherTC) + } + + /// Add a duration to the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func add(_ other: FormattedTimecodeSourceValue) throws { + let otherTC = try Timecode(other, using: properties) + try add(otherTC) + } + + /// Add a duration to the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func add(_ other: RichTimecodeSourceValue) throws { + let otherTC = try Timecode(other) + try add(otherTC) + } + + /// Add a duration to the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func add(_ other: GuaranteedTimecodeSourceValue) throws { + let otherTC = Timecode(other, using: properties) + try add(otherTC) + } + + /// Add a duration to the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func add(_ other: GuaranteedRichTimecodeSourceValue) throws { + let otherTC = Timecode(other) + try add(otherTC) + } + + /// Add a duration to the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func add(_ other: TimecodeSourceValue, by validation: ValidationRule) throws { + let otherTC = try Timecode(other, using: properties) + try add(otherTC, by: validation) } - /// Add a duration to the current timecode and return a new instance with the new timecode. + /// Add a duration to the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func add(_ other: FormattedTimecodeSourceValue, by validation: ValidationRule) throws { + let otherTC = try Timecode(other, using: properties) + try add(otherTC, by: validation) + } + + /// Add a duration to the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func add(_ other: RichTimecodeSourceValue, by validation: ValidationRule) throws { + let otherTC = try Timecode(other) + try add(otherTC, by: validation) + } + + /// Add a duration to the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func add(_ other: GuaranteedTimecodeSourceValue, by validation: ValidationRule) throws { + let otherTC = Timecode(other, using: properties) + try add(otherTC, by: validation) + } + + /// Add a duration to the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func add(_ other: GuaranteedRichTimecodeSourceValue, by validation: ValidationRule) throws { + let otherTC = Timecode(other) + try add(otherTC, by: validation) + } + + // MARK: - Add Components + + /// Add a duration to the current timecode. /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0) /// /// - Throws: ``ValidationError`` - public func adding(_ exactly: Components) throws -> Timecode { - guard let newTC = __add(exactly: exactly, to: components) + public mutating func add(_ source: Components) throws { + guard let newTC = _add(exactly: source, to: components) else { throw ValidationError.outOfBounds } - var newTimecode = self - try newTimecode.setTimecode(exactly: newTC) - - return newTimecode + try _setTimecode(exactly: newTC) } - /// Add a duration to the current timecode and return a new instance with the new timecode. - /// Clamps to valid timecode as set by the `upperLimit` property. + /// Add a duration to the current timecode. /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0) - public func adding(clamping values: Components) -> Timecode { - let newTC = __add(clamping: values, to: components) + public mutating func add(_ source: Components, by validation: ValidationRule) { + let newTC: Timecode.Components - var newTimecode = self - newTimecode.setTimecode(rawValues: newTC) + switch validation { + case .clamping, .clampingComponents: + newTC = _add(clamping: source, to: components) + + case .wrapping: + newTC = _add(wrapping: source, to: components) + + case .allowingInvalid: + newTC = _add(rawValues: source, to: components) + } + _setTimecode(rawValues: newTC) + } + + // MARK: - Adding Timecode + + /// Add a duration to the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public func adding(_ other: Timecode) throws -> Timecode { + try adding( + frameRate == other.frameRate + ? other.components + : converted(to: other.frameRate).components + ) + } + + /// Add a duration to the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public func adding(_ other: Timecode, by validation: ValidationRule) throws -> Timecode { + try adding( + frameRate == other.frameRate + ? other.components + : converted(to: other.frameRate).components, + by: validation + ) + } + + // MARK: - Adding Time Source Value + + /// Add a duration to the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public mutating func adding(_ other: TimecodeSourceValue) throws -> Timecode { + let otherTC = try Timecode(other, using: properties) + return try adding(otherTC) + } + + /// Add a duration to the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public mutating func adding(_ other: FormattedTimecodeSourceValue) throws -> Timecode { + let otherTC = try Timecode(other, using: properties) + return try adding(otherTC) + } + + /// Add a duration to the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public mutating func adding(_ other: RichTimecodeSourceValue) throws -> Timecode { + let otherTC = try Timecode(other) + return try adding(otherTC) + } + + /// Add a duration to the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public mutating func adding(_ other: GuaranteedTimecodeSourceValue) throws -> Timecode { + let otherTC = Timecode(other, using: properties) + return try adding(otherTC) + } + + /// Add a duration to the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public mutating func adding(_ other: GuaranteedRichTimecodeSourceValue) throws -> Timecode { + let otherTC = Timecode(other) + return try adding(otherTC) + } + + /// Add a duration to the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public mutating func adding(_ other: TimecodeSourceValue, by validation: ValidationRule) throws -> Timecode { + let otherTC = try Timecode(other, using: properties) + return try adding(otherTC, by: validation) + } + + /// Add a duration to the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public mutating func adding(_ other: FormattedTimecodeSourceValue, by validation: ValidationRule) throws -> Timecode { + let otherTC = try Timecode(other, using: properties) + return try adding(otherTC, by: validation) + } + + /// Add a duration to the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public mutating func adding(_ other: RichTimecodeSourceValue, by validation: ValidationRule) throws -> Timecode { + let otherTC = try Timecode(other) + return try adding(otherTC, by: validation) + } + + /// Add a duration to the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public mutating func adding(_ other: GuaranteedTimecodeSourceValue, by validation: ValidationRule) throws -> Timecode { + let otherTC = Timecode(other, using: properties) + return try adding(otherTC, by: validation) + } + + /// Add a duration to the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public mutating func adding(_ other: GuaranteedRichTimecodeSourceValue, by validation: ValidationRule) throws -> Timecode { + let otherTC = Timecode(other) + return try adding(otherTC, by: validation) + } + + // MARK: - Adding Components + + /// Add a duration to the current timecode and return a new instance. + /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0) + /// + /// - Throws: ``ValidationError`` + public func adding(_ source: Components) throws -> Timecode { + var newTimecode = self + try newTimecode.add(source) return newTimecode } - /// Add a duration to the current timecode and return a new instance with the new timecode. - /// Wraps around the clock as set by the `upperLimit` property. + /// Add a duration to the current timecode and return a new instance. /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0) - public func adding(wrapping values: Components) -> Timecode { - let newTC = __add(wrapping: values, to: components) - + public func adding(_ source: Components, by validation: ValidationRule) -> Timecode { var newTimecode = self - newTimecode.setTimecode(rawValues: newTC) - + newTimecode.add(source, by: validation) return newTimecode } - // MARK: - Subtract + // MARK: - Subtract Timecode + + /// Subtract a duration from the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func subtract(_ other: Timecode) throws { + try subtract( + frameRate == other.frameRate + ? other.components + : converted(to: other.frameRate).components + ) + } + + /// Subtract a duration from the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func subtract(_ other: Timecode, by validation: ValidationRule) throws { + try subtract( + frameRate == other.frameRate + ? other.components + : converted(to: other.frameRate).components, + by: validation + ) + } + + // MARK: - Subtract Time Source Value + + /// Subtract a duration from the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func subtract(_ other: TimecodeSourceValue) throws { + let otherTC = try Timecode(other, using: properties) + try subtract(otherTC) + } + + /// Subtract a duration from the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func subtract(_ other: FormattedTimecodeSourceValue) throws { + let otherTC = try Timecode(other, using: properties) + try subtract(otherTC) + } + + /// Subtract a duration from the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func subtract(_ other: RichTimecodeSourceValue) throws { + let otherTC = try Timecode(other) + try subtract(otherTC) + } + + /// Subtract a duration from the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func subtract(_ other: GuaranteedTimecodeSourceValue) throws { + let otherTC = Timecode(other, using: properties) + try subtract(otherTC) + } + + /// Subtract a duration from the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func subtract(_ other: GuaranteedRichTimecodeSourceValue) throws { + let otherTC = Timecode(other) + try subtract(otherTC) + } + + /// Subtract a duration from the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func subtract(_ other: TimecodeSourceValue, by validation: ValidationRule) throws { + let otherTC = try Timecode(other, using: properties) + try subtract(otherTC, by: validation) + } + + /// Subtract a duration from the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func subtract(_ other: FormattedTimecodeSourceValue, by validation: ValidationRule) throws { + let otherTC = try Timecode(other, using: properties) + try subtract(otherTC, by: validation) + } + + /// Subtract a duration from the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func subtract(_ other: RichTimecodeSourceValue, by validation: ValidationRule) throws { + let otherTC = try Timecode(other) + try subtract(otherTC, by: validation) + } + + /// Subtract a duration from the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func subtract(_ other: GuaranteedTimecodeSourceValue, by validation: ValidationRule) throws { + let otherTC = Timecode(other, using: properties) + try subtract(otherTC, by: validation) + } + + /// Subtract a duration from the current timecode. + /// + /// - Throws: ``ValidationError`` + public mutating func subtract(_ other: GuaranteedRichTimecodeSourceValue, by validation: ValidationRule) throws { + let otherTC = Timecode(other) + try subtract(otherTC, by: validation) + } + + // MARK: - Subtract Components /// Subtract a duration from the current timecode. /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0) /// /// - Throws: ``ValidationError`` public mutating func subtract(_ exactly: Components) throws { - guard let newTC = __subtract(exactly: exactly, from: components) + guard let newTC = _subtract(exactly: exactly, from: components) else { throw ValidationError.outOfBounds } - try setTimecode(exactly: newTC) + try _setTimecode(exactly: newTC) } /// Subtract a duration from the current timecode. - /// Clamps to valid timecode. /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0) - public mutating func subtract(clamping: Components) { - let newTC = __subtract(clamping: clamping, from: components) + public mutating func subtract(_ source: Components, by validation: ValidationRule) { + let newTC: Timecode.Components + + switch validation { + case .clamping, .clampingComponents: + newTC = _subtract(clamping: source, from: components) + + case .wrapping: + newTC = _subtract(wrapping: source, from: components) + + case .allowingInvalid: + newTC = _subtract(rawValues: source, from: components) + } - setTimecode(rawValues: newTC) + _setTimecode(rawValues: newTC) } - /// Subtract a duration from the current timecode. - /// Wraps around the clock as set by the `upperLimit` property. - /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0) - public mutating func subtract(wrapping: Components) { - let newTC = __subtract(wrapping: wrapping, from: components) - - setTimecode(rawValues: newTC) + // MARK: - Subtracting Timecode + + /// Subtract a duration from the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public func subtracting(_ other: Timecode) throws -> Timecode { + try subtracting( + frameRate == other.frameRate + ? other.components + : converted(to: other.frameRate).components + ) } - /// Subtract a duration from the current timecode and return a new instance with the new timecode. - /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0) + /// Subtract a duration from the current timecode and return a new instance. /// /// - Throws: ``ValidationError`` - public func subtracting(_ exactly: Components) throws -> Timecode { - guard let newTC = __subtract(exactly: exactly, from: components) - else { throw ValidationError.outOfBounds } - - var newTimecode = self - try newTimecode.setTimecode(exactly: newTC) - - return newTimecode + public func subtracting(_ other: Timecode, by validation: ValidationRule) throws -> Timecode { + try subtracting( + frameRate == other.frameRate + ? other.components + : converted(to: other.frameRate).components, + by: validation + ) + } + + // MARK: - Subtracting Time Source Value + + /// Subtract a duration from the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public mutating func subtracting(_ other: TimecodeSourceValue) throws -> Timecode { + let otherTC = try Timecode(other, using: properties) + return try subtracting(otherTC) + } + + /// Subtract a duration from the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public mutating func subtracting(_ other: FormattedTimecodeSourceValue) throws -> Timecode { + let otherTC = try Timecode(other, using: properties) + return try subtracting(otherTC) + } + + /// Subtract a duration from the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public mutating func subtracting(_ other: RichTimecodeSourceValue) throws -> Timecode { + let otherTC = try Timecode(other) + return try subtracting(otherTC) + } + + /// Subtract a duration from the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public mutating func subtracting(_ other: GuaranteedTimecodeSourceValue) throws -> Timecode { + let otherTC = Timecode(other, using: properties) + return try subtracting(otherTC) + } + + /// Subtract a duration from the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public mutating func subtracting(_ other: GuaranteedRichTimecodeSourceValue) throws -> Timecode { + let otherTC = Timecode(other) + return try subtracting(otherTC) + } + + /// Subtract a duration from the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public mutating func subtracting(_ other: TimecodeSourceValue, by validation: ValidationRule) throws -> Timecode { + let otherTC = try Timecode(other, using: properties) + return try subtracting(otherTC, by: validation) } - /// Subtract a duration from the current timecode and return a new instance with the new timecode. - /// Clamps to valid timecode as set by the `upperLimit` property. + /// Subtract a duration from the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public mutating func subtracting(_ other: FormattedTimecodeSourceValue, by validation: ValidationRule) throws -> Timecode { + let otherTC = try Timecode(other, using: properties) + return try subtracting(otherTC, by: validation) + } + + /// Subtract a duration from the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public mutating func subtracting(_ other: RichTimecodeSourceValue, by validation: ValidationRule) throws -> Timecode { + let otherTC = try Timecode(other) + return try subtracting(otherTC, by: validation) + } + + /// Subtract a duration from the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public mutating func subtracting(_ other: GuaranteedTimecodeSourceValue, by validation: ValidationRule) throws -> Timecode { + let otherTC = Timecode(other, using: properties) + return try subtracting(otherTC, by: validation) + } + + /// Subtract a duration from the current timecode and return a new instance. + /// + /// - Throws: ``ValidationError`` + public mutating func subtracting(_ other: GuaranteedRichTimecodeSourceValue, by validation: ValidationRule) throws -> Timecode { + let otherTC = Timecode(other) + return try subtracting(otherTC, by: validation) + } + + // MARK: - Subtracting Components + + /// Subtract a duration from the current timecode and return a new instance. /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0) - public func subtracting(clamping values: Components) -> Timecode { - let newTC = __subtract(clamping: values, from: components) - + public func subtracting(_ source: Components) throws -> Timecode { var newTimecode = self - newTimecode.setTimecode(rawValues: newTC) - + try newTimecode.subtract(source) return newTimecode } - /// Subtract a duration from the current timecode and return a new instance with the new timecode. - /// Wraps around the clock as set by the `upperLimit` property. + /// Subtract a duration from the current timecode and return a new instance. /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0) - public func subtracting(wrapping values: Components) -> Timecode { - let newTC = __subtract(wrapping: values, from: components) - + public func subtracting(_ source: Components, by validation: ValidationRule) -> Timecode { var newTimecode = self - newTimecode.setTimecode(rawValues: newTC) - + newTimecode.subtract(source, by: validation) return newTimecode } - // MARK: - Multiply + // MARK: - Multiply Double - /// Multiply the current timecode by an amount. + /// Multiply the current timecode by floating-point number. /// /// - Throws: ``ValidationError`` public mutating func multiply(_ exactly: Double) throws { - guard let newTC = __multiply(exactly: exactly, with: components) + guard let newTC = _multiply(exactly: exactly, with: components) else { throw ValidationError.outOfBounds } - try setTimecode(exactly: newTC) + try _setTimecode(exactly: newTC) } - /// Multiply the current timecode by an amount. - /// Clamps to valid timecodes as set by the `upperLimit` property. - public mutating func multiply(clamping value: Double) { - let newTC = __multiply(clamping: value, with: components) + /// Multiply the current timecode by floating-point number. + public mutating func multiply(_ source: Double, by validation: ValidationRule) { + let newTC: Timecode.Components - setTimecode(rawValues: newTC) - } - - /// Multiply the current timecode by an amount. - /// Wraps around the clock as set by the `upperLimit` property. - public mutating func multiply(wrapping value: Double) { - let newTC = __multiply(wrapping: value, with: components) + switch validation { + case .clamping, .clampingComponents: + newTC = _multiply(clamping: source, with: components) + + case .wrapping: + newTC = _multiply(wrapping: source, with: components) + + case .allowingInvalid: + newTC = _multiply(rawValues: source, with: components) + } - setTimecode(rawValues: newTC) + _setTimecode(rawValues: newTC) } - /// Multiply a duration from the current timecode and return a new instance with the new timecode. + /// Multiply the current timecode by floating-point number and return a new instance. /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0) /// /// - Throws: ``ValidationError`` - public func multiplying(_ exactly: Double) throws -> Timecode { - guard let newTC = __multiply(exactly: exactly, with: components) - else { throw ValidationError.outOfBounds } - + public func multiplying(_ source: Double) throws -> Timecode { var newTimecode = self - try newTimecode.setTimecode(exactly: newTC) - - return newTimecode - } - - /// Multiply a duration from the current timecode and return a new instance with the new timecode. - /// Clamps to valid timecode. - /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0) - public func multiplying(clamping value: Double) -> Timecode { - let newTC = __multiply(clamping: value, with: components) - - var newTimecode = self - newTimecode.setTimecode(rawValues: newTC) - + try newTimecode.multiply(source) return newTimecode } - /// Multiply a duration from the current timecode and return a new instance with the new timecode. - /// Wraps around the clock as set by the `upperLimit` property. + /// Multiply the current timecode by floating-point number and return a new instance. /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0) - public func multiplying(wrapping value: Double) -> Timecode { - let newTC = __multiply(wrapping: value, with: components) - + public func multiplying(_ source: Double, by validation: ValidationRule) -> Timecode { var newTimecode = self - newTimecode.setTimecode(rawValues: newTC) - + newTimecode.multiply(source, by: validation) return newTimecode } - // MARK: - Divide + // MARK: - Divide Double - /// Divide the current timecode by a duration. + /// Divide the current timecode by floating-point number. /// /// - Throws: ``ValidationError`` public mutating func divide(_ exactly: Double) throws { - guard let newTC = __divide(exactly: exactly, into: components) + guard let newTC = _divide(exactly: exactly, into: components) else { throw ValidationError.outOfBounds } - try setTimecode(exactly: newTC) + try _setTimecode(exactly: newTC) } - /// Divide the current timecode by a duration. - /// Clamps to valid timecode as set by the `upperLimit` property. - public mutating func divide(clamping value: Double) { - let newTC = __divide(clamping: value, into: components) + /// Divide the current timecode by floating-point number. + public mutating func divide(_ source: Double, by validation: ValidationRule) { + let newTC: Timecode.Components - setTimecode(rawValues: newTC) - } - - /// Divide the current timecode by a duration. - /// Wraps around the clock as set by the `upperLimit` property. - public mutating func divide(wrapping value: Double) { - let newTC = __divide(wrapping: value, into: components) + switch validation { + case .clamping, .clampingComponents: + newTC = _divide(clamping: source, into: components) + + case .wrapping: + newTC = _divide(wrapping: source, into: components) + + case .allowingInvalid: + newTC = _divide(rawValues: source, into: components) + } - setTimecode(rawValues: newTC) + _setTimecode(rawValues: newTC) } - /// Divide the current timecode by a duration and return a new instance with the new timecode. - /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0) + // MARK: - Dividing Double -> Timecode + + /// Divide the current timecode by floating-point number and return a new instance. + /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) + /// or (0,0,500,0) /// /// - Throws: ``ValidationError`` - public func dividing(_ exactly: Double) throws -> Timecode { - guard let newTC = __divide(exactly: exactly, into: components) - else { throw ValidationError.outOfBounds } - + public func dividing(_ source: Double) throws -> Timecode { var newTimecode = self - try newTimecode.setTimecode(exactly: newTC) - + try newTimecode.divide(source) return newTimecode } - /// Divide the current timecode by a duration and return a new instance with the new timecode. - /// Clamps to valid timecode as set by the `upperLimit` property. - /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0) - public func dividing(clamping value: Double) -> Timecode { - let newTC = __divide(clamping: value, into: components) - + /// Divide the current timecode by floating-point number and return a new instance. + /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) + /// or (0,0,500,0) + public func dividing(_ source: Double, by validation: ValidationRule) -> Timecode { var newTimecode = self - newTimecode.setTimecode(rawValues: newTC) - + newTimecode.divide(source, by: validation) return newTimecode } - /// Divide the current timecode by a duration and return a new instance with the new timecode. - /// Wraps around the clock as set by the `upperLimit` property. + // MARK: - Dividing Timecode -> Double + + /// Divide the current timecode by floating-point number and return a new instance. + /// + /// - Throws: ``ValidationError`` + public func dividing(_ other: Timecode) throws -> Double { + try dividing( + frameRate == other.frameRate + ? other.components + : converted(to: other.frameRate).components + ) + } + + // MARK: - Dividing Components + + /// Divide the current timecode by a duration and return a new instance. /// Input values can be as large as desired and will be calculated recursively. ie: (0,0,0,1000) or (0,0,500,0) - public func dividing(wrapping value: Double) -> Timecode { - let newTC = __divide(wrapping: value, into: components) - - var newTimecode = self - newTimecode.setTimecode(rawValues: newTC) + /// + /// - Throws: ``ValidationError`` + public func dividing(_ source: Components) throws -> Double { + guard let dbl = _divide(exactly: source, into: components) + else { throw ValidationError.outOfBounds } - return newTimecode + return dbl } // MARK: - Offset / TimecodeInterval @@ -292,14 +678,17 @@ extension Timecode { /// Returns a ``TimecodeInterval`` distance between the current timecode and another timecode. public func interval(to other: Timecode) -> TimecodeInterval { if frameRate == other.frameRate { - return __offset(to: other.components) + return _offset(to: other.components) } else { guard let otherConverted = try? other.converted(to: frameRate) else { assertionFailure("Could not convert other Timecode to self Timecode frameRate.") - return .init(TCC().toTimecode(rawValuesAt: frameRate)) + return .init( + Timecode.Components.zero + .timecode(using: properties, by: .allowingInvalid) + ) } - return __offset(to: otherConverted.components) + return _offset(to: otherConverted.components) } } diff --git a/Sources/TimecodeKit/Timecode/Math/Timecode Operators.swift b/Sources/TimecodeKit/Timecode/Math/Timecode Operators.swift index 6b526c1d..bf0c2e41 100644 --- a/Sources/TimecodeKit/Timecode/Math/Timecode Operators.swift +++ b/Sources/TimecodeKit/Timecode/Math/Timecode Operators.swift @@ -1,121 +1,121 @@ // // Timecode Operators.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // // MARK: - Math operators: Self, Self extension Timecode { - /// a.k.a. `lhs.adding(wrapping: rhs)` + /// a.k.a. `lhs.adding(rhs, by: wrapping)` public static func + (lhs: Self, rhs: Self) -> Timecode { - if lhs.frameRate == rhs.frameRate { - return lhs.adding(wrapping: rhs.components) - } else { - guard let rhsConverted = try? rhs.converted(to: lhs.frameRate) else { - assertionFailure( - "Could not convert right-hand Timecode operand to left-hand Timecode frameRate." - ) - return lhs - } - - return lhs.adding(wrapping: rhsConverted.components) + do { + return try lhs.adding(rhs, by: .wrapping) + } catch { + assertionFailure( + "Could not convert right-hand Timecode operand to left-hand Timecode frameRate." + ) + return lhs } } - /// a.k.a. `lhs.add(wrapping: rhs)` + /// a.k.a. `lhs.add(rhs, by: wrapping)` public static func += (lhs: inout Self, rhs: Self) { - if lhs.frameRate == rhs.frameRate { - lhs.add(wrapping: rhs.components) - } else { - guard let rhsConverted = try? rhs.converted(to: lhs.frameRate) else { - assertionFailure( - "Could not convert right-hand Timecode operand to left-hand Timecode frameRate." - ) - return - } - - return lhs.add(wrapping: rhsConverted.components) + do { + try lhs.add(rhs, by: .wrapping) + } catch { + assertionFailure( + "Could not convert right-hand Timecode operand to left-hand Timecode frameRate." + ) + return } } - /// a.k.a. `lhs.subtracting(wrapping: rhs)` + /// a.k.a. `lhs.subtracting(rhs, by: wrapping)` public static func - (lhs: Self, rhs: Self) -> Timecode { - if lhs.frameRate == rhs.frameRate { - return lhs.subtracting(wrapping: rhs.components) - } else { - guard let rhsConverted = try? rhs.converted(to: lhs.frameRate) else { - assertionFailure( - "Could not convert right-hand Timecode operand to left-hand Timecode frameRate." - ) - return lhs - } - - return lhs.subtracting(wrapping: rhsConverted.components) + do { + return try lhs.subtracting(rhs, by: .wrapping) + } catch { + assertionFailure( + "Could not convert right-hand Timecode operand to left-hand Timecode frameRate." + ) + return lhs } } - /// a.k.a. `lhs.subtract(wrapping: rhs)` + /// a.k.a. `lhs.subtract(rhs, by: wrapping)` public static func -= (lhs: inout Self, rhs: Self) { - if lhs.frameRate == rhs.frameRate { - lhs.subtract(wrapping: rhs.components) - } else { - guard let rhsConverted = try? rhs.converted(to: lhs.frameRate) else { - assertionFailure( - "Could not convert right-hand Timecode operand to left-hand Timecode frameRate." - ) - return - } - - return lhs.subtract(wrapping: rhsConverted.components) + do { + try lhs.subtract(rhs, by: .wrapping) + } catch { + assertionFailure( + "Could not convert right-hand Timecode operand to left-hand Timecode frameRate." + ) + return + } + } + + /// a.k.a. `lhs.dividing(rhs)` + public static func / (lhs: Self, rhs: Self) -> Double { + do { + return try lhs.dividing(rhs) + } catch { + assertionFailure( + "Could not convert right-hand Timecode operand to left-hand Timecode frameRate." + ) + return 1.0 } } } -// MARK: - Math operators: Self, BinaryInteger +// MARK: - Math Operators extension Timecode { - /// a.k.a. `lhs.multiplying(wrapping: rhs)` - public static func * (lhs: Self, rhs: T) -> Self { - lhs.multiplying(wrapping: Double(rhs)) - } + // MARK: - * - /// a.k.a. `lhs.multiply(wrapping: rhs)` - public static func *= (lhs: inout Self, rhs: T) { - lhs.multiply(wrapping: Double(rhs)) + /// a.k.a. `lhs.multiplying(rhs, by: wrapping)` + public static func * (lhs: Self, rhs: Double) -> Self { + lhs.multiplying(rhs, by: .wrapping) } - /// a.k.a. `lhs.dividing(wrapping: rhs)` - public static func / (lhs: Self, rhs: T) -> Self { - lhs.dividing(wrapping: Double(rhs)) + /// a.k.a. `lhs.multiplying(rhs, by: wrapping)` + public static func * (lhs: Self, rhs: T) -> Self { + lhs.multiplying(Double(rhs), by: .wrapping) } - /// a.k.a. `lhs.divide(wrapping: rhs)` - public static func /= (lhs: inout Self, rhs: T) { - lhs.divide(wrapping: Double(rhs)) - } -} - -// MARK: - Math operators: Self, Double - -extension Timecode { - /// a.k.a. `lhs.multiplying(wrapping: rhs)` - public static func * (lhs: Self, rhs: Double) -> Self { - lhs.multiplying(wrapping: rhs) - } + // MARK: - *= - /// a.k.a. `lhs.multiply(wrapping: rhs)` + /// a.k.a. `lhs.multiply(rhs, by: wrapping)` public static func *= (lhs: inout Self, rhs: Double) { - lhs.multiply(wrapping: rhs) + lhs.multiply(rhs, by: .wrapping) } - /// a.k.a. `lhs.dividing(wrapping: rhs)` + /// a.k.a. `lhs.multiply(rhs, by: wrapping)` + public static func *= (lhs: inout Self, rhs: T) { + lhs.multiply(Double(rhs), by: .wrapping) + } + + // MARK: - / + + /// a.k.a. `lhs.dividing(rhs, by: wrapping)` public static func / (lhs: Self, rhs: Double) -> Self { - lhs.dividing(wrapping: rhs) + lhs.dividing(rhs, by: .wrapping) } - /// a.k.a. `lhs.divide(wrapping: rhs)` + /// a.k.a. `lhs.dividing(rhs, by: wrapping)` + public static func / (lhs: Self, rhs: T) -> Self { + lhs.dividing(Double(rhs), by: .wrapping) + } + + // MARK: - /= + + /// a.k.a. `lhs.divide(rhs, by: wrapping)` public static func /= (lhs: inout Self, rhs: Double) { - lhs.divide(wrapping: rhs) + lhs.divide(rhs, by: .wrapping) + } + + /// a.k.a. `lhs.divide(rhs, by: wrapping)` + public static func /= (lhs: inout Self, rhs: T) { + lhs.divide(Double(rhs), by: .wrapping) } } diff --git a/Sources/TimecodeKit/Timecode/Math/Timecode Rounding.swift b/Sources/TimecodeKit/Timecode/Math/Timecode Rounding.swift index 0fc72bda..3f15f2df 100644 --- a/Sources/TimecodeKit/Timecode/Math/Timecode Rounding.swift +++ b/Sources/TimecodeKit/Timecode/Math/Timecode Rounding.swift @@ -1,7 +1,7 @@ // // Timecode Rounding.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // extension Timecode { @@ -16,14 +16,17 @@ extension Timecode { /// /// ie: /// - /// ``` - /// try Timecode("01:02:03:04.00", at: ._24).roundedUp(toNearest: .frames) + /// ```swift + /// try Timecode(.string("01:02:03:04.00"), at: .fps24) + /// .roundedUp(toNearest: .frames) /// // == "01:02:03:04.00" // no change /// - /// try Timecode("01:02:03:04.05", at: ._24).roundedUp(toNearest: .frames) + /// try Timecode(.string("01:02:03:04.05"), at: .fps24) + /// .roundedUp(toNearest: .frames) /// // == "01:02:03:05.00" // rounds up to next whole frame /// - /// try Timecode("01:02:03:04.05", at: ._24).roundedUp(toNearest: .seconds) + /// try Timecode(.string("01:02:03:04.05"), at: .fps24) + /// .roundedUp(toNearest: .seconds) /// // == "01:02:04:00.00" // rounds up to next whole second /// ``` /// @@ -43,14 +46,17 @@ extension Timecode { /// /// ie: /// - /// ``` - /// try Timecode("01:02:03:04.00", at: ._24).roundUp(toNearest: .frames) + /// ```swift + /// try Timecode(.string("01:02:03:04.00"), at: .fps24) + /// .roundUp(toNearest: .frames) /// // == "01:02:03:04.00" // no change /// - /// try Timecode("01:02:03:04.05", at: ._24).roundUp(toNearest: .frames) + /// try Timecode(.string("01:02:03:04.05"), at: .fps24) + /// .roundUp(toNearest: .frames) /// // == "01:02:03:05.00" // rounds up to next whole frame /// - /// try Timecode("01:02:03:04.05", at: ._24).roundUp(toNearest: .seconds) + /// try Timecode(.string("01:02:03:04.05"), at: .fps24) + /// .roundUp(toNearest: .seconds) /// // == "01:02:04:00.00" // rounds up to next whole second /// ``` /// @@ -59,7 +65,7 @@ extension Timecode { switch component { case .days: if hours > 0 || minutes > 0 || seconds > 0 || frames > 0 || subFrames > 0 { - try add(TCC(d: 1)) + try add(Components(d: 1)) hours = 0 minutes = 0 seconds = 0 @@ -68,7 +74,7 @@ extension Timecode { } case .hours: if minutes > 0 || seconds > 0 || frames > 0 || subFrames > 0 { - try add(TCC(h: 1)) + try add(Components(h: 1)) minutes = 0 seconds = 0 frames = 0 @@ -76,20 +82,20 @@ extension Timecode { } case .minutes: if seconds > 0 || frames > 0 || subFrames > 0 { - try add(TCC(m: 1)) + try add(Components(m: 1)) seconds = 0 frames = 0 subFrames = 0 } case .seconds: if frames > 0 || subFrames > 0 { - try add(TCC(s: 1)) + try add(Components(s: 1)) frames = 0 subFrames = 0 } case .frames: if subFrames > 0 { - try add(TCC(f: 1)) + try add(Components(f: 1)) subFrames = 0 } case .subFrames: @@ -109,14 +115,17 @@ extension Timecode { /// /// ie: /// - /// ``` - /// try Timecode("01:02:03:04.00", at: ._24).roundedDown(toNearest: .frames) + /// ```swift + /// try Timecode(.string("01:02:03:04.00"), at: .fps24) + /// .roundedDown(toNearest: .frames) /// // == "01:02:03:04.00" // no change /// - /// try Timecode("01:02:03:04.05", at: ._24).roundedDown(toNearest: .frames) + /// try Timecode(.string("01:02:03:04.05"), at: .fps24) + /// .roundedDown(toNearest: .frames) /// // == "01:02:03:04.00" // subframes set to zero /// - /// try Timecode("01:02:03:04.05", at: ._24).roundedDown(toNearest: .seconds) + /// try Timecode(.string("01:02:03:04.05"), at: .fps24) + /// .roundedDown(toNearest: .seconds) /// // == "01:02:03:00.00" // frames and subframes set to zero /// ``` public func roundedDown(toNearest component: Component) -> Self { @@ -134,14 +143,17 @@ extension Timecode { /// /// ie: /// - /// ``` - /// try Timecode("01:02:03:04.00", at: ._24).roundDown(toNearest: .frames) + /// ```swift + /// try Timecode(.string("01:02:03:04.00"), at: .fps24) + /// .roundDown(toNearest: .frames) /// // == "01:02:03:04.00" // no change /// - /// try Timecode("01:02:03:04.05", at: ._24).roundDown(toNearest: .frames) + /// try Timecode(.string("01:02:03:04.05"), at: .fps24) + /// .roundDown(toNearest: .frames) /// // == "01:02:03:04.00" // subframes set to zero /// - /// try Timecode("01:02:03:04.05", at: ._24).roundDown(toNearest: .seconds) + /// try Timecode(.string("01:02:03:04.05"), at: .fps24) + /// .roundDown(toNearest: .seconds) /// // == "01:02:03:00.00" // frames and subframes set to zero /// ``` public mutating func roundDown(toNearest component: Component) { diff --git a/Sources/TimecodeKit/Timecode/Protocol Adoptions/Codable.swift b/Sources/TimecodeKit/Timecode/Protocol Adoptions/Codable.swift index 8dc6f46f..35fe2749 100644 --- a/Sources/TimecodeKit/Timecode/Protocol Adoptions/Codable.swift +++ b/Sources/TimecodeKit/Timecode/Protocol Adoptions/Codable.swift @@ -1,9 +1,59 @@ // // Codable.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // -// Swift requires Codable conformance to be declared in the same file as the -// original type declaration. -// So see Timecode.swift for Codable protocol adoption. +extension Timecode: Codable { + enum CodingKeys: String, CodingKey { + case frameRate + case upperLimit + case subFramesBase + case stringFormat // deprecated in TimecodeKit 2.0 + + case days + case hours + case minutes + case seconds + case frames + case subFrames + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(properties.frameRate, forKey: .frameRate) + try container.encode(properties.upperLimit, forKey: .upperLimit) + try container.encode(properties.subFramesBase, forKey: .subFramesBase) + + try container.encode(components.days, forKey: .days) + try container.encode(components.hours, forKey: .hours) + try container.encode(components.minutes, forKey: .minutes) + try container.encode(components.seconds, forKey: .seconds) + try container.encode(components.frames, forKey: .frames) + try container.encode(components.subFrames, forKey: .subFrames) + + // skip stringFormat; deprecated in TimecodeKit 2.0 + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + + properties = try Properties( + rate: values.decode(TimecodeFrameRate.self, forKey: .frameRate), + base: values.decode(SubFramesBase.self, forKey: .subFramesBase), + limit: values.decode(UpperLimit.self, forKey: .upperLimit) + ) + + components = try Components( + d: values.decode(Int.self, forKey: .days), + h: values.decode(Int.self, forKey: .hours), + m: values.decode(Int.self, forKey: .minutes), + s: values.decode(Int.self, forKey: .seconds), + f: values.decode(Int.self, forKey: .frames), + sf: values.decode(Int.self, forKey: .subFrames) + ) + + // skip stringFormat; deprecated in TimecodeKit 2.0 + } +} diff --git a/Sources/TimecodeKit/Timecode/Protocol Adoptions/Comparable.swift b/Sources/TimecodeKit/Timecode/Protocol Adoptions/Comparable.swift index 86110e94..a80d48ef 100644 --- a/Sources/TimecodeKit/Timecode/Protocol Adoptions/Comparable.swift +++ b/Sources/TimecodeKit/Timecode/Protocol Adoptions/Comparable.swift @@ -1,7 +1,7 @@ // // Comparable.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // import Darwin @@ -10,8 +10,7 @@ import Foundation extension Timecode: Equatable { public static func == (lhs: Self, rhs: Self) -> Bool { lhs.realTimeValue.rounded(decimalPlaces: 9) - == - rhs.realTimeValue.rounded(decimalPlaces: 9) + == rhs.realTimeValue.rounded(decimalPlaces: 9) } } @@ -29,8 +28,7 @@ extension Timecode: Comparable { /// For comparison based on a timeline that does not start at 00:00:00:00, see ``compare(to:timelineStart:)``. public static func < (lhs: Self, rhs: Self) -> Bool { lhs.realTimeValue.rounded(decimalPlaces: 9) - < - rhs.realTimeValue.rounded(decimalPlaces: 9) + < rhs.realTimeValue.rounded(decimalPlaces: 9) } } @@ -40,12 +38,14 @@ extension Timecode { /// ``upperLimit-swift.property`` property. The timeline is considered linear for 24 hours (or /// 100 days) from this start time, wrapping around the upper limit. /// + /// ## Working with a Non-Zero Timeline Start + /// /// Sometimes a timeline does not have a zero start time (00:00:00:00). For example, many DAW /// software applications such as Pro Tools allows a project start time to be set to any /// timecode. Its timeline then extends for 24 hours from that timecode, wrapping around over /// 00:00:00:00 at some point along the timeline. /// - /// Methods to sort and test sort order of `Timecode` collections are provided. + /// Methods to sort and test sort order of ``Timecode`` collections are provided. /// /// For example, given a 24 hour limit: /// @@ -61,16 +61,22 @@ extension Timecode { /// wrapping timeline, and 18:00:00:00 is `>` 21:00:00:00 since it is later in the wrapping /// timeline. /// - /// Note that passing `timelineStart` of `nil` or zero (00:00:00:00) is the same as using the - /// standard `<`, `==`, or `>` operators as a sort comparator. + /// Note that passing ``TimecodeSortComparator/timelineStart`` of `nil` or zero (00:00:00:00) is + /// the same as using the standard `<`, `==`, or `>` operators as a sort comparator. + /// + /// ## See Also /// - /// See also: ``TimecodeSortComparator``. + /// - ``TimecodeSortComparator`` public func compare(to other: Timecode, timelineStart: Timecode? = nil) -> ComparisonResult { // identical timecodes can early-return if self == other { return .orderedSame } + // zero timeline start + guard let timelineStart = timelineStart, !timelineStart.isZero else { // standard operator compare will work if timeline start is nil or zero (both mean zero) + + // (no need to check for equality, we already checked for .orderedSame early in the func) if self < other { return .orderedAscending } else { return .orderedDescending } } @@ -82,22 +88,29 @@ extension Timecode { var rhsFrameCount = other.frameCount.decimalValue if lhsFrameCount >= timelineStartFrameCount { - let lhsOneDay = Timecode(rawValues: TCC(d: 1), - at: frameRate, - limit: ._100days, - base: subFramesBase) - .frameCount.decimalValue + let lhsOneDay = Timecode( + .components(d: 1), + at: frameRate, + base: subFramesBase, + limit: .max100Days, + by: .allowingInvalid + ) + .frameCount.decimalValue lhsFrameCount -= lhsOneDay } if rhsFrameCount >= timelineStartFrameCount { - let rhsOneDay = Timecode(rawValues: TCC(d: 1), - at: other.frameRate, - limit: ._100days, - base: other.subFramesBase) - .frameCount.decimalValue + let rhsOneDay = Timecode( + .components(d: 1), + at: other.frameRate, + base: other.subFramesBase, + limit: .max100Days, + by: .allowingInvalid + ) + .frameCount.decimalValue rhsFrameCount -= rhsOneDay } + // (no need to check for equality, we already checked for .orderedSame early in the func) if lhsFrameCount < rhsFrameCount { return .orderedAscending } else { @@ -111,17 +124,20 @@ extension Timecode { extension Collection where Element == Timecode { /// Returns `true` if all ``Timecode`` instances are ordered chronologically, either ascending /// or descending according to the `ascending` parameter. - /// Contiguous subsequences of identical timecode are allowed. + /// + /// Neighboring instances of identical timecode are considered sorted. /// Timeline length and wrap point is determined by the ``Timecode/upperLimit-swift.property`` /// property. The timeline is considered linear for 24 hours (or 100 days) from this start time, /// wrapping around the upper limit. /// + /// ## Working with a Non-Zero Timeline Start + /// /// Sometimes a timeline does not have a zero start time (00:00:00:00). For example, many DAW /// software applications such as Pro Tools allows a project start time to be set to any /// timecode. Its timeline then extends for 24 hours from that timecode, wrapping around over /// 00:00:00:00 at some point along the timeline. /// - /// Methods to sort and test sort order of `Timecode` collections are provided. + /// Methods to sort and test sort order of ``Timecode`` collections are provided. /// /// For example, given a 24 hour limit: /// @@ -137,12 +153,16 @@ extension Collection where Element == Timecode { /// wrapping timeline, and 18:00:00:00 is `>` 21:00:00:00 since it is later in the wrapping /// timeline. /// - /// Note that passing `timelineStart` of `nil` or zero (00:00:00:00) is the same as using the - /// standard `<`, `==`, or `>` operators as a sort comparator. + /// Note that passing ``TimecodeSortComparator/timelineStart`` of `nil` or zero (00:00:00:00) is + /// the same as using the standard `<`, `==`, or `>` operators as a sort comparator. + /// + /// ## See Also /// - /// See also: ``TimecodeSortComparator``. - public func isSorted(ascending: Bool = true, - timelineStart: Timecode? = nil) -> Bool { + /// - ``TimecodeSortComparator`` + public func isSorted( + ascending: Bool = true, + timelineStart: Timecode? = nil + ) -> Bool { guard count > 1 else { return true } var priorIdx = startIndex @@ -168,17 +188,20 @@ extension Collection where Element == Timecode { extension Collection where Element == Timecode { /// Returns a collection sorting all ``Timecode`` instances chronologically, either ascending /// or descending. - /// Contiguous subsequences of identical timecode are allowed. + /// + /// Neighboring instances of identical timecode are considered sorted. /// Timeline length and wrap point is determined by the ``Timecode/upperLimit-swift.property`` /// property. The timeline is considered linear for 24 hours (or 100 days) from this start time, /// wrapping around the upper limit. /// + /// ## Working with a Non-Zero Timeline Start + /// /// Sometimes a timeline does not have a zero start time (00:00:00:00). For example, many DAW /// software applications such as Pro Tools allows a project start time to be set to any /// timecode. Its timeline then extends for 24 hours from that timecode, wrapping around over /// 00:00:00:00 at some point along the timeline. /// - /// Methods to sort and test sort order of `Timecode` collections are provided. + /// Methods to sort and test sort order of ``Timecode`` collections are provided. /// /// For example, given a 24 hour limit: /// @@ -194,37 +217,43 @@ extension Collection where Element == Timecode { /// wrapping timeline, and 18:00:00:00 is `>` 21:00:00:00 since it is later in the wrapping /// timeline. /// - /// Note that passing `timelineStart` of `nil` or zero (00:00:00:00) is the same as using the - /// standard `<`, `==`, or `>` operators as a sort comparator. + /// Note that passing ``TimecodeSortComparator/timelineStart`` of `nil` or zero (00:00:00:00) is + /// the same as using the standard `<`, `==`, or `>` operators as a sort comparator. /// - /// See also: ``TimecodeSortComparator``. - public func sorted(ascending: Bool = true, - timelineStart: Timecode) -> [Element] { + /// ## See Also + /// + /// - ``TimecodeSortComparator`` + public func sorted( + ascending: Bool = true, + timelineStart: Timecode + ) -> [Element] { sorted { $0.compare(to: $1, timelineStart: timelineStart) - != (ascending ? .orderedDescending : .orderedAscending ) + != (ascending ? .orderedDescending : .orderedAscending) } } } extension MutableCollection -where Element == Timecode, - Self: RandomAccessCollection, - Element: Comparable + where Element == Timecode, + Self: RandomAccessCollection { /// Sorts the collection in place by sorting all ``Timecode`` instances chronologically, either /// ascending or descending. - /// Contiguous subsequences of identical timecode are allowed. + /// + /// Neighboring instances of identical timecode are considered sorted. /// Timeline length and wrap point is determined by the ``Timecode/upperLimit-swift.property`` /// property. The timeline is considered linear for 24 hours (or 100 days) from this start time, /// wrapping around the upper limit. /// + /// ## Working with a Non-Zero Timeline Start + /// /// Sometimes a timeline does not have a zero start time (00:00:00:00). For example, many DAW /// software applications such as Pro Tools allows a project start time to be set to any /// timecode. Its timeline then extends for 24 hours from that timecode, wrapping around over /// 00:00:00:00 at some point along the timeline. /// - /// Methods to sort and test sort order of `Timecode` collections are provided. + /// Methods to sort and test sort order of ``Timecode`` collections are provided. /// /// For example, given a 24 hour limit: /// @@ -240,32 +269,40 @@ where Element == Timecode, /// wrapping timeline, and 18:00:00:00 is `>` 21:00:00:00 since it is later in the wrapping /// timeline. /// - /// Note that passing `timelineStart` of `nil` or zero (00:00:00:00) is the same as using the - /// standard `<`, `==`, or `>` operators as a sort comparator. + /// Note that passing ``TimecodeSortComparator/timelineStart`` of `nil` or zero (00:00:00:00) is + /// the same as using the standard `<`, `==`, or `>` operators as a sort comparator. + /// + /// ## See Also /// - /// See also: ``TimecodeSortComparator``. - public mutating func sort(ascending: Bool = true, - timelineStart: Timecode) { + /// - ``TimecodeSortComparator`` + public mutating func sort( + ascending: Bool = true, + timelineStart: Timecode + ) { sort { $0.compare(to: $1, timelineStart: timelineStart) - != (ascending ? .orderedDescending : .orderedAscending ) + != (ascending ? .orderedDescending : .orderedAscending) } } } -/// Sort comparator for ``Timecode``, optionally supplying a timeline start time. +/// Sort comparator for ``Timecode`` with optional timeline start time support. /// -/// Contiguous subsequences of identical timecode are allowed. +/// This custom `SortComparator` is provided to aid in comparing or sorting ``Timecode`` instances. +/// +/// Neighboring instances of identical timecode are considered sorted. /// Timeline length and wrap point is determined by the ``Timecode/upperLimit-swift.property`` /// property. The timeline is considered linear for 24 hours (or 100 days) from this start time, /// wrapping around the upper limit. /// +/// ## Working with a Non-Zero Timeline Start +/// /// Sometimes a timeline does not have a zero start time (00:00:00:00). For example, many DAW /// software applications such as Pro Tools allows a project start time to be set to any /// timecode. Its timeline then extends for 24 hours from that timecode, wrapping around over /// 00:00:00:00 at some point along the timeline. /// -/// Methods to sort and test sort order of `Timecode` collections are provided. +/// Methods to sort and test sort order of ``Timecode`` collections are provided. /// /// For example, given a 24 hour limit: /// @@ -281,7 +318,7 @@ where Element == Timecode, /// wrapping timeline, and 18:00:00:00 is `>` 21:00:00:00 since it is later in the wrapping /// timeline. /// -/// Note that passing `timelineStart` of `nil` or zero (00:00:00:00) is the same as using the +/// Note that passing ``timelineStart`` of `nil` or zero (00:00:00:00) is the same as using the /// standard `<`, `==`, or `>` operators as a sort comparator. @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) public struct TimecodeSortComparator: SortComparator { @@ -299,6 +336,7 @@ public struct TimecodeSortComparator: SortComparator { return result.inverted } } + public init(order: SortOrder = .forward, timelineStart: Timecode? = nil) { self.order = order self.timelineStart = timelineStart diff --git a/Sources/TimecodeKit/Timecode/Protocol Adoptions/CustomStringConvertible.swift b/Sources/TimecodeKit/Timecode/Protocol Adoptions/CustomStringConvertible.swift index 601eb79c..558d1e5b 100644 --- a/Sources/TimecodeKit/Timecode/Protocol Adoptions/CustomStringConvertible.swift +++ b/Sources/TimecodeKit/Timecode/Protocol Adoptions/CustomStringConvertible.swift @@ -1,25 +1,25 @@ // // CustomStringConvertible.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // extension Timecode: CustomStringConvertible, CustomDebugStringConvertible { public var description: String { - stringValue + stringValue() } public var debugDescription: String { // include Days even if it's 0 if we have a mode set that enables Days let daysString = - upperLimit == ._100days + upperLimit == .max100Days ? "\(days):" : "" - return "Timecode<\(daysString)\(stringValue) @ \(frameRate.stringValue)>" + return "Timecode<\(daysString)\(stringValue(format: .showSubFrames)) @ \(frameRate.stringValueVerbose)>" } public var verboseDescription: String { - "\(stringValue) @ \(frameRate.stringValue)" + "\(stringValue(format: .showSubFrames)) @ \(frameRate.stringValueVerbose)" } } diff --git a/Sources/TimecodeKit/Timecode/Protocol Adoptions/Hashable.swift b/Sources/TimecodeKit/Timecode/Protocol Adoptions/Hashable.swift index c8653879..17b6934e 100644 --- a/Sources/TimecodeKit/Timecode/Protocol Adoptions/Hashable.swift +++ b/Sources/TimecodeKit/Timecode/Protocol Adoptions/Hashable.swift @@ -1,7 +1,7 @@ // // Hashable.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // extension Timecode: Hashable { diff --git a/Sources/TimecodeKit/Timecode/Protocol Adoptions/Strideable.swift b/Sources/TimecodeKit/Timecode/Protocol Adoptions/Strideable.swift index 1c1ef49f..129d5aeb 100644 --- a/Sources/TimecodeKit/Timecode/Protocol Adoptions/Strideable.swift +++ b/Sources/TimecodeKit/Timecode/Protocol Adoptions/Strideable.swift @@ -1,7 +1,7 @@ // // Strideable.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // import Darwin @@ -10,10 +10,11 @@ extension Timecode: Strideable { public typealias Stride = Int /// Returns a new instance advanced by specified time components. - /// Same as calling `.adding(clamping: TCC(f: n))` but implemented in order to allow Timecode to conform to `Strideable`. + /// Same as calling `.adding(clamping: Timecode.Components(f: n))` but implemented in order to allow Timecode to conform to + /// `Strideable`. /// Will clamp to valid timecode range. public func advanced(by n: Stride) -> Self { - adding(clamping: Components(f: n)) + adding(Components(f: n), by: .clamping) } /// Distance between two timecode expressed as number of frames. diff --git a/Sources/TimecodeKit/Timecode/Source/Protocols/Timecode Source Protocols.swift b/Sources/TimecodeKit/Timecode/Source/Protocols/Timecode Source Protocols.swift new file mode 100644 index 00000000..0cc9dfea --- /dev/null +++ b/Sources/TimecodeKit/Timecode/Source/Protocols/Timecode Source Protocols.swift @@ -0,0 +1,120 @@ +// +// Timecode Source Protocols.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + +// MARK: - Protocols + +/// A protocol for timecode time value sources that do not supply their own frame +/// rate information. +protocol TimecodeSource { + /// Sets the timecode for a ``Timecode`` instance from a time value source. + /// Not meant to be called directly; instead, pass this instance into a ``Timecode`` initializer. + func set(timecode: inout Timecode) throws + + /// Sets the timecode for a ``Timecode`` instance from a time value source. + /// Not meant to be called directly; instead, pass this instance into a ``Timecode`` initializer. + func set(timecode: inout Timecode, by validation: Timecode.ValidationRule) +} + +/// A protocol for formatted timecode time value sources that do not supply their own frame +/// rate information. +protocol FormattedTimecodeSource { + /// Sets the timecode for a ``Timecode`` instance from a time value source. + /// Not meant to be called directly; instead, pass this instance into a ``Timecode`` initializer. + func set(timecode: inout Timecode) throws + + /// Sets the timecode for a ``Timecode`` instance from a time value source. + /// Not meant to be called directly; instead, pass this instance into a ``Timecode`` initializer. + func set(timecode: inout Timecode, by validation: Timecode.ValidationRule) throws +} + +/// A protocol for timecode time value sources that are able to supply frame rate information. +protocol RichTimecodeSource { + /// Sets the timecode for a ``Timecode`` instance from a time value source. + /// Not meant to be called directly; instead, pass this instance into a ``Timecode`` initializer. + func set(timecode: inout Timecode) throws -> Timecode.Properties +} + +/// A protocol for timecode time value sources that are guaranteed to be valid regardless of properties. +protocol GuaranteedTimecodeSource { + /// Sets the timecode for a ``Timecode`` instance from a time value source. + /// Not meant to be called directly; instead, pass this instance into a ``Timecode`` initializer. + func set(timecode: inout Timecode) +} + +/// A protocol for timecode time value sources that are able to supply frame rate information and +/// are guaranteed to be valid regardless of properties. +protocol GuaranteedRichTimecodeSource { + /// Sets the timecode for a ``Timecode`` instance from a time value source. + /// Not meant to be called directly; instead, pass this instance into a ``Timecode`` initializer. + func set(timecode: inout Timecode) -> Timecode.Properties +} + +// MARK: - Type Erasure + +/// Box containing a concrete `TimecodeSource` instance. +/// +/// > Note: +/// > This struct is not designed to be used directly. Use the static construction methods to form a value instead. +/// > See ``Timecode`` for more details and examples. +public struct TimecodeSourceValue { + var value: TimecodeSource + + init(value: TimecodeSource) { + self.value = value + } +} + +/// Box containing a concrete `FormattedTimecodeSource` instance. +/// +/// > Note: +/// > This struct is not designed to be used directly. Use the static construction methods to form a value instead. +/// > See ``Timecode`` for more details and examples. +public struct FormattedTimecodeSourceValue { + var value: FormattedTimecodeSource + + init(value: FormattedTimecodeSource) { + self.value = value + } +} + +/// Box containing a concrete `RichTimecodeSource` instance. +/// +/// > Note: +/// > This struct is not designed to be used directly. Use the static construction methods to form a value instead. +/// > See ``Timecode`` for more details and examples. +public struct RichTimecodeSourceValue { + var value: RichTimecodeSource + + init(value: RichTimecodeSource) { + self.value = value + } +} + +/// Box containing a concrete `GuaranteedTimecodeSource` instance. +/// +/// > Note: +/// > This struct is not designed to be used directly. Use the static construction methods to form a value instead. +/// > See ``Timecode`` for more details and examples. +public struct GuaranteedTimecodeSourceValue { + var value: GuaranteedTimecodeSource + + init(value: GuaranteedTimecodeSource) { + self.value = value + } +} + +/// Box containing a concrete `GuaranteedRichTimecodeSource` instance. +/// +/// > Note: +/// > This struct is not designed to be used directly. Use the static construction methods to form a value instead. +/// > See ``Timecode`` for more details and examples. +public struct GuaranteedRichTimecodeSourceValue { + var value: GuaranteedRichTimecodeSource + + init(value: GuaranteedRichTimecodeSource) { + self.value = value + } +} diff --git a/Sources/TimecodeKit/Timecode/Source/Timecode AVAsset.swift b/Sources/TimecodeKit/Timecode/Source/Timecode AVAsset.swift new file mode 100644 index 00000000..74181bf1 --- /dev/null +++ b/Sources/TimecodeKit/Timecode/Source/Timecode AVAsset.swift @@ -0,0 +1,170 @@ +// +// Timecode AVAsset.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + +// AVAssetReader is unavailable on watchOS so we can't support any AVAsset operations +#if canImport(AVFoundation) && !os(watchOS) && !os(visionOS) + +import AVFoundation +import Foundation + +// MARK: - TimecodeSource + +struct AVAssetTimecodeSource { + var asset: AVAsset + var attribute: RangeAttribute +} + +extension AVAssetTimecodeSource: TimecodeSource { + func set(timecode: inout Timecode) throws { + let rate: TimecodeFrameRate = timecode.frameRate + let base: Timecode.SubFramesBase = timecode.subFramesBase + let limit: Timecode.UpperLimit = timecode.upperLimit + + switch attribute { + case .start: + guard let tc = try asset.startTimecode( + at: rate, + base: base, + limit: limit + ) else { + throw Timecode.MediaParseError.unknownTimecode + } + timecode = tc + + case .end: + guard let tc = try asset.endTimecode( + at: rate, + base: base, + limit: limit + ) else { + throw Timecode.MediaParseError.unknownTimecode + } + timecode = tc + + case .duration: + timecode = try asset.durationTimecode( + at: rate, + base: base, + limit: limit + ) + } + } + + func set(timecode: inout Timecode, by validation: Timecode.ValidationRule) { + let rate: TimecodeFrameRate = timecode.frameRate + let base: Timecode.SubFramesBase = timecode.subFramesBase + let limit: Timecode.UpperLimit = timecode.upperLimit + + func zeroTimecode() -> Timecode { + Timecode(.zero, using: timecode.properties) + } + + switch attribute { + case .start: + timecode = (try? asset.startTimecode( + at: rate, + base: base, + limit: limit + )) ?? zeroTimecode() + + case .end: + timecode = (try? asset.endTimecode( + at: rate, + base: base, + limit: limit + )) ?? zeroTimecode() + + case .duration: + timecode = (try? asset.durationTimecode( + at: rate, + base: base, + limit: limit + )) ?? zeroTimecode() + } + } +} + +// MARK: - Static Constructors + +extension TimecodeSourceValue { + /// Read start, end or duration of an `AVAsset`. + /// Frame rate will be overridden by the passed properties, and will not be auto-detected from + /// the asset. + public static func avAsset( + _ asset: AVAsset, + _ attribute: RangeAttribute + ) -> Self { + .init(value: AVAssetTimecodeSource( + asset: asset, + attribute: attribute + )) + } +} + +// MARK: - RichTimecodeSource + +struct AVAssetRichTimecodeSource { + var asset: AVAsset + var attribute: RangeAttribute +} + +extension AVAssetRichTimecodeSource: RichTimecodeSource { + func set( + timecode: inout Timecode + ) throws -> Timecode.Properties { + let base: Timecode.SubFramesBase = timecode.subFramesBase + let limit: Timecode.UpperLimit = timecode.upperLimit + + switch attribute { + case .start: + guard let tc = try asset.startTimecode( + at: nil, // auto-detect from asset + base: base, + limit: limit + ) else { + throw Timecode.MediaParseError.unknownTimecode + } + timecode = tc + + case .end: + guard let tc = try asset.endTimecode( + at: nil, // auto-detect from asset + base: base, + limit: limit + ) else { + throw Timecode.MediaParseError.unknownTimecode + } + timecode = tc + + case .duration: + timecode = try asset.durationTimecode( + at: nil, // auto-detect from asset + base: base, + limit: limit + ) + } + + return timecode.properties + } +} + +// MARK: - Static Constructors + +extension RichTimecodeSourceValue { + /// Read start, end or duration of an `AVAsset`. + /// Frame rate will be automatically detected from the asset if possible. + public static func avAsset( + _ asset: AVAsset, + _ attribute: RangeAttribute + ) -> Self { + .init(value: AVAssetRichTimecodeSource( + asset: asset, + attribute: attribute + )) + } +} + +#endif diff --git a/Sources/TimecodeKit/Timecode/Source/Timecode Components.swift b/Sources/TimecodeKit/Timecode/Source/Timecode Components.swift new file mode 100644 index 00000000..cb012e4d --- /dev/null +++ b/Sources/TimecodeKit/Timecode/Source/Timecode Components.swift @@ -0,0 +1,110 @@ +// +// Timecode Components.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + +// MARK: - TimecodeSource + +extension Timecode.Components: TimecodeSource { + func set(timecode: inout Timecode) throws { + try timecode._setTimecode(exactly: self) + } + + func set(timecode: inout Timecode, by validation: Timecode.ValidationRule) { + switch validation { + case .clamping: + timecode._setTimecode(clamping: self) + case .clampingComponents: + timecode._setTimecode(clampingComponents: self) + case .wrapping: + timecode._setTimecode(wrapping: self) + case .allowingInvalid: + timecode._setTimecode(rawValues: self) + } + } +} + +// MARK: - Static Constructors + +extension TimecodeSourceValue { + /// Timecode components. + public static func components(_ source: Timecode.Components) -> Self { + .init(value: source) + } + + /// Timecode components. + public static func components( + d: Int = 0, + h: Int = 0, + m: Int = 0, + s: Int = 0, + f: Int = 0, + sf: Int = 0 + ) -> Self { + .init(value: Timecode.Components(d: d, h: h, m: m, s: s, f: f, sf: sf)) + } +} + +// MARK: - Get + +// no getter - Timecode contains stored components property + +// MARK: - Set + +extension Timecode { + /// Set timecode from tuple values. + /// + /// Returns true/false depending on whether the string values are valid or not. + /// + /// Values which are out-of-bounds will return false. + /// + /// (Validation is based on the frame rate and `upperLimit` property.) + /// + /// - Throws: ``ValidationError`` + mutating func _setTimecode(exactly values: Components) throws { + guard values + .invalidComponents(using: properties) + .isEmpty + else { throw ValidationError.outOfBounds } + + components = values + } + + /// Set timecode from components. + /// Clamps to valid timecode as set by the `upperLimit` property. + /// + /// (Validation is based on the frame rate and `upperLimit` property.) + mutating func _setTimecode(clamping source: Components) { + let result = _add(clamping: source, to: .zero) + + _setTimecode(rawValues: result) + } + + /// Set timecode from components, clamping individual values if necessary. + /// + /// (Validation is based on the frame rate and `upperLimit` property.) + mutating func _setTimecode(clampingComponents values: Components) { + components = values + + clampComponents() + } + + /// Set timecode from tuple values. + /// + /// Timecode will wrap if out-of-bounds. Will handle negative values and wrap accordingly. + /// + /// (Wrapping is based on the frame rate and `upperLimit` property.) + mutating func _setTimecode(wrapping values: Components) { + _setTimecode(rawValues: _add( + wrapping: values, + to: .zero + )) + } + + /// Set timecode from tuple values. + /// Timecode values will not be validated or rejected if they overflow. + mutating func _setTimecode(rawValues values: Components) { + components = values + } +} diff --git a/Sources/TimecodeKit/Timecode/Source/Timecode FeetAndFrames.swift b/Sources/TimecodeKit/Timecode/Source/Timecode FeetAndFrames.swift new file mode 100644 index 00000000..08709e14 --- /dev/null +++ b/Sources/TimecodeKit/Timecode/Source/Timecode FeetAndFrames.swift @@ -0,0 +1,146 @@ +// +// Timecode FeetAndFrames.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + +import Foundation + +// MARK: - TimecodeSource + +extension FeetAndFrames: TimecodeSource { + func set(timecode: inout Timecode) throws { + try timecode._setTimecode(exactly: self) + } + + func set(timecode: inout Timecode, by validation: Timecode.ValidationRule) { + switch validation { + case .clamping, .clampingComponents: + timecode._setTimecode(clamping: self) + case .wrapping: + timecode._setTimecode(wrapping: self) + case .allowingInvalid: + timecode._setTimecode(rawValues: self) + } + } +} + +// MARK: - Static Constructors + +extension TimecodeSourceValue { + /// Feet and Frames time value. + public static func feetAndFrames(_ source: FeetAndFrames) -> Self { + .init(value: source) + } + + /// Feet and Frames time value. + public static func feetAndFrames( + feet: Int, + frames: Int, + subFrames: Int = 0, + subFramesBase: Timecode.SubFramesBase = .default() + ) -> Self { + .init(value: FeetAndFrames( + feet: feet, + frames: frames, + subFrames: subFrames, + subFramesBase: subFramesBase + )) + } + + /// Feet and Frames time value. + public static func feetAndFrames( + _ string: S, + subFramesBase: Timecode.SubFramesBase = .default() + ) throws -> Self { + try .init(value: FeetAndFrames( + string, + subFramesBase: subFramesBase + )) + } +} + +// MARK: - Get + +extension Timecode { + /// Returns the timecode expressed as Feet+Frames. + /// + /// When used as a counter in the audio-world the footage count refers to 35mm 4-perf. Detailed + /// discussion can be found [in this thread.]( + /// https://gearspace.com/board/post-production-forum/898755-timecode-feet-frames.html + /// ) + public var feetAndFramesValue: FeetAndFrames { + let fc = frameCount.wholeFrames + let feet = fc / 16 + let frames = fc % 16 + + return FeetAndFrames( + feet: feet, + frames: frames, + subFrames: subFrames, + subFramesBase: subFramesBase + ) + } +} + +// MARK: - Set + +extension Timecode { + mutating func _setTimecode(exactly feetAndFrames: FeetAndFrames) throws { + try _setTimecode(exactly: feetAndFrames.frameCount) + } + + mutating func _setTimecode(clamping feetAndFrames: FeetAndFrames) { + _setTimecode(clamping: feetAndFrames.frameCount) + } + + mutating func _setTimecode(wrapping feetAndFrames: FeetAndFrames) { + _setTimecode(wrapping: feetAndFrames.frameCount) + } + + mutating func _setTimecode(rawValues feetAndFrames: FeetAndFrames) { + _setTimecode(rawValues: feetAndFrames.frameCount) + } +} + +extension FeetAndFrames { + /// Utility to decode a Feet+Frames string into its component values, without validating component values. + /// + /// An error is thrown if the string is malformed and cannot be reasonably parsed. + /// Raw values themselves will be passed as-is and not validated. + /// + /// - Throws: ``Timecode/StringParseError`` + static func decode(feetAndFrames string: S) throws -> (feet: Int, frames: Int, subFrames: Int) { + let pattern = #"^([\d]+)\+([\d]+)(?:\.([\d]+))?$"# + + let matches = string.regexMatches(captureGroupsFromPattern: pattern) + + guard matches.count == 4 else { + throw Timecode.StringParseError.malformed + } + + guard let ftString = matches[1], + let ft = Int(ftString), + let frString = matches[2], + let fr = Int(frString) + else { + throw Timecode.StringParseError.malformed + } + + let feet = ft + let frames = fr + + // subframes are optional and may not be present + let subFrames: Int + if let sfrString = matches[3] { + guard let sfr = Int(sfrString) else { + throw Timecode.StringParseError.malformed + } + subFrames = sfr + } else { + subFrames = 0 + } + + return (feet: feet, frames: frames, subFrames: subFrames) + } +} diff --git a/Sources/TimecodeKit/Timecode/Source/Timecode FrameCount Value.swift b/Sources/TimecodeKit/Timecode/Source/Timecode FrameCount Value.swift new file mode 100644 index 00000000..fa07915b --- /dev/null +++ b/Sources/TimecodeKit/Timecode/Source/Timecode FrameCount Value.swift @@ -0,0 +1,138 @@ +// +// Timecode FrameCount Value.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + +import Foundation + +// MARK: - TimecodeSource + +extension Timecode.FrameCount.Value: TimecodeSource { + func set(timecode: inout Timecode) throws { + try timecode._setTimecode(exactly: self) + } + + func set(timecode: inout Timecode, by validation: Timecode.ValidationRule) { + switch validation { + case .clamping, .clampingComponents: + timecode._setTimecode(clamping: self) + case .wrapping: + timecode._setTimecode(wrapping: self) + case .allowingInvalid: + timecode._setTimecode(rawValues: self) + } + } +} + +// MARK: - Static Constructors + +extension TimecodeSourceValue { + /// Total elapsed frames count. + public static func frames(_ source: Timecode.FrameCount.Value) -> Self { + .init(value: source) + } + + /// Total elapsed frames count, and optional subframes count. + /// (Same as ``Timecode/FrameCount-swift.struct/Value-swift.enum/split(frames:subFrames:)``.) + public static func frames(_ frames: Int, subFrames: Int = 0) -> Self { + if subFrames == 0 { + return .init(value: Timecode.FrameCount.Value.frames(frames)) + } else { + return .init(value: Timecode.FrameCount.Value.split(frames: frames, subFrames: subFrames)) + } + } + + /// Total elapsed frames, expressed as a `Double` where the integer portion is whole frames and the fractional portion is the subframes + /// unit interval. + /// (Same as ``Timecode/FrameCount-swift.struct/Value-swift.enum/combined(frames:)``.) + public static func frames(_ frames: Double) -> Self { + .init(value: Timecode.FrameCount.Value.combined(frames: frames)) + } + + /// Total elapsed whole frames, and subframes expressed as a floating-point unit interval (`0.0..<1.0`). + /// (Same as ``Timecode/FrameCount-swift.struct/Value-swift.enum/splitUnitInterval(frames:subFramesUnitInterval:)``.) + public static func frames(_ frames: Int, subFramesUnitInterval: Double) -> Self { + .init(value: Timecode.FrameCount.Value.splitUnitInterval( + frames: frames, + subFramesUnitInterval: subFramesUnitInterval + )) + } +} + +// MARK: - Set + +extension Timecode { + /// Set timecode from total elapsed frames ("frame number"). + /// + /// Subframes are represented by the fractional portion of the number. + /// Timecode is updated as long as the value passed is in valid range. + /// (Validation is based on the frame rate and `upperLimit` property.) + /// + /// - Throws: ``ValidationError`` + mutating func _setTimecode(exactly frameCountValue: FrameCount.Value) throws { + components = try components(exactly: frameCountValue) + } + + /// Set timecode from total elapsed frames ("frame number"). + /// + /// Clamps to valid timecode. + /// + /// Subframes are represented by the fractional portion of the number. + mutating func _setTimecode(clamping source: FrameCount.Value) { + let convertedComponents = components(rawValues: source) + _setTimecode(clamping: convertedComponents) + } + + /// Set timecode from total elapsed frames ("frame number"). + /// + /// Timecode will be wrapped around the timecode clock if out-of-bounds. + /// + /// Subframes are represented by the fractional portion of the number. + mutating func _setTimecode(wrapping source: FrameCount.Value) { + let convertedComponents = components(rawValues: source) + _setTimecode(wrapping: convertedComponents) + } + + /// Set timecode from total elapsed frames ("frame number"). + /// + /// Allows for invalid raw values (in this case, unbounded Days component). + /// + /// Subframes are represented by the fractional portion of the number. + mutating func _setTimecode(rawValues source: FrameCount.Value) { + let convertedComponents = components(rawValues: source) + _setTimecode(rawValues: convertedComponents) + } + + // MARK: Helper Methods + + /// Internal: + /// Returns frame count value converted to components using the instance's + /// frame rate and subframes base. + /// + /// - Throws: ``ValidationError`` + func components(exactly source: FrameCount.Value) throws -> Components { + let fc = FrameCount(source, base: subFramesBase) + + guard fc.subFrameCount >= 0, + fc <= maxFrameCountExpressible + else { throw ValidationError.outOfBounds } + + return Self.components( + of: fc, + at: frameRate + ) + } + + /// Internal: + /// Returns frame count value converted to components using the instance's + /// frame rate and subframes base. + func components(rawValues source: FrameCount.Value) -> Components { + let fc = FrameCount(source, base: subFramesBase) + + return Self.components( + of: fc, + at: frameRate + ) + } +} diff --git a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode FrameCount.swift b/Sources/TimecodeKit/Timecode/Source/Timecode FrameCount.swift similarity index 61% rename from Sources/TimecodeKit/Timecode/Data Interchange/Timecode FrameCount.swift rename to Sources/TimecodeKit/Timecode/Source/Timecode FrameCount.swift index 00402fa9..a76b3204 100644 --- a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode FrameCount.swift +++ b/Sources/TimecodeKit/Timecode/Source/Timecode FrameCount.swift @@ -1,88 +1,41 @@ // // Timecode FrameCount.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // import Foundation -// MARK: - Init +// MARK: - TimecodeSource -extension Timecode { - /// Instance exactly from total elapsed frames ("frame number") at a given frame rate. - /// - /// Validation is based on the `upperLimit` and `subFramesBase` properties. - /// - /// - Throws: ``ValidationError`` - public init( - _ exactly: FrameCount, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - format: StringFormat = .default() - ) throws { - frameRate = rate - upperLimit = limit - subFramesBase = exactly.subFramesBase - stringFormat = format - - try setTimecode(exactly: exactly.value) +extension Timecode.FrameCount: TimecodeSource { + func set(timecode: inout Timecode) throws { + timecode.subFramesBase = subFramesBase + try timecode._setTimecode(exactly: self) } - /// Instance exactly from total elapsed frames ("frame number") at a given frame rate, clamping - /// to valid timecode if necessary. - /// - /// Clamping is based on the `upperLimit` and `subFramesBase` properties. - public init( - clamping source: FrameCount, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = source.subFramesBase - stringFormat = format - - setTimecode(clamping: source.value) - } - - /// Instance exactly from total elapsed frames ("frame number") at a given frame rate, wrapping - /// timecode if necessary. - /// - /// Timecode will be wrapped around the timecode clock if out-of-bounds. - public init( - wrapping source: FrameCount, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = source.subFramesBase - stringFormat = format - - setTimecode(wrapping: source.value) + func set(timecode: inout Timecode, by validation: Timecode.ValidationRule) { + switch validation { + case .clamping, .clampingComponents: + timecode._setTimecode(clamping: self) + case .wrapping: + timecode._setTimecode(wrapping: self) + case .allowingInvalid: + timecode._setTimecode(rawValues: self) + } } - - /// Instance exactly from total elapsed frames ("frame number") at a given frame rate - /// - /// Allows for invalid raw values (in this case, unbounded Days component). - public init( - rawValues source: FrameCount, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - format: StringFormat = .default() - ) { - frameRate = rate - upperLimit = limit - subFramesBase = source.subFramesBase - stringFormat = format - - setTimecode(rawValues: source.value) +} + +// MARK: - Static Constructors + +extension TimecodeSourceValue { + /// Timecode total elapsed frame count value (``Timecode/FrameCount``). + public static func frames(_ source: Timecode.FrameCount) -> Self { + .init(value: source) } } -// MARK: - Get and Set +// MARK: - Get extension Timecode { /// Returns the total number of frames elapsed. @@ -93,7 +46,11 @@ extension Timecode { base: subFramesBase ) } - +} + +// MARK: - Set + +extension Timecode { /// Set timecode from total elapsed frames ("frame number"). /// /// Subframes are represented by the fractional portion of the number. @@ -101,15 +58,8 @@ extension Timecode { /// (Validation is based on the frame rate and `upperLimit` property.) /// /// - Throws: ``ValidationError`` - public mutating func setTimecode(exactly source: FrameCount) throws { - let convertedComponents = try components(exactly: source) - - days = convertedComponents.d - hours = convertedComponents.h - minutes = convertedComponents.m - seconds = convertedComponents.s - frames = convertedComponents.f - subFrames = convertedComponents.sf + mutating func _setTimecode(exactly source: FrameCount) throws { + components = try components(exactly: source) } /// Set timecode from total elapsed frames ("frame number"). @@ -117,9 +67,9 @@ extension Timecode { /// Clamps to valid timecode. /// /// Subframes are represented by the fractional portion of the number. - public mutating func setTimecode(clamping source: FrameCount) { + mutating func _setTimecode(clamping source: FrameCount) { let convertedComponents = components(rawValues: source) - setTimecode(clamping: convertedComponents) + _setTimecode(clamping: convertedComponents) } /// Set timecode from total elapsed frames ("frame number"). @@ -127,9 +77,9 @@ extension Timecode { /// Timecode will be wrapped around the timecode clock if out-of-bounds. /// /// Subframes are represented by the fractional portion of the number. - public mutating func setTimecode(wrapping source: FrameCount) { + mutating func _setTimecode(wrapping source: FrameCount) { let convertedComponents = components(rawValues: source) - setTimecode(wrapping: convertedComponents) + _setTimecode(wrapping: convertedComponents) } /// Set timecode from total elapsed frames ("frame number"). @@ -137,19 +87,19 @@ extension Timecode { /// Allows for invalid raw values (in this case, unbounded Days component). /// /// Subframes are represented by the fractional portion of the number. - public mutating func setTimecode(rawValues source: FrameCount) { + mutating func _setTimecode(rawValues source: FrameCount) { let convertedComponents = components(rawValues: source) - setTimecode(rawValues: convertedComponents) + _setTimecode(rawValues: convertedComponents) } - // MARK: Internal Methods + // MARK: Helper Methods /// Internal: /// Returns frame count value converted to components using the instance's /// frame rate and subframes base. /// /// - Throws: ``ValidationError`` - internal func components(exactly source: FrameCount) throws -> Components { + func components(exactly source: FrameCount) throws -> Components { // early return if we don't need to scale subframes if source.subFramesBase == subFramesBase || source.subFrames == 0 { return try components(exactly: source.value) @@ -157,8 +107,8 @@ extension Timecode { // scale subframes between subframes bases var convertedComponents = components(rawValues: source) - convertedComponents.sf = source.subFramesBase.convert( - subFrames: convertedComponents.sf, + convertedComponents.subFrames = source.subFramesBase.convert( + subFrames: convertedComponents.subFrames, to: subFramesBase ) return convertedComponents @@ -167,7 +117,7 @@ extension Timecode { /// Internal: /// Returns frame count value converted to components using the instance's /// frame rate and subframes base. - internal func components(rawValues source: FrameCount) -> Components { + func components(rawValues source: FrameCount) -> Components { var convertedComponents = components(rawValues: source.value) // early return if we don't need to scale subframes @@ -176,8 +126,8 @@ extension Timecode { } // scale subframes between subframes bases - convertedComponents.sf = source.subFramesBase.convert( - subFrames: convertedComponents.sf, + convertedComponents.subFrames = source.subFramesBase.convert( + subFrames: convertedComponents.subFrames, to: subFramesBase ) return convertedComponents @@ -193,19 +143,19 @@ extension Timecode { at frameRate: TimecodeFrameRate, base: SubFramesBase = .default() ) -> FrameCount { - let subFramesUnitInterval = Double(values.sf) / Double(base.rawValue) + let subFramesUnitInterval = Double(values.subFrames) / Double(base.rawValue) let frameCountValue: FrameCount.Value switch frameRate.isDrop { case true: - let totalMinutes = (24 * 60 * values.d) + (60 * values.h) + values.m + let totalMinutes = (24 * 60 * values.days) + (60 * values.hours) + values.minutes - let base = (frameRate.maxFrames * 60 * 60 * 24 * values.d) - + (frameRate.maxFrames * 60 * 60 * values.h) - + (frameRate.maxFrames * 60 * values.m) - + (frameRate.maxFrames * values.s) - + (values.f) + let base = (frameRate.maxFrames * 60 * 60 * 24 * values.days) + + (frameRate.maxFrames * 60 * 60 * values.hours) + + (frameRate.maxFrames * 60 * values.minutes) + + (frameRate.maxFrames * values.seconds) + + (values.frames) let dropOffset = Int(frameRate.framesDroppedPerMinute) * (totalMinutes - (totalMinutes / 10)) let totalWholeFrames = base - dropOffset @@ -216,11 +166,11 @@ extension Timecode { ) case false: - let dd = Double(values.d) * 24 * 60 * 60 * frameRate.frameRateForElapsedFramesCalculation - let hh = Double(values.h) * 60 * 60 * frameRate.frameRateForElapsedFramesCalculation - let mm = Double(values.m) * 60 * frameRate.frameRateForElapsedFramesCalculation - let ss = Double(values.s) * frameRate.frameRateForElapsedFramesCalculation - let totalWholeFrames = Int(round(dd + hh + mm + ss)) + values.f + let dd = Double(values.days) * 24 * 60 * 60 * frameRate.frameRateForElapsedFramesCalculation + let hh = Double(values.hours) * 60 * 60 * frameRate.frameRateForElapsedFramesCalculation + let mm = Double(values.minutes) * 60 * frameRate.frameRateForElapsedFramesCalculation + let ss = Double(values.seconds) * frameRate.frameRateForElapsedFramesCalculation + let totalWholeFrames = Int(round(dd + hh + mm + ss)) + values.frames frameCountValue = .splitUnitInterval( frames: totalWholeFrames, @@ -332,11 +282,11 @@ extension Timecode { extension Timecode.Components { /// Negates the largest non-zero component. fileprivate mutating func negate() { - if d != 0 { d.negate() ; return } - if h != 0 { h.negate() ; return } - if m != 0 { m.negate() ; return } - if s != 0 { s.negate() ; return } - if f != 0 { f.negate() ; return } - if sf != 0 { sf.negate() ; return } + if days != 0 { days.negate(); return } + if hours != 0 { hours.negate(); return } + if minutes != 0 { minutes.negate(); return } + if seconds != 0 { seconds.negate(); return } + if frames != 0 { frames.negate(); return } + if subFrames != 0 { subFrames.negate(); return } } } diff --git a/Sources/TimecodeKit/Timecode/Source/Timecode Rational CMTime.swift b/Sources/TimecodeKit/Timecode/Source/Timecode Rational CMTime.swift new file mode 100644 index 00000000..a1084742 --- /dev/null +++ b/Sources/TimecodeKit/Timecode/Source/Timecode Rational CMTime.swift @@ -0,0 +1,111 @@ +// +// Timecode Rational CMTime.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + +#if canImport(CoreMedia) + +import CoreMedia +import Foundation + +// MARK: - TimecodeSource + +@available(macOS 10.7, iOS 4.0, tvOS 9.0, watchOS 6.0, *) +extension CMTime: TimecodeSource { + func set(timecode: inout Timecode) throws { + try timecode._setTimecode(exactly: self) + } + + func set(timecode: inout Timecode, by validation: Timecode.ValidationRule) { + switch validation { + case .clamping, .clampingComponents: + timecode._setTimecode(clamping: self) + case .wrapping: + timecode._setTimecode(wrapping: self) + case .allowingInvalid: + timecode._setTimecode(rawValues: self) + } + } +} + +// MARK: - Static Constructors + +@available(macOS 10.7, iOS 4.0, tvOS 9.0, watchOS 6.0, *) +extension TimecodeSourceValue { + /// `CMTime` value. + /// + /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to represent + /// times and durations. + public static func cmTime(_ source: CMTime) -> Self { + .init(value: source) + } +} + +// MARK: - Get + +@available(macOS 10.7, iOS 4.0, tvOS 9.0, watchOS 6.0, *) +extension Timecode { + /// Returns the time location as a `CMTime` instance. + /// + /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to represent + /// times and durations. + public var cmTimeValue: CMTime { + let fraction = rationalValue + return CMTime( + value: CMTimeValue(fraction.numerator), // aka Int64 + timescale: CMTimeScale(fraction.denominator) // aka Int32 + ) + } +} + +// MARK: - Set + +@available(macOS 10.7, iOS 4.0, tvOS 9.0, watchOS 6.0, *) +extension Timecode { + /// Instance from elapsed time `CMTime`. + /// + /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to represent + /// times and durations. + /// + /// - Throws: ``ValidationError`` + mutating func _setTimecode(exactly: CMTime) throws { + let fraction = Fraction(Int(exactly.value), Int(exactly.timescale)) + try _setTimecode(exactly: fraction) + } + + /// Instance from elapsed time `CMTime`. + /// + /// Clamps to valid timecode. + /// + /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to represent + /// times and durations. + mutating func _setTimecode(clamping cmTime: CMTime) { + let fraction = Fraction(Int(cmTime.value), Int(cmTime.timescale)) + _setTimecode(clamping: fraction) + } + + /// Instance from elapsed time `CMTime`. + /// + /// Wraps timecode if necessary. + /// + /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to represent + /// times and durations. + mutating func _setTimecode(wrapping cmTime: CMTime) { + let fraction = Fraction(Int(cmTime.value), Int(cmTime.timescale)) + _setTimecode(wrapping: fraction) + } + + /// Instance from elapsed time `CMTime`. + /// + /// Allows for invalid raw values (in this case, unbounded Days component). + /// + /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to represent + /// times and durations. + mutating func _setTimecode(rawValues cmTime: CMTime) { + let fraction = Fraction(Int(cmTime.value), Int(cmTime.timescale)) + _setTimecode(rawValues: fraction) + } +} + +#endif diff --git a/Sources/TimecodeKit/Timecode/Source/Timecode Rational.swift b/Sources/TimecodeKit/Timecode/Source/Timecode Rational.swift new file mode 100644 index 00000000..eb007a83 --- /dev/null +++ b/Sources/TimecodeKit/Timecode/Source/Timecode Rational.swift @@ -0,0 +1,138 @@ +// +// Timecode Rational.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + +import Foundation + +// MARK: - TimecodeSource + +extension Fraction: TimecodeSource { + func set(timecode: inout Timecode) throws { + try timecode._setTimecode(exactly: self) + } + + func set(timecode: inout Timecode, by validation: Timecode.ValidationRule) { + switch validation { + case .clamping, .clampingComponents: + timecode._setTimecode(clamping: self) + case .wrapping: + timecode._setTimecode(wrapping: self) + case .allowingInvalid: + timecode._setTimecode(rawValues: self) + } + } +} + +// MARK: - Static Constructors + +extension TimecodeSourceValue { + /// Numerical fraction containing a numerator and a denominator. + public static func rational(_ source: Fraction) -> Self { + .init(value: source) + } + + /// Numerical fraction containing a numerator and a denominator. + public static func rational(_ numerator: Int, _ denominator: Int) -> Self { + .init(value: Fraction(numerator, denominator)) + } +} + +// MARK: - Get + +extension Timecode { + /// Returns the time location as a rational fraction. + /// + /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in + /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate + /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as + /// fractions.) + public var rationalValue: Fraction { + let frFrac = frameRate.frameDuration + let n = frFrac.numerator * frameCount.subFrameCount + let d = frFrac.denominator * subFramesBase.rawValue + + return Fraction(n, d).reduced() + } +} + +// MARK: - Set + +extension Timecode { + /// Sets the timecode from elapsed time expressed as a rational fraction. + /// + /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in + /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate + /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as + /// fractions.) + /// + /// - Note: A negative fraction will throw an error. Use ``TimecodeInterval`` init instead. + /// + /// - Throws: ``ValidationError`` + mutating func _setTimecode(exactly rational: Fraction) throws { + let frameCount = floatingFrameCount(of: rational) + try _setTimecode(exactly: .combined(frames: frameCount)) + } + + /// Sets the timecode from elapsed time expressed as a rational fraction. + /// + /// Clamps to valid timecode. + /// + /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in + /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate + /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as + /// fractions.) + mutating func _setTimecode(clamping rational: Fraction) { + let frameCount = frameCount(of: rational) + _setTimecode(clamping: .frames(frameCount)) + } + + /// Sets the timecode from elapsed time expressed as a rational fraction. + /// + /// Wraps timecode if necessary. + /// + /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in + /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate + /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as + /// fractions.) + mutating func _setTimecode(wrapping rational: Fraction) { + let frameCount = frameCount(of: rational) + _setTimecode(wrapping: .frames(frameCount)) + } + + /// Sets the timecode from elapsed time expressed as a rational fraction. + /// + /// Allows for invalid raw values (in this case, unbounded Days component). + /// + /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in + /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate + /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as + /// fractions.) + mutating func _setTimecode(rawValues rational: Fraction) { + let frameCount = frameCount(of: rational) + _setTimecode(rawValues: .frames(frameCount)) + } + + // MARK: Helper Methods + + /// Internal: + /// Returns frame count of the rational fraction at current frame rate. + /// Truncates subframes if present. + func frameCount(of rational: Fraction) -> Int { + let frFrac = frameRate.frameDuration + let frameCount = (rational.numerator * frFrac.denominator) / + (rational.denominator * frFrac.numerator) + return frameCount + } + + /// Internal: + /// Returns frame count of the rational fraction at current frame rate. + /// Preserves subframes as floating-point potion of a frame. + func floatingFrameCount(of rational: Fraction) -> Double { + let frFrac = frameRate.frameDuration + let frameCount = (Double(rational.numerator) * Double(frFrac.denominator)) / + (Double(rational.denominator) * Double(frFrac.numerator)) + return frameCount + } +} diff --git a/Sources/TimecodeKit/Timecode/Source/Timecode Real Time.swift b/Sources/TimecodeKit/Timecode/Source/Timecode Real Time.swift new file mode 100644 index 00000000..9ad2f007 --- /dev/null +++ b/Sources/TimecodeKit/Timecode/Source/Timecode Real Time.swift @@ -0,0 +1,124 @@ +// +// Timecode Real Time.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + +import Foundation + +// MARK: - TimecodeSource + +extension TimeInterval: TimecodeSource { + func set(timecode: inout Timecode) throws { + try timecode._setTimecode(exactlyRealTime: self) + } + + func set(timecode: inout Timecode, by validation: Timecode.ValidationRule) { + switch validation { + case .clamping, .clampingComponents: + timecode._setTimecode(clampingRealTime: self) + case .wrapping: + timecode._setTimecode(wrappingRealTime: self) + case .allowingInvalid: + timecode._setTimecode(rawValuesRealTime: self) + } + } +} + +// MARK: - Static Constructors + +extension TimecodeSourceValue { + /// Real time (wall time) in seconds. + public static func realTime(seconds: TimeInterval) -> Self { + .init(value: seconds) + } +} + +// MARK: - Get + +extension Timecode { + /// (Lossy) Returns the current timecode converted to a duration in + /// real-time (wall-clock time), based on the frame rate. + public var realTimeValue: TimeInterval { + frameCount.doubleValue * (1.0 / frameRate.frameRateForRealTimeCalculation) + } +} + +// MARK: - Set + +extension Timecode { + /// Sets the timecode to the nearest frame at the current frame rate + /// from real-time (wall-clock time). + /// + /// Throws an error if it underflows or overflows valid timecode range. + /// (Validation is based on the frame rate and `upperLimit` property.) + /// + /// - Throws: ``ValidationError`` + mutating func _setTimecode(exactlyRealTime: TimeInterval) throws { + let convertedComponents = components(realTime: exactlyRealTime) + try _setTimecode(exactly: convertedComponents) + } + + /// Sets the timecode to the nearest frame at the current frame rate + /// from real-time (wall-clock time). + /// + /// Clamps to valid timecode. + mutating func _setTimecode(clampingRealTime: TimeInterval) { + let convertedComponents = components(realTime: clampingRealTime) + _setTimecode(clamping: convertedComponents) + } + + /// Sets the timecode to the nearest frame at the current frame rate + /// from real-time (wall-clock time). + /// + /// Wraps timecode if necessary. + mutating func _setTimecode(wrappingRealTime: TimeInterval) { + let convertedComponents = components(realTime: wrappingRealTime) + _setTimecode(wrapping: convertedComponents) + } + + /// Sets the timecode to the nearest frame at the current frame rate + /// from real-time (wall-clock time). + /// + /// Allows for invalid raw values (in this case, unbounded Days component). + mutating func _setTimecode(rawValuesRealTime: TimeInterval) { + let convertedComponents = components(realTime: rawValuesRealTime) + _setTimecode(rawValues: convertedComponents) + } + + // MARK: Helper Methods + + /// Internal: + /// Converts a real-time value (wall-clock time) to components using the instance's + /// frame rate and subframes base. + func components(realTime: TimeInterval) -> Components { + let elapsedFrames = elapsedFrames(realTime: realTime) + + return Self.components( + of: .init(.combined(frames: elapsedFrames), base: subFramesBase), + at: frameRate + ) + } + + /// Internal: + /// Calculates elapsed frames at current frame rate from real-time (wall-clock time). + func elapsedFrames(realTime: TimeInterval) -> Double { + var calc = realTime / (1.0 / frameRate.frameRateForRealTimeCalculation) + + // over-estimate so real time is just past the equivalent timecode + // since raw time values in practise can be a hair under the actual elapsed real time that would trigger the equivalent timecode + // (due to precision and rounding behaviors that may not be in our control, depending on where the passed real time value + // originated) + + let magicNumber = 0.000_010 // 10 microseconds + + switch calc.sign { + case .plus: + calc += magicNumber + case .minus: + calc -= magicNumber + } + + return calc + } +} diff --git a/Sources/TimecodeKit/Timecode/Source/Timecode Samples.swift b/Sources/TimecodeKit/Timecode/Source/Timecode Samples.swift new file mode 100644 index 00000000..ad7dd654 --- /dev/null +++ b/Sources/TimecodeKit/Timecode/Source/Timecode Samples.swift @@ -0,0 +1,242 @@ +// +// Timecode Samples.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + +// MARK: - Payload + +struct SamplesPayload { + public var samples: Double + public var sampleRate: Int +} + +// MARK: - TimecodeSource + +extension SamplesPayload: TimecodeSource { + func set(timecode: inout Timecode) throws { + try timecode._setTimecode(samples: samples, sampleRate: sampleRate) + } + + func set(timecode: inout Timecode, by validation: Timecode.ValidationRule) { + switch validation { + case .clamping, .clampingComponents: + timecode._setTimecode(clampingSamples: samples, sampleRate: sampleRate) + case .wrapping: + timecode._setTimecode(wrappingSamples: samples, sampleRate: sampleRate) + case .allowingInvalid: + timecode._setTimecode(rawValuesSamples: samples, sampleRate: sampleRate) + } + } +} + +// MARK: - Static Constructors + +extension TimecodeSourceValue { + /// Audio samples at a given sample rate. + public static func samples(_ samples: Int, sampleRate: Int) -> Self { + .init(value: SamplesPayload(samples: Double(samples), sampleRate: sampleRate)) + } + + /// Audio samples at a given sample rate. + public static func samples(_ samples: Double, sampleRate: Int) -> Self { + .init(value: SamplesPayload(samples: samples, sampleRate: sampleRate)) + } +} + +// MARK: - Get + +extension Timecode { + /// (Lossy) + /// Returns the current timecode converted to a duration in audio samples + /// at the given sample rate, rounded to the nearest sample. + /// Sample rate is expressed in Hz. (ie: 48KHz would be passed as 48000) + public func samplesValue(sampleRate: Int) -> Int { + Int(samplesDoubleValue(sampleRate: sampleRate).rounded()) + } + + /// (Lossy) + /// Returns the current timecode converted to a duration in audio samples + /// at the given sample rate, with floating-point sub-sample duration. + /// Sample rate is expressed in Hz. (ie: 48KHz would be passed as 48000) + public func samplesDoubleValue(sampleRate: Int) -> Double { + realTimeValue * Double(sampleRate) + } +} + +// MARK: - Set + +extension Timecode { + // MARK: - Int + + /// (Lossy) + /// Sets the timecode to the nearest elapsed frame at the current frame rate + /// from elapsed audio samples. + /// Throws an error if it underflows or overflows valid timecode range. + /// Sample rate must be expressed as an Integer of Hz (ie: 48KHz would be passed as 48000) + /// + /// - Throws: ``ValidationError`` + mutating func _setTimecode( + samples: Int, + sampleRate: Int + ) throws { + try _setTimecode( + samples: Double(samples), + sampleRate: sampleRate + ) + } + + /// (Lossy) + /// Sets the timecode to the nearest elapsed frame at the current frame rate + /// from elapsed audio samples. + /// Clamps to valid timecode. + /// Sample rate must be expressed as an Integer of Hz (ie: 48KHz would be passed as 48000) + /// + /// - Throws: ``ValidationError`` + mutating func _setTimecode( + clampingSamples: Int, + sampleRate: Int + ) { + _setTimecode( + clampingSamples: Double(clampingSamples), + sampleRate: sampleRate + ) + } + + /// (Lossy) + /// Sets the timecode to the nearest elapsed frame at the current frame rate + /// from elapsed audio samples. + /// Wraps timecode if necessary. + /// Sample rate must be expressed as an Integer of Hz (ie: 48KHz would be passed as 48000) + /// + /// - Throws: ``ValidationError`` + mutating func _setTimecode( + wrappingSamples: Int, + sampleRate: Int + ) { + _setTimecode( + wrappingSamples: Double(wrappingSamples), + sampleRate: sampleRate + ) + } + + /// (Lossy) + /// Sets the timecode to the nearest elapsed frame at the current frame rate + /// from elapsed audio samples. + /// Allows for invalid raw values (in this case, unbounded Days component). + /// Sample rate must be expressed as an Integer of Hz (ie: 48KHz would be passed as 48000) + /// + /// - Throws: ``ValidationError`` + mutating func _setTimecode( + rawValuesSamples: Int, + sampleRate: Int + ) { + _setTimecode( + rawValuesSamples: Double(rawValuesSamples), + sampleRate: sampleRate + ) + } + + // MARK: - Double + + /// (Lossy) + /// Sets the timecode to the nearest elapsed frame at the current frame rate + /// from elapsed audio samples, with floating-point sub-sample duration. + /// Throws an error if it underflows or overflows valid timecode range. + /// Sample rate is expressed in Hz. (ie: 48KHz would be passed as 48000) + /// + /// - Throws: ``ValidationError`` + mutating func _setTimecode( + samples: Double, + sampleRate: Int + ) throws { + let convertedComponents = components( + fromSamples: samples, + sampleRate: sampleRate + ) + try _setTimecode(exactly: convertedComponents) + } + + /// (Lossy) + /// Sets the timecode to the nearest elapsed frame at the current frame rate + /// from elapsed audio samples, with floating-point sub-sample duration. + /// Clamps to valid timecode. + /// Sample rate is expressed in Hz. (ie: 48KHz would be passed as 48000) + /// + /// - Throws: ``ValidationError`` + mutating func _setTimecode( + clampingSamples: Double, + sampleRate: Int + ) { + let convertedComponents = components( + fromSamples: clampingSamples, + sampleRate: sampleRate + ) + _setTimecode(clamping: convertedComponents) + } + + /// (Lossy) + /// Sets the timecode to the nearest elapsed frame at the current frame rate + /// from elapsed audio samples, with floating-point sub-sample duration. + /// Wraps timecode if necessary. + /// Sample rate is expressed in Hz. (ie: 48KHz would be passed as 48000) + /// + /// - Throws: ``ValidationError`` + mutating func _setTimecode( + wrappingSamples: Double, + sampleRate: Int + ) { + let convertedComponents = components( + fromSamples: wrappingSamples, + sampleRate: sampleRate + ) + _setTimecode(wrapping: convertedComponents) + } + + /// (Lossy) + /// Sets the timecode to the nearest elapsed frame at the current frame rate + /// from elapsed audio samples, with floating-point sub-sample duration. + /// Allows for invalid raw values (in this case, unbounded Days component). + /// Sample rate is expressed in Hz. (ie: 48KHz would be passed as 48000) + /// + /// - Throws: ``ValidationError`` + mutating func _setTimecode( + rawValuesSamples: Double, + sampleRate: Int + ) { + let convertedComponents = components( + fromSamples: rawValuesSamples, + sampleRate: sampleRate + ) + _setTimecode(rawValues: convertedComponents) + } + + // MARK: Helper Methods + + func components( + fromSamples: Double, + sampleRate: Int + ) -> Components { + let rtv = fromSamples / Double(sampleRate) + var base = elapsedFrames(realTime: rtv) + + // over-estimate so samples are just past the equivalent timecode + // so calculations of samples back into timecode work reliably + // otherwise, this math produces a samples value that can be a hair under + // the actual elapsed samples that would convert back to equivalent timecode + + let magicNumber = 0.0001 + + if rtv < 0 { + base -= magicNumber + } else { + base += magicNumber + } + + // then derive components + return Self.components( + of: .init(.combined(frames: base), base: subFramesBase), + at: frameRate + ) + } +} diff --git a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode String.swift b/Sources/TimecodeKit/Timecode/Source/Timecode String.swift similarity index 55% rename from Sources/TimecodeKit/Timecode/Data Interchange/Timecode String.swift rename to Sources/TimecodeKit/Timecode/Source/Timecode String.swift index 84fcf264..46d040e5 100644 --- a/Sources/TimecodeKit/Timecode/Data Interchange/Timecode String.swift +++ b/Sources/TimecodeKit/Timecode/Source/Timecode String.swift @@ -1,7 +1,7 @@ // // Timecode String.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // import Foundation @@ -12,121 +12,43 @@ import AppKit import UIKit #endif -// MARK: - Init +// MARK: - FormattedTimecodeSource -extension Timecode { - /// Instance exactly from timecode string and frame rate. - /// - /// An improperly formatted timecode string or one with out-of-bounds values will throw an - /// error. - /// - /// Validation is based on the `upperLimit` and `subFramesBase` properties. - /// - /// - Throws: ``ValidationError`` or ``StringParseError`` - public init( - _ exactlyTimecodeString: String, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) throws { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - try setTimecode(exactly: exactlyTimecodeString) - } - - /// Instance from timecode string and frame rate, clamping to valid timecode if necessary. - /// - /// Clamping is based on the `upperLimit` and `subFramesBase` properties. - /// - /// - Throws: ``StringParseError`` - public init( - clamping timecodeString: String, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) throws { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - try setTimecode(clamping: timecodeString) +extension String: FormattedTimecodeSource { + func set(timecode: inout Timecode) throws { + try timecode._setTimecode(exactly: self) } - /// Instance from timecode string and frame rate, clamping values if necessary. - /// - /// Individual components which are out-of-bounds will be clamped to minimum or maximum possible - /// values. - /// - /// Clamping is based on the `upperLimit` and `subFramesBase` properties. - /// - /// - Throws: ``StringParseError`` - public init( - clampingEach timecodeString: String, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) throws { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - try setTimecode(clampingEach: timecodeString) + func set(timecode: inout Timecode, by validation: Timecode.ValidationRule) throws { + switch validation { + case .clamping: + try timecode._setTimecode(clamping: self) + case .clampingComponents: + try timecode._setTimecode(clampingComponents: self) + case .wrapping: + try timecode._setTimecode(wrapping: self) + case .allowingInvalid: + try timecode._setTimecode(rawValues: self) + } } - - /// Instance from timecode string and frame rate, wrapping timecode if necessary. - /// - /// An improperly formatted timecode string or one with invalid values will return `nil`. - /// - /// Wrapping is based on the `upperLimit` and `subFramesBase` properties. - /// - /// - Throws: ``StringParseError`` - public init( - wrapping timecodeString: String, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) throws { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - try setTimecode(wrapping: timecodeString) +} + +// MARK: - Static Constructors + +extension FormattedTimecodeSourceValue { + /// Timecode string. + public static func string(_ timecodeString: String) -> Self { + .init(value: timecodeString) } - /// Instance from raw timecode values formatted as a timecode string and frame rate. - /// - /// Timecode values will not be validated or rejected if they overflow. - /// - /// This is useful, for example, when intending on running timecode validation methods against timecode values that are unknown to be valid or not at the time of initializing. - /// - /// - Throws: ``StringParseError`` - public init( - rawValues timecodeString: String, - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, - base: SubFramesBase = .default(), - format: StringFormat = .default() - ) throws { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format - - try setTimecode(rawValues: timecodeString) + /// Timecode string. + @_disfavoredOverload + public static func string(_ timecodeString: S) -> Self { + .init(value: String(timecodeString)) } } -// MARK: - Get and Set +// MARK: - Get extension Timecode { // MARK: stringValue @@ -145,7 +67,9 @@ extension Timecode { /// "0:00:00:00:00" "0:00:00:00;00" /// /// (Validation is based on the frame rate and `upperLimit` property.) - public var stringValue: String { + public func stringValue( + format: StringFormat = .default() + ) -> String { let sepDays = " " let sepMain = ":" let sepFrames = frameRate.isDrop ? ";" : ":" @@ -159,32 +83,33 @@ extension Timecode { output += "\(String(format: "%02d", seconds))\(sepFrames)" output += "\(String(format: "%0\(frameRate.numberOfDigits)d", frames))" - if stringFormat.showSubFrames { + if format.showSubFrames { let numberOfSubFramesDigits = validRange(of: .subFrames).upperBound.numberOfDigits output += "\(sepSubFrames)\(String(format: "%0\(numberOfSubFramesDigits)d", subFrames))" } - return output - } - - /// Forms `.stringValue` using filename-compatible characters. - public var stringValueFileNameCompatible: String { - stringValue - .replacingOccurrences(of: ":", with: "-") - .replacingOccurrences(of: ";", with: "-") - .replacingOccurrences(of: " ", with: "-") + if format.contains(.filenameCompatible) { + return output + .replacingOccurrences(of: ":", with: "-") + .replacingOccurrences(of: ";", with: "-") + .replacingOccurrences(of: " ", with: "-") + } else { + return output + } } // MARK: stringValueValidated - /// Returns `stringValue` as `NSAttributedString`, highlighting invalid values. + /// Returns ``stringValue(format:)`` as `NSAttributedString`, highlighting + /// invalid values. /// /// `invalidAttributes` are the `NSAttributedString` attributes applied to invalid values. /// If `invalidAttributes` are not passed, the default of red foreground color is used. public func stringValueValidated( + format: StringFormat = .default(), invalidAttributes: [NSAttributedString.Key: Any]? = nil, - withDefaultAttributes attrs: [NSAttributedString.Key: Any]? = nil + defaultAttributes attrs: [NSAttributedString.Key: Any]? = nil ) -> NSAttributedString { let sepDays = NSAttributedString(string: " ", attributes: attrs) let sepMain = NSAttributedString(string: ":", attributes: attrs) @@ -194,7 +119,7 @@ extension Timecode { #if os(macOS) let invalidColor = invalidAttributes ?? [.foregroundColor: NSColor.red] - #elseif os(iOS) || os(tvOS) || os(watchOS) + #elseif os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) let invalidColor = invalidAttributes ?? [.foregroundColor: UIColor.red] #else @@ -295,7 +220,7 @@ extension Timecode { // subframes - if stringFormat.showSubFrames { + if format.showSubFrames { let numberOfSubFramesDigits = validRange(of: .subFrames).upperBound.numberOfDigits output.append(sepSubFrames) @@ -322,92 +247,94 @@ extension Timecode { } } -// MARK: Setters +// MARK: - Set extension Timecode { - /// Set timecode from a timecode string. Values which are out-of-bounds will also cause the setter to fail, and return false. An error is thrown if the string is malformed and cannot be reasonably parsed. + /// Set timecode from a timecode string. Values which are out-of-bounds will also cause the setter to fail, and return false. An error + /// is thrown if the string is malformed and cannot be reasonably parsed. /// /// Validation is based on the `upperLimit` and `subFramesBase` properties. /// /// - Throws: ``StringParseError`` or ``ValidationError`` - public mutating func setTimecode(exactly string: String) throws { + mutating func _setTimecode(exactly string: String) throws { let decoded = try Timecode.decode(timecode: string) - - try setTimecode(exactly: decoded) + try _setTimecode(exactly: decoded) } - /// Set timecode from a timecode string, clamping to valid timecode if necessary. An error is thrown if the string is malformed and cannot be reasonably parsed. + /// Set timecode from a timecode string, clamping to valid timecode if necessary. An error is thrown if the string is malformed and + /// cannot be reasonably parsed. /// /// Clamping is based on the `upperLimit` and `subFramesBase` properties. /// /// - Throws: ``StringParseError`` - public mutating func setTimecode(clamping string: String) throws { + mutating func _setTimecode(clamping string: String) throws { let tcVals = try Timecode.decode(timecode: string) - - setTimecode(clamping: tcVals) + _setTimecode(clamping: tcVals) } - /// Set timecode from a timecode string, clamping individual values if necessary. Individual values which are out-of-bounds will be clamped to minimum or maximum possible values. An error is thrown if the string is malformed and cannot be reasonably parsed. + /// Set timecode from a timecode string, clamping individual values if necessary. Individual values which are out-of-bounds will be + /// clamped to minimum or maximum possible values. An error is thrown if the string is malformed and cannot be reasonably parsed. /// /// Returns true/false depending on whether the string is formatted correctly or not. /// /// Clamping is based on the `upperLimit` and `subFramesBase` properties. /// /// - Throws: ``StringParseError`` - public mutating func setTimecode(clampingEach string: String) throws { + mutating func _setTimecode(clampingComponents string: String) throws { let tcVals = try Timecode.decode(timecode: string) - - setTimecode(clampingEach: tcVals) + _setTimecode(clampingComponents: tcVals) } - /// Set timecode from a string. Values which are out-of-bounds will be clamped to minimum or maximum possible values. An error is thrown if the string is malformed and cannot be reasonably parsed. + /// Set timecode from a string. Values which are out-of-bounds will be clamped to minimum or maximum possible values. An error is thrown + /// if the string is malformed and cannot be reasonably parsed. /// /// Clamping is based on the `upperLimit` and `subFramesBase` properties. /// /// - Throws: ``StringParseError`` - public mutating func setTimecode(wrapping string: String) throws { + mutating func _setTimecode(wrapping string: String) throws { let tcVals = try Timecode.decode(timecode: string) - - setTimecode(wrapping: tcVals) + _setTimecode(wrapping: tcVals) } - /// Set timecode from a string, treating components as raw values. Timecode values will not be validated or rejected if they overflow. An error is thrown if the string is malformed and cannot be reasonably parsed. + /// Set timecode from a string, treating components as raw values. Timecode values will not be validated or rejected if they overflow. + /// An error is thrown if the string is malformed and cannot be reasonably parsed. /// - /// This is useful, for example, when intending on running timecode validation methods against timecode values that are unknown to be valid or not at the time of initializing. + /// This is useful, for example, when intending on running timecode validation methods against timecode values that are unknown to be + /// valid or not at the time of initializing. /// /// - Throws: ``StringParseError`` - public mutating func setTimecode(rawValues string: String) throws { + mutating func _setTimecode(rawValues string: String) throws { let tcVals = try Timecode.decode(timecode: string) - - setTimecode(rawValues: tcVals) + _setTimecode(rawValues: tcVals) } } extension Timecode { - /// Decodes a Timecode string into its component values, without validating. + /// Utility to decode a Timecode string into its component values, without validating component values. /// - /// An error is thrown if the string is malformed and cannot be reasonably parsed. Raw values themselves will be passed as-is and not validated based on a frame rate or upper limit. + /// An error is thrown if the string is malformed and cannot be reasonably parsed. Raw values themselves will be passed as-is and not + /// validated based on a frame rate or upper limit. /// /// Valid formats for 24-hour: /// - /// "00:00:00:00" "00:00:00;00" - /// "00:00:00:00.00" "00:00:00;00.00" - /// "00;00;00;00" "00;00;00;00" - /// "00;00;00;00.00" "00;00;00;00.00" + /// "00:00:00:00" "00:00:00;00" + /// "00:00:00:00.00" "00:00:00;00.00" + /// "00;00;00;00" "00;00;00;00" + /// "00;00;00;00.00" "00;00;00;00.00" /// /// Valid formats for 100-day: All of the above, as well as: /// - /// "0 00:00:00:00" "0 00:00:00;00" - /// "0:00:00:00:00" "0:00:00:00;00" - /// "0 00:00:00:00.00" "0 00:00:00;00.00" - /// "0:00:00:00:00.00" "0:00:00:00;00.00" - /// "0 00;00;00;00" "0 00;00;00;00" - /// "0;00;00;00;00" "0;00;00;00;00" - /// "0 00;00;00;00.00" "0 00;00;00;00.00" - /// "0;00;00;00;00.00" "0;00;00;00;00.00" + /// "0 00:00:00:00" "0 00:00:00;00" + /// "0:00:00:00:00" "0:00:00:00;00" + /// "0 00:00:00:00.00" "0 00:00:00;00.00" + /// "0:00:00:00:00.00" "0:00:00:00;00.00" + /// "0 00;00;00;00" "0 00;00;00;00" + /// "0;00;00;00;00" "0;00;00;00;00" + /// "0 00;00;00;00.00" "0 00;00;00;00.00" + /// "0;00;00;00;00.00" "0;00;00;00;00.00" /// /// - Throws: ``StringParseError`` - public static func decode(timecode string: String) throws -> Components { + static func decode(timecode string: S) throws -> Components { let pattern = #"^(\d+)??[\:;\s]??(\d+)[\:;](\d+)[\:;](\d+)[\:\;](\d+)[\.]??(\d+)??$"# let matches = string @@ -439,45 +366,3 @@ extension Timecode { ) } } - -// MARK: - .toTimecode - -extension String { - /// Returns an instance of `Timecode(exactly:)`. - /// If the string is not a valid timecode string, it returns nil. - /// - /// - Throws: ``ValidationError`` or ``StringParseError`` - public func toTimecode( - at rate: TimecodeFrameRate, - limit: Timecode.UpperLimit = ._24hours, - base: Timecode.SubFramesBase = .default(), - format: Timecode.StringFormat = .default() - ) throws -> Timecode { - try Timecode( - self, - at: rate, - limit: limit, - base: base, - format: format - ) - } - - /// Returns an instance of `Timecode(rawValues:)`. - /// If the string is not a valid timecode string, it returns nil. - /// - /// - Throws: ``StringParseError`` - public func toTimecode( - rawValuesAt rate: TimecodeFrameRate, - limit: Timecode.UpperLimit = ._24hours, - base: Timecode.SubFramesBase = .default(), - format: Timecode.StringFormat = .default() - ) throws -> Timecode { - try Timecode( - rawValues: self, - at: rate, - limit: limit, - base: base, - format: format - ) - } -} diff --git a/Sources/TimecodeKit/Timecode/Source/Timecode TimecodeInterval.swift b/Sources/TimecodeKit/Timecode/Source/Timecode TimecodeInterval.swift new file mode 100644 index 00000000..1985de84 --- /dev/null +++ b/Sources/TimecodeKit/Timecode/Source/Timecode TimecodeInterval.swift @@ -0,0 +1,24 @@ +// +// Timecode TimecodeInterval.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + +// MARK: - TimecodeSource + +extension TimecodeInterval: GuaranteedRichTimecodeSource { + func set(timecode: inout Timecode) -> Timecode.Properties { + timecode.set(self) + } +} + +// MARK: - Static Constructors + +extension GuaranteedRichTimecodeSourceValue { + /// Instance by flattening a ``TimecodeInterval``, wrapping as necessary based on the + /// ``Timecode/Properties-swift.struct/upperLimit`` and + /// ``Timecode/Properties-swift.struct/frameRate`` of the interval. + public static func interval(flattening interval: TimecodeInterval) -> Self { + .init(value: interval) + } +} diff --git a/Sources/TimecodeKit/Timecode/Source/Timecode Zero.swift b/Sources/TimecodeKit/Timecode/Source/Timecode Zero.swift new file mode 100644 index 00000000..6a334359 --- /dev/null +++ b/Sources/TimecodeKit/Timecode/Source/Timecode Zero.swift @@ -0,0 +1,29 @@ +// +// Timecode Zero.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + +extension Timecode { + /// Zero timecode. + /// Do not initialize directly; instead, pass `.zero` into a ``Timecode`` initializer. + struct Zero { + public init() { } + } +} + +// MARK: - TimecodeSource + +extension Timecode.Zero: GuaranteedTimecodeSource { + func set(timecode: inout Timecode) { + timecode.set(Timecode.Components.zero, by: .allowingInvalid) + } +} + +// MARK: - Static Constructors + +extension GuaranteedTimecodeSourceValue { + /// Zero timecode (00:00:00:00). + /// This is guaranteed at all frame rates and requires no validation or error handling. + public static let zero: Self = .init(value: Timecode.Zero()) +} diff --git a/Sources/TimecodeKit/Timecode/StringFormat/StringFormat.swift b/Sources/TimecodeKit/Timecode/StringFormat/StringFormat.swift index 406c3dc4..84d7b26b 100644 --- a/Sources/TimecodeKit/Timecode/StringFormat/StringFormat.swift +++ b/Sources/TimecodeKit/Timecode/StringFormat/StringFormat.swift @@ -1,12 +1,12 @@ // // StringFormat.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // extension Timecode { /// `Timecode` string output format configuration. - public typealias StringFormat = Set + public typealias StringFormat = Set } extension Timecode.StringFormat { @@ -14,9 +14,13 @@ extension Timecode.StringFormat { public static func `default`() -> Self { [] } + + /// Initialize with Show Subframes option enabled. + public static let showSubFrames: Self = [.showSubFrames] } extension Timecode.StringFormat { + /// Get or set ``Timecode/StringFormatOption/showSubFrames`` state. public var showSubFrames: Bool { get { contains(.showSubFrames) @@ -26,21 +30,36 @@ extension Timecode.StringFormat { else { remove(.showSubFrames) } } } + + /// Get or set ``Timecode/StringFormatOption/filenameCompatible`` state. + public var filenameCompatible: Bool { + get { + contains(.filenameCompatible) + } + set { + if newValue { insert(.filenameCompatible) } + else { remove(.filenameCompatible) } + } + } } extension Timecode { - /// `Timecode` string output format configuration parameter. - public enum StringFormatParameter: Equatable, Hashable, CaseIterable { + /// `Timecode` string output format option. + public enum StringFormatOption: Equatable, Hashable, CaseIterable { /// Determines whether subframes are included. /// - /// This does not disable subframes from being stored or calculated, only whether they are output in the string. + /// This does not disable subframes from being stored or calculated, only whether it is present in the string. case showSubFrames + + /// Substitutes illegal characters for filename-compatible characters. + case filenameCompatible } } -extension Timecode.StringFormatParameter: Codable { +extension Timecode.StringFormatOption: Codable { private enum CodingKeys: String, CodingKey { case showSubFrames + case filenameCompatible } public init(from decoder: Decoder) throws { @@ -59,6 +78,8 @@ extension Timecode.StringFormatParameter: Codable { switch keyFromString { case .showSubFrames: self = .showSubFrames + case .filenameCompatible: + self = .filenameCompatible } } @@ -68,6 +89,8 @@ extension Timecode.StringFormatParameter: Codable { switch self { case .showSubFrames: try container.encode(CodingKeys.showSubFrames.rawValue) + case .filenameCompatible: + try container.encode(CodingKeys.filenameCompatible.rawValue) } } } diff --git a/Sources/TimecodeKit/Timecode/SubFramesBase/SubFramesBase.swift b/Sources/TimecodeKit/Timecode/SubFramesBase/SubFramesBase.swift index 0fa4be6e..d0d6b540 100644 --- a/Sources/TimecodeKit/Timecode/SubFramesBase/SubFramesBase.swift +++ b/Sources/TimecodeKit/Timecode/SubFramesBase/SubFramesBase.swift @@ -1,35 +1,47 @@ // // SubFramesBase.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // import Foundation extension Timecode { - /// Represents the base (denominator) a partial division of a frame. + /// Represents the base (denominator) used when dealing with subframes. + /// This defines how many equal parts a single frame may be divided up into. /// /// Some implementations refer to these as SMPTE frame "bits". /// - /// There are no set industry standards regarding subframe divisors. - /// - Cubase/Nuendo, Logic Pro/Final Cut Pro use 80 subframes per frame (0 ... 79); - /// - Pro Tools uses 100 subframes (0 ... 99). + /// Industry standards vary regarding subframe divisors depending on manufacturers and formats, + /// and not all manufacturers support the usage of subframes. + /// See documentation for individual cases for more details. + /// + /// Subframes base may be defined independently of frame rate, insomuch as it simply + /// defines the number of equal subdivisions of a frame at the current frame rate. public enum SubFramesBase: Int, CaseIterable { - case _80SubFrames = 80 - case _100SubFrames = 100 + /// 80 subframes per frame (0 ... 79). + /// DAWs such as Cubase, Nuendo, Logic Pro, and Final Cut Pro use this standard. + case max80SubFrames = 80 + + /// 100 subframes per frame (0 ... 99). + /// DAWs such as Pro Tools use this standard. + case max100SubFrames = 100 + + /// 4 subframes per frame (0 ... 3). + /// Typically used in a MIDI Timecode (MTC) context. case quarterFrames = 4 } } extension Timecode.SubFramesBase { public static func `default`() -> Self { - ._100SubFrames + .max100SubFrames } } extension Timecode.SubFramesBase: CustomStringConvertible { public var description: String { - return "\(rawValue)" + "\(rawValue)" } } @@ -47,7 +59,7 @@ extension Timecode.SubFramesBase { /// Converts a given number of subframes at this subframes base to a different subframes base. public func convert(subFrames: Int, to other: Self) -> Int { // early return if we don't need to scale subframes - guard self != other && subFrames != 0 else { return subFrames } + guard self != other, subFrames != 0 else { return subFrames } let calc = (Double(subFrames) / Double(rawValue)) * Double(other.rawValue) return Int(calc) diff --git a/Sources/TimecodeKit/Timecode/Timecode Conversion.swift b/Sources/TimecodeKit/Timecode/Timecode Conversion.swift index 47f71f42..c48e795e 100644 --- a/Sources/TimecodeKit/Timecode/Timecode Conversion.swift +++ b/Sources/TimecodeKit/Timecode/Timecode Conversion.swift @@ -1,7 +1,7 @@ // // Timecode Conversion.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // extension Timecode { @@ -11,10 +11,13 @@ extension Timecode { /// /// - If `preservingValues` is `false` (default): entire timecode is converted based on the equivalent real time value. /// - /// - If `preservingValues` is `true`: Return a new `Timecode` object at the new frame rate preserving literal timecode values if possible. + /// - If `preservingValues` is `true`: Return a new `Timecode` instance at the new frame rate preserving literal timecode values if + /// possible. /// If any value is not expressible at the new frame rate, the entire timecode will be converted. /// /// - Note: this process may be lossy. + /// + /// - Throws: ``ValidationError`` public func converted( to newFrameRate: TimecodeFrameRate, preservingValues: Bool = false @@ -26,11 +29,10 @@ extension Timecode { if preservingValues, let newTC = try? Timecode( - components, + .components(components), at: newFrameRate, - limit: upperLimit, base: subFramesBase, - format: stringFormat + limit: upperLimit ) { return newTC @@ -39,11 +41,10 @@ extension Timecode { // convert to new frame rate, retaining all ancillary property values return try Timecode( - realTime: realTimeValue, + .realTime(seconds: realTimeValue), at: newFrameRate, - limit: upperLimit, base: subFramesBase, - format: stringFormat + limit: upperLimit ) } diff --git a/Sources/TimecodeKit/Timecode/Timecode Properties.swift b/Sources/TimecodeKit/Timecode/Timecode Properties.swift new file mode 100644 index 00000000..14ab96c8 --- /dev/null +++ b/Sources/TimecodeKit/Timecode/Timecode Properties.swift @@ -0,0 +1,77 @@ +// +// Timecode Properties.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + +import Foundation + +extension Timecode { + /// ``Timecode`` properties. + /// + /// ## Individual Timecode Components + /// + /// Timecode components can be get or set directly as instance properties. + /// + /// ```swift + /// let tc = try "01:12:20:05".timecode(at: .fps23_976) + /// + /// // get + /// tc.days // == 0 + /// tc.hours // == 1 + /// tc.minutes // == 12 + /// tc.seconds // == 20 + /// tc.frames // == 5 + /// tc.subFrames // == 0 + /// + /// // set + /// tc.hours = 5 + /// tc.stringValue() // == "05:12:20:05" + /// ``` + /// + /// ## Components Struct + /// + /// A compact components struct can be used to initialize ``Timecode`` and can also be accessed using ``Timecode/components-swift.property``. + /// + /// See ``Components-swift.struct``. + public struct Properties: Equatable, Hashable { + /// Frame rate. + /// + /// - Note: Several properties are available on the frame rate that is selected, including its + /// ``Timecode/stringValue(format:)`` representation or whether the rate ``TimecodeFrameRate/isDrop``. + /// + /// Setting this value directly does not trigger any validation. + public var frameRate: TimecodeFrameRate + + /// Subframes base (divisor). + /// + /// The number of subframes that make up a single frame. + /// + /// (ie: a divisor of 80 subframes per frame implies a visible value range of 00...79) + /// + /// This will vary depending on application. Most common divisors are 80 or 100. + /// + /// - Note: Setting this value directly does not trigger any validation. + public var subFramesBase: SubFramesBase + + /// Timecode maximum upper bound. + /// + /// This also affects how timecode values wrap when adding or clamping. + /// + /// - Note: Setting this value directly does not trigger any validation. + public var upperLimit: UpperLimit + + /// ``Timecode`` properties. + public init( + rate: TimecodeFrameRate, + base: SubFramesBase = .default(), + limit: UpperLimit = .max24Hours + ) { + frameRate = rate + subFramesBase = base + upperLimit = limit + } + } +} + +extension Timecode.Properties: Codable { } diff --git a/Sources/TimecodeKit/Timecode/Timecode Validation.swift b/Sources/TimecodeKit/Timecode/Timecode Validation.swift index e339f93c..ef71f56b 100644 --- a/Sources/TimecodeKit/Timecode/Timecode Validation.swift +++ b/Sources/TimecodeKit/Timecode/Timecode Validation.swift @@ -1,11 +1,27 @@ // // Timecode Validation.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // import Foundation +extension Timecode { + public enum ValidationRule: Equatable, Hashable, CaseIterable { + /// Clamp timecode to valid timecode range if necessary. + case clamping + + /// Clamp individual components if necessary. + case clampingComponents + + /// Wrap over or under the valid timecode range if necessary. + case wrapping + + /// Raw values are preserved without any validation. + case allowingInvalid + } +} + extension Timecode { /// Returns a set of invalid components, if any. /// A fully valid timecode will return an empty set. @@ -13,9 +29,7 @@ extension Timecode { public var invalidComponents: Set { Self.invalidComponents( in: components, - at: frameRate, - limit: upperLimit, - base: subFramesBase + using: properties ) } } @@ -25,14 +39,21 @@ extension Timecode.Components { /// A fully valid timecode will return an empty set. public func invalidComponents( at frameRate: TimecodeFrameRate, - limit: Timecode.UpperLimit, - base: Timecode.SubFramesBase + base: Timecode.SubFramesBase = .default(), + limit: Timecode.UpperLimit = .max24Hours + ) -> Set { + let properties = Timecode.Properties(rate: frameRate, base: base, limit: limit) + return invalidComponents(using: properties) + } + + /// Returns a set of invalid components, if any. + /// A fully valid timecode will return an empty set. + public func invalidComponents( + using properties: Timecode.Properties ) -> Set { Timecode.invalidComponents( in: self, - at: frameRate, - limit: limit, - base: base + using: properties ) } } @@ -41,77 +62,45 @@ extension Timecode { /// Returns a set of invalid components, if any. /// A fully valid timecode will return an empty set. public static func invalidComponents( - in components: TCC, + in components: Components, at frameRate: TimecodeFrameRate, - limit: UpperLimit, - base: SubFramesBase + base: Timecode.SubFramesBase = .default(), + limit: Timecode.UpperLimit = .max24Hours + ) -> Set { + let properties = Properties(rate: frameRate, base: base, limit: limit) + return invalidComponents(in: components, using: properties) + } + + /// Returns a set of invalid components, if any. + /// A fully valid timecode will return an empty set. + public static func invalidComponents( + in components: Components, + using properties: Timecode.Properties ) -> Set { var invalids: Set = [] - // days - - if !components.validRange( - of: .days, - at: frameRate, - limit: limit, - base: base - ) - .contains(components.d) + if !components.validRange(of: .days, using: properties) + .contains(components.days) { invalids.insert(.days) } - // hours - - if !components.validRange( - of: .hours, - at: frameRate, - limit: limit, - base: base - ) - .contains(components.h) + if !components.validRange(of: .hours, using: properties) + .contains(components.hours) { invalids.insert(.hours) } - // minutes - - if !components.validRange( - of: .minutes, - at: frameRate, - limit: limit, - base: base - ) - .contains(components.m) + if !components.validRange(of: .minutes, using: properties) + .contains(components.minutes) { invalids.insert(.minutes) } - // seconds - - if !components.validRange( - of: .seconds, - at: frameRate, - limit: limit, - base: base - ) - .contains(components.s) + if !components.validRange(of: .seconds, using: properties) + .contains(components.seconds) { invalids.insert(.seconds) } - // frames - - if !components.validRange( - of: .frames, - at: frameRate, - limit: limit, - base: base - ) - .contains(components.f) + if !components.validRange(of: .frames, using: properties) + .contains(components.frames) { invalids.insert(.frames) } - // subframes - - if !components.validRange( - of: .subFrames, - at: frameRate, - limit: limit, - base: base - ) - .contains(components.sf) + if !components.validRange(of: .subFrames, using: properties) + .contains(components.subFrames) { invalids.insert(.subFrames) } return invalids @@ -120,13 +109,8 @@ extension Timecode { extension Timecode { /// Returns valid range of values for a timecode component, given the current `frameRate` and `upperLimit`. - public func validRange(of component: Component) -> (ClosedRange) { - components.validRange( - of: component, - at: frameRate, - limit: upperLimit, - base: subFramesBase - ) + public func validRange(of component: Component) -> ClosedRange { + components.validRange(of: component, using: properties) } } @@ -134,13 +118,22 @@ extension Timecode.Components { /// Returns valid range of values for a timecode component. public func validRange( of component: Timecode.Component, - at rate: TimecodeFrameRate, - limit: Timecode.UpperLimit, - base: Timecode.SubFramesBase - ) -> (ClosedRange) { + at frameRate: TimecodeFrameRate, + base: Timecode.SubFramesBase = .default(), + limit: Timecode.UpperLimit = .max24Hours + ) -> ClosedRange { + let properties = Timecode.Properties(rate: frameRate, base: base, limit: limit) + return validRange(of: component, using: properties) + } + + /// Returns valid range of values for a timecode component. + public func validRange( + of component: Timecode.Component, + using properties: Timecode.Properties + ) -> ClosedRange { switch component { case .days: - return 0 ... limit.maxDaysExpressible + return 0 ... properties.upperLimit.maxDaysExpressible case .hours: return 0 ... 23 @@ -152,21 +145,21 @@ extension Timecode.Components { return 0 ... 59 case .frames: - let startFramePossible = rate.isDrop - ? ((m % 10 != 0 && s == 0) ? 2 : 0) + let startFramePossible = properties.frameRate.isDrop + ? ((minutes % 10 != 0 && seconds == 0) ? 2 : 0) : 0 - return startFramePossible ... rate.maxFrameNumberDisplayable + return startFramePossible ... properties.frameRate.maxFrameNumberDisplayable case .subFrames: // clamp divisor to prevent a possible crash if subFramesBase < 0 - return 0 ... (base.rawValue.clamped(to: 1...) - 1) + return 0 ... (properties.subFramesBase.rawValue.clamped(to: 1...) - 1) } } } extension Timecode { - internal mutating func __clamp(component: Component) { + mutating func _clamp(component: Component) { switch component { case .days: days = days.clamped(to: validRange(of: .days)) @@ -190,14 +183,18 @@ extension Timecode { } extension Timecode { - /// Validates and clamps all timecode components to valid values at the current `frameRate` and `upperLimit` bound. + /// Validates and clamps all timecode components to valid values at the current `frameRate` and + /// `upperLimit` bound. + /// + /// This is not necessary to be run manually if the instance was initialized using the ``ValidationRule/clamping`` or + /// ``ValidationRule/clampingComponents`` validation rule. public mutating func clampComponents() { - __clamp(component: .days) - __clamp(component: .hours) - __clamp(component: .minutes) - __clamp(component: .seconds) - __clamp(component: .frames) - __clamp(component: .subFrames) + _clamp(component: .days) + _clamp(component: .hours) + _clamp(component: .minutes) + _clamp(component: .seconds) + _clamp(component: .frames) + _clamp(component: .subFrames) } } @@ -208,13 +205,8 @@ extension Timecode { .upperBound } - /// Returns the `upperLimit` minus 1 subframe expressed as frames where the integer portion is whole frames and the fractional portion is the subframes unit interval. - public var maxFrameCountExpressibleDouble: Double { - Double(frameRate.maxTotalFramesExpressible(in: upperLimit)) - + (Double(maxSubFramesExpressible) / Double(subFramesBase.rawValue)) - } - - /// Returns the `upperLimit` minus 1 subframe expressed as frames where the integer portion is whole frames and the fractional portion is the subframes unit interval. + /// Returns the `upperLimit` minus 1 subframe expressed as frames where the integer portion is + /// whole frames and the fractional portion is the subframes unit interval. public var maxFrameCountExpressible: FrameCount { FrameCount( .split( diff --git a/Sources/TimecodeKit/Timecode/Timecode init.swift b/Sources/TimecodeKit/Timecode/Timecode init.swift index 3f796111..dc597dde 100644 --- a/Sources/TimecodeKit/Timecode/Timecode init.swift +++ b/Sources/TimecodeKit/Timecode/Timecode init.swift @@ -1,37 +1,293 @@ // // Timecode init.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // import Foundation extension Timecode { - // MARK: - Basic + // MARK: - TimecodeSource - /// Instance with default timecode (00:00:00:00) at a given frame rate. + /// Initialize by converting a time source to timecode at a given frame rate. + /// + /// - Throws: ``ValidationError`` public init( - at rate: TimecodeFrameRate, - limit: UpperLimit = ._24hours, + _ source: TimecodeSourceValue, + at frameRate: TimecodeFrameRate, base: SubFramesBase = .default(), - format: StringFormat = .default() + limit: UpperLimit = .max24Hours + ) throws { + properties = Properties(rate: frameRate, base: base, limit: limit) + try set(source.value) + } + + /// Initialize by converting a time source to timecode at a given frame rate and validation rule. + public init( + _ source: TimecodeSourceValue, + at frameRate: TimecodeFrameRate, + base: SubFramesBase = .default(), + limit: UpperLimit = .max24Hours, + by validation: ValidationRule ) { - frameRate = rate - upperLimit = limit - subFramesBase = base - stringFormat = format + properties = Properties(rate: frameRate, base: base, limit: limit) + set(source.value, by: validation) } - // MARK: - TimecodeInterval + /// Initialize by converting a time source to timecode using the given properties. + /// + /// - Throws: ``ValidationError`` + public init( + _ source: TimecodeSourceValue, + using properties: Properties + ) throws { + self.properties = properties + try set(source.value) + } - /// Instance by flattening a `TimecodeInterval`, wrapping as necessary based on the ``upperLimit-swift.property`` and ``frameRate-swift.property`` of the interval. + /// Initialize by converting a time source to timecode using the given properties. public init( - flattening interval: TimecodeInterval + _ source: TimecodeSourceValue, + using properties: Properties, + by validation: ValidationRule ) { - self = interval.flattened() + self.properties = properties + set(source.value, by: validation) } - // ------------------------------------------------------ - // For additional inits, see the Data Interchange folder. - // ------------------------------------------------------ + // MARK: - FormattedTimecodeSource + + /// Initialize by converting a time source to timecode at a given frame rate. + /// + /// - Throws: ``ValidationError`` + public init( + _ source: FormattedTimecodeSourceValue, + at frameRate: TimecodeFrameRate, + base: SubFramesBase = .default(), + limit: UpperLimit = .max24Hours + ) throws { + properties = Properties(rate: frameRate, base: base, limit: limit) + try set(source.value) + } + + /// Initialize by converting a time source to timecode at a given frame rate and validation rule. + /// + /// - Throws: ``ValidationError`` + public init( + _ source: FormattedTimecodeSourceValue, + at frameRate: TimecodeFrameRate, + base: SubFramesBase = .default(), + limit: UpperLimit = .max24Hours, + by validation: ValidationRule + ) throws { + properties = Properties(rate: frameRate, base: base, limit: limit) + try set(source.value, by: validation) + } + + /// Initialize by converting a time source to timecode using the given properties. + /// + /// - Throws: ``ValidationError`` + public init( + _ source: FormattedTimecodeSourceValue, + using properties: Properties + ) throws { + self.properties = properties + try set(source.value) + } + + /// Initialize by converting a time source to timecode using the given properties. + /// + /// - Throws: ``ValidationError`` + public init( + _ source: FormattedTimecodeSourceValue, + using properties: Properties, + by validation: ValidationRule + ) throws { + self.properties = properties + try set(source.value, by: validation) + } + + // MARK: - RichTimecodeSource + + /// Initialize by converting a rich time source to timecode. + /// + /// - Throws: ``ValidationError`` + public init( + _ source: RichTimecodeSourceValue + ) throws { + properties = Properties(rate: .fps24) // must init to a default first + try set(source.value) + } + + // MARK: - GuaranteedTimecodeSource + + /// Initialize by converting a time source to timecode at a given frame rate. + public init( + _ source: GuaranteedTimecodeSourceValue, + at frameRate: TimecodeFrameRate, + base: SubFramesBase = .default(), + limit: UpperLimit = .max24Hours + ) { + properties = Properties(rate: frameRate, base: base, limit: limit) + set(source.value) + } + + /// Initialize by converting a time source to timecode using the given properties. + public init( + _ source: GuaranteedTimecodeSourceValue, + using properties: Properties + ) { + self.properties = properties + set(source.value) + } + + // MARK: - GuaranteedRichTimecodeSource + + /// Initialize by converting a rich time source to timecode. + public init( + _ source: GuaranteedRichTimecodeSourceValue + ) { + properties = Properties(rate: .fps24) // needs to be initialized with something first + properties = set(source.value) + } +} + +// MARK: - TimecodeSource Category Methods + +extension TimecodeSource { + /// Returns a new ``Timecode`` instance by converting a time source at the given frame rate. + /// + /// - Throws: ``Timecode/ValidationError`` + public func timecode( + at frameRate: TimecodeFrameRate, + base: Timecode.SubFramesBase = .default(), + limit: Timecode.UpperLimit = .max24Hours + ) throws -> Timecode { + let value = TimecodeSourceValue(value: self) + return try Timecode(value, at: frameRate, base: base, limit: limit) + } + + /// Returns a new ``Timecode`` instance by converting a time source at the given frame rate. + public func timecode( + at frameRate: TimecodeFrameRate, + base: Timecode.SubFramesBase = .default(), + limit: Timecode.UpperLimit = .max24Hours, + by validation: Timecode.ValidationRule + ) -> Timecode { + let value = TimecodeSourceValue(value: self) + return Timecode(value, at: frameRate, base: base, limit: limit, by: validation) + } + + /// Returns a new ``Timecode`` instance by converting a time source. + /// + /// - Throws: ``Timecode/ValidationError`` + public func timecode( + using properties: Timecode.Properties + ) throws -> Timecode { + let value = TimecodeSourceValue(value: self) + return try Timecode(value, using: properties) + } + + /// Returns a new ``Timecode`` instance by converting a time source. + public func timecode( + using properties: Timecode.Properties, + by validation: Timecode.ValidationRule + ) -> Timecode { + let value = TimecodeSourceValue(value: self) + return Timecode(value, using: properties, by: validation) + } +} + +// MARK: - FormattedTimecodeSource Category Methods + +extension FormattedTimecodeSource { + /// Returns a new ``Timecode`` instance by converting a time source at the given frame rate. + /// + /// - Throws: ``Timecode/ValidationError`` + public func timecode( + at frameRate: TimecodeFrameRate, + base: Timecode.SubFramesBase = .default(), + limit: Timecode.UpperLimit = .max24Hours + ) throws -> Timecode { + let value = FormattedTimecodeSourceValue(value: self) + return try Timecode(value, at: frameRate, base: base, limit: limit) + } + + /// Returns a new ``Timecode`` instance by converting a time source at the given frame rate. + /// + /// - Throws: ``Timecode/ValidationError`` + public func timecode( + at frameRate: TimecodeFrameRate, + base: Timecode.SubFramesBase = .default(), + limit: Timecode.UpperLimit = .max24Hours, + by validation: Timecode.ValidationRule + ) throws -> Timecode { + let value = FormattedTimecodeSourceValue(value: self) + return try Timecode(value, at: frameRate, base: base, limit: limit, by: validation) + } + + /// Returns a new ``Timecode`` instance by converting a time source. + /// + /// - Throws: ``Timecode/ValidationError`` + public func timecode( + using properties: Timecode.Properties + ) throws -> Timecode { + let value = FormattedTimecodeSourceValue(value: self) + return try Timecode(value, using: properties) + } + + /// Returns a new ``Timecode`` instance by converting a time source. + /// + /// - Throws: ``Timecode/ValidationError`` + public func timecode( + using properties: Timecode.Properties, + by validation: Timecode.ValidationRule + ) throws -> Timecode { + let value = FormattedTimecodeSourceValue(value: self) + return try Timecode(value, using: properties, by: validation) + } +} + +// MARK: - RichTimecodeSource Category Methods + +extension RichTimecodeSource { + /// Returns a new ``Timecode`` instance by converting a time source. + /// + /// - Throws: ``Timecode/ValidationError`` + public func timecode() throws -> Timecode { + let value = RichTimecodeSourceValue(value: self) + return try Timecode(value) + } +} + +// MARK: - GuaranteedTimecodeSource Category Methods + +extension GuaranteedTimecodeSource { + /// Returns a new ``Timecode`` instance by converting a time source at a given frame rate. + public func timecode( + at frameRate: TimecodeFrameRate, + base: Timecode.SubFramesBase = .default(), + limit: Timecode.UpperLimit = .max24Hours + ) -> Timecode { + let value = GuaranteedTimecodeSourceValue(value: self) + return Timecode(value, at: frameRate, base: base, limit: limit) + } + + /// Returns a new ``Timecode`` instance by converting a time source. + public func timecode( + using properties: Timecode.Properties + ) -> Timecode { + let value = GuaranteedTimecodeSourceValue(value: self) + return Timecode(value, using: properties) + } +} + +// MARK: - GuaranteedRichTimecodeSource Category Methods + +extension GuaranteedRichTimecodeSource { + /// Returns a new ``Timecode`` instance by converting a time source. + public func timecode() -> Timecode { + let value = GuaranteedRichTimecodeSourceValue(value: self) + return Timecode(value) + } } diff --git a/Sources/TimecodeKit/Timecode/Timecode set.swift b/Sources/TimecodeKit/Timecode/Timecode set.swift new file mode 100644 index 00000000..2e54a376 --- /dev/null +++ b/Sources/TimecodeKit/Timecode/Timecode set.swift @@ -0,0 +1,219 @@ +// +// Timecode set.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + +import Foundation + +// MARK: - TimecodeSource + +extension Timecode { + /// Set timecode by converting from a time source. + /// + /// - Throws: ``ValidationError`` + public mutating func set(_ source: TimecodeSourceValue) throws { + try set(source.value) + } + + /// Set timecode by converting from a time source. + public mutating func set(_ source: TimecodeSourceValue, by validation: ValidationRule) { + set(source.value, by: validation) + } + + /// Returns a copy of this instance, setting its timecode by converting from a time source. + /// + /// - Throws: ``ValidationError`` + public func setting(_ source: TimecodeSourceValue) throws -> Timecode { + try setting(source.value) + } + + /// Returns a copy of this instance, setting its timecode by converting from a time source. + public func setting(_ source: TimecodeSourceValue, by validation: ValidationRule) -> Timecode { + setting(source.value, by: validation) + } +} + +extension Timecode { + /// - Throws: ``ValidationError`` + mutating func set(_ source: TimecodeSource) throws { + try source.set(timecode: &self) + } + + mutating func set(_ source: TimecodeSource, by validation: ValidationRule) { + source.set(timecode: &self, by: validation) + } + + /// - Throws: ``ValidationError`` + func setting(_ value: TimecodeSource) throws -> Timecode { + var copy = self + try copy.set(value) + return copy + } + + func setting(_ value: TimecodeSource, by validation: ValidationRule) -> Timecode { + var copy = self + copy.set(value, by: validation) + return copy + } +} + +// MARK: - FormattedTimecodeSource + +extension Timecode { + /// Set timecode by converting from a time source. + /// + /// - Throws: ``ValidationError`` + public mutating func set(_ source: FormattedTimecodeSourceValue) throws { + try set(source.value) + } + + /// Set timecode by converting from a time source. + public mutating func set(_ source: FormattedTimecodeSourceValue, by validation: ValidationRule) throws { + try set(source.value, by: validation) + } + + /// Returns a copy of this instance, setting its timecode by converting from a time source. + /// + /// - Throws: ``ValidationError`` + public func setting(_ source: FormattedTimecodeSourceValue) throws -> Timecode { + try setting(source.value) + } + + /// Returns a copy of this instance, setting its timecode by converting from a time source. + public func setting(_ source: FormattedTimecodeSourceValue, by validation: ValidationRule) throws -> Timecode { + try setting(source.value, by: validation) + } +} + +extension Timecode { + /// - Throws: ``ValidationError`` + mutating func set(_ source: FormattedTimecodeSource) throws { + try source.set(timecode: &self) + } + + mutating func set(_ source: FormattedTimecodeSource, by validation: ValidationRule) throws { + try source.set(timecode: &self, by: validation) + } + + /// - Throws: ``ValidationError`` + func setting(_ source: FormattedTimecodeSource) throws -> Timecode { + var copy = self + try copy.set(source) + return copy + } + + func setting(_ source: FormattedTimecodeSource, by validation: ValidationRule) throws -> Timecode { + var copy = self + try copy.set(source, by: validation) + return copy + } +} + +// MARK: - RichTimecodeSource + +extension Timecode { + /// Set timecode by converting from a time source. + /// + /// - Throws: ``ValidationError`` + public mutating func set( + _ source: RichTimecodeSourceValue + ) throws { + try set(source.value) + } + + /// Returns a copy of this instance, setting its timecode by converting from a time source. + /// + /// - Throws: ``ValidationError`` + public func setting( + _ source: RichTimecodeSourceValue + ) throws -> Timecode { + try setting(source.value) + } +} + +extension Timecode { + /// - Throws: ``ValidationError`` + mutating func set( + _ source: RichTimecodeSource + ) throws { + properties = try source.set(timecode: &self) + } + + /// - Throws: ``ValidationError`` + func setting( + _ source: RichTimecodeSource + ) throws -> Timecode { + var copy = self + try copy.set(source) + return copy + } +} + +// MARK: - GuaranteedTimecodeSource + +extension Timecode { + /// Set timecode by converting from a time source. + public mutating func set( + _ source: GuaranteedTimecodeSourceValue + ) { + set(source.value) + } + + /// Returns a copy of this instance, setting its timecode by converting from a time source. + public func setting( + _ source: GuaranteedTimecodeSourceValue + ) -> Timecode { + setting(source.value) + } +} + +extension Timecode { + mutating func set( + _ source: GuaranteedTimecodeSource + ) { + source.set(timecode: &self) + } + + func setting( + _ source: GuaranteedTimecodeSource + ) -> Timecode { + var copy = self + copy.set(source) + return copy + } +} + +// MARK: - GuaranteedRichTimecodeSource + +extension Timecode { + /// Set timecode by converting from a time source. + public mutating func set( + _ source: GuaranteedRichTimecodeSourceValue + ) -> Properties { + set(source.value) + } + + /// Returns a copy of this instance, setting its timecode by converting from a time source. + public func setting( + _ source: GuaranteedRichTimecodeSourceValue + ) -> Timecode { + setting(source.value) + } +} + +extension Timecode { + mutating func set( + _ source: GuaranteedRichTimecodeSource + ) -> Properties { + source.set(timecode: &self) + } + + func setting( + _ source: GuaranteedRichTimecodeSource + ) -> Timecode { + var copy = self + copy.properties = copy.set(source) + return copy + } +} diff --git a/Sources/TimecodeKit/Timecode/Timecode.swift b/Sources/TimecodeKit/Timecode/Timecode.swift index e71f492e..ec622536 100644 --- a/Sources/TimecodeKit/Timecode/Timecode.swift +++ b/Sources/TimecodeKit/Timecode/Timecode.swift @@ -1,92 +1,134 @@ // // Timecode.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // -/// Value type representing SMPTE timecode. +/// Value type representing SMPTE/EBU timecode. /// -/// - A variety of initializers and methods are available for string and numeric representation, validation, and conversion +/// - A variety of initializers and methods are available for string and numeric representation, +/// validation, and conversion /// - Mathematical operators are available between two instances: `+`, `-`, `*`, `\` /// - Comparison operators are available between two instances: `==`, `!=`, `<`, `>` /// - `Range` and `Stride` can be formed between two instances +/// - Many more features are detailed in the documentation public struct Timecode { - // MARK: - Immutable properties + // MARK: - Components - /// Frame rate. - /// - /// Note: Several properties are available on the frame rate that is selected, including its ``stringValue`` representation or whether the rate ``TimecodeFrameRate/isDrop``. - /// - /// Setting this value directly does not trigger any validation. - public var frameRate: TimecodeFrameRate - - /// Timecode maximum upper bound. - /// - /// This also affects how timecode values wrap when adding or clamping. - /// - /// Setting this value directly does not trigger any validation. - public var upperLimit: UpperLimit - - /// Subframes base (divisor). - /// - /// The number of subframes that make up a single frame. - /// - /// (ie: a divisor of 80 subframes per frame implies a visible value range of 00...79) - /// - /// This will vary depending on application. Most common divisors are 80 or 100. - public var subFramesBase: SubFramesBase - - /// Timecode string output format configuration. - public var stringFormat: StringFormat - - // MARK: - Mutable properties + /// Numerical timecode components. + /// (This components store makes it convenient to port around timecode component values agnostic + /// of frame rate or other properties to other Timecode initializers or methods.) + public var components: Components = .zero /// Timecode days component. /// - /// Valid only if ``upperLimit-swift.property`` is set to `._100days`. + /// Valid only if ``upperLimit-swift.property`` is set to `.max100Days`. /// /// Setting this value directly does not trigger any validation. - public var days: Int = 0 + public var days: Int { + get { components.days } + set { components.days = newValue } + } /// Timecode hours component. /// /// Valid range: 0 ... 23. /// /// Setting this value directly does not trigger any validation. - public var hours: Int = 0 + public var hours: Int { + get { components.hours } + set { components.hours = newValue } + } /// Timecode minutes component. /// /// Valid range: 0 ... 59. /// /// Setting this value directly does not trigger any validation. - public var minutes: Int = 0 + public var minutes: Int { + get { components.minutes } + set { components.minutes = newValue } + } /// Timecode seconds component. /// /// Valid range: 0 ... 59. /// /// Setting this value directly does not trigger any validation. - public var seconds: Int = 0 + public var seconds: Int { + get { components.seconds } + set { components.seconds = newValue } + } /// Timecode frames component. /// /// Valid range is dependent on the `frameRate` property. /// /// Setting this value directly does not trigger any validation. - public var frames: Int = 0 + public var frames: Int { + get { components.frames } + set { components.frames = newValue } + } - /// Timecode subframes component. Represents a partial division of a frame. + /// Timecode subframes component. + /// Represents a subdivision of the current frame. /// /// Some implementations refer to these as SMPTE frame "bits". /// - /// There are no set industry standards regarding subframe divisors. - /// - Cubase/Nuendo, Logic Pro/Final Cut Pro use 80 subframes per frame (0 ... 79); - /// - Pro Tools uses 100 subframes (0 ... 99). - public var subFrames: Int = 0 + /// Industry standards vary regarding subframe divisors depending on manufacturers and formats, + /// and not all manufacturers support the usage of subframes. + /// - DAWs such as Cubase, Nuendo, Logic Pro, and Final Cut Pro use 80 subframes per frame (0 ... 79). + /// - DAWs such as Pro Tools use 100 subframes per frame (0 ... 99). + /// - MIDI Timecode (MTC) uses 4 subframes per frame, also known as quarter-frames (0 ... 3). + public var subFrames: Int { + get { components.subFrames } + set { components.subFrames = newValue } + } + + // MARK: - Properties + + /// Timecode properties. + /// (This property store makes it convenient to port around relevant timecode attributes to + /// other Timecode initializers or methods.) + public var properties: Properties + + /// Frame rate. + /// + /// - Note: Several properties are available on the frame rate that is selected, including its + /// ``stringValue(format:)`` representation or whether the rate ``TimecodeFrameRate/isDrop``. + /// + /// - Note: Setting this value directly does not trigger any validation. + public var frameRate: TimecodeFrameRate { + get { properties.frameRate } + set { properties.frameRate = newValue } + } + + /// Subframes base (divisor). + /// + /// The number of subframes that make up a single frame. + /// + /// (ie: a divisor of 80 subframes per frame implies a visible value range of 00...79) + /// + /// This will vary depending on application. Most common divisors are 80 or 100. + /// + /// - Note: Setting this value directly does not trigger any validation. + public var subFramesBase: SubFramesBase { + get { properties.subFramesBase } + set { properties.subFramesBase = newValue } + } + + /// Timecode maximum upper bound. + /// + /// This also affects how timecode values wrap when adding or clamping. + /// + /// - Note: Setting this value directly does not trigger any validation. + public var upperLimit: UpperLimit { + get { properties.upperLimit } + set { properties.upperLimit = newValue } + } + + // Just to disable synthesized init + private init() { + properties = Properties(rate: .fps24) + } } - -// Can't put this in another file since it prevents automatic synthesis -// But for sake of consistency, we'll put it on an extension here since all other -// protocol conformances exist in separate files. Sad panda. -extension Timecode: Codable { } diff --git a/Sources/TimecodeKit/Timecode/UpperLimit/UpperLimit.swift b/Sources/TimecodeKit/Timecode/UpperLimit/UpperLimit.swift index 5c96ca5b..cbc87ee4 100644 --- a/Sources/TimecodeKit/Timecode/UpperLimit/UpperLimit.swift +++ b/Sources/TimecodeKit/Timecode/UpperLimit/UpperLimit.swift @@ -1,55 +1,55 @@ // // UpperLimit.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // extension Timecode { /// Enum describing the maximum timecode ceiling. public enum UpperLimit: String, CaseIterable { - /// Pro Tools' upper limit is "23:59:59:FF" which is 1 day (24 hours) in duration. - case _24hours = "24 hours" + /// Pro Tools' upper limit is "23:59:59:FF" inclusive, which is 1 day (24 hours) in duration. + case max24Hours = "24 hours" - /// Cubase's upper limit is "99 23:59:59:FF" which is 100 days in duration. - case _100days = "100 days" + /// Cubase's upper limit is "99 23:59:59:FF" inclusive, which is 100 days in duration. + case max100Days = "100 days" /// Internal use. - internal var maxDays: Int { + var maxDays: Int { switch self { - case ._24hours: return 1 - case ._100days: return 100 + case .max24Hours: return 1 + case .max100Days: return 100 } } /// Internal use. - internal var maxDaysExpressible: Int { + var maxDaysExpressible: Int { switch self { - case ._24hours: return maxDays - 1 - case ._100days: return maxDays - 1 + case .max24Hours: return maxDays - 1 + case .max100Days: return maxDays - 1 } } /// Internal use. - internal var maxHours: Int { + var maxHours: Int { switch self { - case ._24hours: return 24 - case ._100days: return 24 + case .max24Hours: return 24 + case .max100Days: return 24 } } /// Internal use. - internal var maxHoursExpressible: Int { + var maxHoursExpressible: Int { switch self { - case ._24hours: return maxHours - 1 - case ._100days: return maxHours - 1 + case .max24Hours: return maxHours - 1 + case .max100Days: return maxHours - 1 } } /// Internal use. - internal var maxHoursTotal: Int { + var maxHoursTotal: Int { switch self { - case ._24hours: return maxHours - 1 - case ._100days: return (maxHours * maxDays) - 1 + case .max24Hours: return maxHours - 1 + case .max100Days: return (maxHours * maxDays) - 1 } } } diff --git a/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate CompatibleGroup.swift b/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate CompatibleGroup.swift index 571a42f3..c99ce228 100644 --- a/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate CompatibleGroup.swift +++ b/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate CompatibleGroup.swift @@ -1,62 +1,82 @@ // // TimecodeFrameRate CompatibleGroup.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // extension TimecodeFrameRate { /// Enum describing compatible groupings of frame rates. /// + /// These groupings assert that amidst each group the hours, minutes, and seconds values will always be identical. + /// Frames values may not literally match but will always correspond to the same duration of a timecode-second. + /// + /// For example: + /// + /// At 1 hour of elapsed real (wall-clock) time, 30 and 60 fps are compatible with each other, but 29.97 is not: + /// - `01:00:00:00 @ 30 fps // group A` + /// - `01:00:00:00 @ 60 fps // group A` + /// - `00:59:56:12 @ 29.97 fps // group B` + /// + /// 30 and 60 fps both reach `01:00:00:00` at exactly the same time, then until the next timecode-second only the + /// frame number will differ. They will then both reach `01:00:01:00` at exactly the same time, and so on. + /// /// - note: These are intended for internal logic and not for end-user user interface. public enum CompatibleGroup: Equatable, Hashable, CaseIterable { - case NTSC - case NTSC_drop - case ATSC - case ATSC_drop + case ntscColor + case ntscDrop + case whole + case ntscColorWallTime - /// Constants table of `FrameRate` groups that share HH:MM:SS alignment between them, while only frames value may differ. + /// Constants table of ``TimecodeFrameRate`` groups that share HH:MM:SS alignment between them. + /// + /// These groupings assert that amidst each group the hours, minutes, and seconds values will always be identical. + /// Frames values may not literally match but will always correspond to the same duration of a timecode-second. /// - /// These groupings assert that they are interchangeable in so much as hours, minutes, and seconds values will always be identical between them at the same elapsed real time, but only frames value may differ. + /// For example: /// - /// For example, at the same point of elapsed real time, 30 and 60 fps are compatible with each other, but 29.97 is not: + /// At 1 hour of elapsed real (wall-clock) time, 30 and 60 fps are compatible with each other, but 29.97 is not: + /// - `01:00:00:00 @ 30 fps // group A` + /// - `01:00:00:00 @ 60 fps // group A` + /// - `00:59:56:12 @ 29.97 fps // group B` /// - /// - 01:00:00:00 @ 30 fps - /// - 01:00:00:00 @ 60 fps - /// - 00:59:56:12 @ 29.97 fps + /// 30 and 60 fps both reach `01:00:00:00` at exactly the same time, then until the next timecode-second only the + /// frame number will differ. They will then both reach `01:00:01:00` at exactly the same time, and so on. + /// + /// - note: These are intended for internal logic and not for end-user user interface. public static var table: [CompatibleGroup: [TimecodeFrameRate]] = [ - .NTSC: [ - ._23_976, - ._24_98, - ._29_97, - ._47_952, - ._59_94, - ._95_904, - ._119_88 + .ntscColor: [ + .fps23_976, + .fps24_98, + .fps29_97, + .fps47_952, + .fps59_94, + .fps95_904, + .fps119_88 ], - .NTSC_drop: [ - ._29_97_drop, - ._59_94_drop, - ._119_88_drop + .ntscDrop: [ + .fps29_97d, + .fps59_94d, + .fps119_88d ], - .ATSC: [ - ._24, - ._25, - ._30, - ._48, - ._50, - ._60, - ._96, - ._100, - ._120 + .whole: [ + .fps24, + .fps25, + .fps30, + .fps48, + .fps50, + .fps60, + .fps96, + .fps100, + .fps120 ], - .ATSC_drop: [ - ._30_drop, - ._60_drop, - ._120_drop + .ntscColorWallTime: [ + .fps30d, + .fps60d, + .fps120d ] ] } @@ -70,50 +90,62 @@ extension TimecodeFrameRate.CompatibleGroup: CustomStringConvertible { /// Returns human-readable group string. public var stringValue: String { switch self { - case .NTSC: - return "NTSC" + case .ntscColor: + return "NTSC Color" - case .NTSC_drop: + case .ntscDrop: return "NTSC Drop-Frame" - case .ATSC: - return "ATSC" + case .whole: + return "Whole" - case .ATSC_drop: - return "ATSC Drop-Frame" + case .ntscColorWallTime: + return "NTSC Color Wall Time" } } } extension TimecodeFrameRate { - /// Returns the frame rate's `CompatibleGroup` categorization. + /// Returns the frame rate's ``CompatibleGroup`` categorization. public var compatibleGroup: CompatibleGroup { // Force-unwrap here will never crash because the unit tests ensure the table contains all TimecodeFrameRate cases. Self.CompatibleGroup.table + .lazy .first(where: { $0.value.contains(self) })! .key } - /// Returns the members of the frame rate's `CompatibleGroup` categorization. + /// Returns the members of the frame rate's ``CompatibleGroup`` categorization. public var compatibleGroupRates: [Self] { // Force-unwrap here will never crash because the unit tests ensure the table contains all TimecodeFrameRate cases. Self.CompatibleGroup.table + .lazy .first(where: { $0.value.contains(self) })! .value } - /// Returns true if the source `FrameRate` shares a compatible grouping with the passed `other` frame rate. + /// Returns true if the source ``TimecodeFrameRate`` shares a compatible grouping with the passed `other` frame rate. + /// + /// These groupings assert that amidst each group the hours, minutes, and seconds values will always be identical. + /// Frames values may not literally match but will always correspond to the same duration of a timecode-second. + /// + /// For example: /// - /// For example, at the same point of elapsed real time, 30 and 60 fps are compatible with each other, but 29.97 is not: + /// At 1 hour of elapsed real (wall-clock) time, 30 and 60 fps are compatible with each other, but 29.97 is not: + /// - `01:00:00:00 @ 30 fps // group A` + /// - `01:00:00:00 @ 60 fps // group A` + /// - `00:59:56:12 @ 29.97 fps // group B` /// - /// - 01:00:00:00 @ 30 fps - /// - 01:00:00:00 @ 60 fps - /// - 00:59:56:12 @ 29.97 fps + /// 30 and 60 fps both reach `01:00:00:00` at exactly the same time, then until the next timecode-second only the + /// frame number will differ. They will then both reach `01:00:01:00` at exactly the same time, and so on. + /// + /// - note: These are intended for internal logic and not for end-user user interface. public func isCompatible(with other: Self) -> Bool { Self.CompatibleGroup.table .values + .lazy .first(where: { $0.contains(self) })? .contains(other) ?? false diff --git a/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate Conversions.swift b/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate Conversions.swift index 9a29cae5..000a2701 100644 --- a/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate Conversions.swift +++ b/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate Conversions.swift @@ -1,40 +1,41 @@ // // TimecodeFrameRate Conversions.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // import Foundation extension TimecodeFrameRate { /// Returns the corresponding ``VideoFrameRate`` case. + /// Returns `nil` if there is no corresponding video rate. /// /// - Parameters: /// - interlaced: Whether video frame rate is interlaced (`true`) or progressive (`false`). public func videoFrameRate(interlaced: Bool) -> VideoFrameRate? { switch self { - case ._23_976: return interlaced ? nil : ._23_98p - case ._24: return interlaced ? nil : ._24p - case ._24_98: return interlaced ? nil : ._25p // TODO: needs testing - case ._25: return interlaced ? ._25i : ._25i - case ._29_97: return interlaced ? ._29_97i : ._29_97p - case ._29_97_drop: return interlaced ? ._29_97i : ._29_97p - case ._30: return interlaced ? nil : ._30p // 30i could exist? - case ._30_drop: return interlaced ? nil : ._30p - case ._47_952: return interlaced ? nil : ._47_95p - case ._48: return interlaced ? nil : ._48p - case ._50: return interlaced ? ._50i : ._50p - case ._59_94: return interlaced ? nil : ._59_94p // TODO: 59.94i exists - case ._59_94_drop: return interlaced ? nil : ._59_94p // TODO: 59.94i exists - case ._60: return interlaced ? ._60i : ._60p - case ._60_drop: return interlaced ? nil : ._60p - case ._95_904: return interlaced ? nil : ._95_9p - case ._96: return interlaced ? nil : ._96p - case ._100: return interlaced ? nil : ._100p - case ._119_88: return interlaced ? nil : ._119_88p // 119.88i could exist? - case ._119_88_drop: return interlaced ? nil : ._119_88p // 119.88i could exist? - case ._120: return interlaced ? nil : ._120p // 120i could exist? - case ._120_drop: return interlaced ? nil : ._120p + case .fps23_976: return interlaced ? nil : .fps23_98p + case .fps24: return interlaced ? nil : .fps24p + case .fps24_98: return interlaced ? nil : .fps25p // TODO: needs testing + case .fps25: return interlaced ? .fps25i : .fps25i + case .fps29_97: return interlaced ? .fps29_97i : .fps29_97p + case .fps29_97d: return interlaced ? .fps29_97i : .fps29_97p + case .fps30: return interlaced ? nil : .fps30p // 30i could exist? + case .fps30d: return interlaced ? nil : .fps30p + case .fps47_952: return interlaced ? nil : .fps47_95p + case .fps48: return interlaced ? nil : .fps48p + case .fps50: return interlaced ? .fps50i : .fps50p + case .fps59_94: return interlaced ? nil : .fps59_94p // TODO: 59.94i exists + case .fps59_94d: return interlaced ? nil : .fps59_94p // TODO: 59.94i exists + case .fps60: return interlaced ? .fps60i : .fps60p + case .fps60d: return interlaced ? nil : .fps60p + case .fps95_904: return interlaced ? nil : .fps95_9p + case .fps96: return interlaced ? nil : .fps96p + case .fps100: return interlaced ? nil : .fps100p + case .fps119_88: return interlaced ? nil : .fps119_88p // 119.88i could exist? + case .fps119_88d: return interlaced ? nil : .fps119_88p // 119.88i could exist? + case .fps120: return interlaced ? nil : .fps120p // 120i could exist? + case .fps120d: return interlaced ? nil : .fps120p } } } @@ -104,7 +105,7 @@ import CoreMedia extension TimecodeFrameRate { /// Initialize from a frame rate (fps) expressed as a rational number (fraction). /// - /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to communicate + /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to represent /// times and durations. /// /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in @@ -123,7 +124,7 @@ extension TimecodeFrameRate { /// Initialize from a frame rate's frame duration expressed as a rational number (fraction). /// - /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to communicate + /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to represent /// times and durations. /// /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in @@ -140,39 +141,6 @@ extension TimecodeFrameRate { ) } - /// Returns the frame rate (fps) as a rational number (fraction) - /// as a CoreMedia `CMTime` instance. - /// - /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to communicate - /// times and durations. - /// - /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in - /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate - /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as - /// fractions.) - public var rateCMTime: CMTime { - CMTime( - value: CMTimeValue(rate.numerator), - timescale: CMTimeScale(rate.denominator) - ) - } - - /// Returns the duration of 1 frame as a rational number (fraction) - /// as a CoreMedia `CMTime` instance. - /// - /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to communicate - /// times and durations. - /// - /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in - /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate - /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as - /// fractions.) - public var frameDurationCMTime: CMTime { - CMTime( - value: CMTimeValue(frameDuration.numerator), - timescale: CMTimeScale(frameDuration.denominator) - ) - } + // NOTE: `rateCMTime` and `frameDurationCMTime` properties are implemented on `FrameRateProtocol` } #endif - diff --git a/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate Formats.swift b/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate Formats.swift index f8701065..8752a1ef 100644 --- a/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate Formats.swift +++ b/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate Formats.swift @@ -1,7 +1,7 @@ // // TimecodeFrameRate Formats.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // extension TimecodeFrameRate { diff --git a/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate Properties.swift b/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate Properties.swift index 3a8f9c2e..58e68046 100644 --- a/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate Properties.swift +++ b/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate Properties.swift @@ -1,7 +1,7 @@ // // TimecodeFrameRate Properties.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // // MARK: stringValue @@ -10,60 +10,60 @@ extension TimecodeFrameRate { /// Returns human-readable frame rate string. public var stringValue: String { switch self { - case ._23_976: return "23.976" - case ._24: return "24" - case ._24_98: return "24.98" - case ._25: return "25" - case ._29_97: return "29.97" - case ._29_97_drop: return "29.97d" - case ._30: return "30" - case ._30_drop: return "30d" - case ._47_952: return "47.952" - case ._48: return "48" - case ._50: return "50" - case ._59_94: return "59.94" - case ._59_94_drop: return "59.94d" - case ._60: return "60" - case ._60_drop: return "60d" - case ._95_904: return "95.904" - case ._96: return "96" - case ._100: return "100" - case ._119_88: return "119.88" - case ._119_88_drop: return "119.88d" - case ._120: return "120" - case ._120_drop: return "120d" + case .fps23_976: return "23.976" + case .fps24: return "24" + case .fps24_98: return "24.98" + case .fps25: return "25" + case .fps29_97: return "29.97" + case .fps29_97d: return "29.97d" + case .fps30: return "30" + case .fps30d: return "30d" + case .fps47_952: return "47.952" + case .fps48: return "48" + case .fps50: return "50" + case .fps59_94: return "59.94" + case .fps59_94d: return "59.94d" + case .fps60: return "60" + case .fps60d: return "60d" + case .fps95_904: return "95.904" + case .fps96: return "96" + case .fps100: return "100" + case .fps119_88: return "119.88" + case .fps119_88d: return "119.88d" + case .fps120: return "120" + case .fps120d: return "120d" } } /// Returns human-readable frame rate string in long form. public var stringValueVerbose: String { switch self { - case ._23_976: return "23.976 fps" - case ._24: return "24 fps" - case ._24_98: return "24.98 fps" - case ._25: return "25 fps" - case ._29_97: return "29.97 fps" - case ._29_97_drop: return "29.97 fps drop" - case ._30: return "30 fps" - case ._30_drop: return "30 fps drop" - case ._47_952: return "47.952 fps" - case ._48: return "48 fps" - case ._50: return "50 fps" - case ._59_94: return "59.94 fps" - case ._59_94_drop: return "59.94 fps drop" - case ._60: return "60 fps" - case ._60_drop: return "60 fps drop" - case ._95_904: return "95.904 fps" - case ._96: return "96 fps" - case ._100: return "100 fps" - case ._119_88: return "119.88 fps" - case ._119_88_drop: return "119.88 fps drop" - case ._120: return "120 fps" - case ._120_drop: return "120 fps drop" + case .fps23_976: return "23.976 fps" + case .fps24: return "24 fps" + case .fps24_98: return "24.98 fps" + case .fps25: return "25 fps" + case .fps29_97: return "29.97 fps" + case .fps29_97d: return "29.97 fps drop" + case .fps30: return "30 fps" + case .fps30d: return "30 fps drop" + case .fps47_952: return "47.952 fps" + case .fps48: return "48 fps" + case .fps50: return "50 fps" + case .fps59_94: return "59.94 fps" + case .fps59_94d: return "59.94 fps drop" + case .fps60: return "60 fps" + case .fps60d: return "60 fps drop" + case .fps95_904: return "95.904 fps" + case .fps96: return "96 fps" + case .fps100: return "100 fps" + case .fps119_88: return "119.88 fps" + case .fps119_88d: return "119.88 fps drop" + case .fps120: return "120 fps" + case .fps120d: return "120 fps drop" } } - /// Initializes from a ``stringValue`` string. Case-sensitive. + /// Initializes from the human-readable ``stringValue`` string. Case-sensitive. public init?(stringValue: String) { if let findMatch = Self.allCases .first(where: { $0.stringValue == stringValue }) @@ -84,35 +84,37 @@ extension TimecodeFrameRate { /// rate (and not a video rate), its status as a drop or non-drop rate must be stored /// independently and recalled. (``isDrop``) /// - /// // == frame rate - /// Double(numerator) / Double(denominator) + /// ```swift + /// Double(numerator) / Double(denominator) + /// // == frame rate /// - /// // == duration of 1 frame in seconds - /// Double(denominator) / Double(numerator) + /// Double(denominator) / Double(numerator) + /// // == duration of 1 frame in seconds + /// ``` public var rate: Fraction { switch self { - case ._23_976: return Fraction(24000, 1001) - case ._24: return Fraction(24, 1) - case ._24_98: return Fraction(25000, 1001) - case ._25: return Fraction(25, 1) - case ._29_97: return Fraction(30000, 1001) - case ._29_97_drop: return Fraction(30000, 1001) - case ._30: return Fraction(30, 1) - case ._30_drop: return Fraction(30, 1) - case ._47_952: return Fraction(48000, 1001) - case ._48: return Fraction(48, 1) - case ._50: return Fraction(50, 1) - case ._59_94: return Fraction(60000, 1001) - case ._59_94_drop: return Fraction(60000, 1001) - case ._60: return Fraction(60, 1) - case ._60_drop: return Fraction(60, 1) - case ._95_904: return Fraction(96000, 1001) - case ._96: return Fraction(96, 1) - case ._100: return Fraction(100, 1) - case ._119_88: return Fraction(120_000, 1001) - case ._119_88_drop: return Fraction(120_000, 1001) - case ._120: return Fraction(120, 1) - case ._120_drop: return Fraction(120, 1) + case .fps23_976: return Fraction(24000, 1001) + case .fps24: return Fraction(24, 1) + case .fps24_98: return Fraction(25000, 1001) + case .fps25: return Fraction(25, 1) + case .fps29_97: return Fraction(30000, 1001) + case .fps29_97d: return Fraction(30000, 1001) + case .fps30: return Fraction(30, 1) + case .fps30d: return Fraction(30, 1) + case .fps47_952: return Fraction(48000, 1001) + case .fps48: return Fraction(48, 1) + case .fps50: return Fraction(50, 1) + case .fps59_94: return Fraction(60000, 1001) + case .fps59_94d: return Fraction(60000, 1001) + case .fps60: return Fraction(60, 1) + case .fps60d: return Fraction(60, 1) + case .fps95_904: return Fraction(96000, 1001) + case .fps96: return Fraction(96, 1) + case .fps100: return Fraction(100, 1) + case .fps119_88: return Fraction(120_000, 1001) + case .fps119_88d: return Fraction(120_000, 1001) + case .fps120: return Fraction(120, 1) + case .fps120d: return Fraction(120, 1) } } @@ -126,28 +128,28 @@ extension TimecodeFrameRate { /// Potentially compatible outside of that range but untested. public var frameDuration: Fraction { switch self { - case ._23_976: return Fraction(1001, 24000) - case ._24: return Fraction(100, 2400) - case ._24_98: return Fraction(1001, 25000) // TODO: inferred - case ._25: return Fraction(100, 2500) - case ._29_97: return Fraction(1001, 30000) - case ._29_97_drop: return Fraction(1001, 30000) - case ._30: return Fraction(100, 3000) - case ._30_drop: return Fraction(100, 3000) // TODO: needs checking - case ._47_952: return Fraction(1001, 48000) // TODO: inferred - case ._48: return Fraction(100, 4800) - case ._50: return Fraction(100, 5000) - case ._59_94: return Fraction(1001, 60000) // TODO: inferred - case ._59_94_drop: return Fraction(1001, 60000) // TODO: inferred - case ._60: return Fraction(100, 6000) - case ._60_drop: return Fraction(100, 6000) // TODO: needs checking - case ._95_904: return Fraction(1001, 96000) // TODO: inferred - case ._96: return Fraction(100, 9600) // TODO: inferred - case ._100: return Fraction(100, 10000) - case ._119_88: return Fraction(1001, 120000) // TODO: inferred - case ._119_88_drop: return Fraction(1001, 120000) // TODO: inferred - case ._120: return Fraction(100, 12000) - case ._120_drop: return Fraction(100, 12000) // TODO: needs checking + case .fps23_976: return Fraction(1001, 24000) + case .fps24: return Fraction(100, 2400) + case .fps24_98: return Fraction(1001, 25000) // TODO: inferred + case .fps25: return Fraction(100, 2500) + case .fps29_97: return Fraction(1001, 30000) + case .fps29_97d: return Fraction(1001, 30000) + case .fps30: return Fraction(100, 3000) + case .fps30d: return Fraction(100, 3000) // TODO: needs checking + case .fps47_952: return Fraction(1001, 48000) // TODO: inferred + case .fps48: return Fraction(100, 4800) + case .fps50: return Fraction(100, 5000) + case .fps59_94: return Fraction(1001, 60000) // TODO: inferred + case .fps59_94d: return Fraction(1001, 60000) // TODO: inferred + case .fps60: return Fraction(100, 6000) + case .fps60d: return Fraction(100, 6000) // TODO: needs checking + case .fps95_904: return Fraction(1001, 96000) // TODO: inferred + case .fps96: return Fraction(100, 9600) // TODO: inferred + case .fps100: return Fraction(100, 10000) + case .fps119_88: return Fraction(1001, 120000) // TODO: inferred + case .fps119_88d: return Fraction(1001, 120000) // TODO: inferred + case .fps120: return Fraction(100, 12000) + case .fps120d: return Fraction(100, 12000) // TODO: needs checking } } @@ -156,56 +158,56 @@ extension TimecodeFrameRate { /// so we can fall back to these values a secondary checks. public var alternateFrameDuration: Fraction? { switch self { - case ._23_976: return Fraction(1000, 23976) // seen in the wild - case ._24: return nil - case ._24_98: return Fraction(1000, 24980) // TODO: inferred - case ._25: return nil - case ._29_97: return Fraction(1000, 29970) // seen in the wild - case ._29_97_drop: return Fraction(1000, 29970) // seen in the wild - case ._30: return nil - case ._30_drop: return nil // TODO: needs checking - case ._47_952: return Fraction(1000, 47952) // TODO: inferred - case ._48: return nil - case ._50: return nil - case ._59_94: return Fraction(1000, 59940) // TODO: inferred - case ._59_94_drop: return Fraction(1000, 59940) // TODO: inferred - case ._60: return nil - case ._60_drop: return nil // TODO: needs checking - case ._95_904: return Fraction(1000, 95904) // TODO: inferred - case ._96: return nil - case ._100: return nil - case ._119_88: return Fraction(1000, 119880) // TODO: inferred - case ._119_88_drop: return Fraction(1000, 119880) // TODO: inferred - case ._120: return nil - case ._120_drop: return nil // TODO: needs checking + case .fps23_976: return Fraction(1000, 23976) // seen in the wild + case .fps24: return nil + case .fps24_98: return Fraction(1000, 24980) // TODO: inferred + case .fps25: return nil + case .fps29_97: return Fraction(1000, 29970) // seen in the wild + case .fps29_97d: return Fraction(1000, 29970) // seen in the wild + case .fps30: return nil + case .fps30d: return nil // TODO: needs checking + case .fps47_952: return Fraction(1000, 47952) // TODO: inferred + case .fps48: return nil + case .fps50: return nil + case .fps59_94: return Fraction(1000, 59940) // TODO: inferred + case .fps59_94d: return Fraction(1000, 59940) // TODO: inferred + case .fps60: return nil + case .fps60d: return nil // TODO: needs checking + case .fps95_904: return Fraction(1000, 95904) // TODO: inferred + case .fps96: return nil + case .fps100: return nil + case .fps119_88: return Fraction(1000, 119880) // TODO: inferred + case .fps119_88d: return Fraction(1000, 119880) // TODO: inferred + case .fps120: return nil + case .fps120d: return nil // TODO: needs checking } } /// Returns `true` if frame rate is drop. public var isDrop: Bool { switch self { - case ._23_976: return false - case ._24: return false - case ._24_98: return false - case ._25: return false - case ._29_97: return false - case ._29_97_drop: return true - case ._30: return false - case ._30_drop: return true - case ._47_952: return false - case ._48: return false - case ._50: return false - case ._59_94: return false - case ._59_94_drop: return true - case ._60: return false - case ._60_drop: return true - case ._95_904: return false - case ._96: return false - case ._100: return false - case ._119_88: return false - case ._119_88_drop: return true - case ._120: return false - case ._120_drop: return true + case .fps23_976: return false + case .fps24: return false + case .fps24_98: return false + case .fps25: return false + case .fps29_97: return false + case .fps29_97d: return true + case .fps30: return false + case .fps30d: return true + case .fps47_952: return false + case .fps48: return false + case .fps50: return false + case .fps59_94: return false + case .fps59_94d: return true + case .fps60: return false + case .fps60d: return true + case .fps95_904: return false + case .fps96: return false + case .fps100: return false + case .fps119_88: return false + case .fps119_88d: return true + case .fps120: return false + case .fps120d: return true } } @@ -214,31 +216,31 @@ extension TimecodeFrameRate { /// ie: 24 or 30 fps would return 2, but 120 fps would return 3. public var numberOfDigits: Int { switch self { - case ._23_976, - ._24, - ._24_98, - ._25, - ._29_97, - ._29_97_drop, - ._30, - ._30_drop, - ._47_952, - ._48, - ._50, - ._59_94, - ._59_94_drop, - ._60, - ._60_drop, - ._95_904, - ._96, - ._100: + case .fps23_976, + .fps24, + .fps24_98, + .fps25, + .fps29_97, + .fps29_97d, + .fps30, + .fps30d, + .fps47_952, + .fps48, + .fps50, + .fps59_94, + .fps59_94d, + .fps60, + .fps60d, + .fps95_904, + .fps96, + .fps100: return 2 - case ._119_88, - ._119_88_drop, - ._120, - ._120_drop: + case .fps119_88, + .fps119_88d, + .fps120, + .fps120d: return 3 } @@ -252,34 +254,34 @@ extension TimecodeFrameRate { /// Returns max total frames from 0 to and including rolling over to `extent`. public func maxTotalFrames(in extent: Timecode.UpperLimit) -> Int { switch extent { - case ._24hours: + case .max24Hours: switch self { - case ._23_976: return 2_073_600 // @ 24hours - case ._24: return 2_073_600 // @ 24hours - case ._24_98: return 2_160_000 // @ 24hours - case ._25: return 2_160_000 // @ 24hours - case ._29_97: return 2_592_000 // @ 24hours - case ._29_97_drop: return 2_589_408 // @ 24hours - case ._30: return 2_592_000 // @ 24hours - case ._30_drop: return 2_589_408 // @ 24hours - case ._47_952: return 4_147_200 // @ 24hours - case ._48: return 4_147_200 // @ 24hours - case ._50: return 4_320_000 // @ 24hours - case ._59_94: return 5_184_000 // @ 24hours (._29_97 * 2 in theory) - case ._59_94_drop: return 5_178_816 // @ 24hours (._29_97_drop * 2, in theory) - case ._60: return 5_184_000 // @ 24hours - case ._60_drop: return 5_178_816 // @ 24hours - case ._95_904: return 8_294_400 // @ 24hours - case ._96: return 8_294_400 // @ 24hours - case ._100: return 8_640_000 // @ 24hours - case ._119_88: return 10_368_000 // @ 24hours (._29_97 * 4 in theory) - case ._119_88_drop: return 10_357_632 // @ 24hours (._29_97_drop * 4, in theory) - case ._120: return 10_368_000 // @ 24hours - case ._120_drop: return 10_357_632 // @ 24hours + case .fps23_976: return 2_073_600 // @ 24hours + case .fps24: return 2_073_600 // @ 24hours + case .fps24_98: return 2_160_000 // @ 24hours + case .fps25: return 2_160_000 // @ 24hours + case .fps29_97: return 2_592_000 // @ 24hours + case .fps29_97d: return 2_589_408 // @ 24hours + case .fps30: return 2_592_000 // @ 24hours + case .fps30d: return 2_589_408 // @ 24hours + case .fps47_952: return 4_147_200 // @ 24hours + case .fps48: return 4_147_200 // @ 24hours + case .fps50: return 4_320_000 // @ 24hours + case .fps59_94: return 5_184_000 // @ 24hours (.fps29_97 * 2 in theory) + case .fps59_94d: return 5_178_816 // @ 24hours (.fps29_97d * 2, in theory) + case .fps60: return 5_184_000 // @ 24hours + case .fps60d: return 5_178_816 // @ 24hours + case .fps95_904: return 8_294_400 // @ 24hours + case .fps96: return 8_294_400 // @ 24hours + case .fps100: return 8_640_000 // @ 24hours + case .fps119_88: return 10_368_000 // @ 24hours (.fps29_97 * 4 in theory) + case .fps119_88d: return 10_357_632 // @ 24hours (.fps29_97d * 4, in theory) + case .fps120: return 10_368_000 // @ 24hours + case .fps120d: return 10_357_632 // @ 24hours } - case ._100days: - return maxTotalFrames(in: ._24hours) * extent.maxDays + case .max100Days: + return maxTotalFrames(in: .max24Hours) * extent.maxDays } } @@ -312,120 +314,121 @@ extension TimecodeFrameRate { extension TimecodeFrameRate { /// Internal use. /// Constant for total number of elapsed frames that comprise 1 'second' of timecode. - internal var maxFrames: Int { + var maxFrames: Int { switch self { - case ._23_976: return 24 - case ._24: return 24 - case ._24_98: return 25 - case ._25: return 25 - case ._29_97: return 30 - case ._29_97_drop: return 30 - case ._30: return 30 - case ._30_drop: return 30 - case ._47_952: return 48 - case ._48: return 48 - case ._50: return 50 - case ._59_94: return 60 - case ._59_94_drop: return 60 - case ._60: return 60 - case ._60_drop: return 60 - case ._95_904: return 96 - case ._96: return 96 - case ._100: return 100 - case ._119_88: return 120 - case ._119_88_drop: return 120 - case ._120: return 120 - case ._120_drop: return 120 + case .fps23_976: return 24 + case .fps24: return 24 + case .fps24_98: return 25 + case .fps25: return 25 + case .fps29_97: return 30 + case .fps29_97d: return 30 + case .fps30: return 30 + case .fps30d: return 30 + case .fps47_952: return 48 + case .fps48: return 48 + case .fps50: return 50 + case .fps59_94: return 60 + case .fps59_94d: return 60 + case .fps60: return 60 + case .fps60d: return 60 + case .fps95_904: return 96 + case .fps96: return 96 + case .fps100: return 100 + case .fps119_88: return 120 + case .fps119_88d: return 120 + case .fps120: return 120 + case .fps120d: return 120 } } /// Internal use. /// Constant used when calculating total frame count, audio samples, etc. - internal var frameRateForElapsedFramesCalculation: Double { + var frameRateForElapsedFramesCalculation: Double { switch self { - case ._23_976: return 24.0 - case ._24: return 24.0 - case ._24_98: return 25.0 - case ._25: return 25.0 - case ._29_97: return 30.0 - case ._29_97_drop: return 29.97 - case ._30: return 30.0 - case ._30_drop: return 29.97 - case ._47_952: return 48.0 - case ._48: return 48.0 - case ._50: return 50.0 - case ._59_94: return 60.0 - case ._59_94_drop: return 59.94 - case ._60: return 60.0 - case ._60_drop: return 59.94 - case ._95_904: return 96.0 - case ._96: return 96.0 - case ._100: return 100.0 - case ._119_88: return 120.0 - case ._119_88_drop: return 119.88 - case ._120: return 120.0 - case ._120_drop: return 119.88 + case .fps23_976: return 24.0 + case .fps24: return 24.0 + case .fps24_98: return 25.0 + case .fps25: return 25.0 + case .fps29_97: return 30.0 + case .fps29_97d: return 29.97 + case .fps30: return 30.0 + case .fps30d: return 29.97 + case .fps47_952: return 48.0 + case .fps48: return 48.0 + case .fps50: return 50.0 + case .fps59_94: return 60.0 + case .fps59_94d: return 59.94 + case .fps60: return 60.0 + case .fps60d: return 59.94 + case .fps95_904: return 96.0 + case .fps96: return 96.0 + case .fps100: return 100.0 + case .fps119_88: return 120.0 + case .fps119_88d: return 119.88 + case .fps120: return 120.0 + case .fps120d: return 119.88 } } /// Internal use. /// Constant used in real time conversion, SMF export, etc. - internal var frameRateForRealTimeCalculation: Double { + var frameRateForRealTimeCalculation: Double { switch self { - case ._23_976: return 24.0 / 1.001 - case ._24: return 24.0 - case ._24_98: return 25.0 / 1.001 - case ._25: return 25.0 - case ._29_97: return 30.0 / 1.001 - case ._29_97_drop: return 30.0 / 1.001 - case ._30: return 30.0 - case ._30_drop: return 30.0 - case ._47_952: return 48.0 / 1.001 - case ._48: return 48.0 - case ._50: return 50.0 - case ._59_94: return 60.0 / 1.001 - case ._59_94_drop: return 60.0 / 1.001 - case ._60: return 60.0 - case ._60_drop: return 60.0 - case ._95_904: return 96.0 / 1.001 - case ._96: return 96.0 - case ._100: return 100.0 - case ._119_88: return 120.0 / 1.001 - case ._119_88_drop: return 120.0 / 1.001 - case ._120: return 120.0 - case ._120_drop: return 120.0 + case .fps23_976: return 24.0 / 1.001 + case .fps24: return 24.0 + case .fps24_98: return 25.0 / 1.001 + case .fps25: return 25.0 + case .fps29_97: return 30.0 / 1.001 + case .fps29_97d: return 30.0 / 1.001 + case .fps30: return 30.0 + case .fps30d: return 30.0 + case .fps47_952: return 48.0 / 1.001 + case .fps48: return 48.0 + case .fps50: return 50.0 + case .fps59_94: return 60.0 / 1.001 + case .fps59_94d: return 60.0 / 1.001 + case .fps60: return 60.0 + case .fps60d: return 60.0 + case .fps95_904: return 96.0 / 1.001 + case .fps96: return 96.0 + case .fps100: return 100.0 + case .fps119_88: return 120.0 / 1.001 + case .fps119_88d: return 120.0 / 1.001 + case .fps120: return 120.0 + case .fps120d: return 120.0 } } /// Internal use. - internal var framesDroppedPerMinute: Double { + var framesDroppedPerMinute: Double { switch self { - case ._29_97_drop: return 2.0 - case ._30_drop: return 2.0 - case ._59_94_drop: return 4.0 - case ._60_drop: return 4.0 - case ._119_88_drop: return 8.0 - case ._120_drop: return 8.0 + case .fps29_97d: return 2.0 + case .fps30d: return 2.0 + case .fps59_94d: return 4.0 + case .fps60d: return 4.0 + case .fps119_88d: return 8.0 + case .fps120d: return 8.0 - case ._23_976, - ._24, - ._24_98, - ._25, - ._29_97, - ._30, - ._47_952, - ._48, - ._50, - ._59_94, - ._60, - ._95_904, - ._96, - ._100, - ._119_88, - ._120: + case .fps23_976, + .fps24, + .fps24_98, + .fps25, + .fps29_97, + .fps30, + .fps47_952, + .fps48, + .fps50, + .fps59_94, + .fps60, + .fps95_904, + .fps96, + .fps100, + .fps119_88, + .fps120: // this value is not actually used - // this is only here so that when adding frame rates to the framework, the compiler will throw an error to remind you to add the enum case here + // this is only here so that when adding frame rates to the framework, the compiler will throw an error to remind you to add the + // enum case here return 0.0 } } diff --git a/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate String Extensions.swift b/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate String Extensions.swift index a263b2a2..ecd57835 100644 --- a/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate String Extensions.swift +++ b/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate String Extensions.swift @@ -1,13 +1,13 @@ // // TimecodeFrameRate String Extensions.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // extension String { - /// Convenience method to call `TimecodeFrameRate(stringValue: self)` + /// Convenience method to call ``TimecodeFrameRate/init(stringValue:)``. @_disfavoredOverload - public var toTimecodeFrameRate: TimecodeFrameRate? { + public var timecodeFrameRate: TimecodeFrameRate? { TimecodeFrameRate(stringValue: self) } } diff --git a/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate.swift b/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate.swift index 557db44f..3476fb0b 100644 --- a/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate.swift +++ b/Sources/TimecodeKit/TimecodeFrameRate/TimecodeFrameRate.swift @@ -1,138 +1,188 @@ // // TimecodeFrameRate.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // // MARK: - FrameRate -/// Timecode frame rate. /// Industry-standard BITC (burn-in timecode) display rates. +/// Certain rates may be drop-frame or non-drop-frame. +/// +/// Timecode is way of encoding a frame number, therefore _timecode frame rate_ may be independent of _video frame rate_. +/// +/// Part of common confusion with timecode can arise from the use of “frames per second” in both the timecode and the actual video frame +/// rate. When used to describe timecode, frames per second represents how many frames of timecode are counted before one second of timecode +/// increments. When describing video frame rates, frames per second represents how many literal video frames are played back during the +/// span of one second of real wall-clock time. +/// +/// For example, 24p video typically uses 24 fps timecode rate. +/// However, 29.97p video may use 29.97 fps or 29.97-drop fps timecode rate depending on post-production facility requirements, and +/// timecode at these rates do not match wall-clock time exactly. +/// +/// Some video rates may correspond (or generally be compatible with) certain timecode rates and vice-versa. +/// To return a timecode rate's corresponding video rate, see ``videoFrameRate(interlaced:)``. +/// +/// ## Topics +/// +/// ### Rates +/// +/// - ``fps23_976`` +/// - ``fps24`` +/// - ``fps24_98`` +/// - ``fps25`` +/// - ``fps29_97`` +/// - ``fps29_97d`` +/// - ``fps30`` +/// - ``fps30d`` +/// - ``fps47_952`` +/// - ``fps48`` +/// - ``fps50`` +/// - ``fps59_94`` +/// - ``fps59_94d`` +/// - ``fps60`` +/// - ``fps60d`` +/// - ``fps95_904`` +/// - ``fps96`` +/// - ``fps100`` +/// - ``fps119_88`` +/// - ``fps119_88d`` +/// - ``fps120`` +/// - ``fps120d`` +/// +/// ### String Extensions +/// +/// - ``Swift/String/timecodeFrameRate`` +/// public enum TimecodeFrameRate: String, FrameRateProtocol { /// 23.976 fps (24/1.001) /// - /// Also known as 24p for HD video, sometimes rounded up to 23.98 fps. started out as the format for dealing with 24fps film in a NTSC post environment. - case _23_976 = "23.976" + /// Also known as 24p for HD video, sometimes rounded up to 23.98 fps. Started out as the format for dealing with 24fps film in a NTSC + /// post environment. + /// + /// This frame rate is used for film that is being transferred to NTSC video and must be slowed down for a 2-3 pull-down telecine + /// transfer. + case fps23_976 = "23.976" /// 24 fps /// - /// (film, ATSC, 2k, 4k, 6k) - case _24 = "24" + /// The true speed of standard film cameras. (Film, ATSC, 2k, 4k, 6k) + case fps24 = "24" /// 24.98 fps (25/1.001) /// - /// This frame rate is commonly used to facilitate transfers between PAL and NTSC video and film sources. It is mostly used to compensate for some error. - case _24_98 = "24.98" + /// This frame rate is commonly used to facilitate transfers between PAL and NTSC video and film sources. It is mostly used to + /// compensate for some error. + case fps24_98 = "24.98" /// 25 fps /// - /// (PAL, used in Europe, Uruguay, Argentina, Australia), SECAM, DVB, ATSC) - case _25 = "25" + /// PAL video used in Europe, Uruguay, Argentina, Australia. (SECAM, DVB, ATSC) + case fps25 = "25" /// 29.97 fps (30/1.001) /// - /// (NTSC American System (US, Canada, Mexico, Colombia, etc.), ATSC, PAL-M (Brazil)) - case _29_97 = "29.97" + /// NTSC video used in the US, Canada, Mexico, Colombia, etc. (ATSC, PAL-M Brazil) + case fps29_97 = "29.97" /// 29.97 fps drop - case _29_97_drop = "29.97d" + /// + /// NTSC video used in the US, Canada, Mexico, Colombia, etc. (ATSC, PAL-M Brazil) + case fps29_97d = "29.97d" /// 30 fps /// - /// (ATSC) This is the frame count of NTSC broadcast video. However, the actual frame rate or speed of the video format runs at 29.97 fps. + /// This frame rate is not a true video standard anymore but is sometimes used in music recording. Decades ago, it was the black and + /// white NTSC broadcast standard. It is equal to NTSC video being pulled up to film speed after a 2-3 telecine transfer. /// /// This timecode clock does not run in realtime. It is slightly slower by 0.1%. - /// ie: 1:00:00:00:00 (1 day/24 hours) at 30 fps is approx 1:00:00:00;02 in 29.97df - case _30 = "30" + /// ie: 1:00:00:00:00 (1 day/24 hours) at 30 fps is approx 1:00:00;02 in 29.97df + case fps30 = "30" /// 30 fps drop /// - /// The 30 fps drop count is an adaptation that allows a timecode display running at 29.97 fps to actually show the clock-on-the-wall-time of the timeline by “dropping” or skipping specific frame numbers in order to “catch the clock up” to realtime. - /// - /// - Warning: This is not a video frame rate - it is a display rate only. - case _30_drop = "30d" + /// This is an adaptation that allows a timecode display running at 29.97 fps to actually show the wall-clock time of the timeline by + /// “dropping” or skipping specific frame numbers in order to “catch the clock up” to realtime. + case fps30d = "30d" /// 47.952 (48/1.001) /// /// Double 23.976 fps - case _47_952 = "47.952" + case fps47_952 = "47.952" /// 48 fps /// - /// Double 24 fps - case _48 = "48" + /// Double 24 fps. + case fps48 = "48" /// 50 fps /// /// Double 25 fps - case _50 = "50" + case fps50 = "50" /// 59.94 fps (60/1.001) /// - /// Double 29.97 fps + /// Double 29.97 fps. /// /// This video frame rate is supported by high definition cameras and is compatible with NTSC (29.97 fps). - case _59_94 = "59.94" + case fps59_94 = "59.94" /// 59.94 fps drop /// /// Double 29.97 fps drop - case _59_94_drop = "59.94d" + case fps59_94d = "59.94d" /// 60 fps /// - /// Double 30 fps + /// Double 30 fps. /// - /// This video frame rate is supported by many high definition cameras. However, the NTSC compatible 59.94 fps frame rate is much more common. - case _60 = "60" + /// This video frame rate is supported by many high definition cameras. However, the NTSC compatible 59.94 fps frame rate is much more + /// common. + case fps60 = "60" /// 60 fps drop /// - /// Double 30 fps + /// Double 30 fps. /// /// See the description for 30 drop for more info. - /// - /// - Warning: This is not a video frame rate - it is a display rate only. - case _60_drop = "60d" + case fps60d = "60d" /// 95.904 fps (96/1.001) /// /// Double 47.952 fps / quadruple 23.976 fps - case _95_904 = "95.904" + case fps95_904 = "95.904" /// 96 fps /// - /// Double 48 fps / quadruple 24 fps - case _96 = "96" + /// Double 48 fps / quadruple 24 fps. + case fps96 = "96" /// 100 fps /// - /// Double 50 fps / quadruple 25 fps - case _100 = "100" + /// Double 50 fps / quadruple 25 fps. + case fps100 = "100" /// 119.88 fps (120/1.001) /// - /// Double 59.94 fps / quadruple 29.97 fps - case _119_88 = "119.88" + /// Double 59.94 fps / quadruple 29.97 fps. + case fps119_88 = "119.88" /// 119.88 fps drop /// - /// Double 59.94 fps drop / quadruple 29.97 fps drop - case _119_88_drop = "119.88d" + /// Double 59.94 fps drop / quadruple 29.97 fps drop. + case fps119_88d = "119.88d" /// 120 fps /// - /// Double 60 fps / quadruple 30 fps - case _120 = "120" + /// Double 60 fps / quadruple 30 fps. + case fps120 = "120" /// 120 fps drop /// - /// Double 60 fps drop / quadruple 30 fps drop + /// Double 60 fps drop / quadruple 30 fps drop. /// /// See the description for 30 drop for more info. - /// - /// - Warning: This is not a video frame rate - it is a display rate only. - case _120_drop = "120d" + case fps120d = "120d" } extension TimecodeFrameRate: CaseIterable { diff --git a/Sources/TimecodeKit/TimecodeInterval/TimecodeInterval Rational CMTime.swift b/Sources/TimecodeKit/TimecodeInterval/TimecodeInterval Rational CMTime.swift index 0e9d209c..fde973d0 100644 --- a/Sources/TimecodeKit/TimecodeInterval/TimecodeInterval Rational CMTime.swift +++ b/Sources/TimecodeKit/TimecodeInterval/TimecodeInterval Rational CMTime.swift @@ -1,13 +1,13 @@ // // TimecodeInterval Rational CMTime.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if canImport(CoreMedia) -import Foundation import CoreMedia +import Foundation @available(macOS 10.7, iOS 4.0, tvOS 9.0, watchOS 6.0, *) extension TimecodeInterval { @@ -17,19 +17,30 @@ extension TimecodeInterval { /// - Throws: ``Timecode/ValidationError`` public init( _ cmTime: CMTime, - at rate: TimecodeFrameRate, - limit: Timecode.UpperLimit = ._24hours, + at frameRate: TimecodeFrameRate, base: Timecode.SubFramesBase = .default(), - format: Timecode.StringFormat = .default() + limit: Timecode.UpperLimit = .max24Hours ) throws { let fraction = Fraction(cmTime) - try self.init(fraction, at: rate, limit: limit, base: base, format: format) + try self.init(fraction, at: frameRate, base: base, limit: limit) } /// Returns the rational fraction for the timecode interval as `CMTime`. - public var cmTime: CMTime { + public var cmTimeValue: CMTime { CMTime(rationalValue) } + + /// Initialize from a time duration represented as a rational fraction. + /// A negative fraction will produce a negative time interval. + /// + /// - Throws: ``Timecode/ValidationError`` + public init( + _ cmTime: CMTime, + using properties: Timecode.Properties + ) throws { + let fraction = Fraction(cmTime) + try self.init(fraction, using: properties) + } } @available(macOS 10.7, iOS 4.0, tvOS 9.0, watchOS 6.0, *) @@ -39,19 +50,23 @@ extension CMTime { /// A negative fraction will produce a negative time interval. /// /// - Throws: ``Timecode/ValidationError`` - public func toTimecodeInterval( - at rate: TimecodeFrameRate, - limit: Timecode.UpperLimit = ._24hours, + public func timecodeInterval( + at frameRate: TimecodeFrameRate, base: Timecode.SubFramesBase = .default(), - format: Timecode.StringFormat = .default() + limit: Timecode.UpperLimit = .max24Hours + ) throws -> TimecodeInterval { + try TimecodeInterval(self, at: frameRate, base: base, limit: limit) + } + + /// Convenience function to initialize a `TimecodeInterval` instance from a time duration + /// represented as a rational fraction. + /// A negative fraction will produce a negative time interval. + /// + /// - Throws: ``Timecode/ValidationError`` + public func timecodeInterval( + using properties: Timecode.Properties ) throws -> TimecodeInterval { - try TimecodeInterval( - self, - at: rate, - limit: limit, - base: base, - format: format - ) + try TimecodeInterval(self, using: properties) } } diff --git a/Sources/TimecodeKit/TimecodeInterval/TimecodeInterval Rational.swift b/Sources/TimecodeKit/TimecodeInterval/TimecodeInterval Rational.swift index 788b07a6..600052f4 100644 --- a/Sources/TimecodeKit/TimecodeInterval/TimecodeInterval Rational.swift +++ b/Sources/TimecodeKit/TimecodeInterval/TimecodeInterval Rational.swift @@ -1,7 +1,7 @@ // // TimecodeInterval Rational.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // import Foundation @@ -16,21 +16,33 @@ extension TimecodeInterval { /// - Throws: ``Timecode/ValidationError`` public init( _ rational: Fraction, - at rate: TimecodeFrameRate, - limit: Timecode.UpperLimit = ._24hours, + at frameRate: TimecodeFrameRate, base: Timecode.SubFramesBase = .default(), - format: Timecode.StringFormat = .default() + limit: Timecode.UpperLimit = .max24Hours ) throws { let neg = rational.isNegative let absRational = rational.abs() - let absTimecode = try Timecode( - absRational, - at: rate, - limit: limit, - base: base, - format: format - ) + let absTimecode = try Timecode(.rational(absRational), at: frameRate, base: base, limit: limit) + + self.init(absTimecode, neg ? .minus : .plus) + } + + /// Initialize from a time duration represented as a rational fraction. + /// A negative fraction will produce a negative time interval. + /// + /// - Note: The fraction is treated as an absolute value regardless of whether it is negative or + /// positive. The sign simply determines whether the interval is negative or positive. + /// + /// - Throws: ``Timecode/ValidationError`` + public init( + _ rational: Fraction, + using properties: Timecode.Properties + ) throws { + let neg = rational.isNegative + let absRational = rational.abs() + + let absTimecode = try Timecode(.rational(absRational), using: properties) self.init(absTimecode, neg ? .minus : .plus) } @@ -52,18 +64,25 @@ extension Fraction { /// positive. The sign simply determines whether the interval is positive or negative. /// /// - Throws: ``Timecode/ValidationError`` - public func toTimecodeInterval( - at rate: TimecodeFrameRate, - limit: Timecode.UpperLimit = ._24hours, + public func timecodeInterval( + at frameRate: TimecodeFrameRate, base: Timecode.SubFramesBase = .default(), - format: Timecode.StringFormat = .default() + limit: Timecode.UpperLimit = .max24Hours + ) throws -> TimecodeInterval { + try TimecodeInterval(self, at: frameRate, base: base, limit: limit) + } + + /// Convenience function to initialize a `TimecodeInterval` instance from a time duration + /// represented as a rational fraction. + /// A negative fraction will produce a negative time interval. + /// + /// - Note: The fraction is treated as an absolute value regardless of whether it is negative or + /// positive. The sign simply determines whether the interval is positive or negative. + /// + /// - Throws: ``Timecode/ValidationError`` + public func timecodeInterval( + using properties: Timecode.Properties ) throws -> TimecodeInterval { - try TimecodeInterval( - self, - at: rate, - limit: limit, - base: base, - format: format - ) + try TimecodeInterval(self, using: properties) } } diff --git a/Sources/TimecodeKit/TimecodeInterval/TimecodeInterval Unary Operators.swift b/Sources/TimecodeKit/TimecodeInterval/TimecodeInterval Unary Operators.swift index 2133a8c7..46f16583 100644 --- a/Sources/TimecodeKit/TimecodeInterval/TimecodeInterval Unary Operators.swift +++ b/Sources/TimecodeKit/TimecodeInterval/TimecodeInterval Unary Operators.swift @@ -1,18 +1,18 @@ // // TimecodeInterval Unary Operators.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // import Foundation extension Timecode { - /// Returns self as a negative `TimecodeInterval`. + /// Returns self as a negative ``TimecodeInterval``. public static prefix func - (operand: Self) -> TimecodeInterval { TimecodeInterval(operand, .minus) } - /// Returns self as a positive `TimecodeInterval`. + /// Returns self as a positive ``TimecodeInterval``. public static prefix func + (operand: Self) -> TimecodeInterval { TimecodeInterval(operand, .plus) } diff --git a/Sources/TimecodeKit/TimecodeInterval/TimecodeInterval.swift b/Sources/TimecodeKit/TimecodeInterval/TimecodeInterval.swift index bfe90806..67be8904 100644 --- a/Sources/TimecodeKit/TimecodeInterval/TimecodeInterval.swift +++ b/Sources/TimecodeKit/TimecodeInterval/TimecodeInterval.swift @@ -1,12 +1,13 @@ // // TimecodeInterval.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // import Foundation /// Represents an interval duration of timecode, either positive or negative. +/// See the topic for details. public struct TimecodeInterval: Equatable, Hashable { /// The interval's absolute distance, stripping sign negation if present. /// The ``isNegative`` property determines the delta direction of the interval. @@ -32,7 +33,7 @@ public struct TimecodeInterval: Equatable, Hashable { sign == .minus } - /// Returns real-time (wall-clock time) equivalent of the interval time. + /// Returns the interval time as real-time (wall-clock time) in seconds. /// Expressed as either a positive or negative number. public var realTimeValue: TimeInterval { switch sign { @@ -44,7 +45,8 @@ public struct TimecodeInterval: Equatable, Hashable { } } - /// Flattens the interval and returns it expressed as valid timecode, wrapping as necessary based on the ``Timecode/upperLimit-swift.property`` of the interval. + /// Flattens the interval and returns it expressed as valid timecode, wrapping as necessary based on the + /// ``Timecode/upperLimit-swift.property`` of the interval. /// /// If the interval is already valid timecode and the sign is positive, the interval is returned as-is. public func flattened() -> Timecode { @@ -53,17 +55,18 @@ public struct TimecodeInterval: Equatable, Hashable { switch sign { case .plus: - return absoluteInterval.adding(wrapping: TCC()) + return absoluteInterval + .adding(Timecode.Components(), by: .wrapping) case .minus: return Timecode( - rawValues: TCC(f: 0), + .components(.zero), at: absoluteInterval.frameRate, - limit: absoluteInterval.upperLimit, base: absoluteInterval.subFramesBase, - format: absoluteInterval.stringFormat + limit: absoluteInterval.upperLimit, + by: .allowingInvalid ) - .subtracting(wrapping: absoluteInterval.components) + .subtracting(absoluteInterval.components, by: .wrapping) } } @@ -71,7 +74,7 @@ public struct TimecodeInterval: Equatable, Hashable { /// Internal: /// Returns a `Timecode` value offsetting it by the interval, wrapping around lower/upper timecode limit bounds if necessary. - internal func timecode(offsetting base: Timecode) -> Timecode { + func timecode(offsetting base: Timecode) -> Timecode { switch sign { case .plus: return base + absoluteInterval @@ -96,7 +99,7 @@ extension TimecodeInterval: CustomStringConvertible, CustomDebugStringConvertibl } public var verboseDescription: String { - "TimecodeInterval \(description) @ \(absoluteInterval.frameRate.stringValue)" + "TimecodeInterval \(description) @ \(absoluteInterval.frameRate.stringValueVerbose)" } } diff --git a/Sources/TimecodeKit/TimecodeKit.swift b/Sources/TimecodeKit/TimecodeKit.swift index 36a95fbf..57061b64 100644 --- a/Sources/TimecodeKit/TimecodeKit.swift +++ b/Sources/TimecodeKit/TimecodeKit.swift @@ -1,7 +1,7 @@ // // TimecodeKit.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // // Welcome to TimecodeKit :) diff --git a/Sources/TimecodeKit/TimecodeTransformer/TimecodeTransformer.swift b/Sources/TimecodeKit/TimecodeTransformer/TimecodeTransformer.swift index 940da180..8d1ed6f3 100644 --- a/Sources/TimecodeKit/TimecodeTransformer/TimecodeTransformer.swift +++ b/Sources/TimecodeKit/TimecodeTransformer/TimecodeTransformer.swift @@ -1,10 +1,11 @@ // // TimecodeTransformer.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // /// A timecode transformer containing one or more transform rules in series. +/// See the topic for details. public struct TimecodeTransformer { public var transforms: [Transform] diff --git a/Sources/TimecodeKit/Utilities/Fraction CMTime.swift b/Sources/TimecodeKit/Utilities/Fraction CMTime.swift index 596ff28b..f62c35ed 100644 --- a/Sources/TimecodeKit/Utilities/Fraction CMTime.swift +++ b/Sources/TimecodeKit/Utilities/Fraction CMTime.swift @@ -1,13 +1,13 @@ // // Fraction CMTime.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if canImport(CoreMedia) -import Foundation import CoreMedia +import Foundation @available(macOS 10.7, iOS 4.0, tvOS 9.0, watchOS 6.0, *) extension Fraction { @@ -24,7 +24,8 @@ extension Fraction { } /// Returns the fraction as a new `CMTime` instance. - public func toCMTime() -> CMTime { + @_disfavoredOverload + public var cmTimeValue: CMTime { CMTime(self) } } @@ -39,7 +40,8 @@ extension CMTime { } /// Returns the fraction as a new ``Fraction`` instance. - public func toFraction() -> Fraction { + @_disfavoredOverload + public var fractionValue: Fraction { Fraction(self) } } diff --git a/Sources/TimecodeKit/Utilities/Fraction.swift b/Sources/TimecodeKit/Utilities/Fraction.swift index 3a3b03a6..fcccbfca 100644 --- a/Sources/TimecodeKit/Utilities/Fraction.swift +++ b/Sources/TimecodeKit/Utilities/Fraction.swift @@ -1,12 +1,14 @@ // // Fraction.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // import Foundation /// Numerical fraction containing a numerator and a denominator. +/// +/// Used to convert to/from ``Timecode``, Core Media `CMTime`, or metadata encoding such as Final Cut Pro XML or AAF. public struct Fraction { public let numerator: Int public let denominator: Int @@ -60,12 +62,12 @@ extension Fraction: Equatable { /// Performs a comparison against literal values. public static func == (lhs: Self, rhs: Self) -> Bool { lhs.numerator == rhs.numerator - && lhs.denominator == rhs.denominator + && lhs.denominator == rhs.denominator } /// Returns `true` if both fractions are mathematically equal (can reduce to the same values). public func isEqual(to other: Self) -> Bool { - let lhsReduced = self.reduced().normalized() + let lhsReduced = reduced().normalized() let rhsReduced = other.reduced().normalized() return lhsReduced == rhsReduced @@ -92,14 +94,14 @@ extension Fraction { /// (Returns unmodified if positive, negates if negative.) /// The fraction is also normalized. public func abs() -> Self { - let norm = self.normalized() + let norm = normalized() return isNegative ? norm.negated() : norm } /// Internal: /// Reduce a fraction to its simplest form. /// This also normalizes signs. - internal static func reduce(n: Int, d: Int) -> (n: Int, d: Int) { + static func reduce(n: Int, d: Int) -> (n: Int, d: Int) { let (absN, signN) = n < 0 ? (-n, -1) : (n, 1) let (absD, signD) = d < 0 ? (-d, -1) : (d, 1) var v = n @@ -124,7 +126,7 @@ extension Fraction { /// Normalize a fraction. /// Fractions with two negative signs are normalized to two positive signs. /// Fractions with negative denominator are normalized to negative numerator and positive denominator. - internal static func normalize(n: Int, d: Int) -> (n: Int, d: Int) { + static func normalize(n: Int, d: Int) -> (n: Int, d: Int) { var n = n var d = d if n >= 0 && d >= 0 { return (n: n, d: d) } @@ -167,7 +169,7 @@ extension Double { /// - Parameters: /// - precision: Number of places after the decimal to preserve. /// - Returns: Numerator and denominator. - internal func rational( + func rational( precision: Int = 10 ) -> Fraction { let pad = Int(truncating: pow(10, precision) as NSNumber) diff --git a/Sources/TimecodeKit/Utilities/Outsourced/Decimal.swift b/Sources/TimecodeKit/Utilities/Outsourced/Decimal.swift index 090402fb..ddce2670 100644 --- a/Sources/TimecodeKit/Utilities/Outsourced/Decimal.swift +++ b/Sources/TimecodeKit/Utilities/Outsourced/Decimal.swift @@ -97,7 +97,8 @@ extension Decimal { /// Returns both integral part and fractional part. /// - /// - Note: This method is more computationally efficient than calling both `.integral` and .`fraction` properties separately unless you only require one or the other. + /// - Note: This method is more computationally efficient than calling both `.integral` and .`fraction` properties separately unless you + /// only require one or the other. @_disfavoredOverload var integralAndFraction: (integral: Self, fraction: Self) { let integral = truncated(decimalPlaces: 0) diff --git a/Sources/TimecodeKit/Utilities/Outsourced/FloatingPoint and Darwin.swift b/Sources/TimecodeKit/Utilities/Outsourced/FloatingPoint and Darwin.swift index f165c1c3..847748ef 100644 --- a/Sources/TimecodeKit/Utilities/Outsourced/FloatingPoint and Darwin.swift +++ b/Sources/TimecodeKit/Utilities/Outsourced/FloatingPoint and Darwin.swift @@ -77,7 +77,7 @@ extension FloatingPoint where Self: FloatingPointPowerComputable { /// Truncates decimal places to `decimalPlaces` number of decimal places. /// - /// If `decimalPlaces` <= 0, then `trunc(self)` is returned. + /// If `decimalPlaces <= 0`, then `trunc(self)` is returned. @_disfavoredOverload func truncated(decimalPlaces: Int) -> Self { if decimalPlaces < 1 { @@ -101,7 +101,8 @@ extension FloatingPoint { /// Returns both integral part and fractional part. /// - /// - Note: This method is more computationally efficient than calling both `.integral` and .`fraction` properties separately unless you only require one or the other. + /// - Note: This method is more computationally efficient than calling both `.integral` and .`fraction` properties separately unless you + /// only require one or the other. /// /// This method can result in a non-trivial loss of precision for the fractional part. @_disfavoredOverload diff --git a/Sources/TimecodeKit/Utilities/Outsourced/Integers.swift b/Sources/TimecodeKit/Utilities/Outsourced/Integers.swift index 59067065..cd0793ab 100644 --- a/Sources/TimecodeKit/Utilities/Outsourced/Integers.swift +++ b/Sources/TimecodeKit/Utilities/Outsourced/Integers.swift @@ -20,7 +20,7 @@ extension BinaryInteger { /// - for the integer 10, this would return 2 /// - for the integer 250, this would return 3 @_disfavoredOverload - public var numberOfDigits: Int { + var numberOfDigits: Int { if self < 10 && self >= 0 || self > -10 && self < 0 { return 1 } else { diff --git a/Sources/TimecodeKit/Utilities/RangeAttribute.swift b/Sources/TimecodeKit/Utilities/RangeAttribute.swift new file mode 100644 index 00000000..16e5c2cc --- /dev/null +++ b/Sources/TimecodeKit/Utilities/RangeAttribute.swift @@ -0,0 +1,12 @@ +// +// RangeAttribute.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + +/// An individual time attribute of a time range. +public enum RangeAttribute { + case start + case end + case duration +} diff --git a/Sources/TimecodeKit/Utilities/URL.swift b/Sources/TimecodeKit/Utilities/URL.swift index a7d8d2bc..b96345b4 100644 --- a/Sources/TimecodeKit/Utilities/URL.swift +++ b/Sources/TimecodeKit/Utilities/URL.swift @@ -1,7 +1,7 @@ // // URL.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // import Foundation diff --git a/Sources/TimecodeKit/VideoFrameRate/VideoFrameRate Conversions.swift b/Sources/TimecodeKit/VideoFrameRate/VideoFrameRate Conversions.swift index 14b70fe0..45da9364 100644 --- a/Sources/TimecodeKit/VideoFrameRate/VideoFrameRate Conversions.swift +++ b/Sources/TimecodeKit/VideoFrameRate/VideoFrameRate Conversions.swift @@ -1,38 +1,39 @@ // // VideoFrameRate Conversions.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // import Foundation extension VideoFrameRate { /// Returns the corresponding ``TimecodeFrameRate`` case. + /// Returns `nil` if there is no corresponding timecode rate. /// /// - Parameters: /// - drop: Whether timecode frame rate is drop or non-drop. public func timecodeFrameRate(drop: Bool) -> TimecodeFrameRate? { switch self { - case ._23_98p: return drop ? nil : ._23_976 - case ._24p: return drop ? nil : ._24 - case ._25p: return drop ? nil : ._25 - case ._25i: return drop ? nil : ._25 - case ._29_97p: return drop ? ._29_97_drop : ._29_97 - case ._29_97i: return drop ? ._29_97_drop : ._29_97 - case ._30p: return drop ? ._30_drop : ._30 - case ._47_95p: return drop ? nil : ._47_952 - case ._48p: return drop ? nil : ._48 - case ._50p: return drop ? nil : ._50 - case ._50i: return drop ? nil : ._50 - case ._59_94p: return drop ? ._59_94_drop : ._59_94 - case ._59_94i: return drop ? ._59_94_drop : ._59_94 - case ._60p: return drop ? ._60_drop : ._60 - case ._60i: return drop ? nil : ._60 - case ._95_9p: return drop ? nil : ._95_904 - case ._96p: return drop ? nil : ._96 - case ._100p: return drop ? nil : ._100 - case ._119_88p: return drop ? ._119_88_drop : ._119_88 - case ._120p: return drop ? nil : ._120 + case .fps23_98p: return drop ? nil : .fps23_976 + case .fps24p: return drop ? nil : .fps24 + case .fps25p: return drop ? nil : .fps25 + case .fps25i: return drop ? nil : .fps25 + case .fps29_97p: return drop ? .fps29_97d : .fps29_97 + case .fps29_97i: return drop ? .fps29_97d : .fps29_97 + case .fps30p: return drop ? .fps30d : .fps30 + case .fps47_95p: return drop ? nil : .fps47_952 + case .fps48p: return drop ? nil : .fps48 + case .fps50p: return drop ? nil : .fps50 + case .fps50i: return drop ? nil : .fps50 + case .fps59_94p: return drop ? .fps59_94d : .fps59_94 + case .fps59_94i: return drop ? .fps59_94d : .fps59_94 + case .fps60p: return drop ? .fps60d : .fps60 + case .fps60i: return drop ? nil : .fps60 + case .fps95_9p: return drop ? nil : .fps95_904 + case .fps96p: return drop ? nil : .fps96 + case .fps100p: return drop ? nil : .fps100 + case .fps119_88p: return drop ? .fps119_88d : .fps119_88 + case .fps120p: return drop ? nil : .fps120 } } @@ -56,12 +57,24 @@ extension VideoFrameRate { /// - interlaced: `true` for interlaced, `false` for progressive. /// - strict: Enforces 3 decimal places of precision if `true` otherwise 1 decimal place. public init?(fps: Double, interlaced: Bool = false, strict: Bool = false) { - let findMatches = Self.allCases.filter { - $0.fps.truncated(decimalPlaces: strict ? 3 : 1) - == fps.truncated(decimalPlaces: strict ? 3 : 1) + let decimalPlaces = strict ? 3 : 1 + + // first try truncating decimal places + let fpsTruncated = fps.truncated(decimalPlaces: decimalPlaces) + let truncMatches = Self.allCases.filter { + $0.fps.truncated(decimalPlaces: decimalPlaces) == fpsTruncated + } + if let firstMatch = truncMatches.first(where: { $0.isInterlaced == interlaced }) { + self = firstMatch + return } - if let firstMatch = findMatches.first(where: { $0.isInterlaced == interlaced }) { + // then try rounding decimal places to loosen up requirements + let fpsRounded = fps.rounded(decimalPlaces: decimalPlaces) + let roundMatches = Self.allCases.filter { + $0.fps.rounded(decimalPlaces: decimalPlaces) == fpsRounded + } + if let firstMatch = roundMatches.first(where: { $0.isInterlaced == interlaced }) { self = firstMatch return } @@ -123,7 +136,7 @@ import CoreMedia extension VideoFrameRate { /// Initialize from a frame rate (fps) expressed as a rational number (fraction). /// - /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to communicate + /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to represent /// times and durations. /// /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in @@ -142,7 +155,7 @@ extension VideoFrameRate { /// Initialize from a frame rate's frame duration expressed as a rational number (fraction). /// - /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to communicate + /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to represent /// times and durations. /// /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in @@ -159,39 +172,7 @@ extension VideoFrameRate { ) } - /// Returns the frame rate (fps) as a rational number (fraction) - /// as a CoreMedia `CMTime` instance. - /// - /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to communicate - /// times and durations. - /// - /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in - /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate - /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as - /// fractions.) - public var rateCMTime: CMTime { - CMTime( - value: CMTimeValue(rate.numerator), - timescale: CMTimeScale(rate.denominator) - ) - } - - /// Returns the duration of 1 frame as a rational number (fraction) - /// as a CoreMedia `CMTime` instance. - /// - /// - Note: Many AVFoundation and Core Media objects utilize `CMTime` as a way to communicate - /// times and durations. - /// - /// - Note: Some file formats encode video frame rate and/or time locations (timecode) in - /// rational number notation: a fraction of two whole number integers. (AAF encodes video rate - /// this way, whereas FCPXML (Final Cut Pro) encodes both video rate and time locations as - /// fractions.) - public var frameDurationCMTime: CMTime { - CMTime( - value: CMTimeValue(frameDuration.numerator), - timescale: CMTimeScale(frameDuration.denominator) - ) - } + // NOTE: `rateCMTime` and `frameDurationCMTime` properties are implemented on `FrameRateProtocol` } #endif diff --git a/Sources/TimecodeKit/VideoFrameRate/VideoFrameRate Properties.swift b/Sources/TimecodeKit/VideoFrameRate/VideoFrameRate Properties.swift index d5b4866b..23dd16b8 100644 --- a/Sources/TimecodeKit/VideoFrameRate/VideoFrameRate Properties.swift +++ b/Sources/TimecodeKit/VideoFrameRate/VideoFrameRate Properties.swift @@ -1,7 +1,7 @@ // // VideoFrameRate Properties.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // // MARK: stringValue @@ -10,30 +10,30 @@ extension VideoFrameRate { /// Returns human-readable frame rate string. public var stringValue: String { switch self { - case ._23_98p: return "23.98p" - case ._24p: return "24p" - case ._25p: return "25p" - case ._25i: return "25i" - case ._29_97p: return "29.97p" - case ._29_97i: return "29.97i" - case ._30p: return "30p" - case ._47_95p: return "47.95p" - case ._48p: return "48p" - case ._50p: return "50p" - case ._50i: return "50i" - case ._59_94p: return "59.94p" - case ._59_94i: return "59.94i" - case ._60p: return "60p" - case ._60i: return "60i" - case ._95_9p: return "95.9p" - case ._96p: return "96" - case ._100p: return "100p" - case ._119_88p: return "119.88pp" - case ._120p: return "120p" + case .fps23_98p: return "23.98p" + case .fps24p: return "24p" + case .fps25p: return "25p" + case .fps25i: return "25i" + case .fps29_97p: return "29.97p" + case .fps29_97i: return "29.97i" + case .fps30p: return "30p" + case .fps47_95p: return "47.95p" + case .fps48p: return "48p" + case .fps50p: return "50p" + case .fps50i: return "50i" + case .fps59_94p: return "59.94p" + case .fps59_94i: return "59.94i" + case .fps60p: return "60p" + case .fps60i: return "60i" + case .fps95_9p: return "95.9p" + case .fps96p: return "96" + case .fps100p: return "100p" + case .fps119_88p: return "119.88pp" + case .fps120p: return "120p" } } - /// Initializes from a ``stringValue`` string. Case-sensitive. + /// Initializes from the human-readable ``stringValue`` string. Case-sensitive. public init?(stringValue: String) { if let findMatch = Self.allCases .first(where: { $0.stringValue == stringValue }) @@ -54,33 +54,35 @@ extension VideoFrameRate { /// rate (and not a video rate), its status as a drop or non-drop rate must be stored /// independently and recalled. /// - /// // == frame rate - /// Double(numerator) / Double(denominator) + /// ```swift + /// // == frame rate + /// Double(numerator) / Double(denominator) /// - /// // == duration of 1 frame in seconds - /// Double(denominator) / Double(numerator) + /// // == duration of 1 frame in seconds + /// Double(denominator) / Double(numerator) + /// ``` public var rate: Fraction { switch self { - case ._23_98p: return Fraction(24000, 1001) - case ._24p: return Fraction(24, 1) - case ._25p: return Fraction(25, 1) - case ._25i: return Fraction(25, 1) - case ._29_97p: return Fraction(30000, 1001) - case ._29_97i: return Fraction(30000, 1001) - case ._30p: return Fraction(30, 1) - case ._47_95p: return Fraction(48000, 1001) - case ._48p: return Fraction(48, 1) - case ._50p: return Fraction(50, 1) - case ._50i: return Fraction(50, 1) - case ._59_94p: return Fraction(60000, 1001) - case ._59_94i: return Fraction(60000, 1001) - case ._60p: return Fraction(60, 1) - case ._60i: return Fraction(60, 1) - case ._95_9p: return Fraction(96000, 1001) - case ._96p: return Fraction(96, 1) - case ._100p: return Fraction(100, 1) - case ._119_88p: return Fraction(120000, 1001) - case ._120p: return Fraction(120, 1) + case .fps23_98p: return Fraction(24000, 1001) + case .fps24p: return Fraction(24, 1) + case .fps25p: return Fraction(25, 1) + case .fps25i: return Fraction(25, 1) + case .fps29_97p: return Fraction(30000, 1001) + case .fps29_97i: return Fraction(30000, 1001) + case .fps30p: return Fraction(30, 1) + case .fps47_95p: return Fraction(48000, 1001) + case .fps48p: return Fraction(48, 1) + case .fps50p: return Fraction(50, 1) + case .fps50i: return Fraction(50, 1) + case .fps59_94p: return Fraction(60000, 1001) + case .fps59_94i: return Fraction(60000, 1001) + case .fps60p: return Fraction(60, 1) + case .fps60i: return Fraction(60, 1) + case .fps95_9p: return Fraction(96000, 1001) + case .fps96p: return Fraction(96, 1) + case .fps100p: return Fraction(100, 1) + case .fps119_88p: return Fraction(120000, 1001) + case .fps120p: return Fraction(120, 1) } } @@ -94,51 +96,51 @@ extension VideoFrameRate { /// Potentially compatible outside of that range but untested. public var frameDuration: Fraction { switch self { - case ._23_98p: return Fraction(1001, 24000) // confirmed in FCP XML - case ._24p: return Fraction(100, 2400) // confirmed in FCP XML - case ._25p: return Fraction(100, 2500) // confirmed in FCP XML - case ._25i: return Fraction(200, 5000) // confirmed in FCP XML - case ._29_97p: return Fraction(1001, 30000) // confirmed in FCP XML - case ._29_97i: return Fraction(2002, 60000) // confirmed in FCP XML & QT tc track - case ._30p: return Fraction(100, 3000) // confirmed in FCP XML - case ._47_95p: return Fraction(1001, 48000) // inferred - case ._48p: return Fraction(100, 4800) // inferred - case ._50p: return Fraction(100, 5000) // confirmed in FCP XML - case ._50i: return Fraction(200, 10000) // inferred - case ._59_94p: return Fraction(1001, 60000) // confirmed in FCP XML - case ._59_94i: return Fraction(2002, 120000) // inferred - case ._60p: return Fraction(100, 6000) // confirmed in FCP XML - case ._60i: return Fraction(200, 12000) // inferred - case ._95_9p: return Fraction(1001, 96000) // inferred - case ._96p: return Fraction(100, 9600) // inferred - case ._100p: return Fraction(100, 10000) // inferred - case ._119_88p: return Fraction(1001, 120000) // inferred - case ._120p: return Fraction(100, 12000) // inferred + case .fps23_98p: return Fraction(1001, 24000) // confirmed in FCP XML + case .fps24p: return Fraction(100, 2400) // confirmed in FCP XML + case .fps25p: return Fraction(100, 2500) // confirmed in FCP XML + case .fps25i: return Fraction(200, 5000) // confirmed in FCP XML + case .fps29_97p: return Fraction(1001, 30000) // confirmed in FCP XML + case .fps29_97i: return Fraction(2002, 60000) // confirmed in FCP XML & QT tc track + case .fps30p: return Fraction(100, 3000) // confirmed in FCP XML + case .fps47_95p: return Fraction(1001, 48000) // inferred + case .fps48p: return Fraction(100, 4800) // inferred + case .fps50p: return Fraction(100, 5000) // confirmed in FCP XML + case .fps50i: return Fraction(200, 10000) // inferred + case .fps59_94p: return Fraction(1001, 60000) // confirmed in FCP XML + case .fps59_94i: return Fraction(2002, 120000) // inferred + case .fps60p: return Fraction(100, 6000) // confirmed in FCP XML + case .fps60i: return Fraction(200, 12000) // inferred + case .fps95_9p: return Fraction(1001, 96000) // inferred + case .fps96p: return Fraction(100, 9600) // inferred + case .fps100p: return Fraction(100, 10000) // inferred + case .fps119_88p: return Fraction(1001, 120000) // inferred + case .fps120p: return Fraction(100, 12000) // inferred } } public var alternateFrameDuration: Fraction? { switch self { - case ._23_98p: return Fraction(1000, 23976) - case ._24p: return nil - case ._25p: return nil - case ._25i: return nil - case ._29_97p: return Fraction(1000, 29970) - case ._29_97i: return Fraction(2000, 59940) // inferred - case ._30p: return nil - case ._47_95p: return Fraction(1000, 47952) - case ._48p: return nil - case ._50p: return nil - case ._50i: return nil - case ._59_94p: return Fraction(1000, 59940) - case ._59_94i: return Fraction(2000, 119880) // inferred - case ._60p: return nil - case ._60i: return nil - case ._95_9p: return Fraction(1000, 95904) - case ._96p: return nil - case ._100p: return nil - case ._119_88p: return Fraction(1000, 119880) - case ._120p: return nil + case .fps23_98p: return Fraction(1000, 23976) + case .fps24p: return nil + case .fps25p: return nil + case .fps25i: return nil + case .fps29_97p: return Fraction(1000, 29970) + case .fps29_97i: return Fraction(2000, 59940) // inferred + case .fps30p: return nil + case .fps47_95p: return Fraction(1000, 47952) + case .fps48p: return nil + case .fps50p: return nil + case .fps50i: return nil + case .fps59_94p: return Fraction(1000, 59940) + case .fps59_94i: return Fraction(2000, 119880) // inferred + case .fps60p: return nil + case .fps60i: return nil + case .fps95_9p: return Fraction(1000, 95904) + case .fps96p: return nil + case .fps100p: return nil + case .fps119_88p: return Fraction(1000, 119880) + case .fps120p: return nil } } @@ -151,26 +153,26 @@ extension VideoFrameRate { /// Returns `true` if frame rate is interlaced, otherwise `false` if progressive. public var isInterlaced: Bool { switch self { - case ._23_98p: return false - case ._24p: return false - case ._25p: return false - case ._25i: return true - case ._29_97p: return false - case ._29_97i: return true - case ._30p: return false - case ._47_95p: return false - case ._48p: return false - case ._50p: return false - case ._50i: return true - case ._59_94p: return false - case ._59_94i: return true - case ._60p: return false - case ._60i: return true - case ._95_9p: return false - case ._96p: return false - case ._100p: return false - case ._119_88p: return false - case ._120p: return false + case .fps23_98p: return false + case .fps24p: return false + case .fps25p: return false + case .fps25i: return true + case .fps29_97p: return false + case .fps29_97i: return true + case .fps30p: return false + case .fps47_95p: return false + case .fps48p: return false + case .fps50p: return false + case .fps50i: return true + case .fps59_94p: return false + case .fps59_94i: return true + case .fps60p: return false + case .fps60i: return true + case .fps95_9p: return false + case .fps96p: return false + case .fps100p: return false + case .fps119_88p: return false + case .fps120p: return false } } } diff --git a/Sources/TimecodeKit/VideoFrameRate/VideoFrameRate String Extensions.swift b/Sources/TimecodeKit/VideoFrameRate/VideoFrameRate String Extensions.swift index f05e3aaf..70025f5b 100644 --- a/Sources/TimecodeKit/VideoFrameRate/VideoFrameRate String Extensions.swift +++ b/Sources/TimecodeKit/VideoFrameRate/VideoFrameRate String Extensions.swift @@ -1,13 +1,13 @@ // // VideoFrameRate String Extensions.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // extension String { - /// Convenience method to call `VideoFrameRate(stringValue: self)` + /// Convenience method to call ``VideoFrameRate/init(stringValue:)``. @_disfavoredOverload - public var toVideoFrameRate: VideoFrameRate? { + public var videoFrameRate: VideoFrameRate? { VideoFrameRate(stringValue: self) } } diff --git a/Sources/TimecodeKit/VideoFrameRate/VideoFrameRate.swift b/Sources/TimecodeKit/VideoFrameRate/VideoFrameRate.swift index d6f46157..fbbc85b4 100644 --- a/Sources/TimecodeKit/VideoFrameRate/VideoFrameRate.swift +++ b/Sources/TimecodeKit/VideoFrameRate/VideoFrameRate.swift @@ -1,91 +1,136 @@ // // VideoFrameRate.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // import Foundation /// Industry-standard video frame rates. +/// Certain rates may be progressive or interleaved. +/// +/// Timecode is way of encoding a frame number, therefore _timecode frame rate_ may be independent of _video frame rate_. +/// +/// Part of common confusion with timecode can arise from the use of “frames per second” in both the timecode and the actual video frame +/// rate. When used to describe timecode, frames per second represents how many frames of timecode are counted before one second of timecode +/// increments. When describing video frame rates, frames per second represents how many literal video frames are played back during the +/// span of one second of real wall-clock time. +/// +/// For example, 24p video typically uses 24 fps timecode rate. +/// However, 29.97p video may use 29.97 fps or 29.97-drop fps timecode rate depending on post-production facility requirements, and +/// timecode at these rates do not match wall-clock time exactly. +/// +/// Some video rates may correspond (or generally be compatible with) certain timecode rates and vice-versa. +/// To return a video rate's corresponding timecode rate, see ``timecodeFrameRate(drop:)``. +/// +/// ## Topics +/// +/// ### Rates +/// +/// - ``fps23_98p`` +/// - ``fps24p`` +/// - ``fps25p`` +/// - ``fps25i`` +/// - ``fps29_97p`` +/// - ``fps29_97i`` +/// - ``fps30p`` +/// - ``fps47_95p`` +/// - ``fps48p`` +/// - ``fps50p`` +/// - ``fps50i`` +/// - ``fps59_94p`` +/// - ``fps59_94i`` +/// - ``fps60p`` +/// - ``fps60i`` +/// - ``fps95_9p`` +/// - ``fps96p`` +/// - ``fps100p`` +/// - ``fps119_88p`` +/// - ``fps120p`` +/// +/// ### String Extensions +/// +/// - ``Swift/String/videoFrameRate`` +/// public enum VideoFrameRate: String, FrameRateProtocol { // TODO: Seen in professional gear: 1, 2, 3, 4, 5, 6, 8, 10, 12, 12.5, 14.98, 15, 20 // TODO: Triple rates seen in professional gear: 71.9928, 72, 75, 89.91, 90 // TODO: Adobe Premiere offers 10, 12, 12.5 and 15. /// 23.98 fps (23.976) progressive. - case _23_98p = "23.98p" + case fps23_98p = "23.98p" /// 24 fps progressive. - case _24p = "24p" + case fps24p = "24p" /// 25 fps progressive. - case _25p = "25p" + case fps25p = "25p" /// 25 fps interlaced. /// (50 fields producing 25 frames.) - case _25i = "25i" + case fps25i = "25i" /// 29.97 fps progressive. - case _29_97p = "29.97p" + case fps29_97p = "29.97p" /// 29.97 fps interlaced. /// (59.94 fields producing 29.97 frames.) - case _29_97i = "29.97i" + case fps29_97i = "29.97i" /// 30 fps progressive. - case _30p = "30p" + case fps30p = "30p" /// 47.95 fps (47.952) progressive. /// /// Supported by Avid. - case _47_95p = "47.95p" + case fps47_95p = "47.95p" /// 48 fps progressive. /// /// Supported by Avid. - case _48p = "48p" + case fps48p = "48p" /// 50 fps progressive. - case _50p = "50p" + case fps50p = "50p" /// 50 fps interlaced. /// (100 fields producing 50 frames.) - case _50i = "50i" + case fps50i = "50i" /// 59.94 fps progressive. - case _59_94p = "59.94p" + case fps59_94p = "59.94p" /// 59.94 fps interlaced. /// (119.88 fields producing 59.94 frames.) - case _59_94i = "59.94i" + case fps59_94i = "59.94i" /// 60 fps progressive. - case _60p = "60p" + case fps60p = "60p" /// 60 fps interlaced. /// (120 fields producing 60 frames.) - case _60i = "60i" + case fps60i = "60i" /// 95.9 fps (95.904) progressive. - case _95_9p = "95.9p" + case fps95_9p = "95.9p" /// 96 fps progressive. - case _96p = "96p" + case fps96p = "96p" /// 100 fps progressive. - /// - /// Supported by Avid. (Not qualified for smooth playback) - case _100p = "100p" + /// + /// Supported by Avid. (Not qualified for smooth playback.) + case fps100p = "100p" /// 119.88 fps progressive. /// - /// Supported by Avid. (Not qualified for smooth playback) - case _119_88p = "119.88p" + /// Supported by Avid. (Not qualified for smooth playback.) + case fps119_88p = "119.88p" /// 120 fps progressive. /// - /// Supported by Avid. (Not qualified for smooth playback) - case _120p = "120p" + /// Supported by Avid. (Not qualified for smooth playback.) + case fps120p = "120p" } extension VideoFrameRate: CaseIterable { } diff --git a/Sources/TimecodeKitUI/API Evolution/API-2.0.0.swift b/Sources/TimecodeKitUI/API Evolution/API-2.0.0.swift new file mode 100644 index 00000000..b75039ea --- /dev/null +++ b/Sources/TimecodeKitUI/API Evolution/API-2.0.0.swift @@ -0,0 +1,46 @@ +// +// API-2.0.0.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + +// NOTE: +// These are disabled because the API changes from 1.x to 2.x were too extensive to fully/properly +// implement using @available() attributes and was actually causing issues with Xcode's autocomplete +// in the IDE's code editor. +// So instead, a 1.x -> 2.x Migration Guide was written and included in TimecodeKit 2's documentation. + +#if ENABLE_EXTENDED_API_DEPRECATIONS + +#if canImport(SwiftUI) + +import SwiftUI +import TimecodeKit + +// MARK: API Changes in TimecodeKit 2.0.0 UI + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +extension Timecode { + @_disfavoredOverload + @available( + *, + deprecated, + renamed: "stringValueValidatedText(format:invalidModifiers:defaultModifiers:)", + message: "`withDefaultModifiers` parameter has been renamed to `defaultModifiers`." + ) + public func stringValueValidatedText( + format: StringFormat = .default(), + invalidModifiers: ((Text) -> Text)? = nil, + withDefaultModifiers: ((Text) -> Text)? = nil + ) -> Text { + stringValueValidatedText( + format: format, + invalidModifiers: invalidModifiers, + defaultModifiers: withDefaultModifiers + ) + } +} + +#endif + +#endif diff --git a/Sources/TimecodeKitUI/AppKit/TextField.swift b/Sources/TimecodeKitUI/AppKit/TextField.swift index 90766789..661e911c 100644 --- a/Sources/TimecodeKitUI/AppKit/TextField.swift +++ b/Sources/TimecodeKitUI/AppKit/TextField.swift @@ -1,7 +1,7 @@ // // TextField.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if os(macOS) @@ -10,9 +10,10 @@ import AppKit import TimecodeKit extension Timecode { - /// NSTextField subclass with timecode formatting + /// `NSTextField` subclass with timecode formatting. /// - /// Formatter is in effect bypassed until all its properties are set (`frameRate`, `upperLimit`, `displaySubFrames`, `subFramesBase`). These can be set after the class has initialized. + /// Formatter is in effect bypassed until all its properties are set (`frameRate`, `upperLimit`, `displaySubFrames`, `subFramesBase`). + /// These can also be set after the class has initialized. /// /// See `.formatter` property to access these. @objc(TimecodeTextField) @@ -40,7 +41,7 @@ extension Timecode { } extension Timecode { - /// NSTextFieldCell subclass with timecode formatting + /// `NSTextFieldCell` subclass with timecode formatting. @objc(TimecodeTextFieldCell) public class TextFieldCell: NSTextFieldCell { public required init(coder: NSCoder) { diff --git a/Sources/TimecodeKitUI/Documentation.docc/Documentation.md b/Sources/TimecodeKitUI/Documentation.docc/Documentation.md new file mode 100644 index 00000000..c370dfa1 --- /dev/null +++ b/Sources/TimecodeKitUI/Documentation.docc/Documentation.md @@ -0,0 +1,16 @@ +# ``TimecodeKitUI`` + +UI controls and tools for formatting and displaying timecode, including user-editable timecode fields. + +![TimecodeKit](timecodekit-banner.png) + +## Topics + +### AppKit + +- ``TimecodeKit/Timecode/TextField`` +- ``TimecodeKit/Timecode/TextFieldCell`` + +### SwiftUI + +- ``TimecodeKit/Timecode/stringValueValidatedText(format:invalidModifiers:defaultModifiers:)`` diff --git a/Sources/TimecodeKitUI/Documentation.docc/Resources/timecodekit-banner.png b/Sources/TimecodeKitUI/Documentation.docc/Resources/timecodekit-banner.png new file mode 100644 index 00000000..06456995 Binary files /dev/null and b/Sources/TimecodeKitUI/Documentation.docc/Resources/timecodekit-banner.png differ diff --git a/Sources/TimecodeKitUI/SwiftUI/SwiftUI Text.swift b/Sources/TimecodeKitUI/SwiftUI/SwiftUI Text.swift index ebe0a8f0..adcb75ca 100644 --- a/Sources/TimecodeKitUI/SwiftUI/SwiftUI Text.swift +++ b/Sources/TimecodeKitUI/SwiftUI/SwiftUI Text.swift @@ -1,27 +1,52 @@ // // SwiftUI Text.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if canImport(SwiftUI) import SwiftUI -import TimecodeKit +@testable import TimecodeKit @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) extension Timecode { - // MARK: textViewValidated - - /// Returns `stringValue` as SwiftUI `Text`, highlighting invalid values. + /// Returns the same output of `stringValue(format:)` as SwiftUI `Text`, colorizing invalid values. /// - /// `invalidModifiers` are the view modifiers applied to invalid values. - /// If `invalidModifiers` are not passed, the default of red foreground color is used. + /// This method will produce a SwiftUI `Text` view colorizing individual invalid timecode components + /// to indicate to the user that they are not valid. + /// + /// The `Timecode` instance must be initialized using the `.allowingInvalid` validation rule. + /// + /// ```swift + /// Timecode(.components(h: 1, m: 20, s: 75, f: 60), at: .fps23_976, by: .allowingInvalid) + /// .stringValueValidatedText() + /// ``` + /// + /// You can alternatively supply your own invalid component modifiers by setting the `invalidModifiers` argument. + /// + /// ```swift + /// Timecode(.components(h: 1, m: 20, s: 75, f: 60), at: .fps23_976, by: .allowingInvalid) + /// .stringValueValidatedText( + /// invalidModifiers: { + /// $0.foregroundColor(.blue) + /// }, defaultModifiers: { + /// $0.foregroundColor(.black) + /// } + /// ) + /// ``` + /// + /// - Parameters: + /// - format: Raw string format options. + /// - invalidModifiers: View modifiers to apply to invalid timecode components. Defaults to `.red` foreground color. + /// - defaultModifiers: Default view modifiers to apply to valid timecode components. + /// - Returns: SwiftUI `Text` view. public func stringValueValidatedText( + format: StringFormat = .default(), invalidModifiers: ((Text) -> Text)? = nil, - withDefaultModifiers: ((Text) -> Text)? = nil + defaultModifiers: ((Text) -> Text)? = nil ) -> Text { - let withDefaultModifiers = withDefaultModifiers ?? { + let defaultModifiers = defaultModifiers ?? { $0 } @@ -29,19 +54,23 @@ extension Timecode { $0.foregroundColor(Color.red) } - let sepDays = withDefaultModifiers(Text(" ")) - let sepMain = withDefaultModifiers(Text(":")) - let sepFrames = withDefaultModifiers(Text(frameRate.isDrop ? ";" : ":")) - let sepSubFrames = withDefaultModifiers(Text(".")) + let sepDays = defaultModifiers(Text(" ")) + let sepMain = format.filenameCompatible + ? defaultModifiers(Text("-")) + : defaultModifiers(Text(":")) + let sepFrames = format.filenameCompatible + ? defaultModifiers(Text("-")) + : defaultModifiers(Text(frameRate.isDrop ? ";" : ":")) + let sepSubFrames = defaultModifiers(Text(".")) let invalids = invalidComponents // early return logic if invalids.isEmpty { - return withDefaultModifiers(Text(stringValue)) + return defaultModifiers(Text(stringValue(format: format))) } - var output = withDefaultModifiers(Text("")) + var output = defaultModifiers(Text("")) // days if days != 0 { @@ -49,7 +78,7 @@ extension Timecode { if invalids.contains(.days) { output = output + invalidModifiers(daysText) } else { - output = output + withDefaultModifiers(daysText) + output = output + defaultModifiers(daysText) } output = output + sepDays @@ -61,7 +90,7 @@ extension Timecode { if invalids.contains(.hours) { output = output + invalidModifiers(hoursText) } else { - output = output + withDefaultModifiers(hoursText) + output = output + defaultModifiers(hoursText) } output = output + sepMain @@ -72,7 +101,7 @@ extension Timecode { if invalids.contains(.minutes) { output = output + invalidModifiers(minutesText) } else { - output = output + withDefaultModifiers(minutesText) + output = output + defaultModifiers(minutesText) } output = output + sepMain @@ -83,7 +112,7 @@ extension Timecode { if invalids.contains(.seconds) { output = output + invalidModifiers(secondsText) } else { - output = output + withDefaultModifiers(secondsText) + output = output + defaultModifiers(secondsText) } output = output + sepFrames @@ -94,12 +123,12 @@ extension Timecode { if invalids.contains(.frames) { output = output + invalidModifiers(framesText) } else { - output = output + withDefaultModifiers(framesText) + output = output + defaultModifiers(framesText) } // subframes - if stringFormat.showSubFrames { + if format.showSubFrames { let numberOfSubFramesDigits = validRange(of: .subFrames).upperBound.numberOfDigits output = output + sepSubFrames @@ -108,7 +137,7 @@ extension Timecode { if invalids.contains(.subFrames) { output = output + invalidModifiers(subframesText) } else { - output = output + withDefaultModifiers(subframesText) + output = output + defaultModifiers(subframesText) } } diff --git a/Tests/TimecodeKit-Dev-Tests/Timecode Elapsed Frames ExtendedTests.swift b/Tests/TimecodeKit-Dev-Tests/Timecode Elapsed Frames ExtendedTests.swift index 4ebbd03c..d2c4ee47 100644 --- a/Tests/TimecodeKit-Dev-Tests/Timecode Elapsed Frames ExtendedTests.swift +++ b/Tests/TimecodeKit-Dev-Tests/Timecode Elapsed Frames ExtendedTests.swift @@ -1,21 +1,19 @@ // // Timecode Elapsed Frames ExtendedTests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest -// import SegmentedProgress +// import XCTestUtils // // final class Timecode_ExtendedTests: XCTestCase { -// // func testTimecode_Iterative() { -// -// // test conversions from components(from:) and frameCount(of:) +// // test conversions from components(of:) and frameCount(of:) // // // ============================================================================== // // NOTE: @@ -28,37 +26,40 @@ import XCTest // // ======= parameters ======= // // let limit: Timecode.UpperLimit = -// ._24hours -// //._100days +// .max24Hours +// // .max100Days // // let frameRatesToTest: [TimecodeFrameRate] = // TimecodeFrameRate.allCases // // TimecodeFrameRate.allDrop // // TimecodeFrameRate.allNonDrop -// // [._60_drop, ._120_drop] +// // [.fps60d, .fps120d] // // // ======= run ============== // // for fr in frameRatesToTest { -// -// let tc = Timecode(at: fr, limit: limit) +// let tc = Timecode(.zero, at: fr, limit: limit) // // // log status -// print ("Testing all frames in \(tc.upperLimit) at \(fr.stringValue)... ", terminator: "") +// print("Testing all frames in \(tc.upperLimit) at \(fr.stringValue)... ", terminator: "") // -// var failures: [(Int, TCC)] = [] +// var failures: [(Int, Timecode.Components)] = [] // // let ubound = tc.frameRate.maxTotalFrames(in: tc.upperLimit) // -// var per = SegmentedProgress(0...ubound, segments: 20, roundedToPlaces: 0) +// var per = SegmentedProgress(0 ... ubound, segments: 20, roundedToPlaces: 0) // -// for i in 0...ubound { -// let vals = Timecode.components(from: i, -// at: tc.frameRate, -// subFramesBase: tc.subFramesBase) +// for i in 0 ... ubound { +// let vals = Timecode.components( +// of: .init(.frames(i), base: tc.subFramesBase), +// at: tc.frameRate +// ) // -// if i != Int(floor(Timecode.frameCount(of: vals, at: tc.frameRate, -// subFramesBase: tc.subFramesBase))) +// if i != Timecode.frameCount( +// of: vals, +// at: tc.frameRate, +// base: tc.subFramesBase +// ).wholeFrames // // { failures.append((i, vals)) } // @@ -69,58 +70,69 @@ import XCTest // } // print("") // finalize log with newline char // -// XCTAssertEqual(failures.count, 0, "Failed iterative test for \(fr) with \(failures.count) failures.") -// -// if failures.count > 0 { -// print("First", -// fr, -// "failure: input elapsed frames", -// failures.first!.0, -// "converted to components", -// failures.first!.1, -// "converted back to", -// Timecode.frameCount(of: failures.first!.1, -// at: tc.frameRate, -// subFramesBase: tc.subFramesBase), -// "elapsed frames.") -// +// XCTAssertEqual( +// failures.count, +// 0, +// "Failed iterative test for \(fr) with \(failures.count) failures." +// ) +// +// if !failures.isEmpty { +// print( +// "First", +// fr, +// "failure: input elapsed frames", +// failures.first!.0, +// "converted to components", +// failures.first!.1, +// "converted back to", +// Timecode.frameCount( +// of: failures.first!.1, +// at: tc.frameRate, +// base: tc.subFramesBase +// ), +// "elapsed frames." +// ) // } // if failures.count > 1 { -// print("Last", -// fr, -// "failure: input elapsed frames", -// failures.last!.0, -// "converted to components", -// failures.last!.1, -// "converted back to", -// Timecode.frameCount(of: failures.last!.1, -// at: tc.frameRate, -// subFramesBase: tc.subFramesBase), -// "elapsed frames.") -// +// print( +// "Last", +// fr, +// "failure: input elapsed frames", +// failures.last!.0, +// "converted to components", +// failures.last!.1, +// "converted back to", +// Timecode.frameCount( +// of: failures.last!.1, +// at: tc.frameRate, +// base: tc.subFramesBase +// ), +// "elapsed frames." +// ) // } -// // } // // print("Done") -// // } -// // } // final class DevTests: XCTestCase { -// func testDummy() throws { -// let tc = try TCC(d: 1) -// .toTimecode(at: ._30_drop, limit: ._100days) -// print(tc.realTimeValue) -// } -// -// func testDummy2() throws { -// let tc = try Timecode(.frames(2_589_408), -// at: ._30_drop, -// limit: ._100days) -// print(tc) -// print(tc.realTimeValue) -// } +// func testDummy() throws { +// var tc = try Timecode.Components(d: 1) +// .timecode(at: .fps30d, limit: .max100Days) +// print(tc.realTimeValue) +// tc.set(.zero) +// try tc.set("") +// try tc.set(.components(d: 1)) +// } +// +// func testDummy2() throws { +// let tc = try Timecode(.frames(2_589_408), +// at: .fps30d, +// limit: .max100Days) +// print(tc) +// print(tc.realTimeValue) +// } // } + #endif diff --git a/Tests/TimecodeKit-Unit-Tests/Integration Tests/Timecode Integration Tests.swift b/Tests/TimecodeKit-Unit-Tests/Integration Tests/Timecode Integration Tests.swift index f253ed9d..e46b2f00 100644 --- a/Tests/TimecodeKit-Unit-Tests/Integration Tests/Timecode Integration Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Integration Tests/Timecode Integration Tests.swift @@ -1,13 +1,13 @@ // // Timecode Integration Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class TimecodeIntegrationTests: XCTestCase { func testTimecode_Clamping() { @@ -16,25 +16,27 @@ class TimecodeIntegrationTests: XCTestCase { TimecodeFrameRate.allCases.forEach { XCTAssertEqual( Timecode( - clampingEach: TCC(h: -1, m: -1, s: -1, f: -1), - at: $0 + .components(h: -1, m: -1, s: -1, f: -1), + at: $0, + by: .clampingComponents ) .components, - TCC(d: 0, h: 0, m: 0, s: 0, f: 0), + Timecode.Components(d: 0, h: 0, m: 0, s: 0, f: 0), "for \($0)" ) } TimecodeFrameRate.allCases.forEach { let clamped = Timecode( - clampingEach: TCC(h: 99, m: 99, s: 99, f: 10000), - at: $0 + .components(h: 99, m: 99, s: 99, f: 10000), + at: $0, + by: .clampingComponents ) .components XCTAssertEqual( clamped, - TCC(d: 0, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable), + Timecode.Components(d: 0, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable), "for \($0)" ) } @@ -44,25 +46,27 @@ class TimecodeIntegrationTests: XCTestCase { TimecodeFrameRate.allCases.forEach { XCTAssertEqual( Timecode( - clampingEach: TCC(d: -1, h: -1, m: -1, s: -1, f: -1), - at: $0 + .components(d: -1, h: -1, m: -1, s: -1, f: -1), + at: $0, + by: .clampingComponents ) .components, - TCC(d: 0, h: 0, m: 0, s: 0, f: 0), + Timecode.Components(d: 0, h: 0, m: 0, s: 0, f: 0), "for \($0)" ) } TimecodeFrameRate.allCases.forEach { let clamped = Timecode( - clampingEach: TCC(d: 99, h: 99, m: 99, s: 99, f: 10000), - at: $0 + .components(d: 99, h: 99, m: 99, s: 99, f: 10000), + at: $0, + by: .clampingComponents ) .components XCTAssertEqual( clamped, - TCC(d: 0, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable), + Timecode.Components(d: 0, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable), "for \($0)" ) } @@ -72,25 +76,27 @@ class TimecodeIntegrationTests: XCTestCase { TimecodeFrameRate.allCases.forEach { XCTAssertEqual( Timecode( - clampingEach: TCC(h: -1, m: -1, s: -1, f: -1), - at: $0 + .components(h: -1, m: -1, s: -1, f: -1), + at: $0, + by: .clampingComponents ) .components, - TCC(d: 0, h: 0, m: 0, s: 0, f: 0), + Timecode.Components(d: 0, h: 0, m: 0, s: 0, f: 0), "for \($0)" ) } TimecodeFrameRate.allCases.forEach { let clamped = Timecode( - clampingEach: TCC(h: 99, m: 99, s: 99, f: 10000), - at: $0 + .components(h: 99, m: 99, s: 99, f: 10000), + at: $0, + by: .clampingComponents ) .components XCTAssertEqual( clamped, - TCC(d: 0, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable), + Timecode.Components(d: 0, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable), "for \($0)" ) } @@ -100,27 +106,29 @@ class TimecodeIntegrationTests: XCTestCase { TimecodeFrameRate.allCases.forEach { XCTAssertEqual( Timecode( - clampingEach: TCC(d: -1, h: -1, m: -1, s: -1, f: -1), + .components(d: -1, h: -1, m: -1, s: -1, f: -1), at: $0, - limit: ._100days + limit: .max100Days, + by: .clampingComponents ) .components, - TCC(d: 0, h: 0, m: 0, s: 0, f: 0), + Timecode.Components(d: 0, h: 0, m: 0, s: 0, f: 0), "for \($0)" ) } TimecodeFrameRate.allCases.forEach { let clamped = Timecode( - clampingEach: TCC(d: 99, h: 99, m: 99, s: 99, f: 10000), + .components(d: 99, h: 99, m: 99, s: 99, f: 10000), at: $0, - limit: ._100days + limit: .max100Days, + by: .clampingComponents ) .components XCTAssertEqual( clamped, - TCC(d: 99, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable), + Timecode.Components(d: 99, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable), "for \($0)" ) } @@ -131,24 +139,26 @@ class TimecodeIntegrationTests: XCTestCase { TimecodeFrameRate.allCases.forEach { let wrapped = Timecode( - wrapping: TCC(d: 1), - at: $0 + .components(d: 1), + at: $0, + by: .wrapping ) .components - let result = TCC(d: 0, h: 0, m: 0, s: 0, f: 0) + let result = Timecode.Components(d: 0, h: 0, m: 0, s: 0, f: 0) XCTAssertEqual(wrapped, result, "for \($0)") } TimecodeFrameRate.allCases.forEach { let wrapped = Timecode( - wrapping: TCC(f: -1), - at: $0 + .components(f: -1), + at: $0, + by: .wrapping ) .components - let result = TCC(d: 0, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable, sf: 0) + let result = Timecode.Components(d: 0, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable, sf: 0) XCTAssertEqual(wrapped, result, "for \($0)") } @@ -157,12 +167,13 @@ class TimecodeIntegrationTests: XCTestCase { TimecodeFrameRate.allCases.forEach { let wrapped = Timecode( - wrapping: TCC(d: 1, h: 2, m: 30, s: 20, f: 0), - at: $0 + .components(d: 1, h: 2, m: 30, s: 20, f: 0), + at: $0, + by: .wrapping ) .components - let result = TCC(d: 0, h: 2, m: 30, s: 20, f: 0) + let result = Timecode.Components(d: 0, h: 2, m: 30, s: 20, f: 0) XCTAssertEqual(wrapped, result, "for \($0)") } @@ -171,13 +182,14 @@ class TimecodeIntegrationTests: XCTestCase { TimecodeFrameRate.allCases.forEach { let wrapped = Timecode( - wrapping: TCC(d: -1), + .components(d: -1), at: $0, - limit: ._100days + limit: .max100Days, + by: .wrapping ) .components - let result = TCC(d: 99, h: 0, m: 0, s: 0, f: 0) + let result = Timecode.Components(d: 99, h: 0, m: 0, s: 0, f: 0) XCTAssertEqual(wrapped, result, "for \($0)") } diff --git a/Tests/TimecodeKit-Unit-Tests/TestResource/Media Files/VideoTrack_25_VFR_1sec.mov b/Tests/TimecodeKit-Unit-Tests/TestResource/Media Files/VideoTrack_25_VFR_1sec.mov new file mode 100644 index 00000000..a1ec9d4e Binary files /dev/null and b/Tests/TimecodeKit-Unit-Tests/TestResource/Media Files/VideoTrack_25_VFR_1sec.mov differ diff --git a/Tests/TimecodeKit-Unit-Tests/TestResource/Media Files/VideoTrack_25_VFR_2sec.mov b/Tests/TimecodeKit-Unit-Tests/TestResource/Media Files/VideoTrack_25_VFR_2sec.mov new file mode 100644 index 00000000..4a62ee59 Binary files /dev/null and b/Tests/TimecodeKit-Unit-Tests/TestResource/Media Files/VideoTrack_25_VFR_2sec.mov differ diff --git a/Tests/TimecodeKit-Unit-Tests/TestResource/TestResource.swift b/Tests/TimecodeKit-Unit-Tests/TestResource/TestResource.swift index 97142dea..b83c5858 100644 --- a/Tests/TimecodeKit-Unit-Tests/TestResource/TestResource.swift +++ b/Tests/TimecodeKit-Unit-Tests/TestResource/TestResource.swift @@ -1,3 +1,9 @@ +// +// TestResource.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + import Foundation import XCTest @@ -27,6 +33,14 @@ enum TestResource: CaseIterable { name: "VideoAndTimecodeTrack_29_97i_Start-00-00-00-00", ext: "mov", subFolder: "Media Files" ) + static let videoTrack_25_VFR_1sec = TestResource.File( + name: "VideoTrack_25_VFR_1sec", ext: "mov", subFolder: "Media Files" + ) + + static let videoTrack_25_VFR_2sec = TestResource.File( + name: "VideoTrack_25_VFR_2sec", ext: "mov", subFolder: "Media Files" + ) + static let videoTrack_29_97_Start_00_00_00_00 = TestResource.File( name: "VideoTrack_29_97_Start-00-00-00-00", ext: "mp4", subFolder: "Media Files" ) diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/AVFoundation Extensions/AVAsset Frame Rate Read Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/AVFoundation Extensions/AVAsset Frame Rate Read Tests.swift index a4495360..4f31bd9a 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/AVFoundation Extensions/AVAsset Frame Rate Read Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/AVFoundation Extensions/AVAsset Frame Rate Read Tests.swift @@ -1,15 +1,15 @@ // // AVAsset Frame Rate Read Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // // AVAssetReader is unavailable on watchOS so we can't support any AVAsset operations #if shouldTestCurrentPlatform && canImport(AVFoundation) && !os(watchOS) && !os(visionOS) -import XCTest -@testable import TimecodeKit import AVFoundation +@testable import TimecodeKit +import XCTest class AVAsset_FrameRateRead_Tests: XCTestCase { override func setUp() { } @@ -21,21 +21,21 @@ class AVAsset_FrameRateRead_Tests: XCTestCase { let url = try TestResource.timecodeTrack_23_976_Start_00_00_00_00.url() let asset = AVAsset(url: url) let frameRate = try asset.timecodeFrameRate() - XCTAssertEqual(frameRate, ._23_976) + XCTAssertEqual(frameRate, .fps23_976) } func testTimecodeFrameRate_23_976fps_B() throws { let url = try TestResource.timecodeTrack_23_976_Start_00_58_40_00.url() let asset = AVAsset(url: url) let frameRate = try asset.timecodeFrameRate() - XCTAssertEqual(frameRate, ._23_976) + XCTAssertEqual(frameRate, .fps23_976) } func testTimecodeFrameRate_24fps() throws { let url = try TestResource.timecodeTrack_24_Start_00_58_40_00.url() let asset = AVAsset(url: url) let frameRate = try asset.timecodeFrameRate() - XCTAssertEqual(frameRate, ._24) + XCTAssertEqual(frameRate, .fps24) } func testTimecodeFrameRate_29_97dropfps() throws { @@ -43,7 +43,7 @@ class AVAsset_FrameRateRead_Tests: XCTestCase { let asset = AVAsset(url: url) let frameRate = try asset.timecodeFrameRate() XCTAssertEqual(asset.isTimecodeFrameRateDropFrame, true) - XCTAssertEqual(frameRate, ._29_97_drop) + XCTAssertEqual(frameRate, .fps29_97d) } func testTimecodeFrameRate_29_97fps() throws { @@ -51,7 +51,7 @@ class AVAsset_FrameRateRead_Tests: XCTestCase { let asset = AVAsset(url: url) let frameRate = try asset.timecodeFrameRate() XCTAssertEqual(asset.isTimecodeFrameRateDropFrame, false) - XCTAssertEqual(frameRate, ._29_97) + XCTAssertEqual(frameRate, .fps29_97) } func testTimecodeFrameRate_29_97fps_from2997i() throws { @@ -59,7 +59,7 @@ class AVAsset_FrameRateRead_Tests: XCTestCase { let asset = AVAsset(url: url) let frameRate = try asset.timecodeFrameRate() XCTAssertEqual(asset.isTimecodeFrameRateDropFrame, false) - XCTAssertEqual(frameRate, ._29_97) + XCTAssertEqual(frameRate, .fps29_97) } // MARK: - VideoFrameRate @@ -69,7 +69,7 @@ class AVAsset_FrameRateRead_Tests: XCTestCase { let url = try TestResource.timecodeTrack_23_976_Start_00_00_00_00.url() let asset = AVAsset(url: url) let frameRate = try asset.videoFrameRate() - XCTAssertEqual(frameRate, ._23_98p) + XCTAssertEqual(frameRate, .fps23_98p) } /// Even though file has no video tracks, it infers video frame rate from the timecode track. @@ -77,7 +77,7 @@ class AVAsset_FrameRateRead_Tests: XCTestCase { let url = try TestResource.timecodeTrack_23_976_Start_00_58_40_00.url() let asset = AVAsset(url: url) let frameRate = try asset.videoFrameRate() - XCTAssertEqual(frameRate, ._23_98p) + XCTAssertEqual(frameRate, .fps23_98p) } /// Even though file has no video tracks, it infers video frame rate from the timecode track. @@ -85,7 +85,7 @@ class AVAsset_FrameRateRead_Tests: XCTestCase { let url = try TestResource.timecodeTrack_24_Start_00_58_40_00.url() let asset = AVAsset(url: url) let frameRate = try asset.videoFrameRate() - XCTAssertEqual(frameRate, ._24p) + XCTAssertEqual(frameRate, .fps24p) } /// Even though file has no video tracks, it infers video frame rate from the timecode track. @@ -93,21 +93,37 @@ class AVAsset_FrameRateRead_Tests: XCTestCase { let url = try TestResource.timecodeTrack_29_97d_Start_00_00_00_00.url() let asset = AVAsset(url: url) let frameRate = try asset.videoFrameRate() - XCTAssertEqual(frameRate, ._29_97p) + XCTAssertEqual(frameRate, .fps29_97p) } func testVideoFrameRate_29_97i() throws { let url = try TestResource.videoAndTimecodeTrack_29_97i_Start_00_00_00_00.url() let asset = AVAsset(url: url) let frameRate = try asset.videoFrameRate() - XCTAssertEqual(frameRate, ._29_97i) + XCTAssertEqual(frameRate, .fps29_97i) } func testVideoFrameRate_29_97p() throws { let url = try TestResource.videoTrack_29_97_Start_00_00_00_00.url() let asset = AVAsset(url: url) let frameRate = try asset.videoFrameRate() - XCTAssertEqual(frameRate, ._29_97p) + XCTAssertEqual(frameRate, .fps29_97p) + } + + // MARK: - VideoFrameRate (VFR) + + func testVideoFrameRate_25p_VFR_A() throws { + let url = try TestResource.videoTrack_25_VFR_1sec.url() + let asset = AVAsset(url: url) + let frameRate = try asset.videoFrameRate() + XCTAssertEqual(frameRate, .fps25p) + } + + func testVideoFrameRate_25p_VFR_B() throws { + let url = try TestResource.videoTrack_25_VFR_2sec.url() + let asset = AVAsset(url: url) + let frameRate = try asset.videoFrameRate() + XCTAssertEqual(frameRate, .fps25p) } } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/AVFoundation Extensions/AVAsset Timecode Read Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/AVFoundation Extensions/AVAsset Timecode Read Tests.swift index 62ab823a..cb8c3f2b 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/AVFoundation Extensions/AVAsset Timecode Read Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/AVFoundation Extensions/AVAsset Timecode Read Tests.swift @@ -1,15 +1,15 @@ // // AVAsset Timecode Read Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // // AVAssetReader is unavailable on watchOS so we can't support any AVAsset operations #if shouldTestCurrentPlatform && canImport(AVFoundation) && !os(watchOS) && !os(visionOS) -import XCTest -@testable import TimecodeKit import AVFoundation +@testable import TimecodeKit +import XCTest class AVAsset_TimecodeRead_Tests: XCTestCase { override func setUp() { } @@ -36,67 +36,74 @@ class AVAsset_TimecodeRead_Tests: XCTestCase { // MARK: - Start/Duration/End Timecode func testReadTimecodes_23_976fps() throws { - let frameRate: TimecodeFrameRate = ._23_976 + let frameRate: TimecodeFrameRate = .fps23_976 let url = try TestResource.timecodeTrack_23_976_Start_00_58_40_00.url() let asset = AVAsset(url: url) // start - let correctStart = try TCC(m: 58, s: 40).toTimecode(at: frameRate) + let correctStart = try Timecode(.components(m: 58, s: 40), at: frameRate) // auto-detect frame rate XCTAssertEqual(try asset.startTimecode(), correctStart) // manually supply frame rate XCTAssertEqual(try asset.startTimecode(at: frameRate), correctStart) // duration - let correctDur = try TCC(m: 24, s: 10, f: 19, sf: 03).toTimecode(at: frameRate) + let correctDur = try Timecode(.components(m: 24, s: 10, f: 19, sf: 03), at: frameRate) // auto-detect frame rate XCTAssertEqual(try asset.durationTimecode(), correctDur) // manually supply frame rate XCTAssertEqual(try asset.durationTimecode(at: frameRate), correctDur) // end - let correctEnd = try TCC(h: 1, m: 22, s: 50, f: 19, sf: 03) - .toTimecode(at: frameRate, format: [.showSubFrames]) + let correctEnd = try Timecode(.components(h: 1, m: 22, s: 50, f: 19, sf: 03), at: frameRate) // auto-detect frame rate - XCTAssertEqual(try asset.endTimecode(format: [.showSubFrames]), - correctEnd) + XCTAssertEqual( + try asset.endTimecode(), + correctEnd + ) // manually supply frame rate - XCTAssertEqual(try asset.endTimecode(at: frameRate, format: [.showSubFrames]), - correctEnd) + XCTAssertEqual( + try asset.endTimecode(at: frameRate), + correctEnd + ) } func testReadTimecodes_24fps() throws { - let frameRate: TimecodeFrameRate = ._24 + let frameRate: TimecodeFrameRate = .fps24 let url = try TestResource.timecodeTrack_24_Start_00_58_40_00.url() let asset = AVAsset(url: url) // start - let correctStart = try TCC(m: 58, s: 40).toTimecode(at: frameRate) + let correctStart = try Timecode(.components(m: 58, s: 40), at: frameRate) // auto-detect frame rate XCTAssertEqual(try asset.startTimecode(), correctStart) // manually supply frame rate XCTAssertEqual(try asset.startTimecode(at: frameRate), correctStart) // duration - let correctDur = try TCC(m: 24, s: 12, f: 05, sf: 85).toTimecode(at: frameRate) + let correctDur = try Timecode(.components(m: 24, s: 12, f: 05, sf: 85), at: frameRate) // auto-detect frame rate XCTAssertEqual(try asset.durationTimecode(), correctDur) // manually supply frame rate XCTAssertEqual(try asset.durationTimecode(at: frameRate), correctDur) // end - let correctEnd = try TCC(h: 1, m: 22, s: 52, f: 05, sf: 85) - .toTimecode(at: frameRate, format: [.showSubFrames]) + let correctEnd = try Timecode.Components(h: 1, m: 22, s: 52, f: 05, sf: 85) + .timecode(at: frameRate) // auto-detect frame rate - XCTAssertEqual(try asset.endTimecode(format: [.showSubFrames]), - correctEnd) + XCTAssertEqual( + try asset.endTimecode(), + correctEnd + ) // manually supply frame rate - XCTAssertEqual(try asset.endTimecode(at: frameRate, format: [.showSubFrames]), - correctEnd) + XCTAssertEqual( + try asset.endTimecode(at: frameRate), + correctEnd + ) } func testReadTimecodes_29_97fps() throws { - let frameRate: TimecodeFrameRate = ._29_97 + let frameRate: TimecodeFrameRate = .fps29_97 let url = try TestResource.videoTrack_29_97_Start_00_00_00_00.url() let asset = AVAsset(url: url) @@ -108,7 +115,7 @@ class AVAsset_TimecodeRead_Tests: XCTestCase { XCTAssertEqual(try asset.startTimecode(at: frameRate), nil) // duration - let correctDur = try TCC(s: 10).toTimecode(at: frameRate) + let correctDur = try Timecode(.components(s: 10), at: frameRate) // auto-detect frame rate XCTAssertEqual(try asset.durationTimecode(), correctDur) // manually supply frame rate @@ -119,7 +126,7 @@ class AVAsset_TimecodeRead_Tests: XCTestCase { // auto-detect frame rate XCTAssertEqual(try asset.endTimecode(), nil) // manually supply frame rate - XCTAssertEqual(try asset.endTimecode(at: frameRate, format: [.showSubFrames]), nil) + XCTAssertEqual(try asset.endTimecode(at: frameRate), nil) } } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/AVFoundation Extensions/AVAssetTrack Timecode Read Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/AVFoundation Extensions/AVAssetTrack Timecode Read Tests.swift index 3f979317..da33273c 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/AVFoundation Extensions/AVAssetTrack Timecode Read Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/AVFoundation Extensions/AVAssetTrack Timecode Read Tests.swift @@ -1,15 +1,15 @@ // // AVAssetTrack Timecode Read Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // // AVAssetReader is unavailable on watchOS so we can't support any AVAsset operations #if shouldTestCurrentPlatform && canImport(AVFoundation) && !os(watchOS) && !os(visionOS) -import XCTest -@testable import TimecodeKit import AVFoundation +@testable import TimecodeKit +import XCTest class AVAssetTrack_TimecodeRead_Tests: XCTestCase { override func setUp() { } @@ -18,14 +18,14 @@ class AVAssetTrack_TimecodeRead_Tests: XCTestCase { // MARK: - Start/Duration/End Timecode func testReadTimecodeRange_23_976fps() throws { - let frameRate: TimecodeFrameRate = ._23_976 + let frameRate: TimecodeFrameRate = .fps23_976 let url = try TestResource.timecodeTrack_23_976_Start_00_58_40_00.url() let asset = AVAsset(url: url) - let track = try XCTUnwrap(asset.tracks.first) + let loadTrack = try getFirstTrack(of: asset) + let track = try XCTUnwrap(loadTrack) - let correctStart = try TCC().toTimecode(at: frameRate) - let correctEnd = try TCC(m: 24, s: 10, f: 19, sf: 03) - .toTimecode(at: frameRate, format: [.showSubFrames]) + let correctStart = Timecode(.zero, at: frameRate) + let correctEnd = try Timecode(.components(m: 24, s: 10, f: 19, sf: 03), at: frameRate) // even though it's a timecode track, its timeRange property relates to overall timeline of the asset, // so its start is 0. @@ -46,14 +46,14 @@ class AVAssetTrack_TimecodeRead_Tests: XCTestCase { } func testReadDurationTimecode_23_976fps() throws { - let frameRate: TimecodeFrameRate = ._23_976 + let frameRate: TimecodeFrameRate = .fps23_976 let url = try TestResource.timecodeTrack_23_976_Start_00_58_40_00.url() let asset = AVAsset(url: url) - let track = try XCTUnwrap(asset.tracks.first) + let loadTrack = try getFirstTrack(of: asset) + let track = try XCTUnwrap(loadTrack) // duration - let correctDur = try TCC(m: 24, s: 10, f: 19, sf: 03) - .toTimecode(at: frameRate, format: [.showSubFrames]) + let correctDur = try Timecode(.components(m: 24, s: 10, f: 19, sf: 03), at: frameRate) // auto-detect frame rate XCTAssertEqual(try track.durationTimecode(), correctDur) @@ -62,14 +62,14 @@ class AVAssetTrack_TimecodeRead_Tests: XCTestCase { } func testReadDurationTimecode_29_97fps() throws { - let frameRate: TimecodeFrameRate = ._29_97 + let frameRate: TimecodeFrameRate = .fps29_97 let url = try TestResource.videoTrack_29_97_Start_00_00_00_00.url() let asset = AVAsset(url: url) - let track = try XCTUnwrap(asset.tracks.first) + let loadTrack = try getFirstTrack(of: asset) + let track = try XCTUnwrap(loadTrack) // duration - let correctDur = try TCC(s: 10) - .toTimecode(at: frameRate, format: [.showSubFrames]) + let correctDur = try Timecode(.components(s: 10), at: frameRate) // auto-detect frame rate XCTAssertEqual(try track.durationTimecode(), correctDur) @@ -78,4 +78,25 @@ class AVAssetTrack_TimecodeRead_Tests: XCTestCase { } } +// MARK: - Utils + +extension AVAssetTrack_TimecodeRead_Tests { + // /// Wrapper to load asset's first track depending on OS version. + // @available(macOS 10.15, iOS 13, *) + // func getFirstTrack(of asset: AVAsset) async throws -> AVAssetTrack { + // let maybeTrack = try await { + // if #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) { + // return try await asset.load(.tracks).first + // } else { + // return asset.tracks.first + // } + // }() + // return try XCTUnwrap(maybeTrack) + // } + + /// Wrapper to load asset's first track depending on OS version. + func getFirstTrack(of asset: AVAsset) throws -> AVAssetTrack { + try XCTUnwrap(asset.tracks.first) + } +} #endif diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/AVFoundation Extensions/AVFoundation Utils Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/AVFoundation Extensions/AVFoundation Utils Tests.swift index 4b0ec3e6..aef9d802 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/AVFoundation Extensions/AVFoundation Utils Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/AVFoundation Extensions/AVFoundation Utils Tests.swift @@ -1,15 +1,15 @@ // // AVFoundation Utils Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2023 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // // AVAssetReader is unavailable on watchOS so we can't support any AVAsset operations -#if shouldTestCurrentPlatform && canImport(AVFoundation) && !os(watchOS) +#if shouldTestCurrentPlatform && canImport(AVFoundation) && !os(watchOS) && !os(visionOS) -import XCTest -@testable import TimecodeKit import AVFoundation +@testable import TimecodeKit +import XCTest class AVFoundationUtils_Tests: XCTestCase { override func setUp() { } @@ -22,19 +22,19 @@ class AVFoundationUtils_Tests: XCTestCase { // valid XCTAssertEqual( - try CMTimeRange(start: s10, end: s10).timecodeRange(at: ._30), - try Timecode(TCC(s: 10), at: ._30) ... Timecode(TCC(s: 10), at: ._30) + try CMTimeRange(start: s10, end: s10).timecodeRange(at: .fps30), + try Timecode(.components(s: 10), at: .fps30) ... Timecode(.components(s: 10), at: .fps30) ) XCTAssertEqual( - try CMTimeRange(start: s10, duration: s10).timecodeRange(at: ._30), - try Timecode(TCC(s: 10), at: ._30) ... Timecode(TCC(s: 20), at: ._30) + try CMTimeRange(start: s10, duration: s10).timecodeRange(at: .fps30), + try Timecode(.components(s: 10), at: .fps30) ... Timecode(.components(s: 20), at: .fps30) ) // invalid XCTAssertThrowsError( - try CMTimeRange(start: s10, end: s9).timecodeRange(at: ._30) + try CMTimeRange(start: s10, end: s9).timecodeRange(at: .fps30) ) } } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/FeetAndFrames/FeetAndFrames Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/FeetAndFrames/FeetAndFrames Tests.swift index 33e3d64b..60121506 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/FeetAndFrames/FeetAndFrames Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/FeetAndFrames/FeetAndFrames Tests.swift @@ -1,18 +1,66 @@ // // FeetAndFrames Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class FeetAndFrames_Tests: XCTestCase { override func setUp() { } override func tearDown() { } + func testInitString() throws { + // expected formatting + + XCTAssertEqual(try FeetAndFrames("0+00").stringValue, "0+00") + XCTAssertEqual(try FeetAndFrames("0+01").stringValue, "0+01") + XCTAssertEqual(try FeetAndFrames("1+00").stringValue, "1+00") + XCTAssertEqual(try FeetAndFrames("10+00").stringValue, "10+00") + XCTAssertEqual(try FeetAndFrames("100+14").stringValue, "100+14") + XCTAssertEqual(try FeetAndFrames("100+150").stringValue, "100+150") + + XCTAssertEqual(try FeetAndFrames("0+00.00").stringValueVerbose, "0+00.00") + XCTAssertEqual(try FeetAndFrames("0+01.00").stringValueVerbose, "0+01.00") + XCTAssertEqual(try FeetAndFrames("1+00.00").stringValueVerbose, "1+00.00") + XCTAssertEqual(try FeetAndFrames("10+00.00").stringValueVerbose, "10+00.00") + XCTAssertEqual(try FeetAndFrames("100+14.00").stringValueVerbose, "100+14.00") + XCTAssertEqual(try FeetAndFrames("0+00.01").stringValueVerbose, "0+00.01") + XCTAssertEqual(try FeetAndFrames("0+01.01").stringValueVerbose, "0+01.01") + XCTAssertEqual(try FeetAndFrames("1+00.01").stringValueVerbose, "1+00.01") + XCTAssertEqual(try FeetAndFrames("10+00.24").stringValueVerbose, "10+00.24") + XCTAssertEqual(try FeetAndFrames("100+14.150").stringValueVerbose, "100+14.150") + + // loose formatting + + XCTAssertEqual(try FeetAndFrames("1+2").stringValue, "1+02") + XCTAssertEqual(try FeetAndFrames("01+2").stringValue, "1+02") + XCTAssertEqual(try FeetAndFrames("1+02").stringValue, "1+02") + XCTAssertEqual(try FeetAndFrames("01+02").stringValue, "1+02") + XCTAssertEqual(try FeetAndFrames("001+002").stringValue, "1+02") + + // edge cases + + XCTAssertThrowsError(try FeetAndFrames("+")) + XCTAssertThrowsError(try FeetAndFrames("0+")) + XCTAssertThrowsError(try FeetAndFrames("+0")) + XCTAssertThrowsError(try FeetAndFrames("0++0")) + XCTAssertThrowsError(try FeetAndFrames("0++00")) + XCTAssertThrowsError(try FeetAndFrames("0+00.")) + XCTAssertThrowsError(try FeetAndFrames("0+.")) + XCTAssertThrowsError(try FeetAndFrames("0+.0")) + XCTAssertThrowsError(try FeetAndFrames("+.0")) + XCTAssertThrowsError(try FeetAndFrames("Z+ZZ")) + XCTAssertThrowsError(try FeetAndFrames("Z+ZZ.ZZ")) + XCTAssertThrowsError(try FeetAndFrames("1+ZZ")) + XCTAssertThrowsError(try FeetAndFrames("Z+02")) + XCTAssertThrowsError(try FeetAndFrames("1+ZZ.ZZ")) + XCTAssertThrowsError(try FeetAndFrames("Z+02.ZZ")) + } + func testStringValue() { XCTAssertEqual(FeetAndFrames(feet: 0, frames: 0).stringValue, "0+00") XCTAssertEqual(FeetAndFrames(feet: 0, frames: 1).stringValue, "0+01") @@ -42,7 +90,7 @@ class FeetAndFrames_Tests: XCTestCase { func testFrameCount() throws { try TimecodeFrameRate.allCases.forEach { frate in - let tc10Hours = try TCC(h: 10).toTimecode(at: frate) + let tc10Hours = try Timecode(.components(h: 10), at: frate) let ff = tc10Hours.feetAndFramesValue XCTAssertEqual(ff.frameCount, tc10Hours.frameCount) } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode Components Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode Components Tests.swift deleted file mode 100644 index c47eb47a..00000000 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode Components Tests.swift +++ /dev/null @@ -1,296 +0,0 @@ -// -// Timecode Components Tests.swift -// TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License -// - -#if shouldTestCurrentPlatform - -import XCTest -@testable import TimecodeKit - -class Timecode_Components_Tests: XCTestCase { - override func setUp() { } - override func tearDown() { } - - func testTimecode_init_Components_Exactly() throws { - try TimecodeFrameRate.allCases.forEach { - let tc = try Timecode( - TCC(d: 0, h: 0, m: 0, s: 0, f: 0), - at: $0, - limit: ._24hours - ) - - XCTAssertEqual(tc.days, 0, "for \($0)") - XCTAssertEqual(tc.hours, 0, "for \($0)") - XCTAssertEqual(tc.minutes, 0, "for \($0)") - XCTAssertEqual(tc.seconds, 0, "for \($0)") - XCTAssertEqual(tc.frames, 0, "for \($0)") - XCTAssertEqual(tc.subFrames, 0, "for \($0)") - } - - try TimecodeFrameRate.allCases.forEach { - let tc = try Timecode( - TCC(d: 0, h: 1, m: 2, s: 3, f: 4), - at: $0, - limit: ._24hours - ) - - XCTAssertEqual(tc.days, 0, "for \($0)") - XCTAssertEqual(tc.hours, 1, "for \($0)") - XCTAssertEqual(tc.minutes, 2, "for \($0)") - XCTAssertEqual(tc.seconds, 3, "for \($0)") - XCTAssertEqual(tc.frames, 4, "for \($0)") - XCTAssertEqual(tc.subFrames, 0, "for \($0)") - } - } - - func testTimecode_init_Components_Clamping() { - let tc = Timecode( - clamping: TCC(h: 25), - at: ._24, - limit: ._24hours - ) - - XCTAssertEqual( - tc.components, - TCC(h: 23, m: 59, s: 59, f: 23, sf: tc.subFramesBase.rawValue - 1) - ) - } - - func testTimecode_init_Components_ClampingEach() { - let tc = Timecode( - clampingEach: TCC(h: 25), - at: ._24, - limit: ._24hours - ) - - XCTAssertEqual( - tc.components, - TCC(h: 23, m: 00, s: 00, f: 00) - ) - } - - func testTimecode_init_Components_Wrapping() { - TimecodeFrameRate.allCases.forEach { - let tc = Timecode( - wrapping: TCC(h: 25), - at: $0, - limit: ._24hours - ) - - XCTAssertEqual(tc.days, 0, "for \($0)") - XCTAssertEqual(tc.hours, 1, "for \($0)") - XCTAssertEqual(tc.minutes, 0, "for \($0)") - XCTAssertEqual(tc.seconds, 0, "for \($0)") - XCTAssertEqual(tc.frames, 0, "for \($0)") - XCTAssertEqual(tc.subFrames, 0, "for \($0)") - } - } - - func testTimecode_init_Components_RawValues() { - TimecodeFrameRate.allCases.forEach { - let tc = Timecode( - rawValues: TCC(d: 99, h: 99, m: 99, s: 99, f: 99, sf: 99), - at: $0, - limit: ._24hours - ) - - XCTAssertEqual(tc.days, 99, "for \($0)") - XCTAssertEqual(tc.hours, 99, "for \($0)") - XCTAssertEqual(tc.minutes, 99, "for \($0)") - XCTAssertEqual(tc.seconds, 99, "for \($0)") - XCTAssertEqual(tc.frames, 99, "for \($0)") - XCTAssertEqual(tc.subFrames, 99, "for \($0)") - } - } - - func testTimecode_components_24hours() { - // default - - var tc = Timecode(at: ._30) - - XCTAssertEqual(tc.days, 0) - XCTAssertEqual(tc.hours, 0) - XCTAssertEqual(tc.minutes, 0) - XCTAssertEqual(tc.seconds, 0) - XCTAssertEqual(tc.frames, 0) - XCTAssertEqual(tc.subFrames, 0) - - // setter - - tc.components = TCC(h: 1, m: 2, s: 3, f: 4, sf: 5) - - XCTAssertEqual(tc.days, 0) - XCTAssertEqual(tc.hours, 1) - XCTAssertEqual(tc.minutes, 2) - XCTAssertEqual(tc.seconds, 3) - XCTAssertEqual(tc.frames, 4) - XCTAssertEqual(tc.subFrames, 5) - - // getter - - let c = tc.components - - XCTAssertEqual(c.d, 0) - XCTAssertEqual(c.h, 1) - XCTAssertEqual(c.m, 2) - XCTAssertEqual(c.s, 3) - XCTAssertEqual(c.f, 4) - XCTAssertEqual(c.sf, 5) - } - - func testTimecode_components_100days() { - // default - - var tc = Timecode(at: ._30, limit: ._100days) - - XCTAssertEqual(tc.days, 0) - XCTAssertEqual(tc.hours, 0) - XCTAssertEqual(tc.minutes, 0) - XCTAssertEqual(tc.seconds, 0) - XCTAssertEqual(tc.frames, 0) - XCTAssertEqual(tc.subFrames, 0) - - // setter - - tc.components = TCC(d: 5, h: 1, m: 2, s: 3, f: 4, sf: 5) - - XCTAssertEqual(tc.days, 5) - XCTAssertEqual(tc.hours, 1) - XCTAssertEqual(tc.minutes, 2) - XCTAssertEqual(tc.seconds, 3) - XCTAssertEqual(tc.frames, 4) - XCTAssertEqual(tc.subFrames, 5) - - // getter - - let c = tc.components - - XCTAssertEqual(c.d, 5) - XCTAssertEqual(c.h, 1) - XCTAssertEqual(c.m, 2) - XCTAssertEqual(c.s, 3) - XCTAssertEqual(c.f, 4) - XCTAssertEqual(c.sf, 5) - } - - func testSetTimecodeExactly() throws { - // this is not meant to test the underlying logic, simply that .setTimecode produces the intended outcome - - var tc = Timecode(at: ._30) - - try tc.setTimecode(exactly: TCC(h: 1, m: 2, s: 3, f: 4, sf: 5)) - - XCTAssertEqual(tc.days, 0) - XCTAssertEqual(tc.hours, 1) - XCTAssertEqual(tc.minutes, 2) - XCTAssertEqual(tc.seconds, 3) - XCTAssertEqual(tc.frames, 4) - XCTAssertEqual(tc.subFrames, 5) - } - - func testSetTimecodeClamping() { - // this is not meant to test the underlying logic, simply that .setTimecode produces the intended outcome - - var tc = Timecode(at: ._30, base: ._80SubFrames) - - tc.setTimecode(clampingEach: TCC(d: 1, h: 70, m: 70, s: 70, f: 70, sf: 500)) - - XCTAssertEqual(tc.days, 0) - XCTAssertEqual(tc.hours, 23) - XCTAssertEqual(tc.minutes, 59) - XCTAssertEqual(tc.seconds, 59) - XCTAssertEqual(tc.frames, 29) - XCTAssertEqual(tc.subFrames, 79) - } - - func testSetTimecodeWrapping() { - // this is not meant to test the underlying logic, simply that .setTimecode produces the intended outcome - - var tc = Timecode(at: ._30) - - tc.setTimecode(wrapping: TCC(f: -1)) - - XCTAssertEqual(tc.days, 0) - XCTAssertEqual(tc.hours, 23) - XCTAssertEqual(tc.minutes, 59) - XCTAssertEqual(tc.seconds, 59) - XCTAssertEqual(tc.frames, 29) - XCTAssertEqual(tc.subFrames, 0) - } - - // MARK: - .toTimecode() - - func testTCC_toTimecode() throws { - // toTimecode(rawValuesAt:) - - XCTAssertEqual( - try TCC(h: 1, m: 5, s: 20, f: 14) - .toTimecode(at: ._23_976), - try Timecode( - TCC(h: 1, m: 5, s: 20, f: 14), - at: ._23_976 - ) - ) - - // toTimecode(rawValuesAt:) with subframes - - let tcWithSubFrames = try TCC(h: 1, m: 5, s: 20, f: 14, sf: 94) - .toTimecode( - at: ._23_976, - base: ._100SubFrames, - format: [.showSubFrames] - ) - XCTAssertEqual( - tcWithSubFrames, - try Timecode( - TCC(h: 1, m: 5, s: 20, f: 14, sf: 94), - at: ._23_976, - base: ._100SubFrames, - format: [.showSubFrames] - ) - ) - XCTAssertEqual( - tcWithSubFrames.stringValue, - "01:05:20:14.94" - ) - } - - func testTCC_toTimecode_rawValuesAt() throws { - // toTimecode(rawValuesAt:) - - XCTAssertEqual( - TCC(h: 1, m: 5, s: 20, f: 14) - .toTimecode(rawValuesAt: ._23_976), - try Timecode( - TCC(h: 1, m: 5, s: 20, f: 14), - at: ._23_976 - ) - ) - - // toTimecode(rawValuesAt:) with subframes - - let tcWithSubFrames = TCC(h: 1, m: 5, s: 20, f: 14, sf: 94) - .toTimecode( - rawValuesAt: ._23_976, - base: ._100SubFrames, - format: [.showSubFrames] - ) - XCTAssertEqual( - tcWithSubFrames, - try Timecode( - TCC(h: 1, m: 5, s: 20, f: 14, sf: 94), - at: ._23_976, - base: ._100SubFrames, - format: [.showSubFrames] - ) - ) - XCTAssertEqual( - tcWithSubFrames.stringValue, - "01:05:20:14.94" - ) - } -} - -#endif diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode FrameCount Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode FrameCount Tests.swift deleted file mode 100644 index f0c13f69..00000000 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode FrameCount Tests.swift +++ /dev/null @@ -1,285 +0,0 @@ -// -// Timecode FrameCount Tests.swift -// TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License -// - -#if shouldTestCurrentPlatform - -import XCTest -@testable import TimecodeKit - -class Timecode_FrameCount_Tests: XCTestCase { - override func setUp() { } - override func tearDown() { } - - func testTimecode_init_FrameCount_Exactly() throws { - let tc = try Timecode( - Timecode.FrameCount(.frames(670_907), base: ._80SubFrames), - at: ._30, - limit: ._24hours - ) - - XCTAssertEqual(tc.days, 0) - XCTAssertEqual(tc.hours, 6) - XCTAssertEqual(tc.minutes, 12) - XCTAssertEqual(tc.seconds, 43) - XCTAssertEqual(tc.frames, 17) - XCTAssertEqual(tc.subFrames, 0) - } - - func testTimecode_init_FrameCount_Clamping() { - let tc = Timecode( - clamping: Timecode.FrameCount( - .frames(2_073_600 + 86400), // 25 hours @ 24fps - base: ._80SubFrames - ), - at: ._24, - limit: ._24hours - ) - - XCTAssertEqual( - tc.components, - TCC(h: 23, m: 59, s: 59, f: 23, sf: tc.subFramesBase.rawValue - 1) - ) - } - - func testTimecode_init_FrameCount_Wrapping() { - let tc = Timecode( - wrapping: Timecode.FrameCount( - .frames(2073600 + 86400), // 25 hours @ 24fps - base: ._80SubFrames - ), - at: ._24, - limit: ._24hours - ) - - XCTAssertEqual(tc.days, 0) - XCTAssertEqual(tc.hours, 1) - XCTAssertEqual(tc.minutes, 0) - XCTAssertEqual(tc.seconds, 0) - XCTAssertEqual(tc.frames, 0) - XCTAssertEqual(tc.subFrames, 0) - } - - func testTimecode_init_FrameCount_RawValues() { - let tc = Timecode( - rawValues: Timecode.FrameCount( - .frames((2073600 * 2) + 86400), // 2 days + 1 hour @ 24fps - base: ._80SubFrames - ), - at: ._24, - limit: ._24hours - ) - - XCTAssertEqual(tc.days, 2) - XCTAssertEqual(tc.hours, 1) - XCTAssertEqual(tc.minutes, 0) - XCTAssertEqual(tc.seconds, 0) - XCTAssertEqual(tc.frames, 0) - XCTAssertEqual(tc.subFrames, 0) - } - - func testAllFrameRates_ElapsedFrames() { - // duration of 24 hours elapsed, rolling over to 1 day - - // also helps ensure Strideable .distance(to:) returns the correct values - - TimecodeFrameRate.allCases.forEach { - // max frames in 24 hours - - var maxFramesIn24hours: Int - - switch $0 { - case ._23_976: maxFramesIn24hours = 2_073_600 - case ._24: maxFramesIn24hours = 2_073_600 - case ._24_98: maxFramesIn24hours = 2_160_000 - case ._25: maxFramesIn24hours = 2_160_000 - case ._29_97: maxFramesIn24hours = 2_592_000 - case ._29_97_drop: maxFramesIn24hours = 2_589_408 - case ._30: maxFramesIn24hours = 2_592_000 - case ._30_drop: maxFramesIn24hours = 2_589_408 - case ._47_952: maxFramesIn24hours = 4_147_200 - case ._48: maxFramesIn24hours = 4_147_200 - case ._50: maxFramesIn24hours = 4_320_000 - case ._59_94: maxFramesIn24hours = 5_184_000 - case ._59_94_drop: maxFramesIn24hours = 5_178_816 - case ._60: maxFramesIn24hours = 5_184_000 - case ._60_drop: maxFramesIn24hours = 5_178_816 - case ._95_904: maxFramesIn24hours = 8_294_400 - case ._96: maxFramesIn24hours = 8_294_400 - case ._100: maxFramesIn24hours = 8_640_000 - case ._119_88: maxFramesIn24hours = 10_368_000 - case ._119_88_drop: maxFramesIn24hours = 10_357_632 - case ._120: maxFramesIn24hours = 10_368_000 - case ._120_drop: maxFramesIn24hours = 10_357_632 - } - - XCTAssertEqual( - $0.maxTotalFrames(in: ._24hours), - maxFramesIn24hours, - "for \($0)" - ) - } - - // number of total elapsed frames in (24 hours - 1 frame), or essentially the maximum timecode expressible for each frame rate - - TimecodeFrameRate.allCases.forEach { - // max frames in 24 hours - 1 - - var maxFramesExpressibleIn24hours: Int - - switch $0 { - case ._23_976: maxFramesExpressibleIn24hours = 2_073_600 - 1 - case ._24: maxFramesExpressibleIn24hours = 2_073_600 - 1 - case ._24_98: maxFramesExpressibleIn24hours = 2_160_000 - 1 - case ._25: maxFramesExpressibleIn24hours = 2_160_000 - 1 - case ._29_97: maxFramesExpressibleIn24hours = 2_592_000 - 1 - case ._29_97_drop: maxFramesExpressibleIn24hours = 2_589_408 - 1 - case ._30: maxFramesExpressibleIn24hours = 2_592_000 - 1 - case ._30_drop: maxFramesExpressibleIn24hours = 2_589_408 - 1 - case ._47_952: maxFramesExpressibleIn24hours = 4_147_200 - 1 - case ._48: maxFramesExpressibleIn24hours = 4_147_200 - 1 - case ._50: maxFramesExpressibleIn24hours = 4_320_000 - 1 - case ._59_94: maxFramesExpressibleIn24hours = 5_184_000 - 1 - case ._59_94_drop: maxFramesExpressibleIn24hours = 5_178_816 - 1 - case ._60: maxFramesExpressibleIn24hours = 5_184_000 - 1 - case ._60_drop: maxFramesExpressibleIn24hours = 5_178_816 - 1 - case ._95_904: maxFramesExpressibleIn24hours = 8_294_400 - 1 - case ._96: maxFramesExpressibleIn24hours = 8_294_400 - 1 - case ._100: maxFramesExpressibleIn24hours = 8_640_000 - 1 - case ._119_88: maxFramesExpressibleIn24hours = 10_368_000 - 1 - case ._119_88_drop: maxFramesExpressibleIn24hours = 10_357_632 - 1 - case ._120: maxFramesExpressibleIn24hours = 10_368_000 - 1 - case ._120_drop: maxFramesExpressibleIn24hours = 10_357_632 - 1 - } - - XCTAssertEqual( - $0.maxTotalFramesExpressible(in: ._24hours), - maxFramesExpressibleIn24hours, - "for \($0)" - ) - } - } - - func testSetTimecodeExactly() throws { - // this is not meant to test the underlying logic, simply that .setTimecode produces the intended outcome - - var tc = Timecode(at: ._30, base: ._80SubFrames) - - try tc.setTimecode(exactly: Timecode.FrameCount( - .frames(670_907), - base: ._80SubFrames - )) - - XCTAssertEqual(tc.days, 0) - XCTAssertEqual(tc.hours, 6) - XCTAssertEqual(tc.minutes, 12) - XCTAssertEqual(tc.seconds, 43) - XCTAssertEqual(tc.frames, 17) - XCTAssertEqual(tc.subFrames, 0) - } - - func testSetTimecodeFrameCount_Clamping() { - var tc = Timecode(at: ._24, base: ._80SubFrames) - - tc.setTimecode(clamping: Timecode.FrameCount( - .frames(2_073_600 + 86400), // 25 hours @ 24fps - base: ._80SubFrames - )) - - XCTAssertEqual( - tc.components, - TCC(h: 23, m: 59, s: 59, f: 23, sf: tc.subFramesBase.rawValue - 1) - ) - } - - func testSetTimecodeFrameCount_Wrapping() { - var tc = Timecode(at: ._24, base: ._80SubFrames) - - tc.setTimecode(wrapping: Timecode.FrameCount( - .frames(2073600 + 86400), // 25 hours @ 24fps - base: ._80SubFrames - )) - - XCTAssertEqual(tc.days, 0) - XCTAssertEqual(tc.hours, 1) - XCTAssertEqual(tc.minutes, 0) - XCTAssertEqual(tc.seconds, 0) - XCTAssertEqual(tc.frames, 0) - XCTAssertEqual(tc.subFrames, 0) - } - - func testSetTimecodeFrameCount_RawValues() { - var tc = Timecode(at: ._24, base: ._80SubFrames) - - tc.setTimecode(rawValues: Timecode.FrameCount( - .frames((2073600 * 2) + 86400), // 2 days + 1 hour @ 24fps - base: ._80SubFrames - )) - - XCTAssertEqual(tc.days, 2) - XCTAssertEqual(tc.hours, 1) - XCTAssertEqual(tc.minutes, 0) - XCTAssertEqual(tc.seconds, 0) - XCTAssertEqual(tc.frames, 0) - XCTAssertEqual(tc.subFrames, 0) - } - - func testStatic_componentsOfFrameCount_2997d() { - // edge cases - - let totalFramesIn24Hr = 2_589_408 - // let totalSubFramesIn24Hr = 207152640 - - let tcc = Timecode.components( - of: Timecode.FrameCount( - .split(frames: totalFramesIn24Hr - 1, subFrames: 79), - base: ._80SubFrames - ), - at: ._29_97_drop - ) - - XCTAssertEqual(tcc, TCC(d: 0, h: 23, m: 59, s: 59, f: 29, sf: 79)) - } - - func testIsZero() { - // true - - // frames - XCTAssertTrue(Timecode.FrameCount(.frames(0), base: ._80SubFrames).isZero) - // split - XCTAssertTrue(Timecode.FrameCount(.split(frames: 0, subFrames: 0), base: ._80SubFrames).isZero) - // combined - XCTAssertTrue(Timecode.FrameCount(.combined(frames: 0.0), base: ._80SubFrames).isZero) - // split unitinterval - XCTAssertTrue(Timecode.FrameCount(.splitUnitInterval(frames: 0, subFramesUnitInterval: 0.0), base: ._80SubFrames).isZero) - - // false - - // frames - XCTAssertFalse(Timecode.FrameCount(.frames(1), base: ._80SubFrames).isZero) - XCTAssertFalse(Timecode.FrameCount(.frames(-1), base: ._80SubFrames).isZero) - // split - XCTAssertFalse(Timecode.FrameCount(.split(frames: 0, subFrames: 1), base: ._80SubFrames).isZero) - XCTAssertFalse(Timecode.FrameCount(.split(frames: 1, subFrames: 0), base: ._80SubFrames).isZero) - XCTAssertFalse(Timecode.FrameCount(.split(frames: 1, subFrames: 1), base: ._80SubFrames).isZero) - XCTAssertFalse(Timecode.FrameCount(.split(frames: 0, subFrames: -1), base: ._80SubFrames).isZero) - XCTAssertFalse(Timecode.FrameCount(.split(frames: -1, subFrames: 0), base: ._80SubFrames).isZero) - XCTAssertFalse(Timecode.FrameCount(.split(frames: -1, subFrames: -1), base: ._80SubFrames).isZero) - // combined - XCTAssertFalse(Timecode.FrameCount(.combined(frames: 0.1), base: ._80SubFrames).isZero) - XCTAssertFalse(Timecode.FrameCount(.combined(frames: 1.0), base: ._80SubFrames).isZero) - XCTAssertFalse(Timecode.FrameCount(.combined(frames: -0.1), base: ._80SubFrames).isZero) - XCTAssertFalse(Timecode.FrameCount(.combined(frames: -1.0), base: ._80SubFrames).isZero) - // split unitinterval - XCTAssertFalse(Timecode.FrameCount(.splitUnitInterval(frames: 0, subFramesUnitInterval: 0.1), base: ._80SubFrames).isZero) - XCTAssertFalse(Timecode.FrameCount(.splitUnitInterval(frames: 1, subFramesUnitInterval: 0.0), base: ._80SubFrames).isZero) - XCTAssertFalse(Timecode.FrameCount(.splitUnitInterval(frames: 1, subFramesUnitInterval: 0.1), base: ._80SubFrames).isZero) - XCTAssertFalse(Timecode.FrameCount(.splitUnitInterval(frames: 0, subFramesUnitInterval: -0.1), base: ._80SubFrames).isZero) - XCTAssertFalse(Timecode.FrameCount(.splitUnitInterval(frames: -1, subFramesUnitInterval: 0.0), base: ._80SubFrames).isZero) - XCTAssertFalse(Timecode.FrameCount(.splitUnitInterval(frames: -1, subFramesUnitInterval: -0.1), base: ._80SubFrames).isZero) - } -} - -#endif diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode Rational CMTime Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode Rational CMTime Tests.swift deleted file mode 100644 index 1594086d..00000000 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode Rational CMTime Tests.swift +++ /dev/null @@ -1,191 +0,0 @@ -// -// Timecode Rational CMTime Tests.swift -// TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License -// - -#if shouldTestCurrentPlatform - -import XCTest -@testable import TimecodeKit -import CoreMedia - -class Timecode_Rational_CMTime_Tests: XCTestCase { - override func setUp() { } - override func tearDown() { } - - func testTimecode_init_CMTime_Exactly() throws { - try TimecodeFrameRate.allCases.forEach { - let tc = try Timecode( - CMTime(value: 10, timescale: 1), - at: $0, - limit: ._24hours - ) - - // don't imperatively check each result, just make sure that a value was set; - // setter logic is unit-tested elsewhere, we just want to check the Timecode.init interface here. - XCTAssertNotEqual(tc.seconds, 0, "for \($0)") - } - } - - func testTimecode_init_CMTime() throws { - // these rational fractions and timecodes are taken from actual FCP XML files as known truth - - try TimecodeFrameRate.allCases.forEach { fRate in - switch fRate { - case ._23_976: - XCTAssertEqual( - try Timecode(CMTime(value: 335335, timescale: 24000), at: fRate).components, - TCC(h: 00, m: 00, s: 13, f: 23) - ) - case ._24: - XCTAssertEqual( - try Timecode(CMTime(value: 167500, timescale: 12000), at: fRate).components, - TCC(h: 00, m: 00, s: 13, f: 23) - ) - case ._24_98: - break // TODO: finish this - case ._25: // same fraction is found in FCP XML for 25p and 25i video rates - XCTAssertEqual( - try Timecode(CMTime(value: 34900, timescale: 2500), at: fRate).components, - TCC(h: 00, m: 00, s: 13, f: 24) - ) - case ._29_97: // same fraction is found in FCP XML for 29.97p and 29.97i video rates - XCTAssertEqual( - try Timecode(CMTime(value: 838838, timescale: 60000), at: fRate).components, - TCC(h: 00, m: 00, s: 13, f: 29) - ) - XCTAssertEqual( - try Timecode(CMTime(value: 1920919, timescale: 30000), at: fRate).components, - TCC(h: 00, m: 01, s: 03, f: 29) - ) - case ._29_97_drop: - XCTAssertEqual( - try Timecode(CMTime(value: 419419, timescale: 30000), at: fRate).components, - TCC(h: 00, m: 00, s: 13, f: 29) - ) - XCTAssertEqual( - try Timecode(CMTime(value: 1918917, timescale: 30000), at: fRate).components, - TCC(h: 00, m: 01, s: 03, f: 29) - ) - case ._30: - XCTAssertEqual( - try Timecode(CMTime(value: 83800, timescale: 6000), at: fRate).components, - TCC(h: 00, m: 00, s: 13, f: 29) - ) - case ._30_drop: - break // TODO: finish this - case ._47_952: - break // TODO: finish this - case ._48: - break // TODO: finish this - case ._50: - XCTAssertEqual( - try Timecode(CMTime(value: 69900, timescale: 5000), at: fRate).components, - TCC(h: 00, m: 00, s: 13, f: 49) - ) - case ._59_94: - XCTAssertEqual( - try Timecode(CMTime(value: 839839, timescale: 60000), at: fRate).components, - TCC(h: 00, m: 00, s: 13, f: 59) - ) - case ._59_94_drop: - break // TODO: finish this - case ._60: - XCTAssertEqual( - try Timecode(CMTime(value: 83900, timescale: 6000), at: fRate).components, - TCC(h: 00, m: 00, s: 13, f: 59) - ) - case ._60_drop: - break // TODO: finish this - case ._95_904: - break // TODO: finish this - case ._96: - break // TODO: finish this - case ._100: - break // TODO: finish this - case ._119_88: - break // TODO: finish this - case ._119_88_drop: - break // TODO: finish this - case ._120: - break // TODO: finish this - case ._120_drop: - break // TODO: finish this - } - } - } - - func testTimecode_init_CMTime_Clamping() { - let tc = Timecode( - clamping: CMTime(value: 86400 + 3600, timescale: 1), // 25 hours @ 24fps - at: ._24, - limit: ._24hours - ) - - XCTAssertEqual( - tc.components, - TCC(h: 23, m: 59, s: 59, f: 23, sf: tc.subFramesBase.rawValue - 1) - ) - } - - func testTimecode_init_CMTime_Wrapping() { - let tc = Timecode( - wrapping: CMTime(value: 86400 + 3600, timescale: 1), // 25 hours @ 24fps - at: ._24, - limit: ._24hours - ) - - XCTAssertEqual(tc.days, 0) - XCTAssertEqual(tc.hours, 1) - XCTAssertEqual(tc.minutes, 0) - XCTAssertEqual(tc.seconds, 0) - XCTAssertEqual(tc.frames, 0) - XCTAssertEqual(tc.subFrames, 0) - } - - func testTimecode_init_CMTime_RawValues() { - let tc = Timecode( - rawValues: CMTime(value: (86400 * 2) + 3600, timescale: 1), // 2 days + 1 hour @ 24fps - at: ._24, - limit: ._24hours - ) - - XCTAssertEqual(tc.days, 2) - XCTAssertEqual(tc.hours, 1) - XCTAssertEqual(tc.minutes, 0) - XCTAssertEqual(tc.seconds, 0) - XCTAssertEqual(tc.frames, 0) - XCTAssertEqual(tc.subFrames, 0) - } - - func testTimecode_cmTime() throws { - // test a small range of timecodes at each frame rate - // and ensure the fraction can re-form the same timecode - try TimecodeFrameRate.allCases.forEach { fRate in - let s = try TCC(m: 8, f: 20).toTimecode(at: fRate) - let e = try TCC(m: 10, f: 5).toTimecode(at: fRate) - - try (s ... e).forEach { tc in - let f = tc.cmTime - let reformedTC = try Timecode(f, at: fRate) - XCTAssertEqual(tc, reformedTC) - } - } - } - - func testTimecode_cmTime_SpotCheck() throws { - let tc = try TCC(h: 00, m: 00, s: 13, f: 29).toTimecode(at: ._29_97_drop) - XCTAssertEqual(tc.cmTime.value, 419419) - XCTAssertEqual(tc.cmTime.timescale, 30000) - } - - func testCMTime_toTimecode() throws { - XCTAssertEqual( - try CMTime(value: 3600, timescale: 1).toTimecode(at: ._24).components, - TCC(h: 1) - ) - } -} - -#endif diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/FrameCount/FrameCount Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/FrameCount/FrameCount Tests.swift index 1b8cff6f..03e46547 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/FrameCount/FrameCount Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/FrameCount/FrameCount Tests.swift @@ -1,17 +1,17 @@ // // FrameCount Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class FrameCount_Tests: XCTestCase { func testInit_frameCount() { - let subFramesBase: Timecode.SubFramesBase = ._80SubFrames + let subFramesBase: Timecode.SubFramesBase = .max80SubFrames let fc = Timecode.FrameCount( subFrameCount: 40002, @@ -28,43 +28,43 @@ class FrameCount_Tests: XCTestCase { // .frames XCTAssert( - Timecode.FrameCount(.frames(500), base: ._100SubFrames) + Timecode.FrameCount(.frames(500), base: .max100SubFrames) == - Timecode.FrameCount(.frames(500), base: ._100SubFrames) + Timecode.FrameCount(.frames(500), base: .max100SubFrames) ) XCTAssert( - Timecode.FrameCount(.frames(500), base: ._100SubFrames) + Timecode.FrameCount(.frames(500), base: .max100SubFrames) != - Timecode.FrameCount(.frames(501), base: ._100SubFrames) + Timecode.FrameCount(.frames(501), base: .max100SubFrames) ) // .split XCTAssert( - Timecode.FrameCount(.split(frames: 500, subFrames: 2), base: ._100SubFrames) + Timecode.FrameCount(.split(frames: 500, subFrames: 2), base: .max100SubFrames) == - Timecode.FrameCount(.split(frames: 500, subFrames: 2), base: ._100SubFrames) + Timecode.FrameCount(.split(frames: 500, subFrames: 2), base: .max100SubFrames) ) XCTAssert( - Timecode.FrameCount(.split(frames: 500, subFrames: 2), base: ._100SubFrames) + Timecode.FrameCount(.split(frames: 500, subFrames: 2), base: .max100SubFrames) != - Timecode.FrameCount(.split(frames: 500, subFrames: 3), base: ._100SubFrames) + Timecode.FrameCount(.split(frames: 500, subFrames: 3), base: .max100SubFrames) ) // .combined XCTAssert( - Timecode.FrameCount(.combined(frames: 500.025), base: ._100SubFrames) + Timecode.FrameCount(.combined(frames: 500.025), base: .max100SubFrames) == - Timecode.FrameCount(.combined(frames: 500.025), base: ._100SubFrames) + Timecode.FrameCount(.combined(frames: 500.025), base: .max100SubFrames) ) XCTAssert( - Timecode.FrameCount(.combined(frames: 500.025), base: ._100SubFrames) + Timecode.FrameCount(.combined(frames: 500.025), base: .max100SubFrames) != - Timecode.FrameCount(.combined(frames: 500.5), base: ._100SubFrames) + Timecode.FrameCount(.combined(frames: 500.5), base: .max100SubFrames) ) // .splitUnitInterval @@ -72,91 +72,91 @@ class FrameCount_Tests: XCTestCase { XCTAssert( Timecode.FrameCount( .splitUnitInterval(frames: 500, subFramesUnitInterval: 0.025), - base: ._100SubFrames + base: .max100SubFrames ) == Timecode.FrameCount( .splitUnitInterval(frames: 500, subFramesUnitInterval: 0.025), - base: ._100SubFrames + base: .max100SubFrames ) ) XCTAssert( Timecode.FrameCount( .splitUnitInterval(frames: 500, subFramesUnitInterval: 0.025), - base: ._100SubFrames + base: .max100SubFrames ) == - Timecode.FrameCount(.combined(frames: 500.025), base: ._100SubFrames) + Timecode.FrameCount(.combined(frames: 500.025), base: .max100SubFrames) ) XCTAssert( Timecode.FrameCount( .splitUnitInterval(frames: 500, subFramesUnitInterval: 0.025), - base: ._100SubFrames + base: .max100SubFrames ) != Timecode.FrameCount( .splitUnitInterval(frames: 500, subFramesUnitInterval: 0.5), - base: ._100SubFrames + base: .max100SubFrames ) ) XCTAssert( Timecode.FrameCount( .splitUnitInterval(frames: 500, subFramesUnitInterval: 0.025), - base: ._100SubFrames + base: .max100SubFrames ) != - Timecode.FrameCount(.combined(frames: 500.5), base: ._100SubFrames) + Timecode.FrameCount(.combined(frames: 500.5), base: .max100SubFrames) ) } func testOperators() { XCTAssertEqual( - Timecode.FrameCount(.frames(200), base: ._100SubFrames) + Timecode.FrameCount(.frames(200), base: .max100SubFrames) + - Timecode.FrameCount(.frames(200), base: ._100SubFrames), - Timecode.FrameCount(.frames(400), base: ._100SubFrames) + Timecode.FrameCount(.frames(200), base: .max100SubFrames), + Timecode.FrameCount(.frames(400), base: .max100SubFrames) ) XCTAssertEqual( - Timecode.FrameCount(.frames(400), base: ._100SubFrames) + Timecode.FrameCount(.frames(400), base: .max100SubFrames) - - Timecode.FrameCount(.frames(200), base: ._100SubFrames), - Timecode.FrameCount(.frames(200), base: ._100SubFrames) + Timecode.FrameCount(.frames(200), base: .max100SubFrames), + Timecode.FrameCount(.frames(200), base: .max100SubFrames) ) XCTAssertEqual( - Timecode.FrameCount(.frames(200), base: ._100SubFrames) + Timecode.FrameCount(.frames(200), base: .max100SubFrames) * 2, - Timecode.FrameCount(.frames(400), base: ._100SubFrames) + Timecode.FrameCount(.frames(400), base: .max100SubFrames) ) XCTAssertEqual( - Timecode.FrameCount(.frames(400), base: ._100SubFrames) + Timecode.FrameCount(.frames(400), base: .max100SubFrames) / 2, - Timecode.FrameCount(.frames(200), base: ._100SubFrames) + Timecode.FrameCount(.frames(200), base: .max100SubFrames) ) } func testIsNegative() { - XCTAssertFalse(Timecode.FrameCount(.frames(0), base: ._100SubFrames).isNegative) - XCTAssertFalse(Timecode.FrameCount(.frames(-0), base: ._100SubFrames).isNegative) + XCTAssertFalse(Timecode.FrameCount(.frames(0), base: .max100SubFrames).isNegative) + XCTAssertFalse(Timecode.FrameCount(.frames(-0), base: .max100SubFrames).isNegative) - XCTAssertFalse(Timecode.FrameCount(.frames(1), base: ._100SubFrames).isNegative) - XCTAssertTrue(Timecode.FrameCount(.frames(-1), base: ._100SubFrames).isNegative) + XCTAssertFalse(Timecode.FrameCount(.frames(1), base: .max100SubFrames).isNegative) + XCTAssertTrue(Timecode.FrameCount(.frames(-1), base: .max100SubFrames).isNegative) } func testTimecode_framesToSubFrames() { XCTAssertEqual( - Timecode.framesToSubFrames(frames: 500, subFrames: 2, base: ._80SubFrames), + Timecode.framesToSubFrames(frames: 500, subFrames: 2, base: .max80SubFrames), 40002 ) } func testTimecode_subFramesToFrames() { - let converted = Timecode.subFramesToFrames(40002, base: ._80SubFrames) + let converted = Timecode.subFramesToFrames(40002, base: .max80SubFrames) XCTAssertEqual(converted.frames, 500) XCTAssertEqual(converted.subFrames, 2) @@ -169,32 +169,29 @@ class FrameCount_Tests: XCTestCase { XCTAssertEqual( try Timecode( .frames(totalFramesin24Hr - 1), - at: ._29_97_drop, - limit: ._24hours, - base: ._80SubFrames, - format: [.showSubFrames] + at: .fps29_97d, + base: .max80SubFrames, + limit: .max24Hours ).components, - TCC(d: 0, h: 23, m: 59, s: 59, f: 29, sf: 0) + Timecode.Components(d: 0, h: 23, m: 59, s: 59, f: 29, sf: 0) ) XCTAssertEqual( try Timecode( - .split(frames: totalFramesin24Hr - 1, subFrames: 79), - at: ._29_97_drop, - limit: ._24hours, - base: ._80SubFrames, - format: [.showSubFrames] + .frames(totalFramesin24Hr - 1, subFrames: 79), + at: .fps29_97d, + base: .max80SubFrames, + limit: .max24Hours ).components, - TCC(d: 0, h: 23, m: 59, s: 59, f: 29, sf: 79) + Timecode.Components(d: 0, h: 23, m: 59, s: 59, f: 29, sf: 79) ) XCTAssertEqual( try Timecode( - .split(frames: totalFramesin24Hr - 1, subFrames: 79), - at: ._29_97_drop, - limit: ._24hours, - base: ._80SubFrames, - format: [.showSubFrames] + .frames(totalFramesin24Hr - 1, subFrames: 79), + at: .fps29_97d, + base: .max80SubFrames, + limit: .max24Hours ) .frameCount .subFrameCount, @@ -208,10 +205,10 @@ class FrameCount_Tests: XCTestCase { XCTAssertEqual( try Timecode( .frames(totalFramesin24Hr), - at: ._30_drop, - limit: ._100days + at: .fps30d, + limit: .max100Days ).components, - TCC(d: 1) + Timecode.Components(d: 1) ) } } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Math/Timecode Math Public Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Math/Timecode Math Public Tests.swift index ba8072e9..f33152ad 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Math/Timecode Math Public Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Math/Timecode Math Public Tests.swift @@ -1,316 +1,646 @@ // // Timecode Math Public Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class Timecode_Math_Public_Tests: XCTestCase { override func setUp() { } override func tearDown() { } - func testAdd_and_Subtract_Methods() throws { + func testAddTimecode() throws { + var tc = Timecode(.zero, at: .fps23_976, limit: .max24Hours) + + let tc1 = try Timecode( + .components(h: 01, m: 02, s: 03, f: 04), + at: .fps23_976, + limit: .max24Hours + ) + + try tc.add(tc1) + XCTAssertEqual(tc.components, Timecode.Components(h: 01, m: 02, s: 03, f: 04)) + + try tc.add(tc1) + XCTAssertEqual(tc.components, Timecode.Components(h: 02, m: 04, s: 06, f: 08)) + } + + func testAddTimecodeByClamping() throws { + var tc = try Timecode( + .components(h: 10, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours + ) + + let tc1 = try Timecode( + .components(h: 15, m: 02, s: 03, f: 04), + at: .fps23_976, + limit: .max24Hours + ) + + try tc.add(tc1, by: .clamping) + XCTAssertEqual(tc.components, Timecode.Components(h: 23, m: 59, s: 59, f: 23, sf: 99)) + } + + func testAddTimecodeByWrapping() throws { + var tc = try Timecode( + .components(h: 10, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours + ) + + let tc1 = try Timecode( + .components(h: 15, m: 02, s: 03, f: 04), + at: .fps23_976, + limit: .max24Hours + ) + + try tc.add(tc1, by: .wrapping) + XCTAssertEqual(tc.components, Timecode.Components(h: 01, m: 02, s: 03, f: 04)) + + try tc.add(tc1, by: .wrapping) + XCTAssertEqual(tc.components, Timecode.Components(h: 16, m: 04, s: 06, f: 08)) + } + + func testAddingTimecode() throws { + var tc = Timecode(.zero, at: .fps23_976, limit: .max24Hours) + + let tc1 = try Timecode( + .components(h: 01, m: 02, s: 03, f: 04), + at: .fps23_976, + limit: .max24Hours + ) + + tc = try tc.adding(tc1) + XCTAssertEqual(tc.components, Timecode.Components(h: 01, m: 02, s: 03, f: 04)) + + tc = try tc.adding(tc1) + XCTAssertEqual(tc.components, Timecode.Components(h: 02, m: 04, s: 06, f: 08)) + } + + func testAddingTimecodeByClamping() throws { + var tc = try Timecode( + .components(h: 10, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours + ) + + let tc1 = try Timecode( + .components(h: 15, m: 02, s: 03, f: 04), + at: .fps23_976, + limit: .max24Hours + ) + + tc = try tc.adding(tc1, by: .clamping) + XCTAssertEqual(tc.components, Timecode.Components(h: 23, m: 59, s: 59, f: 23, sf: 99)) + } + + func testAddingTimecodeByWrapping() throws { + var tc = try Timecode( + .components(h: 10, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours + ) + + let tc1 = try Timecode( + .components(h: 15, m: 02, s: 03, f: 04), + at: .fps23_976, + limit: .max24Hours + ) + + tc = try tc.adding(tc1, by: .wrapping) + XCTAssertEqual(tc.components, Timecode.Components(h: 01, m: 02, s: 03, f: 04)) + + tc = try tc.adding(tc1, by: .wrapping) + XCTAssertEqual(tc.components, Timecode.Components(h: 16, m: 04, s: 06, f: 08)) + } + + func testSubtractTimecode() throws { + var tc = try Timecode( + .components(h: 10, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours + ) + + let tc1 = try Timecode( + .components(h: 00, m: 00, s: 00, f: 01), + at: .fps23_976, + limit: .max24Hours + ) + + try tc.subtract(tc1) + XCTAssertEqual(tc.components, Timecode.Components(h: 09, m: 59, s: 59, f: 23)) + + try tc.subtract(tc1) + XCTAssertEqual(tc.components, Timecode.Components(h: 09, m: 59, s: 59, f: 22)) + } + + func testSubtractTimecodeByClamping() throws { + var tc = try Timecode( + .components(h: 10, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours + ) + + let tc1 = try Timecode( + .components(h: 06, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours + ) + + try tc.subtract(tc1, by: .clamping) + XCTAssertEqual(tc.components, Timecode.Components(h: 04, m: 00, s: 00, f: 00)) + + try tc.subtract(tc1, by: .clamping) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 00, f: 00)) + } + + func testSubtractTimecodeByWrapping() throws { + var tc = try Timecode( + .components(h: 10, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours + ) + + let tc1 = try Timecode( + .components(h: 06, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours + ) + + try tc.subtract(tc1, by: .wrapping) + XCTAssertEqual(tc.components, Timecode.Components(h: 04, m: 00, s: 00, f: 00)) + + try tc.subtract(tc1, by: .wrapping) + XCTAssertEqual(tc.components, Timecode.Components(h: 22, m: 00, s: 00, f: 00)) + } + + func testSubtractingTimecode() throws { + var tc = try Timecode( + .components(h: 10, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours + ) + + let tc1 = try Timecode( + .components(h: 00, m: 00, s: 00, f: 01), + at: .fps23_976, + limit: .max24Hours + ) + + tc = try tc.subtracting(tc1) + XCTAssertEqual(tc.components, Timecode.Components(h: 09, m: 59, s: 59, f: 23)) + + tc = try tc.subtracting(tc1) + XCTAssertEqual(tc.components, Timecode.Components(h: 09, m: 59, s: 59, f: 22)) + } + + func testSubtractingTimecodeByClamping() throws { + var tc = try Timecode( + .components(h: 10, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours + ) + + let tc1 = try Timecode( + .components(h: 06, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours + ) + + tc = try tc.subtracting(tc1, by: .clamping) + XCTAssertEqual(tc.components, Timecode.Components(h: 04, m: 00, s: 00, f: 00)) + + tc = try tc.subtracting(tc1, by: .clamping) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 00, f: 00)) + } + + func testSubtractingTimecodeByWrapping() throws { + var tc = try Timecode( + .components(h: 10, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours + ) + + let tc1 = try Timecode( + .components(h: 06, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours + ) + + tc = try tc.subtracting(tc1, by: .wrapping) + XCTAssertEqual(tc.components, Timecode.Components(h: 04, m: 00, s: 00, f: 00)) + + tc = try tc.subtracting(tc1, by: .wrapping) + XCTAssertEqual(tc.components, Timecode.Components(h: 22, m: 00, s: 00, f: 00)) + } + + func testAddTimecodeSourceValue() throws { + var tc = Timecode(.zero, at: .fps23_976, limit: .max24Hours) + + try tc.add(.components(h: 1)) + XCTAssertEqual(tc.components, Timecode.Components(h: 01, m: 00, s: 00, f: 00)) + + try tc.add(.string("01:00:00:00")) + XCTAssertEqual(tc.components, Timecode.Components(h: 02, m: 00, s: 00, f: 00)) + } + + /// Just test one of the validation rules to make sure they work. + func testAddTimecodeSourceValueByClamping() throws { + var tc = try Timecode( + .components(h: 10, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours + ) + + try tc.add(.components(h: 10), by: .clamping) + XCTAssertEqual(tc.components, Timecode.Components(h: 20, m: 00, s: 00, f: 00)) + + try tc.add(.string("10:00:00:00"), by: .clamping) + XCTAssertEqual(tc.components, Timecode.Components(h: 23, m: 59, s: 59, f: 23, sf: 99)) + } + + func testAddingTimecodeSourceValue() throws { + var tc = Timecode(.zero, at: .fps23_976, limit: .max24Hours) + + tc = try tc.adding(.components(h: 1)) + XCTAssertEqual(tc.components, Timecode.Components(h: 01, m: 00, s: 00, f: 00)) + + tc = try tc.adding(.string("01:00:00:00")) + XCTAssertEqual(tc.components, Timecode.Components(h: 02, m: 00, s: 00, f: 00)) + } + + /// Just test one of the validation rules to make sure they work. + func testAddingTimecodeSourceValueByClamping() throws { + var tc = try Timecode( + .components(h: 10, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours + ) + + tc = try tc.adding(.components(h: 10), by: .clamping) + XCTAssertEqual(tc.components, Timecode.Components(h: 20, m: 00, s: 00, f: 00)) + + tc = try tc.adding(.string("10:00:00:00"), by: .clamping) + XCTAssertEqual(tc.components, Timecode.Components(h: 23, m: 59, s: 59, f: 23, sf: 99)) + } + + func testSubtractTimecodeSourceValue() throws { + var tc = try Timecode( + .components(h: 10, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours + ) + + try tc.subtract(.components(f: 1)) + XCTAssertEqual(tc.components, Timecode.Components(h: 09, m: 59, s: 59, f: 23)) + + try tc.subtract(.string("00:00:00:01")) + XCTAssertEqual(tc.components, Timecode.Components(h: 09, m: 59, s: 59, f: 22)) + } + + /// Just test one of the validation rules to make sure they work. + func testSubtractTimecodeSourceValueByClamping() throws { + var tc = try Timecode( + .components(h: 10, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours + ) + + try tc.subtract(.components(h: 6), by: .clamping) + XCTAssertEqual(tc.components, Timecode.Components(h: 04, m: 00, s: 00, f: 00)) + + try tc.subtract(.string("06:00:00:00"), by: .clamping) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 00, f: 00)) + } + + func testSubtractingTimecodeSourceValue() throws { + var tc = try Timecode( + .components(h: 10, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours + ) + + tc = try tc.subtracting(.components(f: 1)) + XCTAssertEqual(tc.components, Timecode.Components(h: 09, m: 59, s: 59, f: 23)) + + tc = try tc.subtracting(.string("00:00:00:01")) + XCTAssertEqual(tc.components, Timecode.Components(h: 09, m: 59, s: 59, f: 22)) + } + + /// Just test one of the validation rules to make sure they work. + func testSubtractingTimecodeSourceValueByClamping() throws { + var tc = try Timecode( + .components(h: 10, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours + ) + + tc = try tc.subtracting(.components(h: 06), by: .clamping) + XCTAssertEqual(tc.components, Timecode.Components(h: 04, m: 00, s: 00, f: 00)) + + tc = try tc.subtracting(.string("06:00:00:00"), by: .clamping) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 00, f: 00)) + } + + func testAdd_and_Subtract_Components_Methods() throws { // .add / .subtract methods - var tc = Timecode(at: ._23_976, limit: ._24hours) + var tc = Timecode(.zero, at: .fps23_976, limit: .max24Hours) tc = try Timecode( - TCC(h: 00, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 00, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) - try tc.add(TCC(h: 00, m: 00, s: 00, f: 23)) + try tc.add(Timecode.Components(h: 00, m: 00, s: 00, f: 23)) - XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 23)) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 00, f: 23)) - try tc.add(TCC(h: 00, m: 00, s: 00, f: 01)) + try tc.add(Timecode.Components(h: 00, m: 00, s: 00, f: 01)) - XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 01, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 01, f: 00)) tc = try Timecode( - TCC(h: 00, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 00, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) - try tc.add(TCC(h: 01, m: 15, s: 30, f: 10)) + try tc.add(Timecode.Components(h: 01, m: 15, s: 30, f: 10)) - XCTAssertEqual(tc.components, TCC(h: 01, m: 15, s: 30, f: 10)) + XCTAssertEqual(tc.components, Timecode.Components(h: 01, m: 15, s: 30, f: 10)) - try tc.add(TCC(h: 01, m: 15, s: 30, f: 10)) + try tc.add(Timecode.Components(h: 01, m: 15, s: 30, f: 10)) - XCTAssertEqual(tc.components, TCC(h: 02, m: 31, s: 00, f: 20)) + XCTAssertEqual(tc.components, Timecode.Components(h: 02, m: 31, s: 00, f: 20)) - XCTAssertThrowsError(try tc.add(TCC(h: 23, m: 15, s: 30, f: 10))) + XCTAssertThrowsError(try tc.add(Timecode.Components(h: 23, m: 15, s: 30, f: 10))) - XCTAssertEqual(tc.components, TCC(h: 02, m: 31, s: 00, f: 20)) // unchanged value + XCTAssertEqual(tc.components, Timecode.Components(h: 02, m: 31, s: 00, f: 20)) // unchanged value tc = try Timecode( - TCC(h: 00, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 00, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) - XCTAssertThrowsError(try tc.subtract(TCC(h: 02, m: 31, s: 00, f: 20))) + XCTAssertThrowsError(try tc.subtract(Timecode.Components(h: 02, m: 31, s: 00, f: 20))) tc = try Timecode( - TCC(h: 23, m: 59, s: 59, f: 23), - at: ._23_976, - limit: ._24hours + .components(h: 23, m: 59, s: 59, f: 23), + at: .fps23_976, + limit: .max24Hours ) - try tc.subtract(TCC(h: 23, m: 59, s: 59, f: 23)) + try tc.subtract(Timecode.Components(h: 23, m: 59, s: 59, f: 23)) - XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 00, f: 00)) tc = try Timecode( - TCC(h: 23, m: 59, s: 59, f: 23), - at: ._23_976, - limit: ._24hours + .components(h: 23, m: 59, s: 59, f: 23), + at: .fps23_976, + limit: .max24Hours ) - XCTAssertThrowsError(try tc.subtract(TCC(h: 23, m: 59, s: 59, f: 24))) // 1 frame too many + XCTAssertThrowsError(try tc.subtract(Timecode.Components(h: 23, m: 59, s: 59, f: 24))) // 1 frame too many - XCTAssertEqual(tc.components, TCC(h: 23, m: 59, s: 59, f: 23)) // unchanged value + XCTAssertEqual(tc.components, Timecode.Components(h: 23, m: 59, s: 59, f: 23)) // unchanged value tc = try Timecode( - TCC(h: 00, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 00, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) - try tc.add(TCC(f: 24)) // roll up to 1 sec + try tc.add(Timecode.Components(f: 24)) // roll up to 1 sec - XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 01, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 01, f: 00)) tc = try Timecode( - TCC(h: 00, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 00, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) - try tc.add(TCC(s: 60)) // roll up to 1 min + try tc.add(Timecode.Components(s: 60)) // roll up to 1 min - XCTAssertEqual(tc.components, TCC(h: 00, m: 01, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 01, s: 00, f: 00)) tc = try Timecode( - TCC(h: 00, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 00, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) - try tc.add(TCC(m: 60)) // roll up to 1 hr + try tc.add(Timecode.Components(m: 60)) // roll up to 1 hr - XCTAssertEqual(tc.components, TCC(h: 01, m: 00, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 01, m: 00, s: 00, f: 00)) tc = try Timecode( - TCC(d: 0, h: 00, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._100days + .components(d: 0, h: 00, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max100Days ) - try tc.add(TCC(h: 24)) // roll up to 1 day + try tc.add(Timecode.Components(h: 24)) // roll up to 1 day - XCTAssertEqual(tc.components, TCC(d: 01, h: 00, m: 00, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(d: 01, h: 00, m: 00, s: 00, f: 00)) tc = try Timecode( - TCC(h: 00, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 00, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) - try tc.add(TCC(h: 00, m: 00, s: 00, f: 2_073_599)) + try tc.add(Timecode.Components(h: 00, m: 00, s: 00, f: 2_073_599)) - XCTAssertEqual(tc.components, TCC(h: 23, m: 59, s: 59, f: 23)) + XCTAssertEqual(tc.components, Timecode.Components(h: 23, m: 59, s: 59, f: 23)) tc = try Timecode( - TCC(h: 23, m: 59, s: 59, f: 23), - at: ._23_976, - limit: ._24hours + .components(h: 23, m: 59, s: 59, f: 23), + at: .fps23_976, + limit: .max24Hours ) - try tc.subtract(TCC(h: 00, m: 00, s: 00, f: 2_073_599)) + try tc.subtract(Timecode.Components(h: 00, m: 00, s: 00, f: 2_073_599)) - XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 00, f: 00)) - try tc.add(TCC(h: 00, m: 00, s: 00, f: 200)) + try tc.add(Timecode.Components(h: 00, m: 00, s: 00, f: 200)) - try tc.subtract(TCC(h: 00, m: 00, s: 00, f: 199)) + try tc.subtract(Timecode.Components(h: 00, m: 00, s: 00, f: 199)) - XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 01)) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 00, f: 01)) // clamping tc = try Timecode( - TCC(h: 00, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours, - base: ._80SubFrames + .components(h: 00, m: 00, s: 00, f: 00), + at: .fps23_976, + base: .max80SubFrames, + limit: .max24Hours ) - tc.add(clamping: TCC(h: 25)) + tc.add(Timecode.Components(h: 25), by: .clamping) - XCTAssertEqual(tc.components, TCC(h: 23, m: 59, s: 59, f: 23, sf: 79)) + XCTAssertEqual(tc.components, Timecode.Components(h: 23, m: 59, s: 59, f: 23, sf: 79)) tc = try Timecode( - TCC(h: 00, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 00, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) - tc.subtract(clamping: TCC(h: 4)) + tc.subtract(Timecode.Components(h: 4), by: .clamping) - XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 00, f: 00)) // wrapping tc = try Timecode( - TCC(h: 00, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 00, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) - tc.add(wrapping: TCC(h: 25)) + tc.add(Timecode.Components(h: 25), by: .wrapping) - XCTAssertEqual(tc.components, TCC(h: 01, m: 00, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 01, m: 00, s: 00, f: 00)) tc = try Timecode( - TCC(h: 00, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 00, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) - tc.add(wrapping: TCC(f: -1)) // add negative number + tc.add(Timecode.Components(f: -1), by: .wrapping) // add negative number - XCTAssertEqual(tc.components, TCC(h: 23, m: 59, s: 59, f: 23)) + XCTAssertEqual(tc.components, Timecode.Components(h: 23, m: 59, s: 59, f: 23)) tc = try Timecode( - TCC(h: 00, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 00, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) - tc.subtract(wrapping: TCC(h: 4)) + tc.subtract(Timecode.Components(h: 4), by: .wrapping) - XCTAssertEqual(tc.components, TCC(h: 20, m: 00, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 20, m: 00, s: 00, f: 00)) tc = try Timecode( - TCC(h: 00, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 00, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) - tc.subtract(wrapping: TCC(h: -4)) // subtract negative number + tc.subtract(Timecode.Components(h: -4), by: .wrapping) // subtract negative number - XCTAssertEqual(tc.components, TCC(h: 04, m: 00, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 04, m: 00, s: 00, f: 00)) // drop rates tc = try Timecode( - TCC(h: 00, m: 00, s: 00, f: 00), - at: ._29_97_drop, - limit: ._24hours + .components(h: 00, m: 00, s: 00, f: 00), + at: .fps29_97d, + limit: .max24Hours ) - try tc.add(TCC(h: 00, m: 00, s: 00, f: 29)) + try tc.add(Timecode.Components(h: 00, m: 00, s: 00, f: 29)) - XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 29)) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 00, f: 29)) - try tc.add(TCC(h: 00, m: 00, s: 00, f: 01)) + try tc.add(Timecode.Components(h: 00, m: 00, s: 00, f: 01)) - XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 01, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 01, f: 00)) tc = try Timecode( - TCC(h: 00, m: 00, s: 00, f: 00), - at: ._29_97_drop, - limit: ._24hours + .components(h: 00, m: 00, s: 00, f: 00), + at: .fps29_97d, + limit: .max24Hours ) - try tc.add(TCC(m: 60)) // roll up to 1 hr + try tc.add(Timecode.Components(m: 60)) // roll up to 1 hr - XCTAssertEqual(tc.components, TCC(h: 01, m: 00, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 01, m: 00, s: 00, f: 00)) try tc = Timecode( - TCC(h: 00, m: 00, s: 00, f: 00), - at: ._29_97_drop, - limit: ._24hours + .components(h: 00, m: 00, s: 00, f: 00), + at: .fps29_97d, + limit: .max24Hours ) - try tc.add(TCC(f: 30)) // roll up to 1 sec + try tc.add(Timecode.Components(f: 30)) // roll up to 1 sec - XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 01, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 01, f: 00)) try tc = Timecode( - TCC(h: 00, m: 00, s: 59, f: 00), - at: ._29_97_drop, - limit: ._24hours + .components(h: 00, m: 00, s: 59, f: 00), + at: .fps29_97d, + limit: .max24Hours ) - try tc - .add(TCC( - f: 30 - )) // roll up to 1 sec and 2 frames (2 dropped frames every minute except every 10th minute) + // roll up to 1 sec and 2 frames (2 dropped frames every minute except every 10th minute) + try tc.add(Timecode.Components(f: 30)) - XCTAssertEqual(tc.components, TCC(h: 00, m: 01, s: 00, f: 02)) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 01, s: 00, f: 02)) tc = try Timecode( - TCC(h: 00, m: 01, s: 00, f: 02), - at: ._29_97_drop, - limit: ._24hours + .components(h: 00, m: 01, s: 00, f: 02), + at: .fps29_97d, + limit: .max24Hours ) - try tc - .add(TCC( - m: 01 - )) // roll up to 1 sec and 2 frames (2 dropped frames every minute except every 10th minute) + // roll up to 1 sec and 2 frames (2 dropped frames every minute except every 10th minute) + try tc.add(Timecode.Components(m: 01)) - XCTAssertEqual(tc.components, TCC(h: 00, m: 02, s: 00, f: 02)) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 02, s: 00, f: 02)) - try tc.add(TCC(m: 08)) + try tc.add(Timecode.Components(m: 08)) - XCTAssertEqual(tc.components, TCC(h: 00, m: 10, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 10, s: 00, f: 00)) // .adding() tc = try Timecode( - TCC(h: 00, m: 10, s: 00, f: 00), - at: ._29_97_drop, - limit: ._24hours + .components(h: 00, m: 10, s: 00, f: 00), + at: .fps29_97d, + limit: .max24Hours ) // exactly XCTAssertEqual( - try tc.adding(TCC(h: 1)).components, - TCC(h: 1, m: 10, s: 00, f: 00) + try tc.adding(Timecode.Components(h: 1)).components, + Timecode.Components(h: 1, m: 10, s: 00, f: 00) ) XCTAssertEqual( - tc.adding(wrapping: TCC(h: 26)).components, - TCC(h: 2, m: 10, s: 00, f: 00) + tc.adding(Timecode.Components(h: 26), by: .wrapping).components, + Timecode.Components(h: 2, m: 10, s: 00, f: 00) ) XCTAssertEqual( - tc.adding(clamping: TCC(h: 26)).components, - TCC(h: 23, m: 59, s: 59, f: 29, sf: tc.subFramesBase.rawValue - 1) + tc.adding(Timecode.Components(h: 26), by: .clamping).components, + Timecode.Components(h: 23, m: 59, s: 59, f: 29, sf: tc.subFramesBase.rawValue - 1) ) // .subtracting() tc = try Timecode( - TCC(h: 00, m: 10, s: 00, f: 00), - at: ._29_97_drop, - limit: ._24hours + .components(h: 00, m: 10, s: 00, f: 00), + at: .fps29_97d, + limit: .max24Hours ) // exactly XCTAssertEqual( - try tc.subtracting(TCC(m: 5)).components, - TCC(h: 0, m: 05, s: 00, f: 02) + try tc.subtracting(Timecode.Components(m: 5)).components, + Timecode.Components(h: 0, m: 05, s: 00, f: 02) ) // remember, we're using drop rate! } @@ -318,169 +648,169 @@ class Timecode_Math_Public_Tests: XCTestCase { // .multiply / .divide methods var tc = try Timecode( - TCC(h: 01, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 01, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) try tc.multiply(2) - XCTAssertEqual(tc.components, TCC(h: 02, m: 00, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 02, m: 00, s: 00, f: 00)) tc = try Timecode( - TCC(h: 01, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 01, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) try tc.multiply(2.5) - XCTAssertEqual(tc.components, TCC(h: 02, m: 30, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 02, m: 30, s: 00, f: 00)) tc = try Timecode( - TCC(h: 01, m: 00, s: 00, f: 00), - at: ._29_97_drop, - limit: ._24hours + .components(h: 01, m: 00, s: 00, f: 00), + at: .fps29_97d, + limit: .max24Hours ) try tc.multiply(2) - XCTAssertEqual(tc.components, TCC(h: 02, m: 00, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 02, m: 00, s: 00, f: 00)) tc = try Timecode( - TCC(h: 01, m: 00, s: 00, f: 00), - at: ._29_97_drop, - limit: ._24hours + .components(h: 01, m: 00, s: 00, f: 00), + at: .fps29_97d, + limit: .max24Hours ) try tc.multiply(2.5) - XCTAssertEqual(tc.components, TCC(h: 02, m: 30, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 02, m: 30, s: 00, f: 00)) tc = try Timecode( - TCC(h: 01, m: 00, s: 00, f: 00), - at: ._29_97_drop, - limit: ._24hours + .components(h: 01, m: 00, s: 00, f: 00), + at: .fps29_97d, + limit: .max24Hours ) XCTAssertThrowsError(try tc.multiply(25)) - XCTAssertEqual(tc.components, TCC(h: 01, m: 00, s: 00, f: 00)) // unchanged + XCTAssertEqual(tc.components, Timecode.Components(h: 01, m: 00, s: 00, f: 00)) // unchanged // clamping tc = try Timecode( - TCC(h: 01, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 01, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) - tc.multiply(clamping: 25.0) + tc.multiply(25.0, by: .clamping) XCTAssertEqual( tc.components, - TCC(h: 23, m: 59, s: 59, f: 23, sf: tc.subFramesBase.rawValue - 1) + Timecode.Components(h: 23, m: 59, s: 59, f: 23, sf: tc.subFramesBase.rawValue - 1) ) tc = try Timecode( - TCC(h: 00, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 00, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) - tc.divide(clamping: 4) + tc.divide(4, by: .clamping) - XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 00, f: 00)) // wrapping - multiply tc = try Timecode( - TCC(h: 01, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 01, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) - tc.multiply(wrapping: 25.0) + tc.multiply(25.0, by: .wrapping) - XCTAssertEqual(tc.components, TCC(h: 01, m: 00, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 01, m: 00, s: 00, f: 00)) tc = try Timecode( - TCC(h: 01, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 01, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) - tc.multiply(wrapping: 2) + tc.multiply(2, by: .wrapping) - XCTAssertEqual(tc.components, TCC(h: 02, m: 00, s: 00, f: 00)) // normal, no wrap + XCTAssertEqual(tc.components, Timecode.Components(h: 02, m: 00, s: 00, f: 00)) // normal, no wrap tc = try Timecode( - TCC(h: 01, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 01, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) - tc.multiply(wrapping: 25) + tc.multiply(25, by: .wrapping) - XCTAssertEqual(tc.components, TCC(h: 01, m: 00, s: 00, f: 00)) // wraps + XCTAssertEqual(tc.components, Timecode.Components(h: 01, m: 00, s: 00, f: 00)) // wraps // wrapping - divide tc = try Timecode( - TCC(h: 01, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 01, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) - tc.divide(wrapping: -2) + tc.divide(-2, by: .wrapping) - XCTAssertEqual(tc.components, TCC(h: 23, m: 30, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 23, m: 30, s: 00, f: 00)) tc = try Timecode( - TCC(h: 01, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 01, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) - tc.divide(wrapping: 2) + tc.divide(2, by: .wrapping) - XCTAssertEqual(tc.components, TCC(h: 00, m: 30, s: 00, f: 00)) // normal, no wrap + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 30, s: 00, f: 00)) // normal, no wrap tc = try Timecode( - TCC(h: 12, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 12, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) - tc.divide(wrapping: -2) + tc.divide(-2, by: .wrapping) - XCTAssertEqual(tc.components, TCC(h: 18, m: 00, s: 00, f: 00)) // wraps + XCTAssertEqual(tc.components, Timecode.Components(h: 18, m: 00, s: 00, f: 00)) // wraps // .multiplying() tc = try Timecode( - TCC(h: 04, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 04, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) // exactly XCTAssertEqual( try tc.multiplying(2).components, - TCC(h: 08, m: 00, s: 00, f: 00) + Timecode.Components(h: 08, m: 00, s: 00, f: 00) ) // .dividing() tc = try Timecode( - TCC(h: 04, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 04, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) // exactly XCTAssertEqual( try tc.dividing(2).components, - TCC(h: 02, m: 00, s: 00, f: 00) + Timecode.Components(h: 02, m: 00, s: 00, f: 00) ) } @@ -488,56 +818,56 @@ class Timecode_Math_Public_Tests: XCTestCase { // mutating var tc = try Timecode( - TCC(h: 01, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 01, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) let intervalTC = try Timecode( - TCC(h: 00, m: 01, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 00, m: 01, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) tc.offset(by: .init(intervalTC, .plus)) - XCTAssertEqual(tc.components, TCC(h: 01, m: 01, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 01, m: 01, s: 00, f: 00)) tc.offset(by: .init(intervalTC, .plus)) - XCTAssertEqual(tc.components, TCC(h: 01, m: 02, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 01, m: 02, s: 00, f: 00)) tc.offset(by: .init(intervalTC, .minus)) - XCTAssertEqual(tc.components, TCC(h: 01, m: 01, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 01, m: 01, s: 00, f: 00)) // non-mutating tc = try Timecode( - TCC(h: 01, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 01, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) XCTAssertEqual( tc .offsetting(by: .init(intervalTC, .plus)) .components, - TCC(h: 01, m: 01, s: 00, f: 00) + Timecode.Components(h: 01, m: 01, s: 00, f: 00) ) } func testIntervalTo() throws { let tc1 = try Timecode( - TCC(h: 01, m: 00, s: 00, f: 00), - at: ._23_976, - limit: ._24hours + .components(h: 01, m: 00, s: 00, f: 00), + at: .fps23_976, + limit: .max24Hours ) let tc2 = try Timecode( - TCC(h: 01, m: 04, s: 37, f: 15), - at: ._23_976, - limit: ._24hours + .components(h: 01, m: 04, s: 37, f: 15), + at: .fps23_976, + limit: .max24Hours ) // positive @@ -548,9 +878,9 @@ class Timecode_Math_Public_Tests: XCTestCase { XCTAssertEqual( interval.flattened(), try Timecode( - TCC(h: 00, m: 04, s: 37, f: 15), - at: ._23_976, - limit: ._24hours + .components(h: 00, m: 04, s: 37, f: 15), + at: .fps23_976, + limit: .max24Hours ) ) @@ -562,26 +892,26 @@ class Timecode_Math_Public_Tests: XCTestCase { XCTAssertEqual( interval.absoluteInterval, try Timecode( - TCC(h: 00, m: 04, s: 37, f: 15), - at: ._23_976, - limit: ._24hours + .components(h: 00, m: 04, s: 37, f: 15), + at: .fps23_976, + limit: .max24Hours ) ) XCTAssertEqual( interval.flattened(), try Timecode( - TCC(h: 23, m: 55, s: 22, f: 09), - at: ._23_976, - limit: ._24hours + .components(h: 23, m: 55, s: 22, f: 09), + at: .fps23_976, + limit: .max24Hours ) ) // edge cases let tc3 = try Timecode( - TCC(d: 1, h: 03, m: 04, s: 37, f: 15), - at: ._23_976, - limit: ._100days + .components(d: 1, h: 03, m: 04, s: 37, f: 15), + at: .fps23_976, + limit: .max100Days ) // positive, > 24 hours delta @@ -592,17 +922,17 @@ class Timecode_Math_Public_Tests: XCTestCase { XCTAssertEqual( interval.absoluteInterval, try Timecode( - TCC(d: 1, h: 02, m: 04, s: 37, f: 15), - at: ._23_976, - limit: ._100days + .components(d: 1, h: 02, m: 04, s: 37, f: 15), + at: .fps23_976, + limit: .max100Days ) ) XCTAssertEqual( interval.flattened(), try Timecode( - TCC(h: 02, m: 04, s: 37, f: 15), - at: ._23_976, - limit: ._24hours + .components(h: 02, m: 04, s: 37, f: 15), + at: .fps23_976, + limit: .max24Hours ) ) @@ -614,32 +944,33 @@ class Timecode_Math_Public_Tests: XCTestCase { XCTAssertEqual( interval.absoluteInterval, try Timecode( - TCC(d: 1, h: 02, m: 04, s: 37, f: 15), - at: ._23_976, - limit: ._100days + .components(d: 1, h: 02, m: 04, s: 37, f: 15), + at: .fps23_976, + limit: .max100Days ) ) XCTAssertEqual( interval.flattened(), Timecode( - rawValues: TCC(d: 98, h: 21, m: 55, s: 22, f: 09), - at: ._23_976, - limit: ._100days + .components(d: 98, h: 21, m: 55, s: 22, f: 09), + at: .fps23_976, + limit: .max100Days, + by: .allowingInvalid ) ) } func testTimecodeInterval() throws { let interval = try Timecode( - TCC(h: 02, m: 04, s: 37, f: 15), - at: ._24 + .components(h: 02, m: 04, s: 37, f: 15), + at: .fps24 ).asInterval(.minus) XCTAssertEqual( interval.flattened().components, - TCC(h: 21, m: 55, s: 22, f: 9) + Timecode.Components(h: 21, m: 55, s: 22, f: 9) ) - XCTAssertEqual(interval.flattened().frameRate, ._24) + XCTAssertEqual(interval.flattened().frameRate, .fps24) XCTAssertTrue(interval.isNegative) } } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Math/Timecode Operators Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Math/Timecode Operators Tests.swift index dd16caec..79d6277e 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Math/Timecode Operators Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Math/Timecode Operators Tests.swift @@ -1,104 +1,126 @@ // // Timecode Operators Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class Timecode_Operators_Tests: XCTestCase { override func setUp() { } override func tearDown() { } func testAdd_and_Subtract_Operators() throws { - var tc = Timecode(at: ._30) + var tc = Timecode(.zero, at: .fps30) // + and - operators - tc = try TCC(h: 00, m: 00, s: 00, f: 00).toTimecode(at: ._30) + tc = try Timecode.Components(h: 00, m: 00, s: 00, f: 00).timecode(at: .fps30) - tc = try tc + TCC(h: 00, m: 00, s: 00, f: 05).toTimecode(at: ._30) - XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 05)) + tc = try tc + Timecode.Components(h: 00, m: 00, s: 00, f: 05).timecode(at: .fps30) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 00, f: 05)) - tc = try tc - TCC(h: 00, m: 00, s: 00, f: 04).toTimecode(at: ._30) - XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 01)) + tc = try tc - Timecode.Components(h: 00, m: 00, s: 00, f: 04).timecode(at: .fps30) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 00, f: 01)) // (underflow: wraps) - tc = try tc - TCC(h: 00, m: 00, s: 00, f: 04).toTimecode(at: ._30) - XCTAssertEqual(tc.components, TCC(h: 23, m: 59, s: 59, f: 27)) + tc = try tc - Timecode.Components(h: 00, m: 00, s: 00, f: 04).timecode(at: .fps30) + XCTAssertEqual(tc.components, Timecode.Components(h: 23, m: 59, s: 59, f: 27)) // (overflow: wraps) - tc = try tc + TCC(h: 00, m: 00, s: 00, f: 05).toTimecode(at: ._30) - XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 02)) + tc = try tc + Timecode.Components(h: 00, m: 00, s: 00, f: 05).timecode(at: .fps30) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 00, f: 02)) // += and -= operators - tc = try TCC(h: 00, m: 00, s: 00, f: 00).toTimecode(at: ._30) + tc = try Timecode.Components(h: 00, m: 00, s: 00, f: 00).timecode(at: .fps30) - tc += try TCC(h: 00, m: 00, s: 00, f: 05).toTimecode(at: ._30) - XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 05)) + tc += try Timecode.Components(h: 00, m: 00, s: 00, f: 05).timecode(at: .fps30) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 00, f: 05)) - tc -= try TCC(h: 00, m: 00, s: 00, f: 04).toTimecode(at: ._30) - XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 01)) + tc -= try Timecode.Components(h: 00, m: 00, s: 00, f: 04).timecode(at: .fps30) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 00, f: 01)) // (underflow: wraps) - tc -= try TCC(h: 00, m: 00, s: 00, f: 04).toTimecode(at: ._30) - XCTAssertEqual(tc.components, TCC(h: 23, m: 59, s: 59, f: 27)) + tc -= try Timecode.Components(h: 00, m: 00, s: 00, f: 04).timecode(at: .fps30) + XCTAssertEqual(tc.components, Timecode.Components(h: 23, m: 59, s: 59, f: 27)) // (overflow: wraps) - tc += try TCC(h: 00, m: 00, s: 00, f: 05).toTimecode(at: ._30) - XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 00, f: 02)) + tc += try Timecode.Components(h: 00, m: 00, s: 00, f: 05).timecode(at: .fps30) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 00, f: 02)) } - func testMultiply_and_Divide_Operators() throws { - var tc = Timecode(at: ._30) + func testMultiply_and_Divide_Double_Operators() throws { + var tc = Timecode(.zero, at: .fps30) // * and / operators - tc = try TCC(h: 01, m: 00, s: 00, f: 00).toTimecode(at: ._30) + tc = try Timecode.Components(h: 01, m: 00, s: 00, f: 00).timecode(at: .fps30) tc = tc * 5 - XCTAssertEqual(tc.components, TCC(h: 05, m: 00, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 05, m: 00, s: 00, f: 00)) tc = tc / 5 - XCTAssertEqual(tc.components, TCC(h: 01, m: 00, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 01, m: 00, s: 00, f: 00)) // (overflow: wraps) tc = tc * 30 // == aka 30:00:00:00, 6 hours over 24:00:00:00 - XCTAssertEqual(tc.components, TCC(h: 06, m: 00, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 06, m: 00, s: 00, f: 00)) // (underflow: wraps) tc = tc * -2.5 // == aka -15:00:00:00, 15 hours under 24:00:00:00 - XCTAssertEqual(tc.components, TCC(h: 09, m: 00, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 09, m: 00, s: 00, f: 00)) // (underflow: wraps) tc = tc / -2 // == aka -4:30:00:00, 4 hours 30 min under 24:00:00:00 - XCTAssertEqual(tc.components, TCC(h: 19, m: 30, s: 00, f: 00)) + XCTAssertEqual(tc.components, Timecode.Components(h: 19, m: 30, s: 00, f: 00)) - // += and -= operators + // *= and /= operators - tc = try TCC(h: 01, m: 00, s: 00, f: 00).toTimecode(at: ._30) + tc = try Timecode.Components(h: 01, m: 00, s: 00, f: 00).timecode(at: .fps30) - tc *= 5 - XCTAssertEqual(tc.components, TCC(h: 05, m: 00, s: 00, f: 00)) + tc *= 5 + XCTAssertEqual(tc.components, Timecode.Components(h: 05, m: 00, s: 00, f: 00)) - tc /= 5 - XCTAssertEqual(tc.components, TCC(h: 01, m: 00, s: 00, f: 00)) + tc /= 5 + XCTAssertEqual(tc.components, Timecode.Components(h: 01, m: 00, s: 00, f: 00)) // (overflow: wraps) - tc *= 30 // == aka 30:00:00:00, 6 hours over 24:00:00:00 - XCTAssertEqual(tc.components, TCC(h: 06, m: 00, s: 00, f: 00)) + tc *= 30 // == aka 30:00:00:00, 6 hours over 24:00:00:00 + XCTAssertEqual(tc.components, Timecode.Components(h: 06, m: 00, s: 00, f: 00)) // (underflow: wraps) - tc *= -2.5 // == aka -15:00:00:00, 15 hours under 24:00:00:00 - XCTAssertEqual(tc.components, TCC(h: 09, m: 00, s: 00, f: 00)) + tc *= -2.5 // == aka -15:00:00:00, 15 hours under 24:00:00:00 + XCTAssertEqual(tc.components, Timecode.Components(h: 09, m: 00, s: 00, f: 00)) // (underflow: wraps) - tc /= -2 // == aka -4:30:00:00, 4 hours 30 min under 24:00:00:00 - XCTAssertEqual(tc.components, TCC(h: 19, m: 30, s: 00, f: 00)) + tc /= -2 // == aka -4:30:00:00, 4 hours 30 min under 24:00:00:00 + XCTAssertEqual(tc.components, Timecode.Components(h: 19, m: 30, s: 00, f: 00)) + } + + func testDivide_Timecode_Operator() throws { + // / operator + + XCTAssertEqual( + try Timecode.Components(h: 01, m: 00, s: 00, f: 00).timecode(at: .fps30) / + Timecode.Components(h: 01, m: 00, s: 00, f: 00).timecode(at: .fps30), + 1.0 + ) + + XCTAssertEqual( + try Timecode.Components(h: 01, m: 00, s: 00, f: 00).timecode(at: .fps30) / + Timecode.Components(h: 00, m: 10, s: 00, f: 00).timecode(at: .fps30), + 6.0 + ) + + XCTAssertEqual( + try Timecode.Components(h: 01, m: 00, s: 00, f: 00).timecode(at: .fps30) / + Timecode.Components(h: 00, m: 16, s: 00, f: 00).timecode(at: .fps30), + 3.75 + ) } } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Math/Timecode Rounding Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Math/Timecode Rounding Tests.swift index 9e693735..3deed3b7 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Math/Timecode Rounding Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Math/Timecode Rounding Tests.swift @@ -1,13 +1,13 @@ // // Timecode Rounding Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class Timecode_Rounding_Tests: XCTestCase { override func setUp() { } @@ -17,293 +17,301 @@ class Timecode_Rounding_Tests: XCTestCase { func testRoundedUp_days() throws { XCTAssertEqual( - try Timecode(at: ._24).roundedUp(toNearest: .days) + try Timecode(.zero, at: .fps24) + .roundedUp(toNearest: .days) .components, - TCC() + Timecode.Components() ) XCTAssertEqual( - try Timecode(TCC(sf: 1), at: ._24, limit: ._100days).roundedUp(toNearest: .days) + try Timecode(.components(sf: 1), at: .fps24, limit: .max100Days) + .roundedUp(toNearest: .days) .components, - TCC(d: 1) + Timecode.Components(d: 1) ) XCTAssertEqual( - try Timecode(TCC(f: 1), at: ._24, limit: ._100days).roundedUp(toNearest: .days) + try Timecode(.components(f: 1), at: .fps24, limit: .max100Days) + .roundedUp(toNearest: .days) .components, - TCC(d: 1) + Timecode.Components(d: 1) ) XCTAssertEqual( - try Timecode(TCC(s: 1), at: ._24, limit: ._100days).roundedUp(toNearest: .days) + try Timecode(.components(s: 1), at: .fps24, limit: .max100Days) + .roundedUp(toNearest: .days) .components, - TCC(d: 1) + Timecode.Components(d: 1) ) XCTAssertEqual( - try Timecode(TCC(m: 1), at: ._24, limit: ._100days).roundedUp(toNearest: .days) + try Timecode(.components(m: 1), at: .fps24, limit: .max100Days) + .roundedUp(toNearest: .days) .components, - TCC(d: 1) + Timecode.Components(d: 1) ) XCTAssertEqual( - try Timecode(TCC(h: 1), at: ._24, limit: ._100days).roundedUp(toNearest: .days) + try Timecode(.components(h: 1), at: .fps24, limit: .max100Days) + .roundedUp(toNearest: .days) .components, - TCC(d: 1) + Timecode.Components(d: 1) ) XCTAssertEqual( - try Timecode(TCC(m: 1, s: 1, f: 1, sf: 1), at: ._24, limit: ._100days) + try Timecode(.components(m: 1, s: 1, f: 1, sf: 1), at: .fps24, limit: .max100Days) .roundedUp(toNearest: .days) .components, - TCC(d: 1) + Timecode.Components(d: 1) ) XCTAssertEqual( - try Timecode(TCC(h: 1, m: 1, s: 1, f: 1, sf: 1), at: ._24, limit: ._100days) + try Timecode(.components(h: 1, m: 1, s: 1, f: 1, sf: 1), at: .fps24, limit: .max100Days) .roundedUp(toNearest: .days) .components, - TCC(d: 1) + Timecode.Components(d: 1) ) XCTAssertEqual( - try Timecode(TCC(d: 1, h: 0, m: 1, s: 1, f: 1, sf: 1), at: ._24, limit: ._100days) + try Timecode(.components(d: 1, h: 0, m: 1, s: 1, f: 1, sf: 1), at: .fps24, limit: .max100Days) .roundedUp(toNearest: .days) .components, - TCC(d: 2) + Timecode.Components(d: 2) ) } func testRoundedUp_hours() throws { XCTAssertEqual( - try Timecode(at: ._24).roundedUp(toNearest: .hours) + try Timecode(.zero, at: .fps24) + .roundedUp(toNearest: .hours) .components, - TCC() + Timecode.Components() ) XCTAssertEqual( - try Timecode(TCC(sf: 1), at: ._24).roundedUp(toNearest: .hours) + try Timecode(.components(sf: 1), at: .fps24).roundedUp(toNearest: .hours) .components, - TCC(h: 1) + Timecode.Components(h: 1) ) XCTAssertEqual( - try Timecode(TCC(f: 1), at: ._24).roundedUp(toNearest: .hours) + try Timecode(.components(f: 1), at: .fps24).roundedUp(toNearest: .hours) .components, - TCC(h: 1) + Timecode.Components(h: 1) ) XCTAssertEqual( - try Timecode(TCC(s: 1), at: ._24).roundedUp(toNearest: .hours) + try Timecode(.components(s: 1), at: .fps24).roundedUp(toNearest: .hours) .components, - TCC(h: 1) + Timecode.Components(h: 1) ) XCTAssertEqual( - try Timecode(TCC(m: 1), at: ._24).roundedUp(toNearest: .hours) + try Timecode(.components(m: 1), at: .fps24).roundedUp(toNearest: .hours) .components, - TCC(h: 1) + Timecode.Components(h: 1) ) XCTAssertEqual( - try Timecode(TCC(h: 1), at: ._24).roundedUp(toNearest: .hours) + try Timecode(.components(h: 1), at: .fps24).roundedUp(toNearest: .hours) .components, - TCC(h: 1) + Timecode.Components(h: 1) ) XCTAssertEqual( - try Timecode(TCC(m: 1, s: 1, f: 1, sf: 1), at: ._24).roundedUp(toNearest: .hours) + try Timecode(.components(m: 1, s: 1, f: 1, sf: 1), at: .fps24).roundedUp(toNearest: .hours) .components, - TCC(h: 1) + Timecode.Components(h: 1) ) XCTAssertEqual( - try Timecode(TCC(d: 1, h: 0, m: 1, s: 1, f: 1, sf: 1), at: ._24, limit: ._100days) + try Timecode(.components(d: 1, h: 0, m: 1, s: 1, f: 1, sf: 1), at: .fps24, limit: .max100Days) .roundedUp(toNearest: .hours) .components, - TCC(d: 1, h: 1) + Timecode.Components(d: 1, h: 1) ) } func testRoundedUp_minutes() throws { XCTAssertEqual( - try Timecode(at: ._24).roundedUp(toNearest: .minutes) + try Timecode(.zero, at: .fps24) + .roundedUp(toNearest: .minutes) .components, - TCC() + Timecode.Components() ) XCTAssertEqual( - try Timecode(TCC(sf: 1), at: ._24).roundedUp(toNearest: .minutes) + try Timecode(.components(sf: 1), at: .fps24).roundedUp(toNearest: .minutes) .components, - TCC(m: 1) + Timecode.Components(m: 1) ) XCTAssertEqual( - try Timecode(TCC(f: 1), at: ._24).roundedUp(toNearest: .minutes) + try Timecode(.components(f: 1), at: .fps24).roundedUp(toNearest: .minutes) .components, - TCC(m: 1) + Timecode.Components(m: 1) ) XCTAssertEqual( - try Timecode(TCC(f: 1, sf: 1), at: ._24).roundedUp(toNearest: .minutes) + try Timecode(.components(f: 1, sf: 1), at: .fps24).roundedUp(toNearest: .minutes) .components, - TCC(m: 1) + Timecode.Components(m: 1) ) XCTAssertEqual( - try Timecode(TCC(s: 2), at: ._24).roundedUp(toNearest: .minutes) + try Timecode(.components(s: 2), at: .fps24).roundedUp(toNearest: .minutes) .components, - TCC(m: 1) + Timecode.Components(m: 1) ) XCTAssertEqual( - try Timecode(TCC(s: 2, sf: 1), at: ._24).roundedUp(toNearest: .minutes) + try Timecode(.components(s: 2, sf: 1), at: .fps24).roundedUp(toNearest: .minutes) .components, - TCC(m: 1) + Timecode.Components(m: 1) ) XCTAssertEqual( - try Timecode(TCC(s: 2, f: 1), at: ._24).roundedUp(toNearest: .minutes) + try Timecode(.components(s: 2, f: 1), at: .fps24).roundedUp(toNearest: .minutes) .components, - TCC(m: 1) + Timecode.Components(m: 1) ) XCTAssertEqual( - try Timecode(TCC(s: 2, f: 1, sf: 1), at: ._24).roundedUp(toNearest: .minutes) + try Timecode(.components(s: 2, f: 1, sf: 1), at: .fps24).roundedUp(toNearest: .minutes) .components, - TCC(m: 1) + Timecode.Components(m: 1) ) XCTAssertEqual( - try Timecode(TCC(m: 1), at: ._24).roundedUp(toNearest: .minutes) + try Timecode(.components(m: 1), at: .fps24).roundedUp(toNearest: .minutes) .components, - TCC(m: 1) + Timecode.Components(m: 1) ) XCTAssertEqual( - try Timecode(TCC(m: 1, sf: 1), at: ._24).roundedUp(toNearest: .minutes) + try Timecode(.components(m: 1, sf: 1), at: .fps24).roundedUp(toNearest: .minutes) .components, - TCC(m: 2) + Timecode.Components(m: 2) ) XCTAssertEqual( - try Timecode(TCC(m: 1, f: 1), at: ._24).roundedUp(toNearest: .minutes) + try Timecode(.components(m: 1, f: 1), at: .fps24).roundedUp(toNearest: .minutes) .components, - TCC(m: 2) + Timecode.Components(m: 2) ) XCTAssertEqual( - try Timecode(TCC(m: 1, s: 1), at: ._24).roundedUp(toNearest: .minutes) + try Timecode(.components(m: 1, s: 1), at: .fps24).roundedUp(toNearest: .minutes) .components, - TCC(m: 2) + Timecode.Components(m: 2) ) XCTAssertEqual( - try Timecode(TCC(h: 1), at: ._24).roundedUp(toNearest: .minutes) + try Timecode(.components(h: 1), at: .fps24).roundedUp(toNearest: .minutes) .components, - TCC(h: 1) + Timecode.Components(h: 1) ) XCTAssertEqual( - try Timecode(TCC(h: 1, m: 1), at: ._24).roundedUp(toNearest: .minutes) + try Timecode(.components(h: 1, m: 1), at: .fps24).roundedUp(toNearest: .minutes) .components, - TCC(h: 1, m: 1) + Timecode.Components(h: 1, m: 1) ) XCTAssertEqual( - try Timecode(TCC(h: 1, m: 1, f: 1), at: ._24).roundedUp(toNearest: .minutes) + try Timecode(.components(h: 1, m: 1, f: 1), at: .fps24).roundedUp(toNearest: .minutes) .components, - TCC(h: 1, m: 2) + Timecode.Components(h: 1, m: 2) ) } func testRoundedUp_seconds() throws { XCTAssertEqual( - try Timecode(at: ._24).roundedUp(toNearest: .seconds) + try Timecode(.zero, at: .fps24).roundedUp(toNearest: .seconds) .components, - TCC() + Timecode.Components() ) XCTAssertEqual( - try Timecode(TCC(sf: 1), at: ._24).roundedUp(toNearest: .seconds) + try Timecode(.components(sf: 1), at: .fps24).roundedUp(toNearest: .seconds) .components, - TCC(s: 1) + Timecode.Components(s: 1) ) XCTAssertEqual( - try Timecode(TCC(f: 1), at: ._24).roundedUp(toNearest: .seconds) + try Timecode(.components(f: 1), at: .fps24).roundedUp(toNearest: .seconds) .components, - TCC(s: 1) + Timecode.Components(s: 1) ) XCTAssertEqual( - try Timecode(TCC(f: 1, sf: 1), at: ._24).roundedUp(toNearest: .seconds) + try Timecode(.components(f: 1, sf: 1), at: .fps24).roundedUp(toNearest: .seconds) .components, - TCC(s: 1) + Timecode.Components(s: 1) ) XCTAssertEqual( - try Timecode(TCC(s: 2), at: ._24).roundedUp(toNearest: .seconds) + try Timecode(.components(s: 2), at: .fps24).roundedUp(toNearest: .seconds) .components, - TCC(s: 2) + Timecode.Components(s: 2) ) XCTAssertEqual( - try Timecode(TCC(s: 2, sf: 1), at: ._24).roundedUp(toNearest: .seconds) + try Timecode(.components(s: 2, sf: 1), at: .fps24).roundedUp(toNearest: .seconds) .components, - TCC(s: 3) + Timecode.Components(s: 3) ) XCTAssertEqual( - try Timecode(TCC(s: 2, f: 1), at: ._24).roundedUp(toNearest: .seconds) + try Timecode(.components(s: 2, f: 1), at: .fps24).roundedUp(toNearest: .seconds) .components, - TCC(s: 3) + Timecode.Components(s: 3) ) XCTAssertEqual( - try Timecode(TCC(s: 2, f: 1, sf: 1), at: ._24).roundedUp(toNearest: .seconds) + try Timecode(.components(s: 2, f: 1, sf: 1), at: .fps24).roundedUp(toNearest: .seconds) .components, - TCC(s: 3) + Timecode.Components(s: 3) ) } func testRoundedUp_frames() throws { XCTAssertEqual( - try Timecode(at: ._24).roundedUp(toNearest: .frames) + try Timecode(.zero, at: .fps24).roundedUp(toNearest: .frames) .components, - TCC() + Timecode.Components() ) XCTAssertEqual( - try Timecode(TCC(sf: 1), at: ._24).roundedUp(toNearest: .frames) + try Timecode(.components(sf: 1), at: .fps24).roundedUp(toNearest: .frames) .components, - TCC(f: 1) + Timecode.Components(f: 1) ) XCTAssertEqual( - try Timecode(TCC(f: 1), at: ._24).roundedUp(toNearest: .frames) + try Timecode(.components(f: 1), at: .fps24).roundedUp(toNearest: .frames) .components, - TCC(f: 1) + Timecode.Components(f: 1) ) XCTAssertEqual( - try Timecode(TCC(f: 1, sf: 1), at: ._24).roundedUp(toNearest: .frames) + try Timecode(.components(f: 1, sf: 1), at: .fps24).roundedUp(toNearest: .frames) .components, - TCC(f: 2) + Timecode.Components(f: 2) ) } func testRoundedUp_frames_EdgeCases() throws { XCTAssertEqual( - try Timecode(TCC(h: 23, m: 59, s: 59, f: 23, sf: 0), at: ._24) + try Timecode(.components(h: 23, m: 59, s: 59, f: 23, sf: 0), at: .fps24) .roundedUp(toNearest: .frames) .components, - TCC(h: 23, m: 59, s: 59, f: 23, sf: 0) + Timecode.Components(h: 23, m: 59, s: 59, f: 23, sf: 0) ) // 'exactly' throws error because result would be 24:00:00:00 XCTAssertThrowsError( - try Timecode(TCC(h: 23, m: 59, s: 59, f: 23, sf: 1), at: ._24) + try Timecode(.components(h: 23, m: 59, s: 59, f: 23, sf: 1), at: .fps24) .roundedUp(toNearest: .frames) ) } @@ -312,27 +320,27 @@ class Timecode_Rounding_Tests: XCTestCase { // subFrames has no effect XCTAssertEqual( - try Timecode(at: ._24).roundedUp(toNearest: .subFrames) + try Timecode(.zero, at: .fps24).roundedUp(toNearest: .subFrames) .components, - TCC() + Timecode.Components() ) XCTAssertEqual( - try Timecode(TCC(sf: 1), at: ._24).roundedUp(toNearest: .subFrames) + try Timecode(.components(sf: 1), at: .fps24).roundedUp(toNearest: .subFrames) .components, - TCC(sf: 1) + Timecode.Components(sf: 1) ) XCTAssertEqual( - try Timecode(TCC(f: 1), at: ._24).roundedUp(toNearest: .subFrames) + try Timecode(.components(f: 1), at: .fps24).roundedUp(toNearest: .subFrames) .components, - TCC(f: 1) + Timecode.Components(f: 1) ) XCTAssertEqual( - try Timecode(TCC(f: 1, sf: 1), at: ._24).roundedUp(toNearest: .subFrames) + try Timecode(.components(f: 1, sf: 1), at: .fps24).roundedUp(toNearest: .subFrames) .components, - TCC(f: 1, sf: 1) + Timecode.Components(f: 1, sf: 1) ) } @@ -340,291 +348,298 @@ class Timecode_Rounding_Tests: XCTestCase { func testRoundedDown_days() throws { XCTAssertEqual( - Timecode(at: ._24).roundedDown(toNearest: .days) + Timecode(.zero, at: .fps24).roundedDown(toNearest: .days) .components, - TCC() + Timecode.Components() ) XCTAssertEqual( - try Timecode(TCC(sf: 1), at: ._24, limit: ._100days).roundedDown(toNearest: .days) + try Timecode(.components(sf: 1), at: .fps24, limit: .max100Days) + .roundedDown(toNearest: .days) .components, - TCC(d: 0) + Timecode.Components(d: 0) ) XCTAssertEqual( - try Timecode(TCC(f: 1), at: ._24, limit: ._100days).roundedDown(toNearest: .days) + try Timecode(.components(f: 1), at: .fps24, limit: .max100Days) + .roundedDown(toNearest: .days) .components, - TCC(d: 0) + Timecode.Components(d: 0) ) XCTAssertEqual( - try Timecode(TCC(s: 1), at: ._24, limit: ._100days).roundedDown(toNearest: .days) + try Timecode(.components(s: 1), at: .fps24, limit: .max100Days) + .roundedDown(toNearest: .days) .components, - TCC(d: 0) + Timecode.Components(d: 0) ) XCTAssertEqual( - try Timecode(TCC(m: 1), at: ._24, limit: ._100days).roundedDown(toNearest: .days) + try Timecode(.components(m: 1), at: .fps24, limit: .max100Days) + .roundedDown(toNearest: .days) .components, - TCC(d: 0) + Timecode.Components(d: 0) ) XCTAssertEqual( - try Timecode(TCC(h: 1), at: ._24, limit: ._100days).roundedDown(toNearest: .days) + try Timecode(.components(h: 1), at: .fps24, limit: .max100Days) + .roundedDown(toNearest: .days) .components, - TCC(d: 0) + Timecode.Components(d: 0) ) XCTAssertEqual( - try Timecode(TCC(m: 1, s: 1, f: 1, sf: 1), at: ._24, limit: ._100days) + try Timecode(.components(m: 1, s: 1, f: 1, sf: 1), at: .fps24, limit: .max100Days) .roundedDown(toNearest: .days) .components, - TCC(d: 0) + Timecode.Components(d: 0) ) XCTAssertEqual( - try Timecode(TCC(h: 1, m: 1, s: 1, f: 1, sf: 1), at: ._24, limit: ._100days) + try Timecode(.components(h: 1, m: 1, s: 1, f: 1, sf: 1), at: .fps24, limit: .max100Days) .roundedDown(toNearest: .days) .components, - TCC(d: 0) + Timecode.Components(d: 0) ) XCTAssertEqual( - try Timecode(TCC(d: 1), at: ._24, limit: ._100days).roundedDown(toNearest: .days) + try Timecode(.components(d: 1), at: .fps24, limit: .max100Days) + .roundedDown(toNearest: .days) .components, - TCC(d: 1) + Timecode.Components(d: 1) ) XCTAssertEqual( - try Timecode(TCC(d: 1, h: 0, m: 1, s: 1, f: 1, sf: 1), at: ._24, limit: ._100days) + try Timecode(.components(d: 1, h: 0, m: 1, s: 1, f: 1, sf: 1), at: .fps24, limit: .max100Days) .roundedDown(toNearest: .days) .components, - TCC(d: 1) + Timecode.Components(d: 1) ) } func testRoundedDown_hours() throws { XCTAssertEqual( - Timecode(at: ._24).roundedDown(toNearest: .hours) + Timecode(.zero, at: .fps24).roundedDown(toNearest: .hours) .components, - TCC() + Timecode.Components() ) XCTAssertEqual( - try Timecode(TCC(sf: 1), at: ._24).roundedDown(toNearest: .hours) + try Timecode(.components(sf: 1), at: .fps24).roundedDown(toNearest: .hours) .components, - TCC(h: 0) + Timecode.Components(h: 0) ) XCTAssertEqual( - try Timecode(TCC(f: 1), at: ._24).roundedDown(toNearest: .hours) + try Timecode(.components(f: 1), at: .fps24).roundedDown(toNearest: .hours) .components, - TCC(h: 0) + Timecode.Components(h: 0) ) XCTAssertEqual( - try Timecode(TCC(s: 1), at: ._24).roundedDown(toNearest: .hours) + try Timecode(.components(s: 1), at: .fps24).roundedDown(toNearest: .hours) .components, - TCC(h: 0) + Timecode.Components(h: 0) ) XCTAssertEqual( - try Timecode(TCC(m: 1), at: ._24).roundedDown(toNearest: .hours) + try Timecode(.components(m: 1), at: .fps24).roundedDown(toNearest: .hours) .components, - TCC(h: 0) + Timecode.Components(h: 0) ) XCTAssertEqual( - try Timecode(TCC(h: 1), at: ._24).roundedDown(toNearest: .hours) + try Timecode(.components(h: 1), at: .fps24).roundedDown(toNearest: .hours) .components, - TCC(h: 1) + Timecode.Components(h: 1) ) XCTAssertEqual( - try Timecode(TCC(m: 1, s: 1, f: 1, sf: 1), at: ._24).roundedDown(toNearest: .hours) + try Timecode(.components(m: 1, s: 1, f: 1, sf: 1), at: .fps24).roundedDown(toNearest: .hours) .components, - TCC(h: 0) + Timecode.Components(h: 0) ) XCTAssertEqual( - try Timecode(TCC(d: 1, h: 0, m: 1, s: 1, f: 1, sf: 1), at: ._24, limit: ._100days) + try Timecode(.components(d: 1, h: 0, m: 1, s: 1, f: 1, sf: 1), at: .fps24, limit: .max100Days) .roundedDown(toNearest: .hours) .components, - TCC(d: 1, h: 0) + Timecode.Components(d: 1, h: 0) ) } func testRoundedDown_minutes() throws { XCTAssertEqual( - Timecode(at: ._24).roundedDown(toNearest: .minutes) + Timecode(.zero, at: .fps24) + .roundedDown(toNearest: .minutes) .components, - TCC() + Timecode.Components() ) XCTAssertEqual( - try Timecode(TCC(sf: 1), at: ._24).roundedDown(toNearest: .minutes) + try Timecode(.components(sf: 1), at: .fps24).roundedDown(toNearest: .minutes) .components, - TCC(m: 0) + Timecode.Components(m: 0) ) XCTAssertEqual( - try Timecode(TCC(f: 1), at: ._24).roundedDown(toNearest: .minutes) + try Timecode(.components(f: 1), at: .fps24).roundedDown(toNearest: .minutes) .components, - TCC(m: 0) + Timecode.Components(m: 0) ) XCTAssertEqual( - try Timecode(TCC(f: 1, sf: 1), at: ._24).roundedDown(toNearest: .minutes) + try Timecode(.components(f: 1, sf: 1), at: .fps24).roundedDown(toNearest: .minutes) .components, - TCC(m: 0) + Timecode.Components(m: 0) ) XCTAssertEqual( - try Timecode(TCC(s: 2), at: ._24).roundedDown(toNearest: .minutes) + try Timecode(.components(s: 2), at: .fps24).roundedDown(toNearest: .minutes) .components, - TCC(m: 0) + Timecode.Components(m: 0) ) XCTAssertEqual( - try Timecode(TCC(s: 2, sf: 1), at: ._24).roundedDown(toNearest: .minutes) + try Timecode(.components(s: 2, sf: 1), at: .fps24).roundedDown(toNearest: .minutes) .components, - TCC(m: 0) + Timecode.Components(m: 0) ) XCTAssertEqual( - try Timecode(TCC(s: 2, f: 1), at: ._24).roundedDown(toNearest: .minutes) + try Timecode(.components(s: 2, f: 1), at: .fps24).roundedDown(toNearest: .minutes) .components, - TCC(m: 0) + Timecode.Components(m: 0) ) XCTAssertEqual( - try Timecode(TCC(s: 2, f: 1, sf: 1), at: ._24).roundedDown(toNearest: .minutes) + try Timecode(.components(s: 2, f: 1, sf: 1), at: .fps24).roundedDown(toNearest: .minutes) .components, - TCC(m: 0) + Timecode.Components(m: 0) ) XCTAssertEqual( - try Timecode(TCC(m: 1), at: ._24).roundedDown(toNearest: .minutes) + try Timecode(.components(m: 1), at: .fps24).roundedDown(toNearest: .minutes) .components, - TCC(m: 1) + Timecode.Components(m: 1) ) XCTAssertEqual( - try Timecode(TCC(m: 1, sf: 1), at: ._24).roundedDown(toNearest: .minutes) + try Timecode(.components(m: 1, sf: 1), at: .fps24).roundedDown(toNearest: .minutes) .components, - TCC(m: 1) + Timecode.Components(m: 1) ) XCTAssertEqual( - try Timecode(TCC(m: 1, f: 1), at: ._24).roundedDown(toNearest: .minutes) + try Timecode(.components(m: 1, f: 1), at: .fps24).roundedDown(toNearest: .minutes) .components, - TCC(m: 1) + Timecode.Components(m: 1) ) XCTAssertEqual( - try Timecode(TCC(m: 1, s: 1), at: ._24).roundedDown(toNearest: .minutes) + try Timecode(.components(m: 1, s: 1), at: .fps24).roundedDown(toNearest: .minutes) .components, - TCC(m: 1) + Timecode.Components(m: 1) ) XCTAssertEqual( - try Timecode(TCC(h: 1), at: ._24).roundedDown(toNearest: .minutes) + try Timecode(.components(h: 1), at: .fps24).roundedDown(toNearest: .minutes) .components, - TCC(h: 1) + Timecode.Components(h: 1) ) XCTAssertEqual( - try Timecode(TCC(h: 1, m: 1), at: ._24).roundedDown(toNearest: .minutes) + try Timecode(.components(h: 1, m: 1), at: .fps24).roundedDown(toNearest: .minutes) .components, - TCC(h: 1, m: 1) + Timecode.Components(h: 1, m: 1) ) XCTAssertEqual( - try Timecode(TCC(h: 1, m: 1, f: 1), at: ._24).roundedDown(toNearest: .minutes) + try Timecode(.components(h: 1, m: 1, f: 1), at: .fps24).roundedDown(toNearest: .minutes) .components, - TCC(h: 1, m: 1) + Timecode.Components(h: 1, m: 1) ) } func testRoundedDown_seconds() throws { XCTAssertEqual( - Timecode(at: ._24).roundedDown(toNearest: .seconds) + Timecode(.zero, at: .fps24).roundedDown(toNearest: .seconds) .components, - TCC() + Timecode.Components() ) XCTAssertEqual( - try Timecode(TCC(sf: 1), at: ._24).roundedDown(toNearest: .seconds) + try Timecode(.components(sf: 1), at: .fps24).roundedDown(toNearest: .seconds) .components, - TCC(s: 0) + Timecode.Components(s: 0) ) XCTAssertEqual( - try Timecode(TCC(f: 1), at: ._24).roundedDown(toNearest: .seconds) + try Timecode(.components(f: 1), at: .fps24).roundedDown(toNearest: .seconds) .components, - TCC(s: 0) + Timecode.Components(s: 0) ) XCTAssertEqual( - try Timecode(TCC(f: 1, sf: 1), at: ._24).roundedDown(toNearest: .seconds) + try Timecode(.components(f: 1, sf: 1), at: .fps24).roundedDown(toNearest: .seconds) .components, - TCC(s: 0) + Timecode.Components(s: 0) ) XCTAssertEqual( - try Timecode(TCC(s: 2), at: ._24).roundedDown(toNearest: .seconds) + try Timecode(.components(s: 2), at: .fps24).roundedDown(toNearest: .seconds) .components, - TCC(s: 2) + Timecode.Components(s: 2) ) XCTAssertEqual( - try Timecode(TCC(s: 2, sf: 1), at: ._24).roundedDown(toNearest: .seconds) + try Timecode(.components(s: 2, sf: 1), at: .fps24).roundedDown(toNearest: .seconds) .components, - TCC(s: 2) + Timecode.Components(s: 2) ) XCTAssertEqual( - try Timecode(TCC(s: 2, f: 1), at: ._24).roundedDown(toNearest: .seconds) + try Timecode(.components(s: 2, f: 1), at: .fps24).roundedDown(toNearest: .seconds) .components, - TCC(s: 2) + Timecode.Components(s: 2) ) XCTAssertEqual( - try Timecode(TCC(s: 2, f: 1, sf: 1), at: ._24).roundedDown(toNearest: .seconds) + try Timecode(.components(s: 2, f: 1, sf: 1), at: .fps24).roundedDown(toNearest: .seconds) .components, - TCC(s: 2) + Timecode.Components(s: 2) ) XCTAssertEqual( - try Timecode(TCC(h: 1, s: 2, f: 1, sf: 1), at: ._24).roundedDown(toNearest: .seconds) + try Timecode(.components(h: 1, s: 2, f: 1, sf: 1), at: .fps24).roundedDown(toNearest: .seconds) .components, - TCC(h: 1, s: 2) + Timecode.Components(h: 1, s: 2) ) } func testRoundedDown_frames() throws { XCTAssertEqual( - Timecode(at: ._23_976).roundedDown(toNearest: .frames) + Timecode(.zero, at: .fps23_976).roundedDown(toNearest: .frames) .components, - TCC() + Timecode.Components() ) XCTAssertEqual( - try Timecode(TCC(sf: 1), at: ._23_976).roundedDown(toNearest: .frames) + try Timecode(.components(sf: 1), at: .fps23_976).roundedDown(toNearest: .frames) .components, - TCC() + Timecode.Components() ) XCTAssertEqual( - try Timecode(TCC(f: 1), at: ._23_976).roundedDown(toNearest: .frames) + try Timecode(.components(f: 1), at: .fps23_976).roundedDown(toNearest: .frames) .components, - TCC(f: 1) + Timecode.Components(f: 1) ) XCTAssertEqual( - try Timecode(TCC(f: 1, sf: 1), at: ._23_976).roundedDown(toNearest: .frames) + try Timecode(.components(f: 1, sf: 1), at: .fps23_976).roundedDown(toNearest: .frames) .components, - TCC(f: 1) + Timecode.Components(f: 1) ) } @@ -632,27 +647,27 @@ class Timecode_Rounding_Tests: XCTestCase { // subFrames has no effect XCTAssertEqual( - Timecode(at: ._23_976).roundedDown(toNearest: .subFrames) + Timecode(.zero, at: .fps23_976).roundedDown(toNearest: .subFrames) .components, - TCC() + Timecode.Components() ) XCTAssertEqual( - try Timecode(TCC(sf: 1), at: ._23_976).roundedDown(toNearest: .subFrames) + try Timecode(.components(sf: 1), at: .fps23_976).roundedDown(toNearest: .subFrames) .components, - TCC(sf: 1) + Timecode.Components(sf: 1) ) XCTAssertEqual( - try Timecode(TCC(f: 1), at: ._23_976).roundedDown(toNearest: .subFrames) + try Timecode(.components(f: 1), at: .fps23_976).roundedDown(toNearest: .subFrames) .components, - TCC(f: 1) + Timecode.Components(f: 1) ) XCTAssertEqual( - try Timecode(TCC(f: 1, sf: 1), at: ._23_976).roundedDown(toNearest: .subFrames) + try Timecode(.components(f: 1, sf: 1), at: .fps23_976).roundedDown(toNearest: .subFrames) .components, - TCC(f: 1, sf: 1) + Timecode.Components(f: 1, sf: 1) ) } } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Protocol Adoptions/Codable Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Protocol Adoptions/Codable Tests.swift index cb7a6a25..f62d2474 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Protocol Adoptions/Codable Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Protocol Adoptions/Codable Tests.swift @@ -1,13 +1,13 @@ // // Codable Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class Timecode_Codable_Tests: XCTestCase { override func setUp() { } @@ -22,13 +22,11 @@ class Timecode_Codable_Tests: XCTestCase { try TimecodeFrameRate.allCases.forEach { // set up a timecode object that has all non-defaults - let tc = try "1 12:34:56:11.85" - .toTimecode( - at: $0, - limit: ._100days, - base: ._100SubFrames, - format: [.showSubFrames] - ) + let tc = try "1 12:34:56:11.85".timecode( + at: $0, + base: .max100SubFrames, + limit: .max100Days + ) // encode @@ -42,15 +40,9 @@ class Timecode_Codable_Tests: XCTestCase { XCTAssertEqual(tc, decoded) - XCTAssertEqual(tc.days, decoded.days) - XCTAssertEqual(tc.hours, decoded.hours) - XCTAssertEqual(tc.minutes, decoded.minutes) - XCTAssertEqual(tc.seconds, decoded.seconds) - XCTAssertEqual(tc.frames, decoded.frames) - XCTAssertEqual(tc.frameRate, decoded.frameRate) - XCTAssertEqual(tc.upperLimit, decoded.upperLimit) - XCTAssertEqual(tc.subFramesBase, decoded.subFramesBase) - XCTAssertEqual(tc.stringFormat, decoded.stringFormat) + XCTAssertEqual(tc.components, decoded.components) + XCTAssertEqual(tc.properties, decoded.properties) + // XCTAssertEqual(tc.stringFormat, decoded.stringFormat) // deprecated in TimecodeKit 2.0 } } } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Protocol Adoptions/Comparable Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Protocol Adoptions/Comparable Tests.swift index 6e0cc8d1..097039ed 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Protocol Adoptions/Comparable Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Protocol Adoptions/Comparable Tests.swift @@ -1,13 +1,13 @@ // // Comparable Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class Timecode_Comparable_Tests: XCTestCase { override func setUp() { } @@ -17,30 +17,30 @@ class Timecode_Comparable_Tests: XCTestCase { // == XCTAssertEqual( - try "01:00:00:00".toTimecode(at: ._23_976), - try "01:00:00:00".toTimecode(at: ._23_976) + try "01:00:00:00".timecode(at: .fps23_976), + try "01:00:00:00".timecode(at: .fps23_976) ) XCTAssertEqual( - try "01:00:00:00".toTimecode(at: ._23_976), - try "01:00:00:00".toTimecode(at: ._29_97) + try "01:00:00:00".timecode(at: .fps23_976), + try "01:00:00:00".timecode(at: .fps29_97) ) // == where elapsed frame count matches but frame rate differs (two frame rates where elapsed frames in 24 hours is identical) XCTAssertNotEqual( - try "01:00:00:00".toTimecode(at: ._23_976), - try "01:00:00:00".toTimecode(at: ._24) + try "01:00:00:00".timecode(at: .fps23_976), + try "01:00:00:00".timecode(at: .fps24) ) try TimecodeFrameRate.allCases.forEach { frameRate in XCTAssertEqual( - try "01:00:00:00".toTimecode(at: frameRate), - try "01:00:00:00".toTimecode(at: frameRate) + try "01:00:00:00".timecode(at: frameRate), + try "01:00:00:00".timecode(at: frameRate) ) XCTAssertEqual( - try "01:00:00:01".toTimecode(at: frameRate), - try "01:00:00:01".toTimecode(at: frameRate) + try "01:00:00:01".timecode(at: frameRate), + try "01:00:00:01".timecode(at: frameRate) ) } } @@ -49,33 +49,33 @@ class Timecode_Comparable_Tests: XCTestCase { // < > XCTAssertFalse( - try "01:00:00:00".toTimecode(at: ._23_976) // false because they're == - < "01:00:00:00".toTimecode(at: ._29_97) + try "01:00:00:00".timecode(at: .fps23_976) // false because they're == + < "01:00:00:00".timecode(at: .fps29_97) ) XCTAssertFalse( - try "01:00:00:00".toTimecode(at: ._23_976) // false because they're == - > "01:00:00:00".toTimecode(at: ._29_97) + try "01:00:00:00".timecode(at: .fps23_976) // false because they're == + > "01:00:00:00".timecode(at: .fps29_97) ) XCTAssertFalse( - try "01:00:00:00".toTimecode(at: ._23_976) // false because they're == - < "01:00:00:00".toTimecode(at: ._23_976) + try "01:00:00:00".timecode(at: .fps23_976) // false because they're == + < "01:00:00:00".timecode(at: .fps23_976) ) XCTAssertFalse( - try "01:00:00:00".toTimecode(at: ._23_976) // false because they're == - > "01:00:00:00".toTimecode(at: ._23_976) + try "01:00:00:00".timecode(at: .fps23_976) // false because they're == + > "01:00:00:00".timecode(at: .fps23_976) ) try TimecodeFrameRate.allCases.forEach { frameRate in XCTAssertTrue( - try "01:00:00:00".toTimecode(at: frameRate) < - "01:00:00:01".toTimecode(at: frameRate), + try "01:00:00:00".timecode(at: frameRate) < + "01:00:00:01".timecode(at: frameRate), "\(frameRate)fps" ) XCTAssertTrue( - try "01:00:00:01".toTimecode(at: frameRate) > - "01:00:00:00".toTimecode(at: frameRate), + try "01:00:00:01".timecode(at: frameRate) > + "01:00:00:00".timecode(at: frameRate), "\(frameRate)fps" ) } @@ -84,22 +84,22 @@ class Timecode_Comparable_Tests: XCTestCase { /// Assumes timeline start of zero (00:00:00:00) func testTimecode_Comparable_Sorted() throws { try TimecodeFrameRate.allCases.forEach { frameRate in - let presortedTimecodes: [Timecode] = [ - try "00:00:00:00".toTimecode(at: frameRate), - try "00:00:00:01".toTimecode(at: frameRate), - try "00:00:00:14".toTimecode(at: frameRate), - try "00:00:00:15".toTimecode(at: frameRate), - try "00:00:00:15".toTimecode(at: frameRate), // sequential dupe - try "00:00:01:00".toTimecode(at: frameRate), - try "00:00:01:01".toTimecode(at: frameRate), - try "00:00:01:23".toTimecode(at: frameRate), - try "00:00:02:00".toTimecode(at: frameRate), - try "00:01:00:05".toTimecode(at: frameRate), - try "00:02:00:08".toTimecode(at: frameRate), - try "00:23:00:10".toTimecode(at: frameRate), - try "01:00:00:00".toTimecode(at: frameRate), - try "02:00:00:00".toTimecode(at: frameRate), - try "03:00:00:00".toTimecode(at: frameRate) + let presortedTimecodes: [Timecode] = try [ + "00:00:00:00".timecode(at: frameRate), + "00:00:00:01".timecode(at: frameRate), + "00:00:00:14".timecode(at: frameRate), + "00:00:00:15".timecode(at: frameRate), + "00:00:00:15".timecode(at: frameRate), // sequential dupe + "00:00:01:00".timecode(at: frameRate), + "00:00:01:01".timecode(at: frameRate), + "00:00:01:23".timecode(at: frameRate), + "00:00:02:00".timecode(at: frameRate), + "00:01:00:05".timecode(at: frameRate), + "00:02:00:08".timecode(at: frameRate), + "00:23:00:10".timecode(at: frameRate), + "01:00:00:00".timecode(at: frameRate), + "02:00:00:00".timecode(at: frameRate), + "03:00:00:00".timecode(at: frameRate) ] // shuffle @@ -115,22 +115,22 @@ class Timecode_Comparable_Tests: XCTestCase { func testTimecode_Sorted_1HourStart() throws { try TimecodeFrameRate.allCases.forEach { frameRate in - let presorted: [Timecode] = [ - try "01:00:00:00".toTimecode(at: frameRate), - try "02:00:00:00".toTimecode(at: frameRate), - try "03:00:00:00".toTimecode(at: frameRate), - try "00:00:00:00".toTimecode(at: frameRate), - try "00:00:00:01".toTimecode(at: frameRate), - try "00:00:00:14".toTimecode(at: frameRate), - try "00:00:00:15".toTimecode(at: frameRate), - try "00:00:00:15".toTimecode(at: frameRate), // sequential dupe - try "00:00:01:00".toTimecode(at: frameRate), - try "00:00:01:01".toTimecode(at: frameRate), - try "00:00:01:23".toTimecode(at: frameRate), - try "00:00:02:00".toTimecode(at: frameRate), - try "00:01:00:05".toTimecode(at: frameRate), - try "00:02:00:08".toTimecode(at: frameRate), - try "00:23:00:10".toTimecode(at: frameRate) + let presorted: [Timecode] = try [ + "01:00:00:00".timecode(at: frameRate), + "02:00:00:00".timecode(at: frameRate), + "03:00:00:00".timecode(at: frameRate), + "00:00:00:00".timecode(at: frameRate), + "00:00:00:01".timecode(at: frameRate), + "00:00:00:14".timecode(at: frameRate), + "00:00:00:15".timecode(at: frameRate), + "00:00:00:15".timecode(at: frameRate), // sequential dupe + "00:00:01:00".timecode(at: frameRate), + "00:00:01:01".timecode(at: frameRate), + "00:00:01:23".timecode(at: frameRate), + "00:00:02:00".timecode(at: frameRate), + "00:01:00:05".timecode(at: frameRate), + "00:02:00:08".timecode(at: frameRate), + "00:23:00:10".timecode(at: frameRate) ] // shuffle @@ -138,16 +138,16 @@ class Timecode_Comparable_Tests: XCTestCase { shuffled.guaranteedShuffle() // sort the shuffled array ascending - let sortedAscending = shuffled.sorted( + let sortedAscending = try shuffled.sorted( ascending: true, - timelineStart: try "01:00:00:00".toTimecode(at: frameRate) + timelineStart: "01:00:00:00".timecode(at: frameRate) ) XCTAssertEqual(sortedAscending, presorted, "\(frameRate)fps") // sort the shuffled array descending - let sortedDecending = shuffled.sorted( + let sortedDecending = try shuffled.sorted( ascending: false, - timelineStart: try "01:00:00:00".toTimecode(at: frameRate) + timelineStart: "01:00:00:00".timecode(at: frameRate) ) let presortedReversed = Array(presorted.reversed()) XCTAssertEqual(sortedDecending, presortedReversed, "\(frameRate)fps") @@ -156,22 +156,22 @@ class Timecode_Comparable_Tests: XCTestCase { func testTimecode_Sort_1HourStart() throws { try TimecodeFrameRate.allCases.forEach { frameRate in - let presorted: [Timecode] = [ - try "01:00:00:00".toTimecode(at: frameRate), - try "02:00:00:00".toTimecode(at: frameRate), - try "03:00:00:00".toTimecode(at: frameRate), - try "00:00:00:00".toTimecode(at: frameRate), - try "00:00:00:01".toTimecode(at: frameRate), - try "00:00:00:14".toTimecode(at: frameRate), - try "00:00:00:15".toTimecode(at: frameRate), - try "00:00:00:15".toTimecode(at: frameRate), // sequential dupe - try "00:00:01:00".toTimecode(at: frameRate), - try "00:00:01:01".toTimecode(at: frameRate), - try "00:00:01:23".toTimecode(at: frameRate), - try "00:00:02:00".toTimecode(at: frameRate), - try "00:01:00:05".toTimecode(at: frameRate), - try "00:02:00:08".toTimecode(at: frameRate), - try "00:23:00:10".toTimecode(at: frameRate) + let presorted: [Timecode] = try [ + "01:00:00:00".timecode(at: frameRate), + "02:00:00:00".timecode(at: frameRate), + "03:00:00:00".timecode(at: frameRate), + "00:00:00:00".timecode(at: frameRate), + "00:00:00:01".timecode(at: frameRate), + "00:00:00:14".timecode(at: frameRate), + "00:00:00:15".timecode(at: frameRate), + "00:00:00:15".timecode(at: frameRate), // sequential dupe + "00:00:01:00".timecode(at: frameRate), + "00:00:01:01".timecode(at: frameRate), + "00:00:01:23".timecode(at: frameRate), + "00:00:02:00".timecode(at: frameRate), + "00:01:00:05".timecode(at: frameRate), + "00:02:00:08".timecode(at: frameRate), + "00:23:00:10".timecode(at: frameRate) ] // shuffle @@ -180,16 +180,18 @@ class Timecode_Comparable_Tests: XCTestCase { // sort the shuffled array ascending var sortedAscending = shuffled - sortedAscending.sort( + try sortedAscending.sort( ascending: true, - timelineStart: try "01:00:00:00".toTimecode(at: frameRate) + timelineStart: "01:00:00:00".timecode(at: frameRate) ) XCTAssertEqual(sortedAscending, presorted, "\(frameRate)fps") // sort the shuffled array descending var sortedDecending = shuffled - sortedDecending.sort(ascending: false, - timelineStart: try "01:00:00:00".toTimecode(at: frameRate)) + try sortedDecending.sort( + ascending: false, + timelineStart: "01:00:00:00".timecode(at: frameRate) + ) let presortedReversed = Array(presorted.reversed()) XCTAssertEqual(sortedDecending, presortedReversed, "\(frameRate)fps") } @@ -201,22 +203,22 @@ class Timecode_Comparable_Tests: XCTestCase { } try TimecodeFrameRate.allCases.forEach { frameRate in - let presorted: [Timecode] = [ - try "01:00:00:00".toTimecode(at: frameRate), - try "02:00:00:00".toTimecode(at: frameRate), - try "03:00:00:00".toTimecode(at: frameRate), - try "00:00:00:00".toTimecode(at: frameRate), - try "00:00:00:01".toTimecode(at: frameRate), - try "00:00:00:14".toTimecode(at: frameRate), - try "00:00:00:15".toTimecode(at: frameRate), - try "00:00:00:15".toTimecode(at: frameRate), // sequential dupe - try "00:00:01:00".toTimecode(at: frameRate), - try "00:00:01:01".toTimecode(at: frameRate), - try "00:00:01:23".toTimecode(at: frameRate), - try "00:00:02:00".toTimecode(at: frameRate), - try "00:01:00:05".toTimecode(at: frameRate), - try "00:02:00:08".toTimecode(at: frameRate), - try "00:23:00:10".toTimecode(at: frameRate) + let presorted: [Timecode] = try [ + "01:00:00:00".timecode(at: frameRate), + "02:00:00:00".timecode(at: frameRate), + "03:00:00:00".timecode(at: frameRate), + "00:00:00:00".timecode(at: frameRate), + "00:00:00:01".timecode(at: frameRate), + "00:00:00:14".timecode(at: frameRate), + "00:00:00:15".timecode(at: frameRate), + "00:00:00:15".timecode(at: frameRate), // sequential dupe + "00:00:01:00".timecode(at: frameRate), + "00:00:01:01".timecode(at: frameRate), + "00:00:01:23".timecode(at: frameRate), + "00:00:02:00".timecode(at: frameRate), + "00:01:00:05".timecode(at: frameRate), + "00:02:00:08".timecode(at: frameRate), + "00:23:00:10".timecode(at: frameRate) ] // shuffle @@ -224,18 +226,18 @@ class Timecode_Comparable_Tests: XCTestCase { shuffled.guaranteedShuffle() // sort the shuffled array ascending - let ascendingComparator = TimecodeSortComparator( + let ascendingComparator = try TimecodeSortComparator( order: .forward, - timelineStart: try "01:00:00:00".toTimecode(at: frameRate) + timelineStart: "01:00:00:00".timecode(at: frameRate) ) let sortedAscending = shuffled .sorted(using: ascendingComparator) XCTAssertEqual(sortedAscending, presorted, "\(frameRate)fps") // sort the shuffled array descending - let descendingComparator = TimecodeSortComparator( + let descendingComparator = try TimecodeSortComparator( order: .reverse, - timelineStart: try "01:00:00:00".toTimecode(at: frameRate) + timelineStart: "01:00:00:00".timecode(at: frameRate) ) let sortedDecending = shuffled .sorted(using: descendingComparator) @@ -246,10 +248,10 @@ class Timecode_Comparable_Tests: XCTestCase { /// For comparison with the context of a timeline that is != 00:00:00:00 func testCompareTo() throws { - let frameRate: TimecodeFrameRate = ._24 + let frameRate: TimecodeFrameRate = .fps24 func tc(_ string: String) throws -> Timecode { - try string.toTimecode(at: frameRate) + try string.timecode(at: frameRate) } // orderedSame (==) @@ -356,127 +358,127 @@ class Timecode_Comparable_Tests: XCTestCase { } func testCollection_isSorted() throws { - let frameRate: TimecodeFrameRate = ._24 + let frameRate: TimecodeFrameRate = .fps24 func tc(_ string: String) throws -> Timecode { - try string.toTimecode(at: frameRate) + try string.timecode(at: frameRate) } XCTAssertTrue( - [ - try tc("00:00:00:00"), - try tc("00:00:00:01"), - try tc("00:00:00:14"), - try tc("00:00:00:15"), - try tc("00:00:00:15"), // sequential dupe - try tc("00:00:01:00"), - try tc("00:00:01:01"), - try tc("00:00:01:23"), - try tc("00:00:02:00"), - try tc("00:01:00:05"), - try tc("00:02:00:08"), - try tc("00:23:00:10"), - try tc("01:00:00:00"), - try tc("02:00:00:00"), - try tc("03:00:00:00") + try [ + tc("00:00:00:00"), + tc("00:00:00:01"), + tc("00:00:00:14"), + tc("00:00:00:15"), + tc("00:00:00:15"), // sequential dupe + tc("00:00:01:00"), + tc("00:00:01:01"), + tc("00:00:01:23"), + tc("00:00:02:00"), + tc("00:01:00:05"), + tc("00:02:00:08"), + tc("00:23:00:10"), + tc("01:00:00:00"), + tc("02:00:00:00"), + tc("03:00:00:00") ] - .isSorted() // timelineStart of zero + .isSorted() // timelineStart of zero ) XCTAssertFalse( - [ - try tc("00:00:00:00"), - try tc("00:00:00:01"), - try tc("00:00:00:14"), - try tc("00:00:00:15"), - try tc("00:00:00:15"), // sequential dupe - try tc("00:00:01:00"), - try tc("00:00:01:01"), - try tc("00:00:01:23"), - try tc("00:00:02:00"), - try tc("00:01:00:05"), - try tc("00:02:00:08"), - try tc("00:23:00:10"), - try tc("01:00:00:00"), - try tc("02:00:00:00"), - try tc("03:00:00:00") + try [ + tc("00:00:00:00"), + tc("00:00:00:01"), + tc("00:00:00:14"), + tc("00:00:00:15"), + tc("00:00:00:15"), // sequential dupe + tc("00:00:01:00"), + tc("00:00:01:01"), + tc("00:00:01:23"), + tc("00:00:02:00"), + tc("00:01:00:05"), + tc("00:02:00:08"), + tc("00:23:00:10"), + tc("01:00:00:00"), + tc("02:00:00:00"), + tc("03:00:00:00") ] - .isSorted(timelineStart: try tc("01:00:00:00")) + .isSorted(timelineStart: tc("01:00:00:00")) ) XCTAssertTrue( - [ - try tc("01:00:00:00"), - try tc("02:00:00:00"), - try tc("03:00:00:00"), - try tc("00:00:00:00"), - try tc("00:00:00:01"), - try tc("00:00:00:14"), - try tc("00:00:00:15"), - try tc("00:00:00:15"), // sequential dupe - try tc("00:00:01:00"), - try tc("00:00:01:01"), - try tc("00:00:01:23"), - try tc("00:00:02:00"), - try tc("00:01:00:05"), - try tc("00:02:00:08"), - try tc("00:23:00:10"), - try tc("00:59:59:23") // 1 frame before wrap around + try [ + tc("01:00:00:00"), + tc("02:00:00:00"), + tc("03:00:00:00"), + tc("00:00:00:00"), + tc("00:00:00:01"), + tc("00:00:00:14"), + tc("00:00:00:15"), + tc("00:00:00:15"), // sequential dupe + tc("00:00:01:00"), + tc("00:00:01:01"), + tc("00:00:01:23"), + tc("00:00:02:00"), + tc("00:01:00:05"), + tc("00:02:00:08"), + tc("00:23:00:10"), + tc("00:59:59:23") // 1 frame before wrap around ] - .isSorted(timelineStart: try tc("01:00:00:00")) + .isSorted(timelineStart: tc("01:00:00:00")) ) XCTAssertFalse( - [ - try tc("01:00:00:00"), - try tc("02:00:00:00"), - try tc("03:00:00:00"), - try tc("00:00:00:00"), - try tc("00:00:00:01"), - try tc("00:00:00:14"), - try tc("00:00:00:15"), - try tc("00:00:00:15"), // sequential dupe - try tc("00:00:01:00"), - try tc("00:00:01:01"), - try tc("00:00:01:23"), - try tc("00:00:02:00"), - try tc("00:01:00:05"), - try tc("00:02:00:08"), - try tc("00:23:00:10"), - try tc("00:59:59:23") // 1 frame before wrap around + try [ + tc("01:00:00:00"), + tc("02:00:00:00"), + tc("03:00:00:00"), + tc("00:00:00:00"), + tc("00:00:00:01"), + tc("00:00:00:14"), + tc("00:00:00:15"), + tc("00:00:00:15"), // sequential dupe + tc("00:00:01:00"), + tc("00:00:01:01"), + tc("00:00:01:23"), + tc("00:00:02:00"), + tc("00:01:00:05"), + tc("00:02:00:08"), + tc("00:23:00:10"), + tc("00:59:59:23") // 1 frame before wrap around ] - .isSorted(ascending: false, timelineStart: try tc("01:00:00:00")) + .isSorted(ascending: false, timelineStart: tc("01:00:00:00")) ) XCTAssertTrue( - [ - try tc("00:59:59:23"), // 1 frame before wrap around - try tc("00:23:00:10"), - try tc("00:02:00:08"), - try tc("00:01:00:05"), - try tc("00:00:02:00"), - try tc("00:00:01:23"), - try tc("00:00:01:01"), - try tc("00:00:01:00"), - try tc("00:00:00:15"), - try tc("00:00:00:15"), // sequential dupe - try tc("00:00:00:14"), - try tc("00:00:00:01"), - try tc("00:00:00:00"), - try tc("03:00:00:00"), - try tc("02:00:00:00"), - try tc("01:00:00:00") + try [ + tc("00:59:59:23"), // 1 frame before wrap around + tc("00:23:00:10"), + tc("00:02:00:08"), + tc("00:01:00:05"), + tc("00:00:02:00"), + tc("00:00:01:23"), + tc("00:00:01:01"), + tc("00:00:01:00"), + tc("00:00:00:15"), + tc("00:00:00:15"), // sequential dupe + tc("00:00:00:14"), + tc("00:00:00:01"), + tc("00:00:00:00"), + tc("03:00:00:00"), + tc("02:00:00:00"), + tc("01:00:00:00") ] - .isSorted(ascending: false, timelineStart: try tc("01:00:00:00")) + .isSorted(ascending: false, timelineStart: tc("01:00:00:00")) ) } } // MARK: - Helpers -private extension MutableCollection where Self: RandomAccessCollection, Self: Equatable { +extension MutableCollection where Self: RandomAccessCollection, Self: Equatable { /// Guarantees shuffled array is different than the input. - mutating func guaranteedShuffle() { + fileprivate mutating func guaranteedShuffle() { // avoid endless loop with 0 or 1 array elements not being shuffleable guard count > 1 else { return } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Protocol Adoptions/CustomStringConvertible Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Protocol Adoptions/CustomStringConvertible Tests.swift index e93725dd..5e035c6f 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Protocol Adoptions/CustomStringConvertible Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Protocol Adoptions/CustomStringConvertible Tests.swift @@ -1,13 +1,13 @@ // // CustomStringConvertible Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class Timecode_CustomStringConvertible_Tests: XCTestCase { override func setUp() { } @@ -15,9 +15,9 @@ class Timecode_CustomStringConvertible_Tests: XCTestCase { func testCustomStringConvertible() throws { let tc = try Timecode( - TCC(d: 1, h: 2, m: 3, s: 4, f: 5, sf: 6), - at: ._24, - limit: ._100days + .components(d: 1, h: 2, m: 3, s: 4, f: 5, sf: 6), + at: .fps24, + limit: .max100Days ) XCTAssertNotEqual(tc.description, "") diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Protocol Adoptions/Hashable Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Protocol Adoptions/Hashable Tests.swift index a79a837c..832badeb 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Protocol Adoptions/Hashable Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Protocol Adoptions/Hashable Tests.swift @@ -1,37 +1,37 @@ // // Hashable Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class Timecode_Hashable_Tests: XCTestCase { override func setUp() { } override func tearDown() { } - func testHashValue() { + func testHashValue() throws { // hashValues should be equal XCTAssertEqual( - try "01:00:00:00".toTimecode(at: ._23_976).hashValue, - try "01:00:00:00".toTimecode(at: ._23_976).hashValue + try "01:00:00:00".timecode(at: .fps23_976).hashValue, + try "01:00:00:00".timecode(at: .fps23_976).hashValue ) XCTAssertNotEqual( - try "01:00:00:01".toTimecode(at: ._23_976).hashValue, - try "01:00:00:00".toTimecode(at: ._23_976).hashValue + try "01:00:00:01".timecode(at: .fps23_976).hashValue, + try "01:00:00:00".timecode(at: .fps23_976).hashValue ) XCTAssertNotEqual( - try "01:00:00:00".toTimecode(at: ._23_976).hashValue, - try "01:00:00:00".toTimecode(at: ._24).hashValue + try "01:00:00:00".timecode(at: .fps23_976).hashValue, + try "01:00:00:00".timecode(at: .fps24).hashValue ) XCTAssertNotEqual( - try "01:00:00:00".toTimecode(at: ._23_976).hashValue, - try "01:00:00:00".toTimecode(at: ._29_97).hashValue + try "01:00:00:00".timecode(at: .fps23_976).hashValue, + try "01:00:00:00".timecode(at: .fps29_97).hashValue ) } @@ -39,29 +39,29 @@ class Timecode_Hashable_Tests: XCTestCase { // Dictionary / Set var dict: [Timecode: String] = [:] - dict[try "01:00:00:00".toTimecode(at: ._23_976)] = "A Spot Note Here" - dict[try "01:00:00:06".toTimecode(at: ._23_976)] = "A Spot Note Also Here" + try dict["01:00:00:00".timecode(at: .fps23_976)] = "A Spot Note Here" + try dict["01:00:00:06".timecode(at: .fps23_976)] = "A Spot Note Also Here" XCTAssertEqual(dict.count, 2) - dict[try "01:00:00:00".toTimecode(at: ._24)] = "This should not replace" + try dict["01:00:00:00".timecode(at: .fps24)] = "This should not replace" XCTAssertEqual(dict.count, 3) - XCTAssertEqual(dict[try "01:00:00:00".toTimecode(at: ._23_976)], "A Spot Note Here") - XCTAssertEqual(dict[try "01:00:00:00".toTimecode(at: ._24)], "This should not replace") + XCTAssertEqual(try dict["01:00:00:00".timecode(at: .fps23_976)], "A Spot Note Here") + XCTAssertEqual(try dict["01:00:00:00".timecode(at: .fps24)], "This should not replace") } func testSet() throws { // unique timecodes are based on frame counts, irrespective of frame rate let tcSet: Set = try [ - "01:00:00:00".toTimecode(at: ._23_976), - "01:00:00:00".toTimecode(at: ._24), - "01:00:00:00".toTimecode(at: ._25), - "01:00:00:00".toTimecode(at: ._29_97), - "01:00:00:00".toTimecode(at: ._29_97_drop), - "01:00:00:00".toTimecode(at: ._30), - "01:00:00:00".toTimecode(at: ._59_94), - "01:00:00:00".toTimecode(at: ._59_94_drop), - "01:00:00:00".toTimecode(at: ._60) + "01:00:00:00".timecode(at: .fps23_976), + "01:00:00:00".timecode(at: .fps24), + "01:00:00:00".timecode(at: .fps25), + "01:00:00:00".timecode(at: .fps29_97), + "01:00:00:00".timecode(at: .fps29_97d), + "01:00:00:00".timecode(at: .fps30), + "01:00:00:00".timecode(at: .fps59_94), + "01:00:00:00".timecode(at: .fps59_94d), + "01:00:00:00".timecode(at: .fps60) ] XCTAssertNotEqual( diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Protocol Adoptions/Strideable Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Protocol Adoptions/Strideable Tests.swift index 8b340862..4228012a 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Protocol Adoptions/Strideable Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Protocol Adoptions/Strideable Tests.swift @@ -1,13 +1,13 @@ // // Strideable Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class Timecode_Strideable_Tests: XCTestCase { override func setUp() { } @@ -15,14 +15,14 @@ class Timecode_Strideable_Tests: XCTestCase { func testAdvancedBy() throws { try TimecodeFrameRate.allCases.forEach { - let frames = Timecode.frameCount(of: TCC(h: 1), at: $0).wholeFrames + let frames = Timecode.frameCount(of: Timecode.Components(h: 1), at: $0).wholeFrames - let advanced = try TCC(f: 00) - .toTimecode(at: $0) + let advanced = try Timecode.Components(f: 00) + .timecode(at: $0) .advanced(by: frames) .components - XCTAssertEqual(advanced, TCC(h: 1), "for \($0)") + XCTAssertEqual(advanced, Timecode.Components(h: 1), "for \($0)") } } @@ -30,15 +30,15 @@ class Timecode_Strideable_Tests: XCTestCase { // 24 hours stride frame count test try TimecodeFrameRate.allCases.forEach { - let zero = try TCC(h: 00, m: 00, s: 00, f: 00) - .toTimecode(at: $0) + let zero = try Timecode.Components(h: 00, m: 00, s: 00, f: 00) + .timecode(at: $0) - let target = try TCC(d: 00, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable) - .toTimecode(at: $0) + let target = try Timecode.Components(d: 00, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable) + .timecode(at: $0) let delta = zero.distance(to: target) - XCTAssertEqual(delta, $0.maxTotalFramesExpressible(in: ._24hours), "for \($0)") + XCTAssertEqual(delta, $0.maxTotalFramesExpressible(in: .max24Hours), "for \($0)") } } @@ -46,15 +46,15 @@ class Timecode_Strideable_Tests: XCTestCase { // 100 days stride frame count test try TimecodeFrameRate.allCases.forEach { - let zero = try TCC(h: 00, m: 00, s: 00, f: 00) - .toTimecode(at: $0, limit: ._100days) + let zero = try Timecode.Components(h: 00, m: 00, s: 00, f: 00) + .timecode(at: $0, limit: .max100Days) - let target = try TCC(d: 99, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable) - .toTimecode(at: $0, limit: ._100days) + let target = try Timecode.Components(d: 99, h: 23, m: 59, s: 59, f: $0.maxFrameNumberDisplayable) + .timecode(at: $0, limit: .max100Days) let delta = zero.distance(to: target) - XCTAssertEqual(delta, $0.maxTotalFramesExpressible(in: ._100days), "for \($0)") + XCTAssertEqual(delta, $0.maxTotalFramesExpressible(in: .max100Days), "for \($0)") } } @@ -63,9 +63,9 @@ class Timecode_Strideable_Tests: XCTestCase { func testTimecode_Strideable_Ranges() throws { // Stride through & array - let strideThrough = stride( - from: try "01:00:00:00".toTimecode(at: ._23_976), - through: try "01:00:00:06".toTimecode(at: ._23_976), + let strideThrough = try stride( + from: "01:00:00:00".timecode(at: .fps23_976), + through: "01:00:00:06".timecode(at: .fps23_976), by: 2 ) var array = Array(strideThrough) @@ -73,18 +73,18 @@ class Timecode_Strideable_Tests: XCTestCase { XCTAssertEqual(array.count, 4) XCTAssertEqual( array, - [ - try "01:00:00:00".toTimecode(at: ._23_976), - try "01:00:00:02".toTimecode(at: ._23_976), - try "01:00:00:04".toTimecode(at: ._23_976), - try "01:00:00:06".toTimecode(at: ._23_976) + try [ + "01:00:00:00".timecode(at: .fps23_976), + "01:00:00:02".timecode(at: .fps23_976), + "01:00:00:04".timecode(at: .fps23_976), + "01:00:00:06".timecode(at: .fps23_976) ] ) // Stride to - let strideTo = stride( - from: try "01:00:00:00".toTimecode(at: ._23_976), - to: try "01:00:00:06".toTimecode(at: ._23_976), + let strideTo = try stride( + from: "01:00:00:00".timecode(at: .fps23_976), + to: "01:00:00:06".timecode(at: .fps23_976), by: 2 ) array = Array(strideTo) @@ -92,89 +92,89 @@ class Timecode_Strideable_Tests: XCTestCase { XCTAssertEqual(array.count, 3) XCTAssertEqual( array, - [ - try "01:00:00:00".toTimecode(at: ._23_976), - try "01:00:00:02".toTimecode(at: ._23_976), - try "01:00:00:04".toTimecode(at: ._23_976) + try [ + "01:00:00:00".timecode(at: .fps23_976), + "01:00:00:02".timecode(at: .fps23_976), + "01:00:00:04".timecode(at: .fps23_976) ] ) // Strideable XCTAssertEqual( - try "01:00:00:00".toTimecode(at: ._23_976) + try "01:00:00:00".timecode(at: .fps23_976) .advanced(by: 6), - try "01:00:00:06".toTimecode(at: ._23_976) + try "01:00:00:06".timecode(at: .fps23_976) ) XCTAssertEqual( - try "01:00:00:00".toTimecode(at: ._23_976) - .distance(to: "02:00:00:00".toTimecode(at: ._23_976)), - try "01:00:00:00".toTimecode(at: ._23_976).frameCount.wholeFrames + try "01:00:00:00".timecode(at: .fps23_976) + .distance(to: "02:00:00:00".timecode(at: .fps23_976)), + try "01:00:00:00".timecode(at: .fps23_976).frameCount.wholeFrames ) - let strs = Array( + let strs = try Array( stride( - from: try "01:00:00:05".toTimecode(at: ._23_976), - through: try "01:00:10:05".toTimecode(at: ._23_976), - by: try Timecode(TCC(s: 1), at: ._23_976).frameCount.wholeFrames + from: "01:00:00:05".timecode(at: .fps23_976), + through: "01:00:10:05".timecode(at: .fps23_976), + by: Timecode(.components(s: 1), at: .fps23_976).frameCount.wholeFrames ) ) - .map { $0.stringValue } + .map { $0.stringValue() } XCTAssertEqual(strs.count, 11) - let strs2 = Array( + let strs2 = try Array( stride( - from: try "01:00:00:05".toTimecode(at: ._23_976), - to: try "01:00:10:07".toTimecode(at: ._23_976), - by: try Timecode(TCC(s: 1), at: ._23_976).frameCount.wholeFrames + from: "01:00:00:05".timecode(at: .fps23_976), + to: "01:00:10:07".timecode(at: .fps23_976), + by: Timecode(.components(s: 1), at: .fps23_976).frameCount.wholeFrames ) ) - .map { $0.stringValue } + .map { $0.stringValue() } XCTAssertEqual(strs2.count, 11) // Strideable with drop rates - // TODO: add strideable drop rates tests + // TODO: add Strideable drop rates tests // Range .contains XCTAssertTrue( - try ("01:00:00:00".toTimecode(at: ._23_976) ... "01:00:00:06".toTimecode(at: ._23_976)) - .contains(Timecode("01:00:00:02", at: ._23_976)) + try ("01:00:00:00".timecode(at: .fps23_976) ... "01:00:00:06".timecode(at: .fps23_976)) + .contains(Timecode(.string("01:00:00:02"), at: .fps23_976)) ) XCTAssertFalse( - try ("01:00:00:00".toTimecode(at: ._23_976) ... "01:00:00:06".toTimecode(at: ._23_976)) - .contains(Timecode("01:00:00:10", at: ._23_976)) + try ("01:00:00:00".timecode(at: .fps23_976) ... "01:00:00:06".timecode(at: .fps23_976)) + .contains(Timecode(.string("01:00:00:10"), at: .fps23_976)) ) XCTAssertTrue( - try ("01:00:00:00".toTimecode(at: ._23_976)...) - .contains(Timecode("01:00:00:02", at: ._23_976)) + try ("01:00:00:00".timecode(at: .fps23_976)...) + .contains(Timecode(.string("01:00:00:02"), at: .fps23_976)) ) XCTAssertTrue( - try (..."01:00:00:06".toTimecode(at: ._23_976)) - .contains(Timecode("01:00:00:02", at: ._23_976)) + try (..."01:00:00:06".timecode(at: .fps23_976)) + .contains(Timecode(.string("01:00:00:02"), at: .fps23_976)) ) // (same tests, but with ~= operator instead of .contains(...) which should produce the same result) XCTAssertTrue( - try "01:00:00:00".toTimecode(at: ._23_976) ... "01:00:00:06".toTimecode(at: ._23_976) - ~= Timecode("01:00:00:02", at: ._23_976) + try "01:00:00:00".timecode(at: .fps23_976) ... "01:00:00:06".timecode(at: .fps23_976) + ~= Timecode(.string("01:00:00:02"), at: .fps23_976) ) XCTAssertFalse( - try "01:00:00:00".toTimecode(at: ._23_976) ... "01:00:00:06".toTimecode(at: ._23_976) - ~= Timecode("01:00:00:10", at: ._23_976) + try "01:00:00:00".timecode(at: .fps23_976) ... "01:00:00:06".timecode(at: .fps23_976) + ~= Timecode(.string("01:00:00:10"), at: .fps23_976) ) XCTAssertTrue( - try "01:00:00:00".toTimecode(at: ._23_976)... - ~= Timecode("01:00:00:02", at: ._23_976) + try "01:00:00:00".timecode(at: .fps23_976)... + ~= Timecode(.string("01:00:00:02"), at: .fps23_976) ) XCTAssertTrue( - try ..."01:00:00:06".toTimecode(at: ._23_976) - ~= Timecode("01:00:00:02", at: ._23_976) + try ..."01:00:00:06".timecode(at: .fps23_976) + ~= Timecode(.string("01:00:00:02"), at: .fps23_976) ) } } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode AVAsset Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode AVAsset Tests.swift similarity index 63% rename from Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode AVAsset Tests.swift rename to Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode AVAsset Tests.swift index 31c15f28..49d33f9d 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode AVAsset Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode AVAsset Tests.swift @@ -1,15 +1,15 @@ // // Timecode AVAsset Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // // AVAssetReader is unavailable on watchOS so we can't support any AVAsset operations #if shouldTestCurrentPlatform && canImport(AVFoundation) && !os(watchOS) && !os(visionOS) -import XCTest -@testable import TimecodeKit import AVFoundation +@testable import TimecodeKit +import XCTest class Timecode_AVAsset_Tests: XCTestCase { override func setUp() { } @@ -23,27 +23,27 @@ class Timecode_AVAsset_Tests: XCTestCase { let url = try TestResource.timecodeTrack_23_976_Start_00_58_40_00.url() let asset = AVAsset(url: url) - let timecode = try Timecode(startOf: asset, format: [.showSubFrames]) - XCTAssertEqual(timecode.components, TCC(m: 58, s: 40)) - XCTAssertEqual(timecode.frameRate, ._23_976) + let timecode = try Timecode(.avAsset(asset, .start)) + XCTAssertEqual(timecode.components, Timecode.Components(m: 58, s: 40)) + XCTAssertEqual(timecode.frameRate, .fps23_976) } func testTimecode_init_durationOfAsset() throws { let url = try TestResource.timecodeTrack_23_976_Start_00_58_40_00.url() let asset = AVAsset(url: url) - let timecode = try Timecode(durationOf: asset, format: [.showSubFrames]) - XCTAssertEqual(timecode.components, TCC(m: 24, s: 10, f: 19, sf: 03)) - XCTAssertEqual(timecode.frameRate, ._23_976) + let timecode = try Timecode(.avAsset(asset, .duration)) + XCTAssertEqual(timecode.components, Timecode.Components(m: 24, s: 10, f: 19, sf: 03)) + XCTAssertEqual(timecode.frameRate, .fps23_976) } func testTimecode_init_endOfAsset() throws { let url = try TestResource.timecodeTrack_23_976_Start_00_58_40_00.url() let asset = AVAsset(url: url) - let timecode = try Timecode(endOf: asset, format: [.showSubFrames]) - XCTAssertEqual(timecode.components, TCC(h: 1, m: 22, s: 50, f: 19, sf: 03)) - XCTAssertEqual(timecode.frameRate, ._23_976) + let timecode = try Timecode(.avAsset(asset, .end)) + XCTAssertEqual(timecode.components, Timecode.Components(h: 1, m: 22, s: 50, f: 19, sf: 03)) + XCTAssertEqual(timecode.frameRate, .fps23_976) } } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode Components Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode Components Tests.swift new file mode 100644 index 00000000..557ffc0b --- /dev/null +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode Components Tests.swift @@ -0,0 +1,230 @@ +// +// Timecode Components Tests.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + +#if shouldTestCurrentPlatform + +@testable import TimecodeKit +import XCTest + +class Timecode_Components_Tests: XCTestCase { + override func setUp() { } + override func tearDown() { } + + func testTimecode_init_Components_Exactly() throws { + try TimecodeFrameRate.allCases.forEach { + let tc = try Timecode( + .components(d: 0, h: 0, m: 0, s: 0, f: 0), + at: $0 + ) + + XCTAssertEqual(tc.components, .zero, "for \($0)") + } + + try TimecodeFrameRate.allCases.forEach { + let tc = try Timecode( + .components(d: 0, h: 1, m: 2, s: 3, f: 4), + at: $0 + ) + + XCTAssertEqual( + tc.components, + Timecode.Components(d: 0, h: 1, m: 2, s: 3, f: 4), + "for \($0)" + ) + } + } + + func testTimecode_init_Components_Clamping() { + let tc = Timecode( + .components(h: 25), + at: .fps24, + by: .clamping + ) + + XCTAssertEqual( + tc.components, + Timecode.Components(h: 23, m: 59, s: 59, f: 23, sf: tc.subFramesBase.rawValue - 1) + ) + } + + func testTimecode_init_Components_ClampingEach() { + let tc = Timecode( + .components(h: 25), + at: .fps24, + by: .clampingComponents + ) + + XCTAssertEqual( + tc.components, + Timecode.Components(h: 23, m: 00, s: 00, f: 00) + ) + } + + func testTimecode_init_Components_Wrapping() { + TimecodeFrameRate.allCases.forEach { + let tc = Timecode( + .components(h: 25), + at: $0, + by: .wrapping + ) + + XCTAssertEqual(tc.components, Timecode.Components(h: 1), "for \($0)") + } + } + + func testTimecode_init_Components_RawValues() { + TimecodeFrameRate.allCases.forEach { + let tc = Timecode( + .components(d: 99, h: 99, m: 99, s: 99, f: 99, sf: 99), + at: $0, + by: .allowingInvalid + ) + + XCTAssertEqual( + tc.components, + Timecode.Components(d: 99, h: 99, m: 99, s: 99, f: 99, sf: 99), + "for \($0)" + ) + } + } + + func testTimecode_components_24Hours() { + // default + + var tc = Timecode(.zero, at: .fps30) + + XCTAssertEqual(tc.components, Timecode.Components.zero) + + // setter + + tc.components = Timecode.Components(h: 1, m: 2, s: 3, f: 4, sf: 5) + + XCTAssertEqual(tc.components, Timecode.Components(h: 1, m: 2, s: 3, f: 4, sf: 5)) + } + + func testTimecode_components_100Days() { + // default + + var tc = Timecode(.zero, at: .fps30, limit: .max100Days) + + XCTAssertEqual(tc.components, Timecode.Components.zero) + + // setter + + tc.components = Timecode.Components(d: 5, h: 1, m: 2, s: 3, f: 4, sf: 5) + + XCTAssertEqual(tc.components, Timecode.Components(d: 5, h: 1, m: 2, s: 3, f: 4, sf: 5)) + } + + func testSetTimecodeExactly() throws { + // this is not meant to test the underlying logic, simply that set() produces the intended outcome + + var tc = Timecode(.zero, at: .fps30) + + try tc.set(.components(h: 1, m: 2, s: 3, f: 4, sf: 5)) + + XCTAssertEqual(tc.components, Timecode.Components(h: 1, m: 2, s: 3, f: 4, sf: 5)) + } + + func testSetTimecodeClamping() { + // this is not meant to test the underlying logic, simply that set() produces the intended outcome + + var tc = Timecode(.zero, at: .fps30, base: .max80SubFrames) + + tc.set(.components(d: 1, h: 70, m: 70, s: 70, f: 70, sf: 500), by: .clamping) + + XCTAssertEqual(tc.components, Timecode.Components(d: 0, h: 23, m: 59, s: 59, f: 29, sf: 79)) + } + + func testSetTimecodeClampingEach() { + // this is not meant to test the underlying logic, simply that set() produces the intended outcome + + var tc = Timecode(.zero, at: .fps30, base: .max80SubFrames) + + tc.set(.components(h: 70, m: 00, s: 70, f: 00, sf: 500), by: .clampingComponents) + + XCTAssertEqual(tc.components, Timecode.Components(d: 0, h: 23, m: 00, s: 59, f: 00, sf: 79)) + } + + func testSetTimecodeWrapping() { + // this is not meant to test the underlying logic, simply that set() produces the intended outcome + + var tc = Timecode(.zero, at: .fps30) + + tc.set(.components(f: -1), by: .wrapping) + + XCTAssertEqual(tc.components, Timecode.Components(d: 0, h: 23, m: 59, s: 59, f: 29, sf: 00)) + } + + // MARK: - .timecode() + + func testTimecode_Components_toTimecode() throws { + // timecode(rawValuesAt:) + + XCTAssertEqual( + try Timecode.Components(h: 1, m: 5, s: 20, f: 14) + .timecode(at: .fps23_976), + try Timecode( + .components(h: 1, m: 5, s: 20, f: 14), + at: .fps23_976 + ) + ) + + // timecode(rawValuesAt:) with subframes + + let tcWithSubFrames = try Timecode.Components(h: 1, m: 5, s: 20, f: 14, sf: 94) + .timecode(at: .fps23_976, base: .max100SubFrames) + XCTAssertEqual( + tcWithSubFrames, + try Timecode( + .components(h: 1, m: 5, s: 20, f: 14, sf: 94), + at: .fps23_976, + base: .max100SubFrames + ) + ) + XCTAssertEqual( + tcWithSubFrames.stringValue(format: .showSubFrames), + "01:05:20:14.94" + ) + } + + func testTimecode_Components_toTimecode_rawValuesAt() throws { + // timecode(rawValuesAt:) + + XCTAssertEqual( + Timecode.Components(h: 1, m: 5, s: 20, f: 14) + .timecode(at: .fps23_976, by: .allowingInvalid), + Timecode( + .components(h: 1, m: 5, s: 20, f: 14), + at: .fps23_976, + by: .allowingInvalid + ) + ) + + // toTimecode(rawValuesAt:) with subframes + + let tcWithSubFrames = Timecode.Components(h: 1, m: 5, s: 20, f: 14, sf: 94) + .timecode( + at: .fps23_976, + base: .max100SubFrames, + by: .allowingInvalid + ) + XCTAssertEqual( + tcWithSubFrames, + try Timecode( + .components(h: 1, m: 5, s: 20, f: 14, sf: 94), + at: .fps23_976, + base: .max100SubFrames + ) + ) + XCTAssertEqual( + tcWithSubFrames.stringValue(format: .showSubFrames), + "01:05:20:14.94" + ) + } +} + +#endif diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode FeetAndFrames Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode FeetAndFrames Tests.swift similarity index 73% rename from Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode FeetAndFrames Tests.swift rename to Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode FeetAndFrames Tests.swift index 5a30b09d..2bac8391 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode FeetAndFrames Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode FeetAndFrames Tests.swift @@ -1,84 +1,84 @@ // // Timecode FeetAndFrames Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class Timecode_FeetAndFrames_Tests: XCTestCase { override func setUp() { } override func tearDown() { } func testTimecode_23_976fps_zero() throws { - let ff = Timecode(at: ._23_976).feetAndFramesValue + let ff = Timecode(.zero, at: .fps23_976).feetAndFramesValue XCTAssertEqual(ff.feet, 0) XCTAssertEqual(ff.frames, 0) } func testTimecode_23_976fps_1min() throws { - let ff = try TCC(m: 1).toTimecode(at: ._23_976).feetAndFramesValue + let ff = try Timecode.Components(m: 1).timecode(at: .fps23_976).feetAndFramesValue XCTAssertEqual(ff.feet, 90) XCTAssertEqual(ff.frames, 0) } func testTimecode_24fps_zero() throws { - let ff = Timecode(at: ._24).feetAndFramesValue + let ff = Timecode(.zero, at: .fps24).feetAndFramesValue XCTAssertEqual(ff.feet, 0) XCTAssertEqual(ff.frames, 0) } func testTimecode_24fps_1min() throws { - let ff = try TCC(m: 1).toTimecode(at: ._24).feetAndFramesValue + let ff = try Timecode.Components(m: 1).timecode(at: .fps24).feetAndFramesValue XCTAssertEqual(ff.feet, 90) XCTAssertEqual(ff.frames, 0) } func testTimecode_allRates_complex() throws { try TimecodeFrameRate.allCases.forEach { frate in - let ff = try TCC(h: 1, m: 2, s: 3, f: 4) - .toTimecode(at: frate).feetAndFramesValue + let ff = try Timecode.Components(h: 1, m: 2, s: 3, f: 4) + .timecode(at: frate).feetAndFramesValue // TimecodeFrameRate.maxTotalFrames is a good reference for groupings // which shows frame rates with the same frame counts over time switch frate { - case ._23_976, ._24: + case .fps23_976, .fps24: XCTAssertEqual(ff.feet, 5584, "\(frate)") XCTAssertEqual(ff.frames, 12, "\(frate)") - case ._24_98, ._25: + case .fps24_98, .fps25: XCTAssertEqual(ff.feet, 5817, "\(frate)") XCTAssertEqual(ff.frames, 07, "\(frate)") - case ._29_97, ._30: + case .fps29_97, .fps30: XCTAssertEqual(ff.feet, 6980, "\(frate)") XCTAssertEqual(ff.frames, 14, "\(frate)") - case ._29_97_drop, ._30_drop: + case .fps29_97d, .fps30d: XCTAssertEqual(ff.feet, 6973, "\(frate)") XCTAssertEqual(ff.frames, 14, "\(frate)") - case ._47_952, ._48: + case .fps47_952, .fps48: XCTAssertEqual(ff.feet, 11169, "\(frate)") XCTAssertEqual(ff.frames, 04, "\(frate)") - case ._50: + case .fps50: XCTAssertEqual(ff.feet, 11634, "\(frate)") XCTAssertEqual(ff.frames, 10, "\(frate)") - case ._59_94, ._60: + case .fps59_94, .fps60: XCTAssertEqual(ff.feet, 13961, "\(frate)") XCTAssertEqual(ff.frames, 08, "\(frate)") - case ._59_94_drop, ._60_drop: + case .fps59_94d, .fps60d: XCTAssertEqual(ff.feet, 13947, "\(frate)") XCTAssertEqual(ff.frames, 08, "\(frate)") - case ._95_904, ._96: + case .fps95_904, .fps96: XCTAssertEqual(ff.feet, 22338, "\(frate)") XCTAssertEqual(ff.frames, 04, "\(frate)") - case ._100: + case .fps100: XCTAssertEqual(ff.feet, 23269, "\(frate)") XCTAssertEqual(ff.frames, 00, "\(frate)") - case ._119_88, ._120: + case .fps119_88, .fps120: XCTAssertEqual(ff.feet, 27922, "\(frate)") XCTAssertEqual(ff.frames, 12, "\(frate)") - case ._119_88_drop, ._120_drop: + case .fps119_88d, .fps120d: XCTAssertEqual(ff.feet, 27894, "\(frate)") XCTAssertEqual(ff.frames, 12, "\(frate)") } @@ -90,8 +90,8 @@ class Timecode_FeetAndFrames_Tests: XCTestCase { /// Ensure subFrames are correct when set. func testTimecode_allRates_subFrames() throws { try TimecodeFrameRate.allCases.forEach { frate in - let ff = try TCC(h: 1, m: 2, s: 3, f: 4, sf: 24) - .toTimecode(at: frate).feetAndFramesValue + let ff = try Timecode.Components(h: 1, m: 2, s: 3, f: 4, sf: 24) + .timecode(at: frate).feetAndFramesValue XCTAssertEqual(ff.subFrames, 24, "\(frate)") } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode FrameCount Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode FrameCount Tests.swift new file mode 100644 index 00000000..de99bb71 --- /dev/null +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode FrameCount Tests.swift @@ -0,0 +1,263 @@ +// +// Timecode FrameCount Tests.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + +#if shouldTestCurrentPlatform + +@testable import TimecodeKit +import XCTest + +class Timecode_FrameCount_Tests: XCTestCase { + override func setUp() { } + override func tearDown() { } + + func testTimecode_init_FrameCount_Exactly() throws { + let tc = try Timecode( + .frames(Timecode.FrameCount(.frames(670_907), base: .max80SubFrames)), + at: .fps30 + ) + + XCTAssertEqual(tc.components, Timecode.Components(d: 00, h: 06, m: 12, s: 43, f: 17, sf: 00)) + } + + func testTimecode_init_FrameCount_Clamping() { + let tc = Timecode( + .frames(Timecode.FrameCount( + .frames(2_073_600 + 86400), // 25 hours @ 24fps + base: .max80SubFrames + )), + at: .fps24, + by: .clamping + ) + + XCTAssertEqual( + tc.components, + Timecode.Components(h: 23, m: 59, s: 59, f: 23, sf: tc.subFramesBase.rawValue - 1) + ) + } + + func testTimecode_init_FrameCount_Wrapping() { + let tc = Timecode( + .frames(Timecode.FrameCount( + .frames(2073600 + 86400), // 25 hours @ 24fps + base: .max80SubFrames + )), + at: .fps24, + by: .wrapping + ) + + XCTAssertEqual(tc.components, Timecode.Components(h: 01)) + } + + func testTimecode_init_FrameCount_RawValues() { + let tc = Timecode( + .frames(Timecode.FrameCount( + .frames((2073600 * 2) + 86400), // 2 days + 1 hour @ 24fps + base: .max80SubFrames + )), + at: .fps24, + by: .allowingInvalid + ) + + XCTAssertEqual(tc.components, Timecode.Components(d: 2, h: 01)) + } + + func testAllFrameRates_ElapsedFrames() { + // duration of 24 hours elapsed, rolling over to 1 day + + // also helps ensure Strideable .distance(to:) returns the correct values + + TimecodeFrameRate.allCases.forEach { + // max frames in 24 hours + + var maxFramesIn24hours: Int + + switch $0 { + case .fps23_976: maxFramesIn24hours = 2_073_600 + case .fps24: maxFramesIn24hours = 2_073_600 + case .fps24_98: maxFramesIn24hours = 2_160_000 + case .fps25: maxFramesIn24hours = 2_160_000 + case .fps29_97: maxFramesIn24hours = 2_592_000 + case .fps29_97d: maxFramesIn24hours = 2_589_408 + case .fps30: maxFramesIn24hours = 2_592_000 + case .fps30d: maxFramesIn24hours = 2_589_408 + case .fps47_952: maxFramesIn24hours = 4_147_200 + case .fps48: maxFramesIn24hours = 4_147_200 + case .fps50: maxFramesIn24hours = 4_320_000 + case .fps59_94: maxFramesIn24hours = 5_184_000 + case .fps59_94d: maxFramesIn24hours = 5_178_816 + case .fps60: maxFramesIn24hours = 5_184_000 + case .fps60d: maxFramesIn24hours = 5_178_816 + case .fps95_904: maxFramesIn24hours = 8_294_400 + case .fps96: maxFramesIn24hours = 8_294_400 + case .fps100: maxFramesIn24hours = 8_640_000 + case .fps119_88: maxFramesIn24hours = 10_368_000 + case .fps119_88d: maxFramesIn24hours = 10_357_632 + case .fps120: maxFramesIn24hours = 10_368_000 + case .fps120d: maxFramesIn24hours = 10_357_632 + } + + XCTAssertEqual( + $0.maxTotalFrames(in: .max24Hours), + maxFramesIn24hours, + "for \($0)" + ) + } + + // number of total elapsed frames in (24 hours - 1 frame), or essentially the maximum timecode expressible for each frame rate + + TimecodeFrameRate.allCases.forEach { + // max frames in 24 hours - 1 + + var maxFramesExpressibleIn24hours: Int + + switch $0 { + case .fps23_976: maxFramesExpressibleIn24hours = 2_073_600 - 1 + case .fps24: maxFramesExpressibleIn24hours = 2_073_600 - 1 + case .fps24_98: maxFramesExpressibleIn24hours = 2_160_000 - 1 + case .fps25: maxFramesExpressibleIn24hours = 2_160_000 - 1 + case .fps29_97: maxFramesExpressibleIn24hours = 2_592_000 - 1 + case .fps29_97d: maxFramesExpressibleIn24hours = 2_589_408 - 1 + case .fps30: maxFramesExpressibleIn24hours = 2_592_000 - 1 + case .fps30d: maxFramesExpressibleIn24hours = 2_589_408 - 1 + case .fps47_952: maxFramesExpressibleIn24hours = 4_147_200 - 1 + case .fps48: maxFramesExpressibleIn24hours = 4_147_200 - 1 + case .fps50: maxFramesExpressibleIn24hours = 4_320_000 - 1 + case .fps59_94: maxFramesExpressibleIn24hours = 5_184_000 - 1 + case .fps59_94d: maxFramesExpressibleIn24hours = 5_178_816 - 1 + case .fps60: maxFramesExpressibleIn24hours = 5_184_000 - 1 + case .fps60d: maxFramesExpressibleIn24hours = 5_178_816 - 1 + case .fps95_904: maxFramesExpressibleIn24hours = 8_294_400 - 1 + case .fps96: maxFramesExpressibleIn24hours = 8_294_400 - 1 + case .fps100: maxFramesExpressibleIn24hours = 8_640_000 - 1 + case .fps119_88: maxFramesExpressibleIn24hours = 10_368_000 - 1 + case .fps119_88d: maxFramesExpressibleIn24hours = 10_357_632 - 1 + case .fps120: maxFramesExpressibleIn24hours = 10_368_000 - 1 + case .fps120d: maxFramesExpressibleIn24hours = 10_357_632 - 1 + } + + XCTAssertEqual( + $0.maxTotalFramesExpressible(in: .max24Hours), + maxFramesExpressibleIn24hours, + "for \($0)" + ) + } + } + + func testSetTimecodeExactly() throws { + // this is not meant to test the underlying logic, simply that .setTimecode produces the intended outcome + + var tc = Timecode(.zero, at: .fps30, base: .max80SubFrames) + + try tc.set(Timecode.FrameCount( + .frames(670_907), + base: .max80SubFrames + )) + + XCTAssertEqual(tc.components, Timecode.Components(d: 00, h: 06, m: 12, s: 43, f: 17, sf: 00)) + } + + func testSetTimecodeFrameCount_Clamping() { + var tc = Timecode(.zero, at: .fps24, base: .max80SubFrames) + + tc.set( + Timecode.FrameCount( + .frames(2_073_600 + 86400), // 25 hours @ 24fps + base: .max80SubFrames + ), + by: .clamping + ) + + XCTAssertEqual( + tc.components, + Timecode.Components(h: 23, m: 59, s: 59, f: 23, sf: tc.subFramesBase.rawValue - 1) + ) + } + + func testSetTimecodeFrameCount_Wrapping() { + var tc = Timecode(.zero, at: .fps24, base: .max80SubFrames) + + tc.set( + Timecode.FrameCount( + .frames(2073600 + 86400), // 25 hours @ 24fps + base: .max80SubFrames + ), + by: .wrapping + ) + + XCTAssertEqual(tc.components, Timecode.Components(h: 01)) + } + + func testSetTimecodeFrameCount_RawValues() { + var tc = Timecode(.zero, at: .fps24, base: .max80SubFrames) + + tc.set( + Timecode.FrameCount( + .frames((2073600 * 2) + 86400), // 2 days + 1 hour @ 24fps + base: .max80SubFrames + ), + by: .allowingInvalid + ) + + XCTAssertEqual(tc.components, Timecode.Components(d: 2, h: 01)) + } + + func testStatic_componentsOfFrameCount_2997d() { + // edge cases + + let totalFramesIn24Hr = 2_589_408 + // let totalSubFramesIn24Hr = 207152640 + + let tcc = Timecode.components( + of: Timecode.FrameCount( + .split(frames: totalFramesIn24Hr - 1, subFrames: 79), + base: .max80SubFrames + ), + at: .fps29_97d + ) + + XCTAssertEqual(tcc, Timecode.Components(d: 0, h: 23, m: 59, s: 59, f: 29, sf: 79)) + } + + func testIsZero() { + // true + + // frames + XCTAssertTrue(Timecode.FrameCount(.frames(0), base: .max80SubFrames).isZero) + // split + XCTAssertTrue(Timecode.FrameCount(.split(frames: 0, subFrames: 0), base: .max80SubFrames).isZero) + // combined + XCTAssertTrue(Timecode.FrameCount(.combined(frames: 0.0), base: .max80SubFrames).isZero) + // split unitinterval + XCTAssertTrue(Timecode.FrameCount(.splitUnitInterval(frames: 0, subFramesUnitInterval: 0.0), base: .max80SubFrames).isZero) + + // false + + // frames + XCTAssertFalse(Timecode.FrameCount(.frames(1), base: .max80SubFrames).isZero) + XCTAssertFalse(Timecode.FrameCount(.frames(-1), base: .max80SubFrames).isZero) + // split + XCTAssertFalse(Timecode.FrameCount(.split(frames: 0, subFrames: 1), base: .max80SubFrames).isZero) + XCTAssertFalse(Timecode.FrameCount(.split(frames: 1, subFrames: 0), base: .max80SubFrames).isZero) + XCTAssertFalse(Timecode.FrameCount(.split(frames: 1, subFrames: 1), base: .max80SubFrames).isZero) + XCTAssertFalse(Timecode.FrameCount(.split(frames: 0, subFrames: -1), base: .max80SubFrames).isZero) + XCTAssertFalse(Timecode.FrameCount(.split(frames: -1, subFrames: 0), base: .max80SubFrames).isZero) + XCTAssertFalse(Timecode.FrameCount(.split(frames: -1, subFrames: -1), base: .max80SubFrames).isZero) + // combined + XCTAssertFalse(Timecode.FrameCount(.combined(frames: 0.1), base: .max80SubFrames).isZero) + XCTAssertFalse(Timecode.FrameCount(.combined(frames: 1.0), base: .max80SubFrames).isZero) + XCTAssertFalse(Timecode.FrameCount(.combined(frames: -0.1), base: .max80SubFrames).isZero) + XCTAssertFalse(Timecode.FrameCount(.combined(frames: -1.0), base: .max80SubFrames).isZero) + // split unitinterval + XCTAssertFalse(Timecode.FrameCount(.splitUnitInterval(frames: 0, subFramesUnitInterval: 0.1), base: .max80SubFrames).isZero) + XCTAssertFalse(Timecode.FrameCount(.splitUnitInterval(frames: 1, subFramesUnitInterval: 0.0), base: .max80SubFrames).isZero) + XCTAssertFalse(Timecode.FrameCount(.splitUnitInterval(frames: 1, subFramesUnitInterval: 0.1), base: .max80SubFrames).isZero) + XCTAssertFalse(Timecode.FrameCount(.splitUnitInterval(frames: 0, subFramesUnitInterval: -0.1), base: .max80SubFrames).isZero) + XCTAssertFalse(Timecode.FrameCount(.splitUnitInterval(frames: -1, subFramesUnitInterval: 0.0), base: .max80SubFrames).isZero) + XCTAssertFalse(Timecode.FrameCount(.splitUnitInterval(frames: -1, subFramesUnitInterval: -0.1), base: .max80SubFrames).isZero) + } +} + +#endif diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode FrameCount Value Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode FrameCount Value Tests.swift similarity index 70% rename from Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode FrameCount Value Tests.swift rename to Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode FrameCount Value Tests.swift index cdc0ea6a..6245b8cd 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode FrameCount Value Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode FrameCount Value Tests.swift @@ -1,13 +1,13 @@ // // Timecode FrameCount Value Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class Timecode_FrameCount_Value_Tests: XCTestCase { override func setUp() { } @@ -16,8 +16,8 @@ class Timecode_FrameCount_Value_Tests: XCTestCase { func testTimecode_init_FrameCountValue_Exactly() throws { let tc = try Timecode( .frames(670_907), - at: ._30, - limit: ._24hours + at: .fps30, + limit: .max24Hours ) XCTAssertEqual(tc.days, 0) @@ -30,22 +30,24 @@ class Timecode_FrameCount_Value_Tests: XCTestCase { func testTimecode_init_FrameCountValue_Clamping() { let tc = Timecode( - clamping: .frames(2073600 + 86400), // 25 hours @ 24fps - at: ._24, - limit: ._24hours + .frames(2073600 + 86400), // 25 hours @ 24fps + at: .fps24, + limit: .max24Hours, + by: .clamping ) XCTAssertEqual( tc.components, - TCC(h: 23, m: 59, s: 59, f: 23, sf: tc.subFramesBase.rawValue - 1) + Timecode.Components(h: 23, m: 59, s: 59, f: 23, sf: tc.subFramesBase.rawValue - 1) ) } func testTimecode_init_FrameCountValue_Wrapping() { let tc = Timecode( - wrapping: .frames(2073600 + 86400), // 25 hours @ 24fps - at: ._24, - limit: ._24hours + .frames(2073600 + 86400), // 25 hours @ 24fps + at: .fps24, + limit: .max24Hours, + by: .wrapping ) XCTAssertEqual(tc.days, 0) @@ -58,9 +60,10 @@ class Timecode_FrameCount_Value_Tests: XCTestCase { func testTimecode_init_FrameCountValue_RawValues() { let tc = Timecode( - rawValues: .frames((2073600 * 2) + 86400), // 2 days + 1 hour @ 24fps - at: ._24, - limit: ._24hours + .frames((2073600 * 2) + 86400), // 2 days + 1 hour @ 24fps + at: .fps24, + limit: .max24Hours, + by: .allowingInvalid ) XCTAssertEqual(tc.days, 2) diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode Rational CMTime Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode Rational CMTime Tests.swift new file mode 100644 index 00000000..d81a4840 --- /dev/null +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode Rational CMTime Tests.swift @@ -0,0 +1,194 @@ +// +// Timecode Rational CMTime Tests.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + +#if shouldTestCurrentPlatform + +import CoreMedia +@testable import TimecodeKit +import XCTest + +class Timecode_Rational_CMTime_Tests: XCTestCase { + override func setUp() { } + override func tearDown() { } + + func testTimecode_init_CMTime_Exactly() throws { + try TimecodeFrameRate.allCases.forEach { + let tc = try Timecode( + .cmTime(CMTime(value: 10, timescale: 1)), + at: $0, + limit: .max24Hours + ) + + // don't imperatively check each result, just make sure that a value was set; + // setter logic is unit-tested elsewhere, we just want to check the Timecode.init interface here. + XCTAssertNotEqual(tc.seconds, 0, "for \($0)") + } + } + + func testTimecode_init_CMTime() throws { + // these rational fractions and timecodes are taken from actual FCP XML files as known truth + + try TimecodeFrameRate.allCases.forEach { fRate in + switch fRate { + case .fps23_976: + XCTAssertEqual( + try Timecode(.cmTime(CMTime(value: 335335, timescale: 24000)), at: fRate).components, + Timecode.Components(h: 00, m: 00, s: 13, f: 23) + ) + case .fps24: + XCTAssertEqual( + try Timecode(.cmTime(CMTime(value: 167500, timescale: 12000)), at: fRate).components, + Timecode.Components(h: 00, m: 00, s: 13, f: 23) + ) + case .fps24_98: + break // TODO: finish this + case .fps25: // same fraction is found in FCP XML for 25p and 25i video rates + XCTAssertEqual( + try Timecode(.cmTime(CMTime(value: 34900, timescale: 2500)), at: fRate).components, + Timecode.Components(h: 00, m: 00, s: 13, f: 24) + ) + case .fps29_97: // same fraction is found in FCP XML for 29.97p and 29.97i video rates + XCTAssertEqual( + try Timecode(.cmTime(CMTime(value: 838838, timescale: 60000)), at: fRate).components, + Timecode.Components(h: 00, m: 00, s: 13, f: 29) + ) + XCTAssertEqual( + try Timecode(.cmTime(CMTime(value: 1920919, timescale: 30000)), at: fRate).components, + Timecode.Components(h: 00, m: 01, s: 03, f: 29) + ) + case .fps29_97d: + XCTAssertEqual( + try Timecode(.cmTime(CMTime(value: 419419, timescale: 30000)), at: fRate).components, + Timecode.Components(h: 00, m: 00, s: 13, f: 29) + ) + XCTAssertEqual( + try Timecode(.cmTime(CMTime(value: 1918917, timescale: 30000)), at: fRate).components, + Timecode.Components(h: 00, m: 01, s: 03, f: 29) + ) + case .fps30: + XCTAssertEqual( + try Timecode(.cmTime(CMTime(value: 83800, timescale: 6000)), at: fRate).components, + Timecode.Components(h: 00, m: 00, s: 13, f: 29) + ) + case .fps30d: + break // TODO: finish this + case .fps47_952: + break // TODO: finish this + case .fps48: + break // TODO: finish this + case .fps50: + XCTAssertEqual( + try Timecode(.cmTime(CMTime(value: 69900, timescale: 5000)), at: fRate).components, + Timecode.Components(h: 00, m: 00, s: 13, f: 49) + ) + case .fps59_94: + XCTAssertEqual( + try Timecode(.cmTime(CMTime(value: 839839, timescale: 60000)), at: fRate).components, + Timecode.Components(h: 00, m: 00, s: 13, f: 59) + ) + case .fps59_94d: + break // TODO: finish this + case .fps60: + XCTAssertEqual( + try Timecode(.cmTime(CMTime(value: 83900, timescale: 6000)), at: fRate).components, + Timecode.Components(h: 00, m: 00, s: 13, f: 59) + ) + case .fps60d: + break // TODO: finish this + case .fps95_904: + break // TODO: finish this + case .fps96: + break // TODO: finish this + case .fps100: + break // TODO: finish this + case .fps119_88: + break // TODO: finish this + case .fps119_88d: + break // TODO: finish this + case .fps120: + break // TODO: finish this + case .fps120d: + break // TODO: finish this + } + } + } + + func testTimecode_init_CMTime_Clamping() { + let tc = Timecode( + .cmTime(CMTime(value: 86400 + 3600, timescale: 1)), // 25 hours @ 24fps + at: .fps24, + limit: .max24Hours, + by: .clamping + ) + + XCTAssertEqual( + tc.components, + Timecode.Components(h: 23, m: 59, s: 59, f: 23, sf: tc.subFramesBase.rawValue - 1) + ) + } + + func testTimecode_init_CMTime_Wrapping() { + let tc = Timecode( + .cmTime(CMTime(value: 86400 + 3600, timescale: 1)), // 25 hours @ 24fps + at: .fps24, + limit: .max24Hours, + by: .wrapping + ) + + XCTAssertEqual(tc.days, 0) + XCTAssertEqual(tc.hours, 1) + XCTAssertEqual(tc.minutes, 0) + XCTAssertEqual(tc.seconds, 0) + XCTAssertEqual(tc.frames, 0) + XCTAssertEqual(tc.subFrames, 0) + } + + func testTimecode_init_CMTime_RawValues() { + let tc = Timecode( + .cmTime(CMTime(value: (86400 * 2) + 3600, timescale: 1)), // 2 days + 1 hour @ 24fps + at: .fps24, + limit: .max24Hours, + by: .allowingInvalid + ) + + XCTAssertEqual(tc.days, 2) + XCTAssertEqual(tc.hours, 1) + XCTAssertEqual(tc.minutes, 0) + XCTAssertEqual(tc.seconds, 0) + XCTAssertEqual(tc.frames, 0) + XCTAssertEqual(tc.subFrames, 0) + } + + func testTimecode_cmTimeValue() throws { + // test a small range of timecodes at each frame rate + // and ensure the fraction can re-form the same timecode + try TimecodeFrameRate.allCases.forEach { fRate in + let s = try Timecode.Components(m: 8, f: 20).timecode(at: fRate) + let e = try Timecode.Components(m: 10, f: 5).timecode(at: fRate) + + try (s ... e).forEach { tc in + let f = tc.cmTimeValue + let reformedTC = try Timecode(.cmTime(f), at: fRate) + XCTAssertEqual(tc, reformedTC) + } + } + } + + func testTimecode_cmTimeValue_SpotCheck() throws { + let tc = try Timecode.Components(h: 00, m: 00, s: 13, f: 29).timecode(at: .fps29_97d) + XCTAssertEqual(tc.cmTimeValue.value, 419419) + XCTAssertEqual(tc.cmTimeValue.timescale, 30000) + } + + func testCMTime_timecode() throws { + XCTAssertEqual( + try CMTime(value: 3600, timescale: 1).timecode(at: .fps24).components, + Timecode.Components(h: 1) + ) + } +} + +#endif diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode Rational Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode Rational Tests.swift similarity index 54% rename from Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode Rational Tests.swift rename to Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode Rational Tests.swift index b6602bf6..05fdb715 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode Rational Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode Rational Tests.swift @@ -1,13 +1,13 @@ // // Timecode Rational Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class Timecode_Rational_Tests: XCTestCase { override func setUp() { } @@ -16,9 +16,8 @@ class Timecode_Rational_Tests: XCTestCase { func testTimecode_init_Rational_Exactly() throws { try TimecodeFrameRate.allCases.forEach { let tc = try Timecode( - Fraction(10, 1), - at: $0, - limit: ._24hours + .rational(Fraction(10, 1)), + at: $0 ) // don't imperatively check each result, just make sure that a value was set; @@ -32,84 +31,84 @@ class Timecode_Rational_Tests: XCTestCase { try TimecodeFrameRate.allCases.forEach { fRate in switch fRate { - case ._23_976: + case .fps23_976: XCTAssertEqual( - try Timecode(Fraction(335335, 24000), at: fRate).components, - TCC(h: 00, m: 00, s: 13, f: 23) + try Timecode(.rational(Fraction(335335, 24000)), at: fRate).components, + Timecode.Components(h: 00, m: 00, s: 13, f: 23) ) - case ._24: + case .fps24: XCTAssertEqual( - try Timecode(Fraction(167500, 12000), at: fRate).components, - TCC(h: 00, m: 00, s: 13, f: 23) + try Timecode(.rational(Fraction(167500, 12000)), at: fRate).components, + Timecode.Components(h: 00, m: 00, s: 13, f: 23) ) - case ._24_98: + case .fps24_98: break // TODO: finish this - case ._25: // same fraction is found in FCP XML for 25p and 25i video rates + case .fps25: // same fraction is found in FCP XML for 25p and 25i video rates XCTAssertEqual( - try Timecode(Fraction(34900, 2500), at: fRate).components, - TCC(h: 00, m: 00, s: 13, f: 24) + try Timecode(.rational(Fraction(34900, 2500)), at: fRate).components, + Timecode.Components(h: 00, m: 00, s: 13, f: 24) ) - case ._29_97: // same fraction is found in FCP XML for 29.97p and 29.97i video rates + case .fps29_97: // same fraction is found in FCP XML for 29.97p and 29.97i video rates XCTAssertEqual( - try Timecode(Fraction(838838, 60000), at: fRate).components, - TCC(h: 00, m: 00, s: 13, f: 29) + try Timecode(.rational(Fraction(838838, 60000)), at: fRate).components, + Timecode.Components(h: 00, m: 00, s: 13, f: 29) ) XCTAssertEqual( - try Timecode(Fraction(1920919, 30000), at: fRate).components, - TCC(h: 00, m: 01, s: 03, f: 29) + try Timecode(.rational(Fraction(1920919, 30000)), at: fRate).components, + Timecode.Components(h: 00, m: 01, s: 03, f: 29) ) - case ._29_97_drop: + case .fps29_97d: XCTAssertEqual( - try Timecode(Fraction(419419, 30000), at: fRate).components, - TCC(h: 00, m: 00, s: 13, f: 29) + try Timecode(.rational(Fraction(419419, 30000)), at: fRate).components, + Timecode.Components(h: 00, m: 00, s: 13, f: 29) ) XCTAssertEqual( - try Timecode(Fraction(1918917, 30000), at: fRate).components, - TCC(h: 00, m: 01, s: 03, f: 29) + try Timecode(.rational(Fraction(1918917, 30000)), at: fRate).components, + Timecode.Components(h: 00, m: 01, s: 03, f: 29) ) - case ._30: + case .fps30: XCTAssertEqual( - try Timecode(Fraction(83800, 6000), at: fRate).components, - TCC(h: 00, m: 00, s: 13, f: 29) + try Timecode(.rational(Fraction(83800, 6000)), at: fRate).components, + Timecode.Components(h: 00, m: 00, s: 13, f: 29) ) - case ._30_drop: + case .fps30d: break // TODO: finish this - case ._47_952: + case .fps47_952: break // TODO: finish this - case ._48: + case .fps48: break // TODO: finish this - case ._50: + case .fps50: XCTAssertEqual( - try Timecode(Fraction(69900, 5000), at: fRate).components, - TCC(h: 00, m: 00, s: 13, f: 49) + try Timecode(.rational(Fraction(69900, 5000)), at: fRate).components, + Timecode.Components(h: 00, m: 00, s: 13, f: 49) ) - case ._59_94: + case .fps59_94: XCTAssertEqual( - try Timecode(Fraction(839839, 60000), at: fRate).components, - TCC(h: 00, m: 00, s: 13, f: 59) + try Timecode(.rational(Fraction(839839, 60000)), at: fRate).components, + Timecode.Components(h: 00, m: 00, s: 13, f: 59) ) - case ._59_94_drop: + case .fps59_94d: break // TODO: finish this - case ._60: + case .fps60: XCTAssertEqual( - try Timecode(Fraction(83900, 6000), at: fRate).components, - TCC(h: 00, m: 00, s: 13, f: 59) + try Timecode(.rational(Fraction(83900, 6000)), at: fRate).components, + Timecode.Components(h: 00, m: 00, s: 13, f: 59) ) - case ._60_drop: + case .fps60d: break // TODO: finish this - case ._95_904: + case .fps95_904: break // TODO: finish this - case ._96: + case .fps96: break // TODO: finish this - case ._100: + case .fps100: break // TODO: finish this - case ._119_88: + case .fps119_88: break // TODO: finish this - case ._119_88_drop: + case .fps119_88d: break // TODO: finish this - case ._120: + case .fps120: break // TODO: finish this - case ._120_drop: + case .fps120d: break // TODO: finish this } } @@ -117,80 +116,80 @@ class Timecode_Rational_Tests: XCTestCase { func testTimecode_init_Rational_Clamping() { let tc = Timecode( - clamping: Fraction(86400 + 3600, 1), // 25 hours @ 24fps - at: ._24, - limit: ._24hours + .rational(Fraction(86400 + 3600, 1)), // 25 hours @ 24fps + at: .fps24, + by: .clamping ) XCTAssertEqual( tc.components, - TCC(h: 23, m: 59, s: 59, f: 23, sf: tc.subFramesBase.rawValue - 1) + Timecode.Components(h: 23, m: 59, s: 59, f: 23, sf: tc.subFramesBase.rawValue - 1) ) } func testTimecode_init_Rational_Clamping_Negative() { let tc = Timecode( - clamping: Fraction(-2, 1), - at: ._24, - limit: ._24hours + .rational(Fraction(-2, 1)), + at: .fps24, + by: .clamping ) XCTAssertEqual( tc.components, - TCC(h: 00, m: 00, s: 00, f: 00) + Timecode.Components(h: 00, m: 00, s: 00, f: 00) ) } func testTimecode_init_Rational_Wrapping() { let tc = Timecode( - wrapping: Fraction(86400 + 3600, 1), // 25 hours @ 24fps - at: ._24, - limit: ._24hours + .rational(Fraction(86400 + 3600, 1)), // 25 hours @ 24fps + at: .fps24, + by: .wrapping ) XCTAssertEqual( tc.components, - TCC(d: 00, h: 01, m: 00, s: 00, f: 00, sf: 00) + Timecode.Components(d: 00, h: 01, m: 00, s: 00, f: 00, sf: 00) ) } func testTimecode_init_Rational_Wrapping_Negative() { let tc = Timecode( - wrapping: Fraction(-2, 1), - at: ._24, - limit: ._24hours + .rational(Fraction(-2, 1)), + at: .fps24, + by: .wrapping ) XCTAssertEqual( tc.components, - TCC(d: 00, h: 23, m: 59, s: 58, f: 00, sf: 00) + Timecode.Components(d: 00, h: 23, m: 59, s: 58, f: 00, sf: 00) ) } func testTimecode_init_Rational_RawValues() { let tc = Timecode( - rawValues: Fraction((86400 * 2) + 3600, 1), // 2 days + 1 hour @ 24fps - at: ._24, - limit: ._24hours + .rational(Fraction((86400 * 2) + 3600, 1)), // 2 days + 1 hour @ 24fps + at: .fps24, + by: .allowingInvalid ) XCTAssertEqual( tc.components, - TCC(d: 02, h: 01, m: 00, s: 00, f: 00, sf: 00) + Timecode.Components(d: 02, h: 01, m: 00, s: 00, f: 00, sf: 00) ) } func testTimecode_init_Rational_RawValues_Negative() { let tc = Timecode( - rawValues: Fraction(-(3600 + 60 + 5), 1), - at: ._24, - limit: ._24hours + .rational(Fraction(-(3600 + 60 + 5), 1)), + at: .fps24, + by: .allowingInvalid ) // Negates only the largest non-zero component if input is negative XCTAssertEqual( tc.components, - TCC(d: 00, h: -01, m: 01, s: 05, f: 00, sf: 00) + Timecode.Components(d: 00, h: -01, m: 01, s: 05, f: 00, sf: 00) ) } @@ -198,33 +197,33 @@ class Timecode_Rational_Tests: XCTestCase { // test a small range of timecodes at each frame rate // and ensure the fraction can re-form the same timecode try TimecodeFrameRate.allCases.forEach { fRate in - let s = try TCC(m: 8, f: 20).toTimecode(at: fRate) - let e = try TCC(m: 10, f: 5).toTimecode(at: fRate) + let s = try Timecode.Components(m: 8, f: 20).timecode(at: fRate) + let e = try Timecode.Components(m: 10, f: 5).timecode(at: fRate) try (s ... e).forEach { tc in let f = tc.rationalValue - let reformedTC = try Timecode(f, at: fRate) + let reformedTC = try Timecode(.rational(f), at: fRate) XCTAssertEqual(tc, reformedTC) } } } func testTimecode_RationalValue_SpotCheck() throws { - let tc = try TCC(h: 00, m: 00, s: 13, f: 29).toTimecode(at: ._29_97_drop) + let tc = try Timecode.Components(h: 00, m: 00, s: 13, f: 29).timecode(at: .fps29_97d) XCTAssertEqual(tc.rationalValue.numerator, 419419) XCTAssertEqual(tc.rationalValue.denominator, 30000) } - func testFraction_toTimecode() throws { + func testFraction_timecode() throws { XCTAssertEqual( - try Fraction(3600, 1).toTimecode(at: ._24).components, - TCC(h: 1) + try Fraction(3600, 1).timecode(at: .fps24).components, + Timecode.Components(h: 1) ) } func testTimecode_RationalValue_Subframes() throws { - let tc = try TCC(h: 00, m: 00, s: 01, f: 11, sf: 56) - .toTimecode(at: ._25, base: ._80SubFrames) + let tc = try Timecode.Components(h: 00, m: 00, s: 01, f: 11, sf: 56) + .timecode(at: .fps25, base: .max80SubFrames) XCTAssertEqual(tc.rationalValue, Fraction(367, 250)) } @@ -235,8 +234,8 @@ class Timecode_Rational_Tests: XCTestCase { // FYI: when we convert it back to a fraction from timecode, // the fraction ends up 367/250 let frac = Fraction(11011, 7500) - let tc = try frac.toTimecode(at: ._25, base: ._80SubFrames) - XCTAssertEqual(tc.components, TCC(h: 00, m: 00, s: 01, f: 11, sf: 56)) + let tc = try frac.timecode(at: .fps25, base: .max80SubFrames) + XCTAssertEqual(tc.components, Timecode.Components(h: 00, m: 00, s: 01, f: 11, sf: 56)) XCTAssertEqual(tc.rationalValue, Fraction(367, 250)) } @@ -247,7 +246,7 @@ class Timecode_Rational_Tests: XCTestCase { // FYI: when we convert it back to a fraction from timecode, // the fraction ends up 367/250 let frac = Fraction(11011, 7500) - let tc = try frac.toTimecode(at: ._25, base: ._80SubFrames) + let tc = try frac.timecode(at: .fps25, base: .max80SubFrames) let int = tc.frameCount(of: frac) XCTAssertEqual(int, 36) } @@ -259,7 +258,7 @@ class Timecode_Rational_Tests: XCTestCase { // FYI: when we convert it back to a fraction from timecode, // the fraction ends up 367/250 let frac = Fraction(11011, 7500) - let tc = try frac.toTimecode(at: ._25, base: ._80SubFrames) + let tc = try frac.timecode(at: .fps25, base: .max80SubFrames) let float = tc.floatingFrameCount(of: frac) XCTAssertEqual(float, 36.70333333333333) } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode Real Time Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode Real Time Tests.swift similarity index 57% rename from Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode Real Time Tests.swift rename to Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode Real Time Tests.swift index bed1734c..884f2a53 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode Real Time Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode Real Time Tests.swift @@ -1,13 +1,13 @@ // // Timecode Real Time Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class Timecode_RealTime_Tests: XCTestCase { // pre-computed constants @@ -24,9 +24,8 @@ class Timecode_RealTime_Tests: XCTestCase { func testTimecode_init_RealTimeValue_Exactly() throws { try TimecodeFrameRate.allCases.forEach { let tc = try Timecode( - realTime: 2, - at: $0, - limit: ._24hours + .realTime(seconds: 2), + at: $0 ) // don't imperatively check each result, just make sure that a value was set; @@ -37,58 +36,48 @@ class Timecode_RealTime_Tests: XCTestCase { func testTimecode_init_RealTimeValue_Clamping() { let tc = Timecode( - clampingRealTime: 86400 + 3600, // 25 hours @ 24fps - at: ._24, - limit: ._24hours + .realTime(seconds: 86400 + 3600), // 25 hours @ 24fps + at: .fps24, + by: .clamping ) XCTAssertEqual( tc.components, - TCC(h: 23, m: 59, s: 59, f: 23, sf: tc.subFramesBase.rawValue - 1) + Timecode.Components(h: 23, m: 59, s: 59, f: 23, sf: tc.subFramesBase.rawValue - 1) ) } func testTimecode_init_RealTimeValue_Wrapping() { let tc = Timecode( - wrappingRealTime: 86400 + 3600, // 25 hours @ 24fps - at: ._24, - limit: ._24hours + .realTime(seconds: 86400 + 3600), // 25 hours @ 24fps + at: .fps24, + by: .wrapping ) - XCTAssertEqual(tc.days, 0) - XCTAssertEqual(tc.hours, 1) - XCTAssertEqual(tc.minutes, 0) - XCTAssertEqual(tc.seconds, 0) - XCTAssertEqual(tc.frames, 0) - XCTAssertEqual(tc.subFrames, 0) + XCTAssertEqual(tc.components, Timecode.Components(h: 1)) } func testTimecode_init_RealTimeValue_RawValues() { let tc = Timecode( - rawValuesRealTime: (86400 * 2) + 3600, // 2 days + 1 hour @ 24fps - at: ._24, - limit: ._24hours + .realTime(seconds: (86400 * 2) + 3600), // 2 days + 1 hour @ 24fps + at: .fps24, + by: .allowingInvalid ) - XCTAssertEqual(tc.days, 2) - XCTAssertEqual(tc.hours, 1) - XCTAssertEqual(tc.minutes, 0) - XCTAssertEqual(tc.seconds, 0) - XCTAssertEqual(tc.frames, 0) - XCTAssertEqual(tc.subFrames, 0) + XCTAssertEqual(tc.components, Timecode.Components(d: 2, h: 1)) } func testTimecode_init_RealTimeValue_RawValues_Negative() { let tc = Timecode( - rawValuesRealTime: -(3600 + 60 + 5), - at: ._24, - limit: ._24hours + .realTime(seconds: -(3600 + 60 + 5)), + at: .fps24, + by: .allowingInvalid ) // Negates only the largest non-zero component if input is negative XCTAssertEqual( tc.components, - TCC(d: 00, h: -01, m: 01, s: 05, f: 00, sf: 00) + Timecode.Components(d: 00, h: -01, m: 01, s: 05, f: 00, sf: 00) ) } @@ -99,16 +88,16 @@ class Timecode_RealTime_Tests: XCTestCase { let accuracy = 0.000000001 try TimecodeFrameRate.allCases.forEach { - let tc = try Timecode(TCC(d: 10), at: $0, limit: ._100days) + let tc = try Timecode(.components(d: 10), at: $0, limit: .max100Days) switch $0 { - case ._23_976, - ._24_98, - ._29_97, - ._47_952, - ._59_94, - ._95_904, - ._119_88: + case .fps23_976, + .fps24_98, + .fps29_97, + .fps47_952, + .fps59_94, + .fps95_904, + .fps119_88: XCTAssertEqual( tc.realTimeValue, @@ -117,15 +106,15 @@ class Timecode_RealTime_Tests: XCTestCase { "at: \($0)" ) - case ._24, - ._25, - ._30, - ._48, - ._50, - ._60, - ._96, - ._100, - ._120: + case .fps24, + .fps25, + .fps30, + .fps48, + .fps50, + .fps60, + .fps96, + .fps100, + .fps120: XCTAssertEqual( tc.realTimeValue, @@ -134,9 +123,9 @@ class Timecode_RealTime_Tests: XCTestCase { "at: \($0)" ) - case ._29_97_drop, - ._59_94_drop, - ._119_88_drop: + case .fps29_97d, + .fps59_94d, + .fps119_88d: XCTAssertEqual( tc.realTimeValue, @@ -145,9 +134,9 @@ class Timecode_RealTime_Tests: XCTestCase { "at: \($0)" ) - case ._30_drop, - ._60_drop, - ._120_drop: + case .fps30d, + .fps60d, + .fps120d: XCTAssertEqual( tc.realTimeValue, @@ -160,58 +149,58 @@ class Timecode_RealTime_Tests: XCTestCase { // set timecode from real time - let tcc = TCC(d: 10) + let tcc = Timecode.Components(d: 10) try TimecodeFrameRate.allCases.forEach { - var tc = try Timecode(tcc, at: $0, limit: ._100days) + var tc = try Timecode(.components(tcc), at: $0, limit: .max100Days) switch $0 { - case ._23_976, - ._24_98, - ._29_97, - ._47_952, - ._59_94, - ._95_904, - ._119_88: + case .fps23_976, + .fps24_98, + .fps29_97, + .fps47_952, + .fps59_94, + .fps95_904, + .fps119_88: XCTAssertNoThrow( - try tc.setTimecode(realTime: secInTC10Days_ShrunkFrameRates), + try tc.set(.realTime(seconds: secInTC10Days_ShrunkFrameRates)), "at: \($0)" ) XCTAssertEqual(tc.components, tcc, "at: \($0)") - case ._24, - ._25, - ._30, - ._48, - ._50, - ._60, - ._96, - ._100, - ._120: + case .fps24, + .fps25, + .fps30, + .fps48, + .fps50, + .fps60, + .fps96, + .fps100, + .fps120: XCTAssertNoThrow( - try tc.setTimecode(realTime: secInTC10Days_BaseFrameRates), + try tc.set(.realTime(seconds: secInTC10Days_BaseFrameRates)), "at: \($0)" ) XCTAssertEqual(tc.components, tcc, "at: \($0)") - case ._29_97_drop, - ._59_94_drop, - ._119_88_drop: + case .fps29_97d, + .fps59_94d, + .fps119_88d: XCTAssertNoThrow( - try tc.setTimecode(realTime: secInTC10Days_DropFrameRates), + try tc.set(.realTime(seconds: secInTC10Days_DropFrameRates)), "at: \($0)" ) XCTAssertEqual(tc.components, tcc, "at: \($0)") - case ._30_drop, - ._60_drop, - ._120_drop: + case .fps30d, + .fps60d, + .fps120d: XCTAssertNoThrow( - try tc.setTimecode(realTime: secInTC10Days_30DF), + try tc.set(.realTime(seconds: secInTC10Days_30DF)), "at: \($0)" ) XCTAssertEqual(tc.components, tcc, "at: \($0)") @@ -224,17 +213,17 @@ class Timecode_RealTime_Tests: XCTestCase { // test for precision and rounding issues by iterating every subframe for each frame rate - let subFramesBase: Timecode.SubFramesBase = ._80SubFrames + let subFramesBase: Timecode.SubFramesBase = .max80SubFrames for subFrame in 0 ..< subFramesBase.rawValue { - let tcc = TCC(d: 99, h: 23, sf: subFrame) + let tcc = Timecode.Components(d: 99, h: 23, sf: subFrame) try TimecodeFrameRate.allCases.forEach { var tc = try Timecode( - tcc, + .components(tcc), at: $0, - limit: ._100days, - base: subFramesBase + base: subFramesBase, + limit: .max100Days ) // timecode to samples @@ -244,7 +233,7 @@ class Timecode_RealTime_Tests: XCTestCase { // samples to timecode XCTAssertNoThrow( - try tc.setTimecode(realTime: realTime), + try tc.set(.realTime(seconds: realTime)), "at: \($0) subframe: \(subFrame)" ) @@ -263,7 +252,10 @@ class Timecode_RealTime_Tests: XCTestCase { // Cubase 11 XML file output (high resolution floating-point times in seconds) // the timecodes in the constant variable names are the timecodes as seen in Cubase - // the float-point number constant values are extracted from a Track Archive XML file exported from the Cubase project which outputs very high precision float-point numbers in seconds to define many attributes such as the project start time, and event start times and lengths on tracks which are in absolute time mode (not musical bars/beats mode which gets stored as PPQ values in the XML file instead of float-point seconds) + // the float-point number constant values are extracted from a Track Archive XML file exported from the Cubase project which outputs + // very high precision float-point numbers in seconds to define many attributes such as the project start time, and event start + // times and lengths on tracks which are in absolute time mode (not musical bars/beats mode which gets stored as PPQ values in the + // XML file instead of float-point seconds) // 23.976fps, 80 subframe divisor @@ -281,66 +273,66 @@ class Timecode_RealTime_Tests: XCTestCase { // test timecode formation from real time let start = try Timecode( - realTime: _00_49_27_15_00, - at: ._23_976 + .realTime(seconds: _00_49_27_15_00), + at: .fps23_976 ) XCTAssertEqual( start.components, - TCC(h: 00, m: 49, s: 27, f: 15, sf: 00) + Timecode.Components(h: 00, m: 49, s: 27, f: 15, sf: 00) ) let event1 = try Timecode( - realTime: _00_49_27_15_00 + _00_49_29_17_00_delta, - at: ._23_976 + .realTime(seconds: _00_49_27_15_00 + _00_49_29_17_00_delta), + at: .fps23_976 ) XCTAssertEqual( event1.components, - TCC(h: 00, m: 49, s: 29, f: 17, sf: 00) + Timecode.Components(h: 00, m: 49, s: 29, f: 17, sf: 00) ) let event2 = try Timecode( - realTime: _00_49_27_15_00 + _00_49_31_09_00_delta, - at: ._23_976 + .realTime(seconds: _00_49_27_15_00 + _00_49_31_09_00_delta), + at: .fps23_976 ) XCTAssertEqual( event2.components, - TCC(h: 00, m: 49, s: 31, f: 09, sf: 00) + Timecode.Components(h: 00, m: 49, s: 31, f: 09, sf: 00) ) let event3 = try Timecode( - realTime: _00_49_27_15_00 + _00_49_33_21_79_delta, - at: ._23_976, - base: ._80SubFrames + .realTime(seconds: _00_49_27_15_00 + _00_49_33_21_79_delta), + at: .fps23_976, + base: .max80SubFrames ) XCTAssertEqual( event3.components, - TCC(h: 00, m: 49, s: 33, f: 21, sf: 79) + Timecode.Components(h: 00, m: 49, s: 33, f: 21, sf: 79) ) let event4 = try Timecode( - realTime: _00_49_27_15_00 + _00_49_38_01_79_delta, - at: ._23_976, - base: ._80SubFrames + .realTime(seconds: _00_49_27_15_00 + _00_49_38_01_79_delta), + at: .fps23_976, + base: .max80SubFrames ) XCTAssertEqual( event4.components, - TCC(h: 00, m: 49, s: 38, f: 01, sf: 79) + Timecode.Components(h: 00, m: 49, s: 38, f: 01, sf: 79) ) // test real time matching the seconds constants // start XCTAssertEqual( - try TCC(h: 00, m: 49, s: 27, f: 15, sf: 00) - .toTimecode(at: ._23_976) + try Timecode.Components(h: 00, m: 49, s: 27, f: 15, sf: 00) + .timecode(at: .fps23_976) .realTimeValue, _00_49_27_15_00 ) // event1 XCTAssertEqual( - try TCC(h: 00, m: 49, s: 29, f: 17, sf: 00) - .toTimecode(at: ._23_976) + try Timecode.Components(h: 00, m: 49, s: 29, f: 17, sf: 00) + .timecode(at: .fps23_976) .realTimeValue, _00_49_27_15_00 + _00_49_29_17_00_delta, accuracy: 0.0000005 @@ -348,8 +340,8 @@ class Timecode_RealTime_Tests: XCTestCase { // event2 XCTAssertEqual( - try TCC(h: 00, m: 49, s: 31, f: 09, sf: 00) - .toTimecode(at: ._23_976) + try Timecode.Components(h: 00, m: 49, s: 31, f: 09, sf: 00) + .timecode(at: .fps23_976) .realTimeValue, _00_49_27_15_00 + _00_49_31_09_00_delta, accuracy: 0.0000005 @@ -357,8 +349,8 @@ class Timecode_RealTime_Tests: XCTestCase { // event3 XCTAssertEqual( - try TCC(h: 00, m: 49, s: 33, f: 21, sf: 79) - .toTimecode(at: ._23_976, base: ._80SubFrames) + try Timecode.Components(h: 00, m: 49, s: 33, f: 21, sf: 79) + .timecode(at: .fps23_976, base: .max80SubFrames) .realTimeValue, _00_49_27_15_00 + _00_49_33_21_79_delta, accuracy: 0.0000005 @@ -366,80 +358,70 @@ class Timecode_RealTime_Tests: XCTestCase { // event4 XCTAssertEqual( - try TCC(h: 00, m: 49, s: 38, f: 01, sf: 79) - .toTimecode(at: ._23_976, base: ._80SubFrames) + try Timecode.Components(h: 00, m: 49, s: 38, f: 01, sf: 79) + .timecode(at: .fps23_976, base: .max80SubFrames) .realTimeValue, _00_49_27_15_00 + _00_49_38_01_79_delta, accuracy: 0.0000005 ) } - // MARK: - .toTimecode() + // MARK: - .timecode() - func testTCC_toTimecode() throws { - // toTimecode(rawValuesAt:) + func testTimecode_Components_timecode() throws { + // timecode(rawValuesAt:) XCTAssertEqual( - try TCC(h: 1, m: 5, s: 20, f: 14) - .toTimecode(at: ._23_976), + try Timecode.Components(h: 1, m: 5, s: 20, f: 14) + .timecode(at: .fps23_976), try Timecode( - TCC(h: 1, m: 5, s: 20, f: 14), - at: ._23_976 + .components(h: 1, m: 5, s: 20, f: 14), + at: .fps23_976 ) ) - // toTimecode(rawValuesAt:) with subframes + // timecode(rawValuesAt:) with subframes - let tcWithSubFrames = try TCC(h: 1, m: 5, s: 20, f: 14, sf: 94) - .toTimecode( - at: ._23_976, - base: ._100SubFrames, - format: [.showSubFrames] - ) + let tcWithSubFrames = try Timecode.Components(h: 1, m: 5, s: 20, f: 14, sf: 94) + .timecode(at: .fps23_976, base: .max100SubFrames) XCTAssertEqual( tcWithSubFrames, try Timecode( - TCC(h: 1, m: 5, s: 20, f: 14, sf: 94), - at: ._23_976, - base: ._100SubFrames, - format: [.showSubFrames] + .components(h: 1, m: 5, s: 20, f: 14, sf: 94), + at: .fps23_976, + base: .max100SubFrames ) ) XCTAssertEqual( - tcWithSubFrames.stringValue, + tcWithSubFrames.stringValue(format: .showSubFrames), "01:05:20:14.94" ) } - func testTimeInterval_toTimeCode_at() throws { - // toTimecode(at:) + func testTimeInterval_timeCode() throws { + // timecode(at:) XCTAssertEqual( try TimeInterval(secInTC10Days_BaseFrameRates) - .toTimecode(at: ._24, limit: ._100days) + .timecode(at: .fps24, limit: .max100Days) .components, - TCC(d: 10) + Timecode.Components(d: 10) ) - // toTimecode(at:) with subframes + // timecode(at:) with subframes let tcWithSubFrames = try TimeInterval(3600.0) - .toTimecode( - at: ._24, - base: ._100SubFrames, - format: [.showSubFrames] - ) + .timecode(at: .fps24, limit: .max100Days) XCTAssertEqual( tcWithSubFrames, try Timecode( - TCC(h: 1), - at: ._24, - base: ._100SubFrames, - format: [.showSubFrames] + .components(h: 1), + at: .fps24, + base: .max100SubFrames ) ) XCTAssertEqual( - tcWithSubFrames.stringValue, + tcWithSubFrames.stringValue(format: .showSubFrames), "01:00:00:00.00" ) } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode Samples Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode Samples Tests.swift similarity index 71% rename from Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode Samples Tests.swift rename to Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode Samples Tests.swift index 2bbee6cf..3f9c83cb 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode Samples Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode Samples Tests.swift @@ -1,13 +1,13 @@ // // Timecode Samples Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class Timecode_Samples_Tests: XCTestCase { override func setUp() { } @@ -16,10 +16,8 @@ class Timecode_Samples_Tests: XCTestCase { func testTimecode_init_Samples_Exactly() throws { try TimecodeFrameRate.allCases.forEach { let tc = try Timecode( - samples: 48000 * 2, - sampleRate: 48000, - at: $0, - limit: ._24hours + .samples(48000 * 2, sampleRate: 48000), + at: $0 ) // don't imperatively check each result, just make sure that a value was set; @@ -30,65 +28,58 @@ class Timecode_Samples_Tests: XCTestCase { func testTimecode_init_Samples_Clamping() { let tc = Timecode( - clampingSamples: 4_147_200_000 + 172_800_000, // 25 hours @ 24fps - sampleRate: 48000, - at: ._24, - limit: ._24hours + .samples( + 4_147_200_000 + 172_800_000, // 25 hours @ 24fps + sampleRate: 48000 + ), + at: .fps24, + by: .clamping ) XCTAssertEqual( tc.components, - TCC(h: 23, m: 59, s: 59, f: 23, sf: tc.subFramesBase.rawValue - 1) + Timecode.Components(h: 23, m: 59, s: 59, f: 23, sf: tc.subFramesBase.rawValue - 1) ) } func testTimecode_init_Samples_Wrapping() { let tc = Timecode( - wrappingSamples: 4_147_200_000 + 172_800_000, // 25 hours @ 24fps - sampleRate: 48000, - at: ._24, - limit: ._24hours + .samples( + 4_147_200_000 + 172_800_000, // 25 hours @ 24fps + sampleRate: 48000 + ), + at: .fps24, + by: .wrapping ) - XCTAssertEqual(tc.days, 0) - XCTAssertEqual(tc.hours, 1) - XCTAssertEqual(tc.minutes, 0) - XCTAssertEqual(tc.seconds, 0) - XCTAssertEqual(tc.frames, 0) - XCTAssertEqual(tc.subFrames, 0) + XCTAssertEqual(tc.components, Timecode.Components(h: 1)) } func testTimecode_init_Samples_RawValues() { let tc = Timecode( - rawValuesSamples: (4_147_200_000 * 2) + 172_800_000, // 2 days + 1 hour @ 24fps - sampleRate: 48000, - at: ._24, - limit: ._24hours + .samples( + (4_147_200_000 * 2) + 172_800_000, // 2 days + 1 hour @ 24fps + sampleRate: 48000 + ), + at: .fps24, + by: .allowingInvalid ) - XCTAssertEqual(tc.days, 2) - XCTAssertEqual(tc.hours, 1) - XCTAssertEqual(tc.minutes, 0) - XCTAssertEqual(tc.seconds, 0) - XCTAssertEqual(tc.frames, 0) - XCTAssertEqual(tc.subFrames, 0) + XCTAssertEqual(tc.components, Timecode.Components(d: 2, h: 1)) } func testTimecode_init_Samples_RawValues_Negative() { let tc = Timecode( - rawValuesSamples: -((4_147_200_000 * 2) + 172_800_000), // 2 days + 1 hour @ 24fps - sampleRate: 48000, - at: ._24, - limit: ._24hours + .samples( + -((4_147_200_000 * 2) + 172_800_000), // 2 days + 1 hour @ 24fps + sampleRate: 48000 + ), + at: .fps24, + by: .allowingInvalid ) // Negates only the largest non-zero component if input is negative - XCTAssertEqual(tc.days, -2) - XCTAssertEqual(tc.hours, 1) - XCTAssertEqual(tc.minutes, 0) - XCTAssertEqual(tc.seconds, 0) - XCTAssertEqual(tc.frames, 0) - XCTAssertEqual(tc.subFrames, 0) + XCTAssertEqual(tc.components, Timecode.Components(d: -2, h: 1)) } func testSamplesGetSet_48KHz() throws { @@ -110,10 +101,10 @@ class Timecode_Samples_Tests: XCTestCase { fRate: TimecodeFrameRate, roundedForDropFrame: Bool ) throws { - var tc = Timecode(at: fRate, limit: ._100days) + var tc = Timecode(.zero, at: fRate, limit: .max100Days) // get - try tc.setTimecode(exactly: TCC(d: 1)) + try tc.set(.components(d: 1)) var sv = tc.samplesDoubleValue(sampleRate: sRate) if roundedForDropFrame { // add rounding for real drop rates (ie: 29.97d, not 30d); @@ -128,13 +119,13 @@ class Timecode_Samples_Tests: XCTestCase { ) // set - try tc.setTimecode( - samples: samplesIn1DayTC, + try tc.set(.samples( + samplesIn1DayTC, sampleRate: sRate - ) + )) XCTAssertEqual( tc.components, - TCC(d: 1), + Timecode.Components(d: 1), "at \(fRate)" ) } @@ -145,10 +136,10 @@ class Timecode_Samples_Tests: XCTestCase { sRate: Int, fRate: TimecodeFrameRate ) throws { - var tc = Timecode(at: fRate, limit: ._100days) + var tc = Timecode(.zero, at: fRate, limit: .max100Days) // get - try tc.setTimecode(exactly: TCC(d: 1)) + try tc.set(.components(d: 1)) XCTAssertEqual( tc.samplesValue(sampleRate: sRate), samplesIn1DayTC, @@ -156,13 +147,13 @@ class Timecode_Samples_Tests: XCTestCase { ) // set - try tc.setTimecode( - samples: samplesIn1DayTC, + try tc.set(.samples( + samplesIn1DayTC, sampleRate: sRate - ) + )) XCTAssertEqual( tc.components, - TCC(d: 1), + Timecode.Components(d: 1), "at \(fRate)" ) } @@ -177,35 +168,35 @@ class Timecode_Samples_Tests: XCTestCase { var roundedForDropFrame = false switch fRate { - case ._23_976, - ._24_98, - ._29_97, - ._47_952, - ._59_94, - ._95_904, - ._119_88: + case .fps23_976, + .fps24_98, + .fps29_97, + .fps47_952, + .fps59_94, + .fps95_904, + .fps119_88: samplesIn1DayTCDouble = samplesIn1DayTC_ShrunkFrameRates samplesIn1DayTCInt = Int(samplesIn1DayTCDouble) roundedForDropFrame = false - case ._24, - ._25, - ._30, - ._48, - ._50, - ._60, - ._96, - ._100, - ._120: + case .fps24, + .fps25, + .fps30, + .fps48, + .fps50, + .fps60, + .fps96, + .fps100, + .fps120: samplesIn1DayTCDouble = samplesIn1DayTC_BaseFrameRates samplesIn1DayTCInt = Int(samplesIn1DayTCDouble) roundedForDropFrame = false - case ._29_97_drop, - ._59_94_drop, - ._119_88_drop: + case .fps29_97d, + .fps59_94d, + .fps119_88d: // Cubase: // - reports 4147195853 @ 1 day @@ -218,9 +209,9 @@ class Timecode_Samples_Tests: XCTestCase { samplesIn1DayTCInt = Int(samplesIn1DayTCDouble) roundedForDropFrame = true // DAWs seem to using standard rounding for DF (?) - case ._30_drop, - ._60_drop, - ._120_drop: + case .fps30d, + .fps60d, + .fps120d: samplesIn1DayTCDouble = samplesIn1DayTC_30DF samplesIn1DayTCInt = Int(samplesIn1DayTCDouble) @@ -252,7 +243,7 @@ class Timecode_Samples_Tests: XCTestCase { let logErrors = true - let subFramesBase: Timecode.SubFramesBase = ._80SubFrames + let subFramesBase: Timecode.SubFramesBase = .max80SubFrames var frameRatesWithSetTimecodeErrors: Set = [] var frameRatesWithSetTimecodeErrorsCount = 0 @@ -260,14 +251,14 @@ class Timecode_Samples_Tests: XCTestCase { var frameRatesWithMismatchingComponentsCount = 0 for subFrame in 0 ..< subFramesBase.rawValue { - let tcc = TCC(d: 99, h: 23, sf: subFrame) + let tcc = Timecode.Components(d: 99, h: 23, sf: subFrame) try TimecodeFrameRate.allCases.forEach { var tc = try Timecode( - tcc, + .components(tcc), at: $0, - limit: ._100days, - base: subFramesBase + base: subFramesBase, + limit: .max100Days ) let sRate = 48000 @@ -278,10 +269,10 @@ class Timecode_Samples_Tests: XCTestCase { // samples to timecode - if (try? tc.setTimecode( - samples: samples, + if (try? tc.set(.samples( + samples, sampleRate: sRate - )) == nil { + ))) == nil { frameRatesWithSetTimecodeErrors.insert($0) frameRatesWithSetTimecodeErrorsCount += 1 if logErrors { @@ -296,7 +287,7 @@ class Timecode_Samples_Tests: XCTestCase { if logErrors { let fr = "\($0)".padding(toLength: 8, withPad: " ", startingAt: 0) print( - "TCC match failed @ \(fr) - origin \(tcc) to \(samples) samples converted to \(tc.components)" + "Timecode.Components match failed @ \(fr) - origin \(tcc) to \(samples) samples converted to \(tc.components)" ) } } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode String Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode String Tests.swift similarity index 60% rename from Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode String Tests.swift rename to Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode String Tests.swift index e220dcd2..15f18e34 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Data Interchange/Timecode String Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Source/Timecode String Tests.swift @@ -1,12 +1,12 @@ // // Timecode String Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class Timecode_String_Tests: XCTestCase { override func setUp() { } @@ -15,108 +15,86 @@ class Timecode_String_Tests: XCTestCase { func testTimecode_init_String_Exactly() throws { try TimecodeFrameRate.allCases.forEach { let tc = try Timecode( - "00:00:00:00", - at: $0, - limit: ._24hours + .string("00:00:00:00"), + at: $0 ) - XCTAssertEqual(tc.days, 0, "for \($0)") - XCTAssertEqual(tc.hours, 0, "for \($0)") - XCTAssertEqual(tc.minutes, 0, "for \($0)") - XCTAssertEqual(tc.seconds, 0, "for \($0)") - XCTAssertEqual(tc.frames, 0, "for \($0)") - XCTAssertEqual(tc.subFrames, 0, "for \($0)") + XCTAssertEqual(tc.components, .zero, "for \($0)") } try TimecodeFrameRate.allCases.forEach { let tc = try Timecode( - "01:02:03:04", - at: $0, - limit: ._24hours + .string("01:02:03:04"), + at: $0 ) - XCTAssertEqual(tc.days, 0, "for \($0)") - XCTAssertEqual(tc.hours, 1, "for \($0)") - XCTAssertEqual(tc.minutes, 2, "for \($0)") - XCTAssertEqual(tc.seconds, 3, "for \($0)") - XCTAssertEqual(tc.frames, 4, "for \($0)") - XCTAssertEqual(tc.subFrames, 0, "for \($0)") + XCTAssertEqual(tc.components, Timecode.Components(h: 01, m: 02, s: 03, f: 04), "for \($0)") } } func testTimecode_init_String_Clamping() throws { let tc = try Timecode( - clamping: "25:00:00:00", - at: ._24, - limit: ._24hours + .string("25:00:00:00"), + at: .fps24, + by: .clamping ) XCTAssertEqual( tc.components, - TCC(h: 23, m: 59, s: 59, f: 23, sf: tc.subFramesBase.rawValue - 1) + Timecode.Components(h: 23, m: 59, s: 59, f: 23, sf: tc.subFramesBase.rawValue - 1) ) } func testTimecode_init_String_ClampingEach() throws { let tc = try Timecode( - clampingEach: "25:00:00:00", - at: ._24, - limit: ._24hours + .string("25:00:00:00"), + at: .fps24, + by: .clampingComponents ) XCTAssertEqual( tc.components, - TCC(h: 23, m: 00, s: 00, f: 00) + Timecode.Components(h: 23, m: 00, s: 00, f: 00) ) } func testTimecode_init_String_Wrapping() throws { try TimecodeFrameRate.allCases.forEach { let tc = try Timecode( - wrapping: "25:00:00:00", + .string("25:00:00:00"), at: $0, - limit: ._24hours + by: .wrapping ) - XCTAssertEqual(tc.days, 0, "for \($0)") - XCTAssertEqual(tc.hours, 1, "for \($0)") - XCTAssertEqual(tc.minutes, 0, "for \($0)") - XCTAssertEqual(tc.seconds, 0, "for \($0)") - XCTAssertEqual(tc.frames, 0, "for \($0)") - XCTAssertEqual(tc.subFrames, 0, "for \($0)") + XCTAssertEqual(tc.components, Timecode.Components(h: 01), "for \($0)") } } func testTimecode_init_String_RawValues() throws { try TimecodeFrameRate.allCases.forEach { let tc = try Timecode( - rawValues: "99 99:99:99:99.99", + .string("99 99:99:99:99.99"), at: $0, - limit: ._24hours + by: .allowingInvalid ) - XCTAssertEqual(tc.days, 99, "for \($0)") - XCTAssertEqual(tc.hours, 99, "for \($0)") - XCTAssertEqual(tc.minutes, 99, "for \($0)") - XCTAssertEqual(tc.seconds, 99, "for \($0)") - XCTAssertEqual(tc.frames, 99, "for \($0)") - XCTAssertEqual(tc.subFrames, 99, "for \($0)") + XCTAssertEqual(tc.components, Timecode.Components(d: 99, h: 99, m: 99, s: 99, f: 99, sf: 99), "for \($0)") } } func testStringValue_GetSet_Basic() throws { // basic getter tests - var tc = Timecode(at: ._23_976, limit: ._24hours) + var tc = Timecode(.zero, at: .fps23_976) - try tc.setTimecode(exactly: "01:05:20:14") - XCTAssertEqual(tc.stringValue, "01:05:20:14") + try tc.set("01:05:20:14") + XCTAssertEqual(tc.stringValue(), "01:05:20:14") - XCTAssertThrowsError(try tc.setTimecode(exactly: "50:05:20:14")) - XCTAssertEqual(tc.stringValue, "01:05:20:14") // no change + XCTAssertThrowsError(try tc.set("50:05:20:14")) + XCTAssertEqual(tc.stringValue(), "01:05:20:14") // no change - try tc.setTimecode(clampingEach: "50:05:20:14") - XCTAssertEqual(tc.stringValue, "23:05:20:14") + try tc.set("50:05:20:14", by: .clampingComponents) + XCTAssertEqual(tc.stringValue(), "23:05:20:14") } func testStringValue_Get_Formatting_Basic() throws { @@ -128,9 +106,9 @@ class Timecode_String_Tests: XCTestCase { // non-drop try TimecodeFrameRate.allNonDrop.forEach { - let sv = try TCC(h: 1, m: 02, s: 03, f: 04) - .toTimecode(at: $0) - .stringValue + let sv = try Timecode.Components(h: 1, m: 02, s: 03, f: 04) + .timecode(at: $0) + .stringValue() let t = $0.numberOfDigits == 2 ? "" : "0" @@ -140,9 +118,9 @@ class Timecode_String_Tests: XCTestCase { // drop try TimecodeFrameRate.allDrop.forEach { - let sv = try TCC(h: 1, m: 02, s: 03, f: 04) - .toTimecode(at: $0) - .stringValue + let sv = try Timecode.Components(h: 1, m: 02, s: 03, f: 04) + .timecode(at: $0) + .stringValue() let t = $0.numberOfDigits == 2 ? "" : "0" @@ -154,9 +132,9 @@ class Timecode_String_Tests: XCTestCase { // non-drop try TimecodeFrameRate.allNonDrop.forEach { - let sv = try TCC(h: 1, m: 02, s: 03, f: 04) - .toTimecode(at: $0, limit: ._100days) - .stringValue + let sv = try Timecode.Components(h: 1, m: 02, s: 03, f: 04) + .timecode(at: $0, limit: .max100Days) + .stringValue() let t = $0.numberOfDigits == 2 ? "" : "0" @@ -166,9 +144,9 @@ class Timecode_String_Tests: XCTestCase { // drop try TimecodeFrameRate.allDrop.forEach { - let sv = try TCC(h: 1, m: 02, s: 03, f: 04) - .toTimecode(at: $0, limit: ._100days) - .stringValue + let sv = try Timecode.Components(h: 1, m: 02, s: 03, f: 04) + .timecode(at: $0, limit: .max100Days) + .stringValue() let t = $0.numberOfDigits == 2 ? "" : "0" @@ -183,40 +161,38 @@ class Timecode_String_Tests: XCTestCase { // non-drop try TimecodeFrameRate.allNonDrop.forEach { - var tc = try TCC(h: 1, m: 02, s: 03, f: 04) - .toTimecode(at: $0) + var tc = try Timecode.Components(h: 1, m: 02, s: 03, f: 04) + .timecode(at: $0) tc.days = 2 // set days after init since init fails if we pass days let t = $0.numberOfDigits == 2 ? "" : "0" // still produces days since we have not clamped it yet - var sv = tc.stringValue + var sv = tc.stringValue() XCTAssertEqual(sv, "2 01:02:03:\(t)04", "for \($0)") // now omits days since our limit is 24hr and clamped tc.clampComponents() - sv = tc.stringValue + sv = tc.stringValue() XCTAssertEqual(sv, "01:02:03:\(t)04", "for \($0)") } // drop try TimecodeFrameRate.allDrop.forEach { - var tc = try TCC(h: 1, m: 02, s: 03, f: 04) - .toTimecode(at: $0) - tc - .days = - 2 // set days after init since init fails if we pass days + var tc = try Timecode.Components(h: 1, m: 02, s: 03, f: 04) + .timecode(at: $0) + tc.days = 2 // set days after init since init fails if we pass days let t = $0.numberOfDigits == 2 ? "" : "0" // still produces days since we have not clamped it yet - var sv = tc.stringValue + var sv = tc.stringValue() XCTAssertEqual(sv, "2 01:02:03;\(t)04", "for \($0)") // now omits days since our limit is 24hr and clamped tc.clampComponents() - sv = tc.stringValue + sv = tc.stringValue() XCTAssertEqual(sv, "01:02:03;\(t)04", "for \($0)") } @@ -225,9 +201,9 @@ class Timecode_String_Tests: XCTestCase { // non-drop try TimecodeFrameRate.allNonDrop.forEach { - let sv = try TCC(d: 2, h: 1, m: 02, s: 03, f: 04) - .toTimecode(at: $0, limit: ._100days) - .stringValue + let sv = try Timecode.Components(d: 2, h: 1, m: 02, s: 03, f: 04) + .timecode(at: $0, limit: .max100Days) + .stringValue() let t = $0.numberOfDigits == 2 ? "" : "0" @@ -237,9 +213,9 @@ class Timecode_String_Tests: XCTestCase { // drop try TimecodeFrameRate.allDrop.forEach { - let sv = try TCC(d: 2, h: 1, m: 02, s: 03, f: 04) - .toTimecode(at: $0, limit: ._100days) - .stringValue + let sv = try Timecode.Components(d: 2, h: 1, m: 02, s: 03, f: 04) + .timecode(at: $0, limit: .max100Days) + .stringValue() let t = $0.numberOfDigits == 2 ? "" : "0" @@ -254,38 +230,38 @@ class Timecode_String_Tests: XCTestCase { // non-drop try TimecodeFrameRate.allNonDrop.forEach { - var tc = try TCC(h: 1, m: 02, s: 03, f: 04, sf: 12) - .toTimecode(at: $0, format: [.showSubFrames]) - tc.days = 2 // set days after init since init @ ._24hour limit fails if we pass days + var tc = try Timecode.Components(h: 1, m: 02, s: 03, f: 04, sf: 12) + .timecode(at: $0) + tc.days = 2 // set days after init since init @ .max24Hours limit fails if we pass days let t = $0.numberOfDigits == 2 ? "" : "0" // still produces days since we have not clamped it yet - var sv = tc.stringValue + var sv = tc.stringValue(format: .showSubFrames) XCTAssertEqual(sv, "2 01:02:03:\(t)04.12", "for \($0)") // now omits days since our limit is 24hr and clamped tc.clampComponents() - sv = tc.stringValue + sv = tc.stringValue(format: .showSubFrames) XCTAssertEqual(sv, "01:02:03:\(t)04.12", "for \($0)") } // drop try TimecodeFrameRate.allDrop.forEach { - var tc = try TCC(h: 1, m: 02, s: 03, f: 04, sf: 12) - .toTimecode(at: $0, format: [.showSubFrames]) - tc.days = 2 // set days after init since init @ ._24hour limit fails if we pass days + var tc = try Timecode.Components(h: 1, m: 02, s: 03, f: 04, sf: 12) + .timecode(at: $0) + tc.days = 2 // set days after init since init @ .max24Hours limit fails if we pass days let t = $0.numberOfDigits == 2 ? "" : "0" // still produces days since we have not clamped it yet - var sv = tc.stringValue + var sv = tc.stringValue(format: .showSubFrames) XCTAssertEqual(sv, "2 01:02:03;\(t)04.12", "for \($0)") // now omits days since our limit is 24hr and clamped tc.clampComponents() - sv = tc.stringValue + sv = tc.stringValue(format: .showSubFrames) XCTAssertEqual(sv, "01:02:03;\(t)04.12", "for \($0)") } @@ -294,24 +270,24 @@ class Timecode_String_Tests: XCTestCase { // non-drop try TimecodeFrameRate.allNonDrop.forEach { - let tc = try TCC(d: 2, h: 1, m: 02, s: 03, f: 04, sf: 12) - .toTimecode(at: $0, limit: ._100days, format: [.showSubFrames]) + let tc = try Timecode.Components(d: 2, h: 1, m: 02, s: 03, f: 04, sf: 12) + .timecode(at: $0, limit: .max100Days) let t = $0.numberOfDigits == 2 ? "" : "0" - let sv = tc.stringValue + let sv = tc.stringValue(format: .showSubFrames) XCTAssertEqual(sv, "2 01:02:03:\(t)04.12", "for \($0)") } // drop try TimecodeFrameRate.allDrop.forEach { - let tc = try TCC(d: 2, h: 1, m: 02, s: 03, f: 04, sf: 12) - .toTimecode(at: $0, limit: ._100days, format: [.showSubFrames]) + let tc = try Timecode.Components(d: 2, h: 1, m: 02, s: 03, f: 04, sf: 12) + .timecode(at: $0, limit: .max100Days) let t = $0.numberOfDigits == 2 ? "" : "0" - let sv = tc.stringValue + let sv = tc.stringValue(format: .showSubFrames) XCTAssertEqual(sv, "2 01:02:03;\(t)04.12", "for \($0)") } } @@ -323,168 +299,168 @@ class Timecode_String_Tests: XCTestCase { XCTAssertThrowsError(try Timecode.decode(timecode: "01564523")) XCTAssertEqual( try Timecode.decode(timecode: "0:0:0:0"), - TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) + Timecode.Components(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) ) XCTAssertEqual( try Timecode.decode(timecode: "0:00:00:00"), - TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) + Timecode.Components(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) ) XCTAssertEqual( try Timecode.decode(timecode: "00:00:00:00"), - TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) + Timecode.Components(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) ) XCTAssertEqual( try Timecode.decode(timecode: "1:56:45:23"), - TCC(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 00) + Timecode.Components(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 00) ) XCTAssertEqual( try Timecode.decode(timecode: "01:56:45:23"), - TCC(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 00) + Timecode.Components(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 00) ) XCTAssertEqual( try Timecode.decode(timecode: "3 01:56:45:23"), - TCC(d: 3, h: 01, m: 56, s: 45, f: 23, sf: 00) + Timecode.Components(d: 3, h: 01, m: 56, s: 45, f: 23, sf: 00) ) XCTAssertEqual( try Timecode.decode(timecode: "12 01:56:45:23"), - TCC(d: 12, h: 1, m: 56, s: 45, f: 23, sf: 00) + Timecode.Components(d: 12, h: 1, m: 56, s: 45, f: 23, sf: 00) ) XCTAssertEqual( try Timecode.decode(timecode: "12:01:56:45:23"), - TCC(d: 12, h: 1, m: 56, s: 45, f: 23, sf: 00) + Timecode.Components(d: 12, h: 1, m: 56, s: 45, f: 23, sf: 00) ) // drop frame XCTAssertEqual( try Timecode.decode(timecode: "0:0:0;0"), - TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) + Timecode.Components(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) ) XCTAssertEqual( try Timecode.decode(timecode: "0:00:00;00"), - TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) + Timecode.Components(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) ) XCTAssertEqual( try Timecode.decode(timecode: "00:00:00;00"), - TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) + Timecode.Components(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) ) XCTAssertEqual( try Timecode.decode(timecode: "1:56:45;23"), - TCC(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 0) + Timecode.Components(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 0) ) XCTAssertEqual( try Timecode.decode(timecode: "01:56:45;23"), - TCC(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 0) + Timecode.Components(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 0) ) XCTAssertEqual( try Timecode.decode(timecode: "3 01:56:45;23"), - TCC(d: 3, h: 01, m: 56, s: 45, f: 23, sf: 0) + Timecode.Components(d: 3, h: 01, m: 56, s: 45, f: 23, sf: 0) ) XCTAssertEqual( try Timecode.decode(timecode: "12 01:56:45;23"), - TCC(d: 12, h: 01, m: 56, s: 45, f: 23, sf: 0) + Timecode.Components(d: 12, h: 01, m: 56, s: 45, f: 23, sf: 0) ) XCTAssertEqual( try Timecode.decode(timecode: "12:01:56:45;23"), - TCC(d: 12, h: 01, m: 56, s: 45, f: 23, sf: 0) + Timecode.Components(d: 12, h: 01, m: 56, s: 45, f: 23, sf: 0) ) // all semicolons (such as from Adobe Premiere in its XMP format) XCTAssertEqual( try Timecode.decode(timecode: "0;0;0;0"), - TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) + Timecode.Components(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) ) XCTAssertEqual( try Timecode.decode(timecode: "0;00;00;00"), - TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) + Timecode.Components(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) ) XCTAssertEqual( try Timecode.decode(timecode: "00;00;00;00"), - TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) + Timecode.Components(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) ) XCTAssertEqual( try Timecode.decode(timecode: "1;56;45;23"), - TCC(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 00) + Timecode.Components(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 00) ) XCTAssertEqual( try Timecode.decode(timecode: "01;56;45;23"), - TCC(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 00) + Timecode.Components(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 00) ) XCTAssertEqual( try Timecode.decode(timecode: "3 01;56;45;23"), - TCC(d: 3, h: 01, m: 56, s: 45, f: 23, sf: 00) + Timecode.Components(d: 3, h: 01, m: 56, s: 45, f: 23, sf: 00) ) XCTAssertEqual( try Timecode.decode(timecode: "12 01;56;45;23"), - TCC(d: 12, h: 1, m: 56, s: 45, f: 23, sf: 00) + Timecode.Components(d: 12, h: 1, m: 56, s: 45, f: 23, sf: 00) ) XCTAssertEqual( try Timecode.decode(timecode: "12;01;56;45;23"), - TCC(d: 12, h: 1, m: 56, s: 45, f: 23, sf: 00) + Timecode.Components(d: 12, h: 1, m: 56, s: 45, f: 23, sf: 00) ) // drop frame XCTAssertEqual( try Timecode.decode(timecode: "0:0:0;0"), - TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) + Timecode.Components(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) ) XCTAssertEqual( try Timecode.decode(timecode: "0:00:00;00"), - TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) + Timecode.Components(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) ) XCTAssertEqual( try Timecode.decode(timecode: "00:00:00;00"), - TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) + Timecode.Components(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 00) ) XCTAssertEqual( try Timecode.decode(timecode: "1:56:45;23"), - TCC(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 0) + Timecode.Components(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 0) ) XCTAssertEqual( try Timecode.decode(timecode: "01:56:45;23"), - TCC(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 0) + Timecode.Components(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 0) ) XCTAssertEqual( try Timecode.decode(timecode: "3 01:56:45;23"), - TCC(d: 3, h: 01, m: 56, s: 45, f: 23, sf: 0) + Timecode.Components(d: 3, h: 01, m: 56, s: 45, f: 23, sf: 0) ) XCTAssertEqual( try Timecode.decode(timecode: "12 01:56:45;23"), - TCC(d: 12, h: 01, m: 56, s: 45, f: 23, sf: 0) + Timecode.Components(d: 12, h: 01, m: 56, s: 45, f: 23, sf: 0) ) XCTAssertEqual( try Timecode.decode(timecode: "12:01:56:45;23"), - TCC(d: 12, h: 01, m: 56, s: 45, f: 23, sf: 0) + Timecode.Components(d: 12, h: 01, m: 56, s: 45, f: 23, sf: 0) ) // all periods - not supporting this. @@ -502,37 +478,37 @@ class Timecode_String_Tests: XCTestCase { XCTAssertEqual( try Timecode.decode(timecode: "0:00:00:00.05"), - TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 05) + Timecode.Components(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 05) ) XCTAssertEqual( try Timecode.decode(timecode: "00:00:00:00.05"), - TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 05) + Timecode.Components(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 05) ) XCTAssertEqual( try Timecode.decode(timecode: "1:56:45:23.05"), - TCC(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 05) + Timecode.Components(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 05) ) XCTAssertEqual( try Timecode.decode(timecode: "01:56:45:23.05"), - TCC(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 05) + Timecode.Components(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 05) ) XCTAssertEqual( try Timecode.decode(timecode: "3 01:56:45:23.05"), - TCC(d: 3, h: 01, m: 56, s: 45, f: 23, sf: 05) + Timecode.Components(d: 3, h: 01, m: 56, s: 45, f: 23, sf: 05) ) XCTAssertEqual( try Timecode.decode(timecode: "12 01:56:45:23.05"), - TCC(d: 12, h: 01, m: 56, s: 45, f: 23, sf: 05) + Timecode.Components(d: 12, h: 01, m: 56, s: 45, f: 23, sf: 05) ) XCTAssertEqual( try Timecode.decode(timecode: "12:01:56:45:23.05"), - TCC(d: 12, h: 01, m: 56, s: 45, f: 23, sf: 05) + Timecode.Components(d: 12, h: 01, m: 56, s: 45, f: 23, sf: 05) ) // subframes @@ -540,106 +516,103 @@ class Timecode_String_Tests: XCTestCase { XCTAssertEqual( try Timecode.decode(timecode: "0;00;00;00.05"), - TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 05) + Timecode.Components(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 05) ) XCTAssertEqual( try Timecode.decode(timecode: "00;00;00;00.05"), - TCC(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 05) + Timecode.Components(d: 0, h: 00, m: 00, s: 00, f: 00, sf: 05) ) XCTAssertEqual( try Timecode.decode(timecode: "1;56;45;23.05"), - TCC(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 05) + Timecode.Components(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 05) ) XCTAssertEqual( try Timecode.decode(timecode: "01;56;45;23.05"), - TCC(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 05) + Timecode.Components(d: 0, h: 01, m: 56, s: 45, f: 23, sf: 05) ) XCTAssertEqual( try Timecode.decode(timecode: "3 01;56;45;23.05"), - TCC(d: 3, h: 01, m: 56, s: 45, f: 23, sf: 05) + Timecode.Components(d: 3, h: 01, m: 56, s: 45, f: 23, sf: 05) ) XCTAssertEqual( try Timecode.decode(timecode: "12 01;56;45;23.05"), - TCC(d: 12, h: 01, m: 56, s: 45, f: 23, sf: 05) + Timecode.Components(d: 12, h: 01, m: 56, s: 45, f: 23, sf: 05) ) XCTAssertEqual( try Timecode.decode(timecode: "12;01;56;45;23.05"), - TCC(d: 12, h: 01, m: 56, s: 45, f: 23, sf: 05) + Timecode.Components(d: 12, h: 01, m: 56, s: 45, f: 23, sf: 05) ) } - // MARK: - .toTimecode() + // MARK: - .timecode() func testString_toTimeCode_at() throws { - // toTimecode(at:) + // timecode(at:) XCTAssertEqual( - try "01:05:20:14".toTimecode(at: ._23_976), + try "01:05:20:14".timecode(at: .fps23_976), try Timecode( - TCC(h: 1, m: 5, s: 20, f: 14), - at: ._23_976 + .components(h: 1, m: 5, s: 20, f: 14), + at: .fps23_976 ) ) - // toTimecode(at:) with subframes + // timecode(at:) with subframes let tcWithSubFrames = try "01:05:20:14.94" - .toTimecode( - at: ._23_976, - base: ._100SubFrames, - format: [.showSubFrames] + .timecode( + at: .fps23_976, + base: .max100SubFrames ) XCTAssertEqual( tcWithSubFrames, try Timecode( - TCC(h: 1, m: 5, s: 20, f: 14, sf: 94), - at: ._23_976, - base: ._100SubFrames, - format: [.showSubFrames] + .components(h: 1, m: 5, s: 20, f: 14, sf: 94), + at: .fps23_976, + base: .max100SubFrames ) ) XCTAssertEqual( - tcWithSubFrames.stringValue, + tcWithSubFrames.stringValue(format: .showSubFrames), "01:05:20:14.94" ) } func testString_toTimeCode_rawValuesAt() throws { - // toTimecode(rawValuesAt:) + // timecode(rawValuesAt:) XCTAssertEqual( - try "01:05:20:14".toTimecode(rawValuesAt: ._23_976), + try "01:05:20:14".timecode(at: .fps23_976, by: .allowingInvalid), try Timecode( - TCC(h: 1, m: 5, s: 20, f: 14), - at: ._23_976 + .components(h: 1, m: 5, s: 20, f: 14), + at: .fps23_976 ) ) - // toTimecode(rawValuesAt:) with subframes + // timecode(rawValuesAt:) with subframes let tcWithSubFrames = try "01:05:20:14.94" - .toTimecode( - rawValuesAt: ._23_976, - base: ._100SubFrames, - format: [.showSubFrames] + .timecode( + at: .fps23_976, + base: .max100SubFrames, + by: .allowingInvalid ) XCTAssertEqual( tcWithSubFrames, try Timecode( - TCC(h: 1, m: 5, s: 20, f: 14, sf: 94), - at: ._23_976, - base: ._100SubFrames, - format: [.showSubFrames] + .components(h: 1, m: 5, s: 20, f: 14, sf: 94), + at: .fps23_976, + base: .max100SubFrames ) ) XCTAssertEqual( - tcWithSubFrames.stringValue, + tcWithSubFrames.stringValue(format: .showSubFrames), "01:05:20:14.94" ) } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Timecode Conversion Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Timecode Conversion Tests.swift index c13d6f74..be769a86 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Timecode Conversion Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Timecode Conversion Tests.swift @@ -1,13 +1,13 @@ // // Timecode Conversion Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class Timecode_Conversion_Tests: XCTestCase { override func setUp() { } @@ -18,8 +18,7 @@ class Timecode_Conversion_Tests: XCTestCase { // ensure conversion produces identical output if frame rates are equal try TimecodeFrameRate.allCases.forEach { - let tc = try TCC(h: 1) - .toTimecode(at: $0) + let tc = try Timecode(.components(h: 1), at: $0) let convertedTC = try tc.converted(to: $0) @@ -28,17 +27,15 @@ class Timecode_Conversion_Tests: XCTestCase { // spot-check an example conversion - let convertedTC = try - TCC(h: 1) - .toTimecode( - at: ._23_976, - base: ._100SubFrames, - format: [.showSubFrames] - ) - .converted(to: ._30) + let convertedTC = try Timecode( + .components(h: 1), + at: .fps23_976, + base: .max100SubFrames + ) + .converted(to: .fps30) - XCTAssertEqual(convertedTC.frameRate, ._30) - XCTAssertEqual(convertedTC.components, TCC(h: 1, m: 00, s: 03, f: 18, sf: 00)) + XCTAssertEqual(convertedTC.frameRate, .fps30) + XCTAssertEqual(convertedTC.components, Timecode.Components(h: 1, m: 00, s: 03, f: 18, sf: 00)) } func testConverted_PreservingValues() throws { @@ -46,8 +43,7 @@ class Timecode_Conversion_Tests: XCTestCase { // ensure conversion produces identical output if frame rates are equal try TimecodeFrameRate.allCases.forEach { - let tc = try TCC(h: 1) - .toTimecode(at: $0) + let tc = try Timecode(.components(h: 1), at: $0) let convertedTC = try tc.converted(to: $0, preservingValues: true) @@ -58,52 +54,47 @@ class Timecode_Conversion_Tests: XCTestCase { try TimecodeFrameRate.allCases.forEach { sourceFrameRate in try TimecodeFrameRate.allCases.forEach { destinationFrameRate in - - let convertedTC = try - TCC(h: 2, m: 07, s: 24, f: 11) - .toTimecode( - at: sourceFrameRate, - base: ._100SubFrames, - format: [.showSubFrames] - ) - .converted(to: destinationFrameRate, preservingValues: true) + let convertedTC = try Timecode( + .components(h: 2, m: 07, s: 24, f: 11), + at: sourceFrameRate, + base: .max100SubFrames + ) + .converted(to: destinationFrameRate, preservingValues: true) XCTAssertEqual(convertedTC.frameRate, destinationFrameRate) - XCTAssertEqual(convertedTC.components, TCC(h: 2, m: 07, s: 24, f: 11, sf: 00)) + XCTAssertEqual(convertedTC.components, Timecode.Components(h: 2, m: 07, s: 24, f: 11, sf: 00)) } } // spot-check: frames value too large to preserve; convert timecode instead - let convertedTC = try - TCC(h: 1, m: 0, s: 0, f: 96) - .toTimecode( - at: ._100, - base: ._100SubFrames, - format: [.showSubFrames] - ) - .converted(to: ._50, preservingValues: true) - - XCTAssertEqual(convertedTC.frameRate, ._50) - XCTAssertEqual(convertedTC.components, TCC(h: 1, m: 00, s: 00, f: 48, sf: 00)) + let convertedTC = try Timecode( + .components(h: 1, m: 0, s: 0, f: 96), + at: .fps100, + base: .max100SubFrames + ) + .converted(to: .fps50, preservingValues: true) + + XCTAssertEqual(convertedTC.frameRate, .fps50) + XCTAssertEqual(convertedTC.components, Timecode.Components(h: 1, m: 00, s: 00, f: 48, sf: 00)) } func testTransform() throws { - var tc = try TCC(m: 1).toTimecode(at: ._24) + var tc = try Timecode(.components(m: 1), at: .fps24) let transformer = TimecodeTransformer(.offset(by: .positive(tc))) tc.transform(using: transformer) - XCTAssertEqual(tc, try TCC(m: 2).toTimecode(at: ._24)) + XCTAssertEqual(tc, try Timecode(.components(m: 2), at: .fps24)) } func testTransformed() throws { - let tc = try TCC(m: 1).toTimecode(at: ._24) + let tc = try Timecode(.components(m: 1), at: .fps24) let transformer = TimecodeTransformer(.offset(by: .positive(tc))) let newTC = tc.transformed(using: transformer) - XCTAssertEqual(newTC, try TCC(m: 2).toTimecode(at: ._24)) + XCTAssertEqual(newTC, try Timecode(.components(m: 2), at: .fps24)) } } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Timecode Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Timecode Tests.swift index aacfcb79..b18174db 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Timecode Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Timecode Tests.swift @@ -1,22 +1,21 @@ // // Timecode Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform +/* @testable */ import TimecodeKit import XCTest -@testable import TimecodeKit class Timecode_Tests: XCTestCase { override func setUp() { } override func tearDown() { } + // no tests in this file func testEmpty() throws { - // no tests in this file - - XCTAssertTrue(true) +// Timecode( } } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Timecode Validation Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Timecode Validation Tests.swift index 847c3004..258b5d6c 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Timecode Validation Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Timecode Validation Tests.swift @@ -1,13 +1,13 @@ // // Timecode Validation Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class Timecode_Validation_Tests: XCTestCase { override func setUp() { } @@ -16,14 +16,14 @@ class Timecode_Validation_Tests: XCTestCase { func testValidWithinRanges() { // typical valid values - let fr = TimecodeFrameRate._24 - let limit = Timecode.UpperLimit._24hours + let fr = TimecodeFrameRate.fps24 + let limit = Timecode.UpperLimit.max24Hours - let tc = Timecode(at: fr, limit: limit) + let tc = Timecode(.zero, at: fr, base: .max80SubFrames, limit: limit) XCTAssertEqual(tc.invalidComponents, []) XCTAssertEqual( - tc.components.invalidComponents(at: fr, limit: limit, base: ._80SubFrames), + tc.components.invalidComponents(at: fr, base: .max80SubFrames, limit: limit), [] ) @@ -32,16 +32,16 @@ class Timecode_Validation_Tests: XCTestCase { XCTAssertEqual(tc.validRange(of: .minutes), 0 ... 59) XCTAssertEqual(tc.validRange(of: .seconds), 0 ... 59) XCTAssertEqual(tc.validRange(of: .frames), 0 ... 23) - // XCTAssertThrowsError(tc.validRange(of: .subFrames)) // TODO: test + XCTAssertEqual(tc.validRange(of: .subFrames), 0 ... 79) } func testInvalidOverRanges() { // invalid - over ranges - let fr = TimecodeFrameRate._24 - let limit = Timecode.UpperLimit._24hours + let fr = TimecodeFrameRate.fps24 + let limit = Timecode.UpperLimit.max24Hours - var tc = Timecode(at: fr, limit: limit) + var tc = Timecode(.zero, at: fr, limit: limit) tc.days = 5 tc.hours = 25 tc.minutes = 75 @@ -54,7 +54,7 @@ class Timecode_Validation_Tests: XCTestCase { [.days, .hours, .minutes, .seconds, .frames, .subFrames] ) XCTAssertEqual( - tc.components.invalidComponents(at: fr, limit: limit, base: ._80SubFrames), + tc.components.invalidComponents(at: fr, base: .max80SubFrames, limit: limit), [.days, .hours, .minutes, .seconds, .frames, .subFrames] ) } @@ -62,10 +62,10 @@ class Timecode_Validation_Tests: XCTestCase { func testInvalidUnderRanges() { // invalid - under ranges - let fr = TimecodeFrameRate._24 - let limit = Timecode.UpperLimit._24hours + let fr = TimecodeFrameRate.fps24 + let limit = Timecode.UpperLimit.max24Hours - var tc = Timecode(at: fr, limit: limit) + var tc = Timecode(.zero, at: fr, limit: limit) tc.days = -1 tc.hours = -1 tc.minutes = -1 @@ -78,21 +78,42 @@ class Timecode_Validation_Tests: XCTestCase { [.days, .hours, .minutes, .seconds, .frames, .subFrames] ) XCTAssertEqual( - tc.components.invalidComponents(at: fr, limit: limit, base: ._80SubFrames), + tc.components.invalidComponents(at: fr, base: .max80SubFrames, limit: limit), [.days, .hours, .minutes, .seconds, .frames, .subFrames] ) } + func testSubFrames() { + // test each subframes base range + + let fr = TimecodeFrameRate.fps24 + let limit = Timecode.UpperLimit.max24Hours + + for base in Timecode.SubFramesBase.allCases { + let tc = Timecode(.zero, at: fr, base: base, limit: limit) + + let range: ClosedRange = { + switch base { + case .quarterFrames: return 0 ... 3 + case .max80SubFrames: return 0 ... 79 + case .max100SubFrames: return 0 ... 99 + } + }() + + XCTAssertEqual(tc.validRange(of: .subFrames), range) + } + } + func testDropFrame() { // perform a spot-check to ensure drop rate timecode validation works as expected TimecodeFrameRate.allDrop.forEach { - let limit = Timecode.UpperLimit._24hours + let limit = Timecode.UpperLimit.max24Hours // every 10 minutes, no frames are skipped do { - var tc = Timecode(at: $0, limit: limit) + var tc = Timecode(.zero, at: $0, limit: limit) tc.minutes = 0 tc.frames = 0 @@ -104,8 +125,8 @@ class Timecode_Validation_Tests: XCTestCase { XCTAssertEqual( tc.components.invalidComponents( at: $0, - limit: limit, - base: ._80SubFrames + base: .max80SubFrames, + limit: limit ), [], "for \($0)" @@ -115,7 +136,7 @@ class Timecode_Validation_Tests: XCTestCase { // all other minutes each skip frame 0 and 1 for minute in 1 ... 9 { - var tc = Timecode(at: $0, limit: limit) + var tc = Timecode(.zero, at: $0, limit: limit) tc.minutes = minute tc.frames = 0 @@ -127,14 +148,14 @@ class Timecode_Validation_Tests: XCTestCase { XCTAssertEqual( tc.components.invalidComponents( at: $0, - limit: limit, - base: ._80SubFrames + base: .max80SubFrames, + limit: limit ), [.frames], "for \($0) at \(minute) minutes" ) - tc = Timecode(at: $0, limit: limit) + tc = Timecode(.zero, at: $0, limit: limit) tc.minutes = minute tc.frames = 1 @@ -146,8 +167,8 @@ class Timecode_Validation_Tests: XCTestCase { XCTAssertEqual( tc.components.invalidComponents( at: $0, - limit: limit, - base: ._80SubFrames + base: .max80SubFrames, + limit: limit ), [.frames], "for \($0) at \(minute) minutes" @@ -157,13 +178,13 @@ class Timecode_Validation_Tests: XCTestCase { } func testDropFrameEdgeCases() throws { - let comps = TCC(h: 23, m: 59, s: 59, f: 29, sf: 79) + let comps = Timecode.Components(h: 23, m: 59, s: 59, f: 29, sf: 79) let tc = try Timecode( - comps, - at: ._29_97_drop, - limit: ._24hours, - base: ._80SubFrames + .components(comps), + at: .fps29_97d, + base: .max80SubFrames, + limit: .max24Hours ) XCTAssertEqual(tc.components, comps) @@ -171,12 +192,13 @@ class Timecode_Validation_Tests: XCTestCase { } func testMaxFrames() { - let subFramesBase: Timecode.SubFramesBase = ._80SubFrames + let subFramesBase: Timecode.SubFramesBase = .max80SubFrames let tc = Timecode( - at: ._24, - limit: ._24hours, - base: subFramesBase + .zero, + at: .fps24, + base: subFramesBase, + limit: .max24Hours ) XCTAssertEqual(tc.validRange(of: .subFrames), 0 ... (subFramesBase.rawValue - 1)) @@ -191,7 +213,7 @@ class Timecode_Validation_Tests: XCTestCase { at: tc.frameRate ) - XCTAssertEqual(tcc, TCC( + XCTAssertEqual(tcc, Timecode.Components( d: 0, h: 23, m: 59, diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Timecode init Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Timecode init Tests.swift index 0ac11be7..e239cc88 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Timecode init Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/Timecode init Tests.swift @@ -1,13 +1,13 @@ // // Timecode init Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class Timecode_init_Tests: XCTestCase { override func setUp() { } @@ -20,24 +20,12 @@ class Timecode_init_Tests: XCTestCase { // defaults - var tc = Timecode(at: ._24) - XCTAssertEqual(tc.frameRate, ._24) - XCTAssertEqual(tc.upperLimit, ._24hours) + let tc = Timecode(.zero, at: .fps24) + XCTAssertEqual(tc.frameRate, .fps24) + XCTAssertEqual(tc.upperLimit, .max24Hours) XCTAssertEqual(tc.frameCount.subFrameCount, 0) - XCTAssertEqual(tc.components, TCC(d: 0, h: 0, m: 0, s: 0, f: 0)) - XCTAssertEqual(tc.stringValue, "00:00:00:00") - - // expected initializers - - tc = Timecode(at: ._24) - tc = Timecode(at: ._24, limit: ._24hours) - tc = Timecode(at: ._24, limit: ._24hours, base: ._100SubFrames) - tc = Timecode(at: ._24, limit: ._24hours, base: ._100SubFrames, format: [.showSubFrames]) - - tc = Timecode(at: ._24, base: ._100SubFrames) - tc = Timecode(at: ._24, base: ._100SubFrames, format: [.showSubFrames]) - - tc = Timecode(at: ._24, format: [.showSubFrames]) + XCTAssertEqual(tc.components, Timecode.Components(d: 0, h: 0, m: 0, s: 0, f: 0)) + XCTAssertEqual(tc.stringValue(), "00:00:00:00") } // MARK: - Misc @@ -45,11 +33,10 @@ class Timecode_init_Tests: XCTestCase { func testTimecode_init_All_DisplaySubFrames() throws { try TimecodeFrameRate.allCases.forEach { let tc = try Timecode( - "00:00:00:00", + .string("00:00:00:00"), at: $0, - limit: ._24hours, - base: ._100SubFrames, - format: [.showSubFrames] + base: .max100SubFrames, + limit: .max24Hours ) var frm: String @@ -63,7 +50,7 @@ class Timecode_init_Tests: XCTestCase { let frSep = $0.isDrop ? ";" : ":" - XCTAssertEqual(tc.stringValue, "00:00:00\(frSep)\(frm).00") + XCTAssertEqual(tc.stringValue(format: .showSubFrames), "00:00:00\(frSep)\(frm).00") } } } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/UpperLimit/UpperLimit Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/UpperLimit/UpperLimit Tests.swift index 4042ea3c..d44cc76b 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/UpperLimit/UpperLimit Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Timecode/UpperLimit/UpperLimit Tests.swift @@ -1,17 +1,17 @@ // // UpperLimit Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class Timecode_UpperLimit: XCTestCase { func test24Hours() { - let upperLimit: Timecode.UpperLimit = ._24hours + let upperLimit: Timecode.UpperLimit = .max24Hours XCTAssertEqual(upperLimit.maxDays, 1) XCTAssertEqual(upperLimit.maxDaysExpressible, 0) @@ -22,7 +22,7 @@ class Timecode_UpperLimit: XCTestCase { } func test100Days() { - let upperLimit: Timecode.UpperLimit = ._100days + let upperLimit: Timecode.UpperLimit = .max100Days XCTAssertEqual(upperLimit.maxDays, 100) XCTAssertEqual(upperLimit.maxDaysExpressible, 99) diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate CompatibleGroup Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate CompatibleGroup Tests.swift index 6904dc3c..8771fc53 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate CompatibleGroup Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate CompatibleGroup Tests.swift @@ -1,13 +1,13 @@ // // TimecodeFrameRate CompatibleGroup Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class TimecodeFrameRate_CompatibleGroup_Tests: XCTestCase { override func setUp() { } @@ -25,24 +25,24 @@ class TimecodeFrameRate_CompatibleGroup_Tests: XCTestCase { // methods basic spot-check // NTSC - XCTAssertEqual(TimecodeFrameRate._29_97.compatibleGroup, .NTSC) - XCTAssertEqual(TimecodeFrameRate._59_94.compatibleGroup, .NTSC) - XCTAssertTrue(TimecodeFrameRate._29_97.isCompatible(with: ._59_94)) + XCTAssertEqual(TimecodeFrameRate.fps29_97.compatibleGroup, .ntscColor) + XCTAssertEqual(TimecodeFrameRate.fps59_94.compatibleGroup, .ntscColor) + XCTAssertTrue(TimecodeFrameRate.fps29_97.isCompatible(with: .fps59_94)) - // NTSC drop - XCTAssertEqual(TimecodeFrameRate._29_97_drop.compatibleGroup, .NTSC_drop) - XCTAssertEqual(TimecodeFrameRate._59_94_drop.compatibleGroup, .NTSC_drop) - XCTAssertTrue(TimecodeFrameRate._29_97_drop.isCompatible(with: ._59_94_drop)) + // NTSC Drop + XCTAssertEqual(TimecodeFrameRate.fps29_97d.compatibleGroup, .ntscDrop) + XCTAssertEqual(TimecodeFrameRate.fps59_94d.compatibleGroup, .ntscDrop) + XCTAssertTrue(TimecodeFrameRate.fps29_97d.isCompatible(with: .fps59_94d)) - // ATSC - XCTAssertEqual(TimecodeFrameRate._24.compatibleGroup, .ATSC) - XCTAssertEqual(TimecodeFrameRate._30.compatibleGroup, .ATSC) - XCTAssertTrue(TimecodeFrameRate._24.isCompatible(with: ._30)) + // Whole + XCTAssertEqual(TimecodeFrameRate.fps24.compatibleGroup, .whole) + XCTAssertEqual(TimecodeFrameRate.fps30.compatibleGroup, .whole) + XCTAssertTrue(TimecodeFrameRate.fps24.isCompatible(with: .fps30)) - // ATSC drop - XCTAssertEqual(TimecodeFrameRate._30_drop.compatibleGroup, .ATSC_drop) - XCTAssertEqual(TimecodeFrameRate._60_drop.compatibleGroup, .ATSC_drop) - XCTAssertTrue(TimecodeFrameRate._30_drop.isCompatible(with: ._60_drop)) + // NTSC Color Wall Time + XCTAssertEqual(TimecodeFrameRate.fps30d.compatibleGroup, .ntscColorWallTime) + XCTAssertEqual(TimecodeFrameRate.fps60d.compatibleGroup, .ntscColorWallTime) + XCTAssertTrue(TimecodeFrameRate.fps30d.isCompatible(with: .fps60d)) } func testCompatibleGroup_compatibleGroupRates() { diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate Conversions Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate Conversions Tests.swift index 99176ccd..045a814f 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate Conversions Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate Conversions Tests.swift @@ -1,14 +1,14 @@ // // TimecodeFrameRate Conversions Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest -@testable import TimecodeKit import CoreMedia +@testable import TimecodeKit +import XCTest class TimecodeFrameRate_Conversions_Tests: XCTestCase { override func setUp() { } @@ -31,11 +31,11 @@ class TimecodeFrameRate_Conversions_Tests: XCTestCase { // 24 XCTAssertEqual( TimecodeFrameRate(rate: Fraction(24, 1), drop: false), - ._24 + .fps24 ) XCTAssertEqual( TimecodeFrameRate(rate: Fraction(240, 10), drop: false), - ._24 + .fps24 ) // 24d is not a valid frame rate @@ -44,17 +44,17 @@ class TimecodeFrameRate_Conversions_Tests: XCTestCase { // 30 XCTAssertEqual( TimecodeFrameRate(rate: Fraction(30, 1), drop: false), - ._30 + .fps30 ) XCTAssertEqual( TimecodeFrameRate(rate: Fraction(300, 10), drop: false), - ._30 + .fps30 ) // 30d XCTAssertEqual( TimecodeFrameRate(rate: Fraction(30, 1), drop: true), - ._30_drop + .fps30d ) // edge cases @@ -68,7 +68,7 @@ class TimecodeFrameRate_Conversions_Tests: XCTestCase { XCTAssertNil(TimecodeFrameRate(rate: Fraction(0, -1), drop: false)) XCTAssertNil(TimecodeFrameRate(rate: Fraction(-1, 0), drop: false)) XCTAssertNil(TimecodeFrameRate(rate: Fraction(-1, -1), drop: false)) - XCTAssertEqual(TimecodeFrameRate(rate: Fraction(-30, -1), drop: false), ._30) + XCTAssertEqual(TimecodeFrameRate(rate: Fraction(-30, -1), drop: false), .fps30) XCTAssertNil(TimecodeFrameRate(rate: Fraction(-30, 1), drop: false)) XCTAssertNil(TimecodeFrameRate(rate: Fraction(30, -1), drop: false)) @@ -93,11 +93,11 @@ class TimecodeFrameRate_Conversions_Tests: XCTestCase { // 24 XCTAssertEqual( TimecodeFrameRate(frameDuration: Fraction(1, 24), drop: false), - ._24 + .fps24 ) XCTAssertEqual( TimecodeFrameRate(frameDuration: Fraction(10, 240), drop: false), - ._24 + .fps24 ) // 24d is not a valid frame rate @@ -106,17 +106,17 @@ class TimecodeFrameRate_Conversions_Tests: XCTestCase { // 30 XCTAssertEqual( TimecodeFrameRate(frameDuration: Fraction(1, 30), drop: false), - ._30 + .fps30 ) XCTAssertEqual( TimecodeFrameRate(frameDuration: Fraction(10, 300), drop: false), - ._30 + .fps30 ) // 30d XCTAssertEqual( TimecodeFrameRate(frameDuration: Fraction(1, 30), drop: true), - ._30_drop + .fps30d ) // edge cases @@ -130,7 +130,7 @@ class TimecodeFrameRate_Conversions_Tests: XCTestCase { XCTAssertNil(TimecodeFrameRate(frameDuration: Fraction(-1, 0), drop: false)) XCTAssertNil(TimecodeFrameRate(frameDuration: Fraction(0, -1), drop: false)) XCTAssertNil(TimecodeFrameRate(frameDuration: Fraction(-1, -1), drop: false)) - XCTAssertEqual(TimecodeFrameRate(frameDuration: Fraction(-1, -30), drop: false), ._30) + XCTAssertEqual(TimecodeFrameRate(frameDuration: Fraction(-1, -30), drop: false), .fps30) XCTAssertNil(TimecodeFrameRate(frameDuration: Fraction(1, -30), drop: false)) XCTAssertNil(TimecodeFrameRate(frameDuration: Fraction(-1, 30), drop: false)) @@ -149,14 +149,14 @@ class TimecodeFrameRate_Conversions_CMTime_Tests: XCTestCase { rate: CMTime(value: 30000, timescale: 1001), drop: false ), - ._29_97 + .fps29_97 ) XCTAssertEqual( TimecodeFrameRate( rate: CMTime(value: 30000, timescale: 1001), drop: true ), - ._29_97_drop + .fps29_97d ) } @@ -166,28 +166,30 @@ class TimecodeFrameRate_Conversions_CMTime_Tests: XCTestCase { frameDuration: CMTime(value: 1001, timescale: 30000), drop: false ), - ._29_97 + .fps29_97 ) XCTAssertEqual( TimecodeFrameRate( frameDuration: CMTime(value: 1001, timescale: 30000), drop: true ), - ._29_97_drop + .fps29_97d ) } func testrateCMTime() throws { XCTAssertEqual( - TimecodeFrameRate._29_97.rateCMTime, + TimecodeFrameRate.fps29_97.rateCMTime, CMTime(value: 30000, timescale: 1001) ) } func testframeDurationCMTime() throws { // spot-check - XCTAssertEqual(TimecodeFrameRate._29_97.frameDurationCMTime, - CMTime(value: 1001, timescale: 30000)) + XCTAssertEqual( + TimecodeFrameRate.fps29_97.frameDurationCMTime, + CMTime(value: 1001, timescale: 30000) + ) // ensure the CMTime instance returns correct 1 frame duration in seconds. // due to floating-point dithering, it tends to be accurate up to @@ -196,8 +198,7 @@ class TimecodeFrameRate_Conversions_CMTime_Tests: XCTestCase { try TimecodeFrameRate.allCases.forEach { let cmTimeSeconds = $0.frameDurationCMTime.seconds - let oneFrameDuration = try TCC(f: 1) - .toTimecode(at: $0) + let oneFrameDuration = try Timecode(.components(f: 1), at: $0) .realTimeValue XCTAssertEqual( diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate Formats Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate Formats Tests.swift index b0c5b1e4..b7dc231c 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate Formats Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate Formats Tests.swift @@ -1,13 +1,13 @@ // // TimecodeFrameRate Formats Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class TimecodeFrameRate_Formats_Tests: XCTestCase { override func setUp() { } @@ -30,8 +30,7 @@ class TimecodeFrameRate_Formats_Tests: XCTestCase { let editRateSeconds = Double(editRateComponents[1]) / Double(editRateComponents[0]) - let oneFrameDuration = try TCC(f: 1) - .toTimecode(at: $0) + let oneFrameDuration = try Timecode(.components(f: 1), at: $0) .realTimeValue XCTAssertEqual( diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate Properties Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate Properties Tests.swift index cd479b60..3a112879 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate Properties Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate Properties Tests.swift @@ -1,19 +1,19 @@ // // TimecodeFrameRate Properties Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class TimecodeFrameRate_Properties_Tests: XCTestCase { func testProperties() { // spot-check that properties behave as expected - let frameRate: TimecodeFrameRate = ._30 + let frameRate: TimecodeFrameRate = .fps30 XCTAssertEqual(frameRate.stringValue, "30") @@ -26,29 +26,29 @@ class TimecodeFrameRate_Properties_Tests: XCTestCase { XCTAssertEqual(frameRate.maxFrameNumberDisplayable, 29) XCTAssertEqual( - frameRate.maxTotalFrames(in: ._24hours), + frameRate.maxTotalFrames(in: .max24Hours), 2_592_000 ) XCTAssertEqual( - frameRate.maxTotalFrames(in: ._100days), + frameRate.maxTotalFrames(in: .max100Days), 2_592_000 * 100 ) XCTAssertEqual( - frameRate.maxTotalFramesExpressible(in: ._24hours), + frameRate.maxTotalFramesExpressible(in: .max24Hours), 2_592_000 - 1 ) XCTAssertEqual( - frameRate.maxTotalFramesExpressible(in: ._100days), + frameRate.maxTotalFramesExpressible(in: .max100Days), (2_592_000 * 100) - 1 ) XCTAssertEqual( frameRate.maxTotalSubFrames( - in: ._24hours, - base: ._80SubFrames + in: .max24Hours, + base: .max80SubFrames ), 2_592_000 * 80 ) @@ -57,16 +57,16 @@ class TimecodeFrameRate_Properties_Tests: XCTestCase { #if !(arch(arm) || arch(i386)) XCTAssertEqual( frameRate.maxTotalSubFrames( - in: ._100days, - base: ._80SubFrames + in: .max100Days, + base: .max80SubFrames ), 2_592_000 * 100 * 80 ) XCTAssertEqual( frameRate.maxSubFrameCountExpressible( - in: ._100days, - base: ._80SubFrames + in: .max100Days, + base: .max80SubFrames ), (2_592_000 * 100 * 80) - 1 ) @@ -74,8 +74,8 @@ class TimecodeFrameRate_Properties_Tests: XCTestCase { XCTAssertEqual( frameRate.maxSubFrameCountExpressible( - in: ._24hours, - base: ._80SubFrames + in: .max24Hours, + base: .max80SubFrames ), (2_592_000 * 80) - 1 ) diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate Sorted Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate Sorted Tests.swift index a6ba440b..215494b5 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate Sorted Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate Sorted Tests.swift @@ -1,28 +1,28 @@ // // TimecodeFrameRate Sorted Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class TimecodeFrameRate_Sorted_Tests: XCTestCase { func testSortOrder() { let unsorted: [TimecodeFrameRate] = [ - ._29_97, - ._30, - ._24, - ._120 + .fps29_97, + .fps30, + .fps24, + .fps120 ] let correctOrder: [TimecodeFrameRate] = [ - ._24, - ._29_97, - ._30, - ._120 + .fps24, + .fps29_97, + .fps30, + .fps120 ] let sorted = unsorted.sorted() diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate String Extensions Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate String Extensions Tests.swift index 5371ba89..b9f5a06c 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate String Extensions Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeFrameRate/TimecodeFrameRate String Extensions Tests.swift @@ -1,24 +1,24 @@ // // TimecodeFrameRate String Extensions Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class TimecodeFrameRate_StringExtensions_Tests: XCTestCase { - func testString_toTimecodeFrameRate() { + func testString_timecodeFrameRate() { // do a spot-check to ensure this functions as expected - XCTAssertEqual("23.976".toTimecodeFrameRate, ._23_976) - XCTAssertEqual("29.97d".toTimecodeFrameRate, ._29_97_drop) + XCTAssertEqual("23.976".timecodeFrameRate, .fps23_976) + XCTAssertEqual("29.97d".timecodeFrameRate, .fps29_97d) - XCTAssertNil("".toTimecodeFrameRate) - XCTAssertNil(" ".toTimecodeFrameRate) - XCTAssertNil("BogusString".toTimecodeFrameRate) + XCTAssertNil("".timecodeFrameRate) + XCTAssertNil(" ".timecodeFrameRate) + XCTAssertNil("BogusString".timecodeFrameRate) } } #endif diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeInterval/TimecodeInterval Rational CMTime Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeInterval/TimecodeInterval Rational CMTime Tests.swift index 4cdd9f4d..4998091b 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeInterval/TimecodeInterval Rational CMTime Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeInterval/TimecodeInterval Rational CMTime Tests.swift @@ -1,14 +1,14 @@ // // TimecodeInterval Rational CMTime Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest -@testable import TimecodeKit import CoreMedia +@testable import TimecodeKit +import XCTest class TimecodeInterval_Rational_CMTime_Tests: XCTestCase { override func setUp() { } @@ -17,38 +17,37 @@ class TimecodeInterval_Rational_CMTime_Tests: XCTestCase { func testTimecodeInterval_init_cmTime() throws { let ti = try TimecodeInterval( CMTime(value: 60, timescale: 30), - at: ._24 + at: .fps24 ) XCTAssertEqual(ti.sign, .plus) XCTAssertEqual( ti.absoluteInterval, - try TCC(s: 2).toTimecode(at: ._24) + try Timecode(.components(s: 2), at: .fps24) ) } func testCMTime() throws { let ti = try TimecodeInterval( - TCC(s: 2).toTimecode(at: ._24) + Timecode(.components(s: 2), at: .fps24) ) - let cmTime = ti.cmTime + let cmTime = ti.cmTimeValue XCTAssertEqual(cmTime.seconds.sign, .plus) XCTAssertEqual(cmTime.value, 2) XCTAssertEqual(cmTime.timescale, 1) } - func testCMTime_toTimecodeInterval() throws { - let ti = try CMTime(value: 60, timescale: 30) - .toTimecodeInterval(at: ._24) + func testCMTime_timecodeInterval() throws { + let ti = try CMTime(value: 60, timescale: 30).timecodeInterval(at: .fps24) XCTAssertEqual(ti.sign, .plus) XCTAssertEqual( ti.absoluteInterval, - try TCC(s: 2).toTimecode(at: ._24) + try Timecode(.components(s: 2), at: .fps24) ) } } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeInterval/TimecodeInterval Rational Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeInterval/TimecodeInterval Rational Tests.swift index 427b6f78..15cccd99 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeInterval/TimecodeInterval Rational Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeInterval/TimecodeInterval Rational Tests.swift @@ -1,13 +1,13 @@ // // TimecodeInterval Rational Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class TimecodeInterval_Rational_Tests: XCTestCase { override func setUp() { } @@ -16,19 +16,19 @@ class TimecodeInterval_Rational_Tests: XCTestCase { func testRational() throws { let ti = try TimecodeInterval( Fraction(60, 30), - at: ._24 + at: .fps24 ) XCTAssertEqual(ti.sign, .plus) XCTAssertEqual( ti.absoluteInterval, - try TCC(s: 2).toTimecode(at: ._24) + try Timecode(.components(s: 2), at: .fps24) ) XCTAssertEqual( ti.flattened(), - try TCC(s: 2).toTimecode(at: ._24) + try Timecode(.components(s: 2), at: .fps24) ) XCTAssertEqual(ti.rationalValue, Fraction(2, 1)) @@ -37,32 +37,32 @@ class TimecodeInterval_Rational_Tests: XCTestCase { func testRationalNegative() throws { let ti = try TimecodeInterval( Fraction(-60, 30), - at: ._24 + at: .fps24 ) XCTAssertEqual(ti.sign, .minus) XCTAssertEqual( ti.absoluteInterval, - try TCC(s: 2).toTimecode(at: ._24) + try Timecode(.components(s: 2), at: .fps24) ) XCTAssertEqual( ti.flattened(), // wraps around clock by underflowing - try TCC(h: 23, m: 59, s: 58, f: 00).toTimecode(at: ._24) + try Timecode(.components(h: 23, m: 59, s: 58, f: 00), at: .fps24) ) XCTAssertEqual(ti.rationalValue, Fraction(-2, 1)) } - func testFraction_toTimecodeInterval() throws { - let ti = try Fraction(60, 30).toTimecodeInterval(at: ._24) + func testFraction_timecodeInterval() throws { + let ti = try Fraction(60, 30).timecodeInterval(at: .fps24) XCTAssertEqual(ti.sign, .plus) XCTAssertEqual( ti.absoluteInterval, - try TCC(s: 2).toTimecode(at: ._24) + try Timecode(.components(s: 2), at: .fps24) ) } } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeInterval/TimecodeInterval Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeInterval/TimecodeInterval Tests.swift index 2002621a..3aedffb7 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeInterval/TimecodeInterval Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeInterval/TimecodeInterval Tests.swift @@ -1,13 +1,13 @@ // // TimecodeInterval Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class TimecodeInterval_Tests: XCTestCase { override func setUp() { } @@ -16,7 +16,7 @@ class TimecodeInterval_Tests: XCTestCase { func testInitA() throws { // positive - let intervalTC = try Timecode(TCC(m: 1), at: ._24) + let intervalTC = try Timecode(.components(m: 1), at: .fps24) let interval = TimecodeInterval(intervalTC) @@ -27,7 +27,7 @@ class TimecodeInterval_Tests: XCTestCase { func testInitB() throws { // negative - let intervalTC = try Timecode(TCC(m: 1), at: ._24) + let intervalTC = try Timecode(.components(m: 1), at: .fps24) let interval = TimecodeInterval(intervalTC, .minus) @@ -36,10 +36,10 @@ class TimecodeInterval_Tests: XCTestCase { } func testInitC() { - // TCC can contain negative values; + // Timecode.Components can contain negative values; // this should not alter the sign however - let intervalTC = Timecode(rawValues: TCC(m: -1), at: ._24) + let intervalTC = Timecode(.components(m: -1), at: .fps24, by: .allowingInvalid) let interval = TimecodeInterval(intervalTC) @@ -49,13 +49,13 @@ class TimecodeInterval_Tests: XCTestCase { func testIsNegative() { XCTAssertEqual( - TimecodeInterval(.init(at: ._24)) + TimecodeInterval(Timecode(.zero, at: .fps24)) .isNegative, false ) XCTAssertEqual( - TimecodeInterval(.init(at: ._24), .minus) + TimecodeInterval(Timecode(.zero, at: .fps24), .minus) .isNegative, true ) @@ -64,7 +64,7 @@ class TimecodeInterval_Tests: XCTestCase { func testTimecodeA() throws { // positive - let intervalTC = try Timecode(TCC(m: 1), at: ._24) + let intervalTC = try Timecode(.components(m: 1), at: .fps24) let interval = TimecodeInterval(intervalTC) @@ -74,59 +74,59 @@ class TimecodeInterval_Tests: XCTestCase { func testTimecodeB() throws { // negative, wrapping - let intervalTC = try Timecode(TCC(m: 1), at: ._24) + let intervalTC = try Timecode(.components(m: 1), at: .fps24) let interval = TimecodeInterval(intervalTC, .minus) XCTAssertEqual( interval.flattened(), - try Timecode(TCC(h: 23, m: 59, s: 00, f: 00), at: ._24) + try Timecode(.components(h: 23, m: 59, s: 00, f: 00), at: .fps24) ) } func testTimecodeC() throws { // positive, wrapping - let intervalTC = Timecode(rawValues: TCC(h: 26), at: ._24) + let intervalTC = Timecode(.components(h: 26), at: .fps24, by: .allowingInvalid) let interval = TimecodeInterval(intervalTC) XCTAssertEqual( interval.flattened(), - try Timecode(TCC(h: 02, m: 00, s: 00, f: 00), at: ._24) + try Timecode(.components(h: 02, m: 00, s: 00, f: 00), at: .fps24) ) } func testTimecodeOffsettingA() throws { // positive - let intervalTC = Timecode(rawValues: TCC(m: 1), at: ._24) + let intervalTC = Timecode(.components(m: 1), at: .fps24, by: .allowingInvalid) let interval = TimecodeInterval(intervalTC) XCTAssertEqual( - interval.timecode(offsetting: try Timecode(TCC(h: 1), at: ._24)), - try Timecode(TCC(h: 01, m: 01, s: 00, f: 00), at: ._24) + try interval.timecode(offsetting: Timecode(.components(h: 1), at: .fps24)), + try Timecode(.components(h: 01, m: 01, s: 00, f: 00), at: .fps24) ) } func testTimecodeOffsettingB() throws { // negative - let intervalTC = Timecode(rawValues: TCC(m: 1), at: ._24) + let intervalTC = Timecode(.components(m: 1), at: .fps24, by: .allowingInvalid) let interval = TimecodeInterval(intervalTC, .minus) XCTAssertEqual( - interval.timecode(offsetting: try Timecode(TCC(h: 1), at: ._24)), - try Timecode(TCC(h: 00, m: 59, s: 00, f: 00), at: ._24) + try interval.timecode(offsetting: Timecode(.components(h: 1), at: .fps24)), + try Timecode(.components(h: 00, m: 59, s: 00, f: 00), at: .fps24) ) } func testRealTimeValueA() throws { // positive - let intervalTC = try Timecode(TCC(h: 1), at: ._24) + let intervalTC = try Timecode(.components(h: 1), at: .fps24) let interval = TimecodeInterval(intervalTC) @@ -136,7 +136,7 @@ class TimecodeInterval_Tests: XCTestCase { func testRealTimeValueB() throws { // negative - let intervalTC = try Timecode(TCC(h: 1), at: ._24) + let intervalTC = try Timecode(.components(h: 1), at: .fps24) let interval = TimecodeInterval(intervalTC, .minus) @@ -144,23 +144,23 @@ class TimecodeInterval_Tests: XCTestCase { } func testStaticConstructors_Positive() throws { - let interval: TimecodeInterval = .positive( - try Timecode(TCC(h: 1), at: ._24) + let interval: TimecodeInterval = try .positive( + Timecode(.components(h: 1), at: .fps24) ) XCTAssertEqual( interval.absoluteInterval, - try Timecode(TCC(h: 1), at: ._24) + try Timecode(.components(h: 1), at: .fps24) ) XCTAssertFalse(interval.isNegative) } func testStaticConstructors_Negative() throws { - let interval: TimecodeInterval = .negative( - try Timecode(TCC(h: 1), at: ._24) + let interval: TimecodeInterval = try .negative( + Timecode(.components(h: 1), at: .fps24) ) XCTAssertEqual( interval.absoluteInterval, - try Timecode(TCC(h: 1), at: ._24) + try Timecode(.components(h: 1), at: .fps24) ) XCTAssertTrue(interval.isNegative) } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeInterval/TimecodeInterval Unary Operators Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeInterval/TimecodeInterval Unary Operators Tests.swift index 360df979..22321dfd 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeInterval/TimecodeInterval Unary Operators Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeInterval/TimecodeInterval Unary Operators Tests.swift @@ -1,29 +1,29 @@ // // TimecodeInterval Unary Operators Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class TimecodeInterval_UnaryOperators_Tests: XCTestCase { override func setUp() { } override func tearDown() { } func testNegative() throws { - let interval = try -Timecode(TCC(m: 1), at: ._24) + let interval = try -Timecode(.components(m: 1), at: .fps24) - XCTAssertEqual(interval.absoluteInterval, try Timecode(TCC(m: 1), at: ._24)) + XCTAssertEqual(interval.absoluteInterval, try Timecode(.components(m: 1), at: .fps24)) XCTAssertTrue(interval.isNegative) } func testPositive() throws { - let interval = try +Timecode(TCC(m: 1), at: ._24) + let interval = try +Timecode(.components(m: 1), at: .fps24) - XCTAssertEqual(interval.absoluteInterval, try Timecode(TCC(m: 1), at: ._24)) + XCTAssertEqual(interval.absoluteInterval, try Timecode(.components(m: 1), at: .fps24)) XCTAssertFalse(interval.isNegative) } } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeTransformer/TimecodeTransformer Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeTransformer/TimecodeTransformer Tests.swift index 9e548e48..eb89dc49 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeTransformer/TimecodeTransformer Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/TimecodeTransformer/TimecodeTransformer Tests.swift @@ -1,13 +1,13 @@ // // TimecodeTransformer Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class TimecodeTransformer_Tests: XCTestCase { override func setUp() { } @@ -19,15 +19,15 @@ class TimecodeTransformer_Tests: XCTestCase { let transformer = TimecodeTransformer(.none) XCTAssertEqual( - transformer.transform(try Timecode(TCC(h: 1), at: ._24)), - try Timecode(TCC(h: 01, m: 00, s: 00, f: 00), at: ._24) + try transformer.transform(Timecode(.components(h: 1), at: .fps24)), + try Timecode(.components(h: 01, m: 00, s: 00, f: 00), at: .fps24) ) } func testOffset() throws { // .offset() - let deltaTC = try Timecode(TCC(m: 1), at: ._24) + let deltaTC = try Timecode(.components(m: 1), at: .fps24) let delta = TimecodeInterval(deltaTC, .plus) var transformer = TimecodeTransformer(.offset(by: delta)) @@ -37,8 +37,8 @@ class TimecodeTransformer_Tests: XCTestCase { transformer.enabled = false XCTAssertEqual( - transformer.transform(try Timecode(TCC(h: 1), at: ._24)), - try Timecode(TCC(h: 01, m: 00, s: 00, f: 00), at: ._24) + try transformer.transform(Timecode(.components(h: 1), at: .fps24)), + try Timecode(.components(h: 01, m: 00, s: 00, f: 00), at: .fps24) ) // enabled @@ -46,8 +46,8 @@ class TimecodeTransformer_Tests: XCTestCase { transformer.enabled = true XCTAssertEqual( - transformer.transform(try Timecode(TCC(h: 1), at: ._24)), - try Timecode(TCC(h: 01, m: 01, s: 00, f: 00), at: ._24) + try transformer.transform(Timecode(.components(h: 1), at: .fps24)), + try Timecode(.components(h: 01, m: 01, s: 00, f: 00), at: .fps24) ) } @@ -55,12 +55,12 @@ class TimecodeTransformer_Tests: XCTestCase { // .custom() let transformer = TimecodeTransformer(.custom { // inputTC -> Timecode in - $0.adding(wrapping: TCC(m: 1)) + $0.adding(Timecode.Components(m: 1), by: .wrapping) }) XCTAssertEqual( - transformer.transform(try Timecode(TCC(h: 1), at: ._24)), - try Timecode(TCC(h: 01, m: 01, s: 00, f: 00), at: ._24) + try transformer.transform(Timecode(.components(h: 1), at: .fps24)), + try Timecode(.components(h: 01, m: 01, s: 00, f: 00), at: .fps24) ) } @@ -69,30 +69,30 @@ class TimecodeTransformer_Tests: XCTestCase { let transformer = TimecodeTransformer([]) XCTAssertEqual( - transformer.transform(try Timecode(TCC(h: 1), at: ._24)), - try Timecode(TCC(h: 01, m: 00, s: 00, f: 00), at: ._24) + try transformer.transform(Timecode(.components(h: 1), at: .fps24)), + try Timecode(.components(h: 01, m: 00, s: 00, f: 00), at: .fps24) ) } func testMultiple_Offsets() throws { // .offset(by:) - let deltaTC1 = try Timecode(TCC(m: 1), at: ._24) + let deltaTC1 = try Timecode(.components(m: 1), at: .fps24) let delta1 = TimecodeInterval(deltaTC1, .plus) - let deltaTC2 = try Timecode(TCC(s: 1), at: ._24) + let deltaTC2 = try Timecode(.components(s: 1), at: .fps24) let delta2 = TimecodeInterval(deltaTC2, .minus) let transformer = TimecodeTransformer([.offset(by: delta1), .offset(by: delta2)]) XCTAssertEqual( - transformer.transform(try Timecode(TCC(h: 1), at: ._24)), - try Timecode(TCC(h: 01, m: 00, s: 59, f: 00), at: ._24) + try transformer.transform(Timecode(.components(h: 1), at: .fps24)), + try Timecode(.components(h: 01, m: 00, s: 59, f: 00), at: .fps24) ) } func testShorthand() throws { - let delta = try TCC().toTimecode(at: ._24) + let delta = Timecode(.zero, at: .fps24) _ = TimecodeTransformer(.offset(by: .positive(delta))) } } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Utilities/Fraction Tests CMTime.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Utilities/Fraction Tests CMTime.swift index e7efa872..8743043d 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Utilities/Fraction Tests CMTime.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Utilities/Fraction Tests CMTime.swift @@ -1,14 +1,14 @@ // -// Fraction CMTime Tests.swift +// Fraction Tests CMTime.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest -@testable import TimecodeKit import CoreMedia +@testable import TimecodeKit +import XCTest class Fraction_CMTime_Tests: XCTestCase { override func setUp() { } @@ -43,14 +43,14 @@ class Fraction_CMTime_Tests: XCTestCase { ) } - func testFraction_toCMTime() { + func testFraction_cmTimeValue() { XCTAssertEqual( - Fraction(3600, 1).toCMTime(), + Fraction(3600, 1).cmTimeValue, CMTime(value: 3600, timescale: 1) ) XCTAssertEqual( - Fraction(-3600, 1).toCMTime(), + Fraction(-3600, 1).cmTimeValue, CMTime(value: -3600, timescale: 1) ) } @@ -67,14 +67,14 @@ class Fraction_CMTime_Tests: XCTestCase { ) } - func testCMTime_toFraction() { + func testCMTime_fractionValue() { XCTAssertEqual( - CMTime(value: 3600, timescale: 1).toFraction(), + CMTime(value: 3600, timescale: 1).fractionValue, Fraction(3600, 1) ) XCTAssertEqual( - CMTime(value: -3600, timescale: 1).toFraction(), + CMTime(value: -3600, timescale: 1).fractionValue, Fraction(-3600, 1) ) } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Utilities/Fraction Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Utilities/Fraction Tests.swift index 42674a90..a2740376 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/Utilities/Fraction Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/Utilities/Fraction Tests.swift @@ -1,13 +1,13 @@ // // Fraction Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class Fraction_Tests: XCTestCase { override func setUp() { } @@ -101,27 +101,27 @@ class Fraction_Tests: XCTestCase { } func testNegativeValues() { - XCTAssertEqual(Fraction(-1,5).doubleValue, -0.2) - XCTAssertEqual(Fraction(1,-5).doubleValue, -0.2) - XCTAssertEqual(Fraction(-1,-5).doubleValue, 0.2) + XCTAssertEqual(Fraction(-1, 5).doubleValue, -0.2) + XCTAssertEqual(Fraction(1, -5).doubleValue, -0.2) + XCTAssertEqual(Fraction(-1, -5).doubleValue, 0.2) } func testNormalized() { - XCTAssertEqual(Fraction(-1,5).normalized(), Fraction(-1,5)) - XCTAssertEqual(Fraction(1,-5).normalized(), Fraction(-1,5)) - XCTAssertEqual(Fraction(-1,-5).normalized(), Fraction(1,5)) + XCTAssertEqual(Fraction(-1, 5).normalized(), Fraction(-1, 5)) + XCTAssertEqual(Fraction(1, -5).normalized(), Fraction(-1, 5)) + XCTAssertEqual(Fraction(-1, -5).normalized(), Fraction(1, 5)) } func testEdgeCases() { // test that division by zero crashes don't occur etc. - XCTAssertEqual(Fraction(1,0).doubleValue, .infinity) - XCTAssertEqual(Fraction(0,0).doubleValue.isNaN, true) - XCTAssertEqual(Fraction(0,1).doubleValue, 0.0) + XCTAssertEqual(Fraction(1, 0).doubleValue, .infinity) + XCTAssertEqual(Fraction(0, 0).doubleValue.isNaN, true) + XCTAssertEqual(Fraction(0, 1).doubleValue, 0.0) - XCTAssertEqual(Fraction(-1,0).doubleValue, -.infinity) - XCTAssertEqual(Fraction(0,0).doubleValue.isNaN, true) - XCTAssertEqual(Fraction(0,-1).doubleValue, 0.0) + XCTAssertEqual(Fraction(-1, 0).doubleValue, -.infinity) + XCTAssertEqual(Fraction(0, 0).doubleValue.isNaN, true) + XCTAssertEqual(Fraction(0, -1).doubleValue, 0.0) } } diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/VideoFrameRate/VideoFrameRate Conversions Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/VideoFrameRate/VideoFrameRate Conversions Tests.swift index aa8a2fa6..a211b511 100644 --- a/Tests/TimecodeKit-Unit-Tests/Unit Tests/VideoFrameRate/VideoFrameRate Conversions Tests.swift +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/VideoFrameRate/VideoFrameRate Conversions Tests.swift @@ -1,13 +1,13 @@ // // VideoFrameRate Conversions Tests.swift // TimecodeKit • https://github.com/orchetect/TimecodeKit -// © 2022 Steffan Andrews • Licensed under MIT License +// © 2020-2023 Steffan Andrews • Licensed under MIT License // #if shouldTestCurrentPlatform -import XCTest @testable import TimecodeKit +import XCTest class VideoFrameRate_Conversions_Tests: XCTestCase { override func setUp() { } @@ -15,130 +15,132 @@ class VideoFrameRate_Conversions_Tests: XCTestCase { func testInit_raw_nonStrict() { XCTAssertEqual(VideoFrameRate(fps: 23, strict: false), nil) - XCTAssertEqual(VideoFrameRate(fps: 23.9, strict: false), ._23_98p) - XCTAssertEqual(VideoFrameRate(fps: 23.98, strict: false), ._23_98p) - XCTAssertEqual(VideoFrameRate(fps: 23.976, strict: false), ._23_98p) - XCTAssertEqual(VideoFrameRate(fps: 23.976023976, strict: false), ._23_98p) + XCTAssertEqual(VideoFrameRate(fps: 23.9, strict: false), .fps23_98p) + XCTAssertEqual(VideoFrameRate(fps: 23.98, strict: false), .fps23_98p) + XCTAssertEqual(VideoFrameRate(fps: 23.976, strict: false), .fps23_98p) + XCTAssertEqual(VideoFrameRate(fps: 23.976023976, strict: false), .fps23_98p) - XCTAssertEqual(VideoFrameRate(fps: 24, strict: false), ._24p) + XCTAssertEqual(VideoFrameRate(fps: 24, strict: false), .fps24p) - XCTAssertEqual(VideoFrameRate(fps: 25, strict: false), ._25p) - XCTAssertEqual(VideoFrameRate(fps: 25, interlaced: true, strict: false), ._25i) + XCTAssertEqual(VideoFrameRate(fps: 25, strict: false), .fps25p) + XCTAssertEqual(VideoFrameRate(fps: 25, interlaced: true, strict: false), .fps25i) + XCTAssertEqual(VideoFrameRate(fps: 24.997648, interlaced: false, strict: false), .fps25p) // VFR-like XCTAssertEqual(VideoFrameRate(fps: 29, strict: false), nil) - XCTAssertEqual(VideoFrameRate(fps: 29.9, strict: false), ._29_97p) - XCTAssertEqual(VideoFrameRate(fps: 29.97, strict: false), ._29_97p) - XCTAssertEqual(VideoFrameRate(fps: 29.97002997, strict: false), ._29_97p) + XCTAssertEqual(VideoFrameRate(fps: 29.9, strict: false), .fps29_97p) + XCTAssertEqual(VideoFrameRate(fps: 29.97, strict: false), .fps29_97p) + XCTAssertEqual(VideoFrameRate(fps: 29.97002997, strict: false), .fps29_97p) XCTAssertEqual(VideoFrameRate(fps: 29, interlaced: true, strict: false), nil) - XCTAssertEqual(VideoFrameRate(fps: 29.9, interlaced: true, strict: false), ._29_97i) - XCTAssertEqual(VideoFrameRate(fps: 29.97, interlaced: true, strict: false), ._29_97i) - XCTAssertEqual(VideoFrameRate(fps: 29.97002997, interlaced: true, strict: false), ._29_97i) + XCTAssertEqual(VideoFrameRate(fps: 29.9, interlaced: true, strict: false), .fps29_97i) + XCTAssertEqual(VideoFrameRate(fps: 29.97, interlaced: true, strict: false), .fps29_97i) + XCTAssertEqual(VideoFrameRate(fps: 29.97002997, interlaced: true, strict: false), .fps29_97i) - XCTAssertEqual(VideoFrameRate(fps: 30, strict: false), ._30p) + XCTAssertEqual(VideoFrameRate(fps: 30, strict: false), .fps30p) XCTAssertEqual(VideoFrameRate(fps: 47, strict: false), nil) - XCTAssertEqual(VideoFrameRate(fps: 47.9, strict: false), ._47_95p) - XCTAssertEqual(VideoFrameRate(fps: 47.95, strict: false), ._47_95p) - XCTAssertEqual(VideoFrameRate(fps: 47.952, strict: false), ._47_95p) - XCTAssertEqual(VideoFrameRate(fps: 47.952047952, strict: false), ._47_95p) + XCTAssertEqual(VideoFrameRate(fps: 47.9, strict: false), .fps47_95p) + XCTAssertEqual(VideoFrameRate(fps: 47.95, strict: false), .fps47_95p) + XCTAssertEqual(VideoFrameRate(fps: 47.952, strict: false), .fps47_95p) + XCTAssertEqual(VideoFrameRate(fps: 47.952047952, strict: false), .fps47_95p) - XCTAssertEqual(VideoFrameRate(fps: 48, strict: false), ._48p) + XCTAssertEqual(VideoFrameRate(fps: 48, strict: false), .fps48p) - XCTAssertEqual(VideoFrameRate(fps: 50, strict: false), ._50p) - XCTAssertEqual(VideoFrameRate(fps: 50, interlaced: true, strict: false), ._50i) + XCTAssertEqual(VideoFrameRate(fps: 50, strict: false), .fps50p) + XCTAssertEqual(VideoFrameRate(fps: 50, interlaced: true, strict: false), .fps50i) XCTAssertEqual(VideoFrameRate(fps: 59, strict: false), nil) - XCTAssertEqual(VideoFrameRate(fps: 59.9, strict: false), ._59_94p) - XCTAssertEqual(VideoFrameRate(fps: 59.94, strict: false), ._59_94p) - XCTAssertEqual(VideoFrameRate(fps: 59.9400599401, strict: false), ._59_94p) + XCTAssertEqual(VideoFrameRate(fps: 59.9, strict: false), .fps59_94p) + XCTAssertEqual(VideoFrameRate(fps: 59.94, strict: false), .fps59_94p) + XCTAssertEqual(VideoFrameRate(fps: 59.9400599401, strict: false), .fps59_94p) XCTAssertEqual(VideoFrameRate(fps: 59, interlaced: true, strict: false), nil) - XCTAssertEqual(VideoFrameRate(fps: 59.9, interlaced: true, strict: false), ._59_94i) - XCTAssertEqual(VideoFrameRate(fps: 59.94, interlaced: true, strict: false), ._59_94i) - XCTAssertEqual(VideoFrameRate(fps: 59.9400599401, interlaced: true, strict: false), ._59_94i) + XCTAssertEqual(VideoFrameRate(fps: 59.9, interlaced: true, strict: false), .fps59_94i) + XCTAssertEqual(VideoFrameRate(fps: 59.94, interlaced: true, strict: false), .fps59_94i) + XCTAssertEqual(VideoFrameRate(fps: 59.9400599401, interlaced: true, strict: false), .fps59_94i) - XCTAssertEqual(VideoFrameRate(fps: 60, strict: false), ._60p) + XCTAssertEqual(VideoFrameRate(fps: 60, strict: false), .fps60p) XCTAssertEqual(VideoFrameRate(fps: 95, strict: false), nil) - XCTAssertEqual(VideoFrameRate(fps: 95.9, strict: false), ._95_9p) - XCTAssertEqual(VideoFrameRate(fps: 95.904, strict: false), ._95_9p) - XCTAssertEqual(VideoFrameRate(fps: 95.9040959041, strict: false), ._95_9p) + XCTAssertEqual(VideoFrameRate(fps: 95.9, strict: false), .fps95_9p) + XCTAssertEqual(VideoFrameRate(fps: 95.904, strict: false), .fps95_9p) + XCTAssertEqual(VideoFrameRate(fps: 95.9040959041, strict: false), .fps95_9p) - XCTAssertEqual(VideoFrameRate(fps: 96, strict: false), ._96p) + XCTAssertEqual(VideoFrameRate(fps: 96, strict: false), .fps96p) - XCTAssertEqual(VideoFrameRate(fps: 100, strict: false), ._100p) + XCTAssertEqual(VideoFrameRate(fps: 100, strict: false), .fps100p) XCTAssertEqual(VideoFrameRate(fps: 119, strict: false), nil) - XCTAssertEqual(VideoFrameRate(fps: 119.8, strict: false), ._119_88p) - XCTAssertEqual(VideoFrameRate(fps: 119.88, strict: false), ._119_88p) - XCTAssertEqual(VideoFrameRate(fps: 119.8801198801, strict: false), ._119_88p) + XCTAssertEqual(VideoFrameRate(fps: 119.8, strict: false), .fps119_88p) + XCTAssertEqual(VideoFrameRate(fps: 119.88, strict: false), .fps119_88p) + XCTAssertEqual(VideoFrameRate(fps: 119.8801198801, strict: false), .fps119_88p) - XCTAssertEqual(VideoFrameRate(fps: 120, strict: false), ._120p) + XCTAssertEqual(VideoFrameRate(fps: 120, strict: false), .fps120p) } func testInit_raw_strict() { XCTAssertEqual(VideoFrameRate(fps: 23, strict: true), nil) XCTAssertEqual(VideoFrameRate(fps: 23.9, strict: true), nil) XCTAssertEqual(VideoFrameRate(fps: 23.98, strict: true), nil) - XCTAssertEqual(VideoFrameRate(fps: 23.976, strict: true), ._23_98p) - XCTAssertEqual(VideoFrameRate(fps: 23.976023976, strict: true), ._23_98p) + XCTAssertEqual(VideoFrameRate(fps: 23.976, strict: true), .fps23_98p) + XCTAssertEqual(VideoFrameRate(fps: 23.976023976, strict: true), .fps23_98p) - XCTAssertEqual(VideoFrameRate(fps: 24, strict: true), ._24p) + XCTAssertEqual(VideoFrameRate(fps: 24, strict: true), .fps24p) - XCTAssertEqual(VideoFrameRate(fps: 25, strict: true), ._25p) - XCTAssertEqual(VideoFrameRate(fps: 25, interlaced: true, strict: true), ._25i) + XCTAssertEqual(VideoFrameRate(fps: 25, strict: true), .fps25p) + XCTAssertEqual(VideoFrameRate(fps: 25, interlaced: true, strict: true), .fps25i) + XCTAssertEqual(VideoFrameRate(fps: 24.997648, interlaced: false, strict: true), nil) // VFR-like XCTAssertEqual(VideoFrameRate(fps: 29, strict: true), nil) XCTAssertEqual(VideoFrameRate(fps: 29.9, strict: true), nil) - XCTAssertEqual(VideoFrameRate(fps: 29.97, strict: true), ._29_97p) - XCTAssertEqual(VideoFrameRate(fps: 29.97002997, strict: true), ._29_97p) + XCTAssertEqual(VideoFrameRate(fps: 29.97, strict: true), .fps29_97p) + XCTAssertEqual(VideoFrameRate(fps: 29.97002997, strict: true), .fps29_97p) XCTAssertEqual(VideoFrameRate(fps: 29, interlaced: true, strict: true), nil) XCTAssertEqual(VideoFrameRate(fps: 29.9, interlaced: true, strict: true), nil) - XCTAssertEqual(VideoFrameRate(fps: 29.97, interlaced: true, strict: true), ._29_97i) - XCTAssertEqual(VideoFrameRate(fps: 29.97002997, interlaced: true, strict: true), ._29_97i) + XCTAssertEqual(VideoFrameRate(fps: 29.97, interlaced: true, strict: true), .fps29_97i) + XCTAssertEqual(VideoFrameRate(fps: 29.97002997, interlaced: true, strict: true), .fps29_97i) - XCTAssertEqual(VideoFrameRate(fps: 30, strict: true), ._30p) + XCTAssertEqual(VideoFrameRate(fps: 30, strict: true), .fps30p) XCTAssertEqual(VideoFrameRate(fps: 47, strict: true), nil) XCTAssertEqual(VideoFrameRate(fps: 47.9, strict: true), nil) XCTAssertEqual(VideoFrameRate(fps: 47.95, strict: true), nil) - XCTAssertEqual(VideoFrameRate(fps: 47.952, strict: true), ._47_95p) - XCTAssertEqual(VideoFrameRate(fps: 47.952047952, strict: true), ._47_95p) + XCTAssertEqual(VideoFrameRate(fps: 47.952, strict: true), .fps47_95p) + XCTAssertEqual(VideoFrameRate(fps: 47.952047952, strict: true), .fps47_95p) - XCTAssertEqual(VideoFrameRate(fps: 48, strict: true), ._48p) + XCTAssertEqual(VideoFrameRate(fps: 48, strict: true), .fps48p) - XCTAssertEqual(VideoFrameRate(fps: 50, strict: true), ._50p) - XCTAssertEqual(VideoFrameRate(fps: 50, interlaced: true, strict: true), ._50i) + XCTAssertEqual(VideoFrameRate(fps: 50, strict: true), .fps50p) + XCTAssertEqual(VideoFrameRate(fps: 50, interlaced: true, strict: true), .fps50i) XCTAssertEqual(VideoFrameRate(fps: 59, strict: true), nil) XCTAssertEqual(VideoFrameRate(fps: 59.9, strict: true), nil) - XCTAssertEqual(VideoFrameRate(fps: 59.94, strict: true), ._59_94p) - XCTAssertEqual(VideoFrameRate(fps: 59.9400599401, strict: true), ._59_94p) + XCTAssertEqual(VideoFrameRate(fps: 59.94, strict: true), .fps59_94p) + XCTAssertEqual(VideoFrameRate(fps: 59.9400599401, strict: true), .fps59_94p) XCTAssertEqual(VideoFrameRate(fps: 59, interlaced: true, strict: true), nil) XCTAssertEqual(VideoFrameRate(fps: 59.9, interlaced: true, strict: true), nil) - XCTAssertEqual(VideoFrameRate(fps: 59.94, interlaced: true, strict: true), ._59_94i) - XCTAssertEqual(VideoFrameRate(fps: 59.9400599401, interlaced: true, strict: true), ._59_94i) + XCTAssertEqual(VideoFrameRate(fps: 59.94, interlaced: true, strict: true), .fps59_94i) + XCTAssertEqual(VideoFrameRate(fps: 59.9400599401, interlaced: true, strict: true), .fps59_94i) - XCTAssertEqual(VideoFrameRate(fps: 60, strict: true), ._60p) + XCTAssertEqual(VideoFrameRate(fps: 60, strict: true), .fps60p) XCTAssertEqual(VideoFrameRate(fps: 95, strict: true), nil) XCTAssertEqual(VideoFrameRate(fps: 95.9, strict: true), nil) - XCTAssertEqual(VideoFrameRate(fps: 95.904, strict: true), ._95_9p) - XCTAssertEqual(VideoFrameRate(fps: 95.9040959041, strict: true), ._95_9p) + XCTAssertEqual(VideoFrameRate(fps: 95.904, strict: true), .fps95_9p) + XCTAssertEqual(VideoFrameRate(fps: 95.9040959041, strict: true), .fps95_9p) - XCTAssertEqual(VideoFrameRate(fps: 96, strict: true), ._96p) + XCTAssertEqual(VideoFrameRate(fps: 96, strict: true), .fps96p) - XCTAssertEqual(VideoFrameRate(fps: 100, strict: true), ._100p) + XCTAssertEqual(VideoFrameRate(fps: 100, strict: true), .fps100p) XCTAssertEqual(VideoFrameRate(fps: 119, strict: true), nil) XCTAssertEqual(VideoFrameRate(fps: 119.8, strict: true), nil) - XCTAssertEqual(VideoFrameRate(fps: 119.88, strict: true), ._119_88p) - XCTAssertEqual(VideoFrameRate(fps: 119.8801198801, strict: true), ._119_88p) + XCTAssertEqual(VideoFrameRate(fps: 119.88, strict: true), .fps119_88p) + XCTAssertEqual(VideoFrameRate(fps: 119.8801198801, strict: true), .fps119_88p) - XCTAssertEqual(VideoFrameRate(fps: 120, strict: true), ._120p) + XCTAssertEqual(VideoFrameRate(fps: 120, strict: true), .fps120p) } func testInit_raw_invalid_nonStrict() { @@ -220,41 +222,41 @@ class VideoFrameRate_Conversions_Tests: XCTestCase { // 24p XCTAssertEqual( VideoFrameRate(rate: Fraction(24, 1)), - ._24p + .fps24p ) XCTAssertEqual( VideoFrameRate(rate: Fraction(240, 10)), - ._24p + .fps24p ) // 25p XCTAssertEqual( VideoFrameRate(rate: Fraction(25, 1), interlaced: false), - ._25p + .fps25p ) XCTAssertEqual( VideoFrameRate(rate: Fraction(250, 10), interlaced: false), - ._25p + .fps25p ) // 25i XCTAssertEqual( VideoFrameRate(rate: Fraction(25, 1), interlaced: true), - ._25i + .fps25i ) XCTAssertEqual( VideoFrameRate(rate: Fraction(250, 10), interlaced: true), - ._25i + .fps25i ) // 30p XCTAssertEqual( VideoFrameRate(rate: Fraction(30, 1)), - ._30p + .fps30p ) XCTAssertEqual( VideoFrameRate(rate: Fraction(300, 10)), - ._30p + .fps30p ) // edge cases @@ -268,7 +270,7 @@ class VideoFrameRate_Conversions_Tests: XCTestCase { XCTAssertNil(VideoFrameRate(rate: Fraction(0, -1))) XCTAssertNil(VideoFrameRate(rate: Fraction(-1, 0))) XCTAssertNil(VideoFrameRate(rate: Fraction(-1, -1))) - XCTAssertEqual(VideoFrameRate(rate: Fraction(-30, -1)), ._30p) + XCTAssertEqual(VideoFrameRate(rate: Fraction(-30, -1)), .fps30p) XCTAssertNil(VideoFrameRate(rate: Fraction(-30, 1))) XCTAssertNil(VideoFrameRate(rate: Fraction(30, -1))) @@ -293,41 +295,41 @@ class VideoFrameRate_Conversions_Tests: XCTestCase { // 24p XCTAssertEqual( VideoFrameRate(frameDuration: Fraction(1, 24)), - ._24p + .fps24p ) XCTAssertEqual( VideoFrameRate(frameDuration: Fraction(10, 240)), - ._24p + .fps24p ) // 25p XCTAssertEqual( VideoFrameRate(frameDuration: Fraction(1, 25), interlaced: false), - ._25p + .fps25p ) XCTAssertEqual( VideoFrameRate(frameDuration: Fraction(10, 250), interlaced: false), - ._25p + .fps25p ) // 25i XCTAssertEqual( VideoFrameRate(frameDuration: Fraction(1, 25), interlaced: true), - ._25i + .fps25i ) XCTAssertEqual( VideoFrameRate(frameDuration: Fraction(10, 250), interlaced: true), - ._25i + .fps25i ) // 30p XCTAssertEqual( VideoFrameRate(frameDuration: Fraction(1, 30)), - ._30p + .fps30p ) XCTAssertEqual( VideoFrameRate(frameDuration: Fraction(10, 300)), - ._30p + .fps30p ) // edge cases @@ -341,7 +343,7 @@ class VideoFrameRate_Conversions_Tests: XCTestCase { XCTAssertNil(VideoFrameRate(frameDuration: Fraction(-1, 0))) XCTAssertNil(VideoFrameRate(frameDuration: Fraction(0, -1))) XCTAssertNil(VideoFrameRate(frameDuration: Fraction(-1, -1))) - XCTAssertEqual(VideoFrameRate(frameDuration: Fraction(-1, -30)), ._30p) + XCTAssertEqual(VideoFrameRate(frameDuration: Fraction(-1, -30)), .fps30p) XCTAssertNil(VideoFrameRate(frameDuration: Fraction(1, -30))) XCTAssertNil(VideoFrameRate(frameDuration: Fraction(-1, 30))) @@ -360,14 +362,14 @@ class VideoFrameRate_Conversions_CMTime_Tests: XCTestCase { rate: CMTime(value: 30000, timescale: 1001), interlaced: false ), - ._29_97p + .fps29_97p ) XCTAssertEqual( VideoFrameRate( rate: CMTime(value: 30000, timescale: 1001), interlaced: true ), - ._29_97i + .fps29_97i ) } @@ -377,28 +379,30 @@ class VideoFrameRate_Conversions_CMTime_Tests: XCTestCase { frameDuration: CMTime(value: 1001, timescale: 30000), interlaced: false ), - ._29_97p + .fps29_97p ) XCTAssertEqual( VideoFrameRate( frameDuration: CMTime(value: 1001, timescale: 30000), interlaced: true ), - ._29_97i + .fps29_97i ) } func testRateCMTime() throws { XCTAssertEqual( - VideoFrameRate._29_97p.rateCMTime, + VideoFrameRate.fps29_97p.rateCMTime, CMTime(value: 30000, timescale: 1001) ) } func testFrameDurationCMTime() throws { // spot-check - XCTAssertEqual(VideoFrameRate._29_97p.frameDurationCMTime, - CMTime(value: 1001, timescale: 30000)) + XCTAssertEqual( + VideoFrameRate.fps29_97p.frameDurationCMTime, + CMTime(value: 1001, timescale: 30000) + ) // ensure the CMTime instance returns correct 1 frame duration in seconds. // due to floating-point dithering, it tends to be accurate up to @@ -407,8 +411,8 @@ class VideoFrameRate_Conversions_CMTime_Tests: XCTestCase { try VideoFrameRate.allCases.forEach { let cmTimeSeconds = $0.frameDurationCMTime.seconds - let oneFrameDuration = try TCC(f: 1) - .toTimecode(at: $0.timecodeFrameRate(drop: false)!) + let oneFrameDuration = try Timecode.Components(f: 1) + .timecode(at: $0.timecodeFrameRate(drop: false)!) .realTimeValue XCTAssertEqual( diff --git a/Tests/TimecodeKit-Unit-Tests/Unit Tests/VideoFrameRate/VideoFrameRate String Extensions Tests.swift b/Tests/TimecodeKit-Unit-Tests/Unit Tests/VideoFrameRate/VideoFrameRate String Extensions Tests.swift new file mode 100644 index 00000000..2060eb3b --- /dev/null +++ b/Tests/TimecodeKit-Unit-Tests/Unit Tests/VideoFrameRate/VideoFrameRate String Extensions Tests.swift @@ -0,0 +1,25 @@ +// +// VideoFrameRate String Extensions Tests.swift +// TimecodeKit • https://github.com/orchetect/TimecodeKit +// © 2020-2023 Steffan Andrews • Licensed under MIT License +// + +#if shouldTestCurrentPlatform + +@testable import TimecodeKit +import XCTest + +class VideoFrameRate_StringExtensions_Tests: XCTestCase { + func testString_videoFrameRate() { + // do a spot-check to ensure this functions as expected + + XCTAssertEqual("24p".videoFrameRate, .fps24p) + XCTAssertEqual("23.98p".videoFrameRate, .fps23_98p) + XCTAssertEqual("29.97p".videoFrameRate, .fps29_97p) + + XCTAssertNil("".videoFrameRate) + XCTAssertNil(" ".videoFrameRate) + XCTAssertNil("BogusString".videoFrameRate) + } +} +#endif