Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions Timecode Display.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
42426FBE2A3866C800D87E93 /* CocoaAsyncSocket.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CocoaAsyncSocket.framework; sourceTree = BUILT_PRODUCTS_DIR; };
42C7EBCE2A38B49B005E7730 /* CocoaAsyncSocket.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CocoaAsyncSocket.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9829C93429B91C3200156461 /* Timecode Display.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Timecode Display.app"; sourceTree = BUILT_PRODUCTS_DIR; };
9829C93729B91C3200156461 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
9829C93929B91C3300156461 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
Expand All @@ -57,11 +59,20 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
85582DA569CC3830D32E1076 /* Pods */ = {
isa = PBXGroup;
children = (
);
path = Pods;
sourceTree = "<group>";
};
9829C92B29B91C3200156461 = {
isa = PBXGroup;
children = (
9829C93629B91C3200156461 /* Timecode Display */,
9829C93529B91C3200156461 /* Products */,
85582DA569CC3830D32E1076 /* Pods */,
D7F912904332858EC72210C1 /* Frameworks */,
);
sourceTree = "<group>";
};
Expand Down Expand Up @@ -89,6 +100,15 @@
path = "Timecode Display";
sourceTree = "<group>";
};
D7F912904332858EC72210C1 /* Frameworks */ = {
isa = PBXGroup;
children = (
42C7EBCE2A38B49B005E7730 /* CocoaAsyncSocket.framework */,
42426FBE2A3866C800D87E93 /* CocoaAsyncSocket.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -118,7 +138,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1410;
LastUpgradeCheck = 1410;
LastUpgradeCheck = 1430;
TargetAttributes = {
9829C93329B91C3200156461 = {
CreatedOnToolsVersion = 14.1;
Expand Down Expand Up @@ -215,6 +235,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
Expand Down Expand Up @@ -275,6 +296,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
Expand All @@ -301,10 +323,13 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "Timecode Display/Timecode_Display.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 19999;
DEVELOPMENT_TEAM = 7672N4CCJM;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 4KB97DBP5J;
ENABLE_HARDENED_RUNTIME = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
Expand All @@ -318,9 +343,11 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
MARKETING_VERSION = 2.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.figure53.Timecode-Display";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
};
Expand All @@ -332,10 +359,13 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "Timecode Display/Timecode_Display.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 19999;
DEVELOPMENT_TEAM = 7672N4CCJM;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 4KB97DBP5J;
ENABLE_HARDENED_RUNTIME = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
Expand All @@ -349,9 +379,11 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)";
MARKETING_VERSION = 2.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.figure53.Timecode-Display";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
};
Expand Down
10 changes: 5 additions & 5 deletions Timecode Display/Base.lproj/MainMenu.xib
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21701"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
Expand Down Expand Up @@ -123,7 +123,7 @@
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES" utility="YES" HUD="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="1209" y="834" width="355" height="106"/>
<rect key="screenRect" x="0.0" y="0.0" width="1728" height="1079"/>
<rect key="screenRect" x="0.0" y="0.0" width="2056" height="1285"/>
<view key="contentView" id="LEA-bi-bNV">
<rect key="frame" x="0.0" y="0.0" width="355" height="106"/>
<autoresizingMask key="autoresizingMask"/>
Expand All @@ -149,7 +149,7 @@
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES" utility="YES" HUD="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="1087" y="168" width="480" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="1728" height="1079"/>
<rect key="screenRect" x="0.0" y="0.0" width="2056" height="1285"/>
<value key="minSize" type="size" width="240" height="100"/>
<view key="contentView" id="Ar4-u8-qMF">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
Expand All @@ -159,7 +159,7 @@
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<clipView key="contentView" drawsBackground="NO" id="Dce-cQ-jH5">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView wantsLayer="YES" importsGraphics="NO" richText="NO" verticallyResizable="YES" smartInsertDelete="YES" id="4pt-Um-hYX">
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
Expand Down
147 changes: 121 additions & 26 deletions Timecode Display/TimecodeAnalyzer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,136 @@
//

import AppKit
import Foundation
import Network
import SystemConfiguration

class TimecodeAnalyzer: MIDIReceiverDelegate {
private var timeLastFrameReceived: TimeInterval?
private var lastReceivedTimecode: F53Timecode?
private var freewheelTimer: Timer?

func midiReceiver(_ sender: MIDIReceiver, didReceive timecode: F53Timecode) {
let now = NSDate.timeIntervalSinceReferenceDate
let timecodeString = timecode.stringRepresentation
let framerateString = timecode.framerate.speedAgnosticDescription
let appDelegate = NSApp.delegate as! AppDelegate
private let targetIPAddress = "172.20.102.255" // Replace with the IP address of your Art-Net device
private let targetPort: UInt16 = 6454 // Replace with the port number of your Art-Net device
private var udpConnection: NWConnection?

init() {
setupUDPConnection()
}

func setupUDPConnection() {
let udpParameters = NWParameters.udp
udpParameters.allowLocalEndpointReuse = true // Allow multiple connections to use the same port

// Create a broadcast endpoint
let endpoint = NWEndpoint.hostPort(host: .ipv4(IPv4Address(targetIPAddress)!), port: NWEndpoint.Port(rawValue: targetPort)!)

udpConnection = NWConnection(to: endpoint, using: udpParameters)

udpConnection?.stateUpdateHandler = { [weak self] newState in
switch newState {
case .ready:
print("UDP connection established")
case .failed(let error):
print("UDP connection failed: \(error)")
default:
break
}
}

udpConnection?.start(queue: DispatchQueue.main)
}

func midiReceiver(_ sender: MIDIReceiver, didReceive timecode: F53Timecode) {
let now = NSDate.timeIntervalSinceReferenceDate
let timecodeString = timecode.stringRepresentation
let framerateString = timecode.framerate.speedAgnosticDescription
let appDelegate = NSApp.delegate as! AppDelegate

// Test for new starts or discontinuities
if timeLastFrameReceived == nil || now - timeLastFrameReceived! > 0.1 {
// It's been long enough that this is a new start.
appDelegate.appendLog("MTC start at \(timecodeString) - \(framerateString)")
} else {
if let lastReceivedTimecode, timecode.framesFromZero != lastReceivedTimecode.framesFromZero + 1 {
appDelegate.appendLog("MTC discontinuity - from \(lastReceivedTimecode.stringRepresentation) to \(timecodeString)")
// Test for new starts or discontinuities
if timeLastFrameReceived == nil || now - timeLastFrameReceived! > 0.1 {
// It's been long enough that this is a new start.
appDelegate.appendLog("MTC start at \(timecodeString) - \(framerateString)")
} else {
if let lastReceivedTimecode = lastReceivedTimecode, timecode.framesFromZero != lastReceivedTimecode.framesFromZero + 1 {
appDelegate.appendLog("MTC discontinuity - from \(lastReceivedTimecode.stringRepresentation) to \(timecodeString)")
}
}

// Convert MIDI timecode to Art-Net timecode
let artnetTimecode = convertToArtNetTimecode(timecode)

// Send Art-Net timecode via UDP
sendArtNetTimecode(artnetTimecode)

// Show in timecode window
appDelegate.timecodeView.stringValue = timecode.stringRepresentation

// Freewheel
freewheelTimer?.invalidate()
freewheelTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false, block: { timer in
appDelegate.appendLog("MTC stop at \(timecodeString)")
self.timeLastFrameReceived = nil
self.lastReceivedTimecode = nil
appDelegate.reserReceiver()
})

lastReceivedTimecode = timecode
timeLastFrameReceived = now
}

// Show in timecode window
appDelegate.timecodeView.stringValue = timecode.stringRepresentation
private func convertToArtNetTimecode(_ midiTimecode: F53Timecode) -> Data {
// Create an Art-Net timecode packet
var packet: [UInt8] = [
// Art-Net ID
0x41, 0x72, 0x74, 0x2D, 0x4E, 0x65, 0x74, 0x00,
// ArtTimecode OpCode
0x00, 0x97,
// ProtVer
0x00, 0x0E,
// Filler1-2
0x00, 0x00,
// ArtTimeCode
0x00, // Frames
0x00, // Seonds
0x00, // Minutes
0x00, // Hours
0x00, // Type Frame rate (0 = 24fps, 1 = 25fps, 2 = 30fps, 3 = 29.97fps)
]

// Set the hours, minutes, seconds, and frames based on the MIDI timecode
packet[14] = UInt8(midiTimecode.ff)
packet[15] = UInt8(midiTimecode.ss)
packet[16] = UInt8(midiTimecode.mm)
packet[17] = UInt8(midiTimecode.hh)

// Freewheel
freewheelTimer?.invalidate()
freewheelTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false, block: { timer in
appDelegate.appendLog("MTC stop at \(timecodeString)")
self.timeLastFrameReceived = nil
self.lastReceivedTimecode = nil
appDelegate.reserReceiver()
})
// Set the frame rate based on the MIDI timecode frame rate
let frameRate: UInt8
switch midiTimecode.framerate {
case ._24:
frameRate = 0x00
case ._25:
frameRate = 0x01
case ._30nd:
frameRate = 0x02
case ._2997nd:
frameRate = 0x03
default:
frameRate = 0x00
}
packet[18] = frameRate

lastReceivedTimecode = timecode
timeLastFrameReceived = now
// Convert the packet to Data
let packetData = Data(packet)
return packetData
}

private func sendArtNetTimecode(_ timecode: Data) {
udpConnection?.send(content: timecode, completion: .contentProcessed { error in
if let error = error {
print("Failed to send Art-Net timecode: \(error)")
} else {
print("Art-Net timecode sent successfully")
}
})
}
}
}
12 changes: 8 additions & 4 deletions Timecode Display/Timecode_Display.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
Loading