226 lines
7.3 KiB
Swift
226 lines
7.3 KiB
Swift
//
|
|
// Timecode.swift
|
|
// Live Link VCAM
|
|
//
|
|
// Created by Brian Smith on 12/26/19.
|
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
//
|
|
|
|
// FYI much of this is lifted from the Live Link Face app
|
|
|
|
import Foundation
|
|
import CoreMedia
|
|
import Tentacle
|
|
import Kronos
|
|
|
|
enum TimecodeSource : Int{
|
|
case none = 0
|
|
case systemTime = 1
|
|
case ntp = 2
|
|
case tentacleSync = 3
|
|
case unknown = 9999
|
|
}
|
|
|
|
func clockFrequency() -> Double {
|
|
|
|
var info = mach_timebase_info()
|
|
guard mach_timebase_info(&info) == KERN_SUCCESS else { return 0 }
|
|
|
|
return (Double(info.denom) / Double(info.numer)) * 1000000000.0
|
|
}
|
|
|
|
@objc class Timecode : NSObject {
|
|
|
|
static let secondsToMach = clockFrequency()
|
|
|
|
static var targetFrameRate = UInt8(60)
|
|
var tentacleTimecode : TentacleTimecode?
|
|
|
|
var source : TimecodeSource!
|
|
var current = true
|
|
var valid = false
|
|
|
|
@objc public var hours : Int32 = 0
|
|
@objc public var minutes : Int32 = 0
|
|
@objc public var seconds : Int32 = 0
|
|
@objc public var frames : Int32 = 0
|
|
@objc public var fraction : Float = 0
|
|
@objc public var dropFrame : Bool = false
|
|
|
|
class func sourceToString(_ source : TimecodeSource) -> String {
|
|
|
|
switch source {
|
|
case .systemTime:
|
|
return NSLocalizedString("timecode-source-system", value: "System Timer", comment: "The internal device timer will be used as the timecode source")
|
|
case .ntp:
|
|
return NSLocalizedString("timecode-source-ntp", value: "NTP", comment: "An NTP (network time protocol) pool will be used as the timecode source")
|
|
case .tentacleSync:
|
|
return NSLocalizedString("timecode-source-tentacle", value: "Tentacle Sync", comment: "An Tentacle Sync will be used as the timecode source")
|
|
default:
|
|
return Localized.unknown()
|
|
}
|
|
}
|
|
|
|
class func sourceIsSystemTime() -> Bool {
|
|
return AppSettings.shared.timecodeSourceEnum() == .systemTime;
|
|
}
|
|
|
|
class func sourceIsTentacle() -> Bool {
|
|
return AppSettings.shared.timecodeSourceEnum() == .tentacleSync;
|
|
}
|
|
|
|
class func sourceIsNTP() -> Bool {
|
|
return AppSettings.shared.timecodeSourceEnum() == .ntp;
|
|
}
|
|
|
|
|
|
class func create() -> Timecode {
|
|
|
|
switch AppSettings.shared.timecodeSourceEnum() {
|
|
|
|
case .tentacleSync:
|
|
return Timecode(tentacleDevice: Tentacle.shared?.activeDevice, atTimeInterval: CACurrentMediaTime())
|
|
case .ntp:
|
|
return Timecode(annotatedTime: Clock.annotatedNow)
|
|
|
|
default:
|
|
return Timecode(timeInterval: CACurrentMediaTime())
|
|
}
|
|
}
|
|
|
|
init(fromString str: String) {
|
|
|
|
source = .unknown
|
|
|
|
let components = str.components(separatedBy: ":")
|
|
if components.count == 4 {
|
|
|
|
if let h = Int32(components[0]),
|
|
let m = Int32(components[1]),
|
|
let s = Int32(components[2]) {
|
|
|
|
if let f = Int32(components[3]) {
|
|
frames = f
|
|
valid = true
|
|
} else {
|
|
let frameAndFraction = components[3].components(separatedBy: ".")
|
|
if frameAndFraction.count == 2 {
|
|
if let f = Int32(frameAndFraction[0]),
|
|
let frac = Float(frameAndFraction[1]) {
|
|
frames = f
|
|
fraction = frac / 1000
|
|
valid = true
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if valid {
|
|
hours = h
|
|
minutes = m
|
|
seconds = s
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public init(annotatedTime: AnnotatedTime?) {
|
|
|
|
source = .ntp
|
|
|
|
if let an = annotatedTime {
|
|
valid = true
|
|
|
|
let components = Calendar.current.dateComponents([.hour, .minute, .second, .nanosecond], from: an.date)
|
|
|
|
hours = Int32(components.hour ?? 0)
|
|
minutes = Int32(components.minute ?? 0)
|
|
seconds = Int32(components.second ?? 0)
|
|
|
|
if let usec = components.nanosecond {
|
|
let frameWithFraction = Double(usec)/1000000000.0 * Double(Timecode.targetFrameRate)
|
|
frames = Int32(frameWithFraction)
|
|
fraction = Float(frameWithFraction.truncatingRemainder(dividingBy: 1))
|
|
}
|
|
} else {
|
|
current = false
|
|
}
|
|
}
|
|
|
|
public init(timeInterval : CFTimeInterval, source timecodeSource : TimecodeSource) {
|
|
|
|
source = timecodeSource
|
|
valid = true
|
|
|
|
hours = Int32(timeInterval / 3600.0) % 24
|
|
minutes = Int32(UInt64(timeInterval / 60.0) % 60)
|
|
seconds = Int32(UInt64(timeInterval) % 60)
|
|
|
|
let frameWithFraction = Float(timeInterval - floor(timeInterval)) * Float(Timecode.targetFrameRate)
|
|
frames = Int32(frameWithFraction)
|
|
fraction = frameWithFraction.truncatingRemainder(dividingBy: 1)
|
|
}
|
|
|
|
convenience init(timeInterval: CFTimeInterval) {
|
|
|
|
self.init(timeInterval:timeInterval, source : .systemTime)
|
|
}
|
|
|
|
convenience init(tentacleDevice device: TentacleDevice?, atTimeInterval timeInterval: Double) {
|
|
|
|
if device != nil {
|
|
|
|
var timecode = device!.timecode
|
|
var advertisement = device!.advertisement
|
|
let seconds = TentacleTimecodeSecondsAtTimestamp(&timecode, TentacleAdvertisementGetFrameRate(&advertisement), device!.advertisement.dropFrame, timeInterval)
|
|
self.init(timeInterval:seconds)
|
|
tentacleTimecode = device!.timecode
|
|
dropFrame = device!.advertisement.dropFrame
|
|
current = true// !TentacleDeviceIsDisappeared(&device!, timeInterval)
|
|
|
|
} else {
|
|
self.init(timeInterval: 0)
|
|
valid = false
|
|
current = false
|
|
}
|
|
|
|
source = .tentacleSync
|
|
}
|
|
|
|
func toString(includeFractional : Bool) -> String {
|
|
|
|
if valid {
|
|
if includeFractional {
|
|
return String(format:"%02d:%02d:%02d:%02d.%03d", hours, minutes, seconds, frames, Int(fraction * 1000))
|
|
} else {
|
|
return String(format:"%02d:%02d:%02d:%02d", hours, minutes, seconds, frames)
|
|
}
|
|
} else {
|
|
|
|
switch source {
|
|
case .ntp:
|
|
return NSLocalizedString("timecode-error-ntp", value: "NO DATA", comment: "An error string for NTP input")
|
|
case .tentacleSync:
|
|
return NSLocalizedString("timecode-error-tentacle", value: "NO DEVICE", comment: "An error string for tentacle sync input")
|
|
default :
|
|
return NSLocalizedString("timecode-error-system", value: "SYSTEM ERROR", comment: "An error string for system timer input")
|
|
}
|
|
}
|
|
}
|
|
|
|
func toTimeInterval() -> CFTimeInterval {
|
|
|
|
if valid {
|
|
return Double(hours * 3600) + Double(minutes * 60) + Double(seconds) + (Double(frames) + Double(fraction)) / Double(Timecode.targetFrameRate)
|
|
} else {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func offsetBy(_ offset : CFTimeInterval) -> Timecode {
|
|
|
|
return Timecode(timeInterval: toTimeInterval() + offset, source: source)
|
|
}
|
|
}
|