Last active
June 30, 2018 20:43
-
-
Save iluvcapra/eb622baf9ea67c7d00d4ed81a5295761 to your computer and use it in GitHub Desktop.
CMX3600 EDL parser, also auto-detects file32 form
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Cmx3600.swift | |
// EDL Scene List | |
// | |
// Created by Jamie Hardt on 6/29/18. | |
// Copyright © 2018 Jamie Hardt. All rights reserved. | |
// | |
import Foundation | |
/* | |
http://xmil.biz/EDL-X/CMX3600.pdf | |
*/ | |
extension String { | |
var isStrictlyInteger : Bool { | |
get { | |
let numbers = CharacterSet(charactersIn: "0123456789") | |
return CharacterSet(charactersIn: String( self ) ).isStrictSubset(of: numbers) | |
} | |
} | |
func collimate(columnWidths : [Int]) -> [String] { | |
var offset = 0 | |
var retVal : [String] = [] | |
for width in columnWidths { | |
retVal += [ String(self[offset...].prefix(width)) ] | |
offset += width | |
} | |
return retVal | |
} | |
} | |
class CMX3600Parser { | |
enum Statement { | |
case Title(String) | |
case FrameCountMode(dropFrame : Bool) | |
case StandardForm(eventNumber : Int, | |
source : String, | |
channels : ChannelSelection, | |
eventType : EventType, | |
sourceIn: String, sourceOut : String, | |
recordIn : String, recordOut : String) | |
case AudioNote(channelThree : Bool, channelFour : Bool) | |
case Unrecognized(String) | |
} | |
enum ChannelSelection { | |
case None // NONE | |
case Video // V | |
case AudioOneOnly // A | |
case AudioTwoOnly // A2 | |
case AudioOneAndTwo // AA | |
case AudioOneAndVideo // B | |
case AudioTwoAndVideo // A2/V | |
case AudioOneAndTwoAndVideo // AA/V | |
static func from(string : String) -> ChannelSelection? { | |
switch string { | |
case "NONE": return ChannelSelection.None | |
case "V": return .Video | |
case "A": return .AudioOneOnly | |
case "A2": return .AudioTwoOnly | |
case "AA": return .AudioOneAndTwo | |
case "B": return .AudioOneAndVideo | |
case "A2/V": return .AudioTwoAndVideo | |
case "AA/V": return .AudioOneAndTwoAndVideo | |
default: return nil | |
} | |
} | |
} | |
enum EventType { | |
case Cut | |
case Dissolve(duration : Int) | |
case Wipe(pattern : Int, duration : Int) | |
case KeyBackground(fade : Bool) | |
case Key(duration : Int) | |
case KeyOut(duration : Int) | |
static func from(typeString : String, operandString : String) -> EventType? { | |
if typeString == "C" { | |
return .Cut | |
} else if typeString == "KB" { | |
if operandString == "F" { | |
return .KeyBackground(fade : true) | |
} else { | |
return .KeyBackground(fade : false) | |
} | |
} else { | |
guard let dur = Int(operandString), operandString.isStrictlyInteger, (1...255).contains(dur) else { | |
return nil | |
} | |
if typeString == "D" { | |
return .Dissolve(duration : dur ) | |
} else if typeString.prefix(1) == "W" { | |
guard let wipePattern = Int(typeString.suffix(3)) else { | |
return nil | |
} | |
return .Wipe(pattern : wipePattern, duration : dur) | |
} else if typeString == "K" { | |
return .Key(duration : dur) | |
} else if typeString == "KO" { | |
return .KeyOut(duration : dur) | |
} else { | |
return nil | |
} | |
} | |
} | |
} | |
private var document : String | |
init(url : URL) throws { | |
let fileString = try String(contentsOf: url) | |
document = fileString | |
} | |
func statements() -> [Statement] { | |
let delimiter = "\r\n" | |
let statementStrings = document.components(separatedBy: delimiter) | |
return statementStrings.map(stringToStatement) | |
} | |
private func stringToStatement(_ str : String) -> Statement { | |
if str.prefix(6) == "TITLE:" { | |
return .Title(String(str[7...]) ) | |
} else if str.prefix(4) == "FCM:" { | |
return parseFCM(from: str) | |
} else if String(str.prefix(6)).isStrictlyInteger { | |
return parseFile32StandardForm(from : str) | |
} else if String( str.prefix(3)).isStrictlyInteger { | |
return parseStandardForm(from: str) | |
} else if str.prefix(3) == "AUD" { | |
return parseAudioNote(from: str) | |
} | |
else { | |
return .Unrecognized(str) | |
} | |
} | |
private func parseFCM(from : String) -> Statement { | |
let field = from[5...] | |
if field.hasPrefix("NON-DROP FRAME") { | |
return .FrameCountMode(dropFrame: false) | |
} else if field.hasPrefix("DROP FRAME") { | |
return .FrameCountMode(dropFrame: true) | |
} else { | |
return .Unrecognized(from) | |
} | |
} | |
private func parseStandardForm(from : String) -> Statement { | |
return parseColumnsForStandardForm(from: from, eventColumnWidth: 3, sourceColumnWidth: 8) | |
} | |
private func parseFile32StandardForm(from : String ) -> Statement { | |
return parseColumnsForStandardForm(from: from, eventColumnWidth: 6, sourceColumnWidth: 32) | |
} | |
private func parseAudioNote(from: String) -> Statement { | |
switch from.trimmingCharacters(in: CharacterSet.whitespaces){ | |
case "AUD 3" : return .AudioNote(channelThree: true, channelFour: false) | |
case "AUD 4" : return .AudioNote(channelThree: false, channelFour: true) | |
case "AUD 3 4" : return .AudioNote(channelThree: true, channelFour: true) | |
default: | |
return .Unrecognized(from) | |
} | |
} | |
private func parseColumnsForStandardForm(from : String, eventColumnWidth : Int, sourceColumnWidth : Int) -> Statement { | |
let columnWidths = [eventColumnWidth,2, | |
sourceColumnWidth,1, | |
4,2, // chans | |
4,1, // trans | |
3,1, // trans op | |
11,1, | |
11,1, | |
11,1, | |
11] | |
let ws = CharacterSet.whitespaces | |
guard from.count >= columnWidths.reduce(0, +) else { return .Unrecognized(from) } | |
let columns = from.collimate(columnWidths: columnWidths).map { $0.trimmingCharacters(in: ws) } | |
let number = columns[0] | |
let source = columns[2] | |
let chans = columns[4] | |
let trans = columns[6] | |
let transOp = columns[8] | |
let sourceIn = columns[10] | |
let sourceOut = columns[12] | |
let recIn = columns[14] | |
let recOut = columns[16] | |
guard let chanType = ChannelSelection.from(string: String(chans) ) else { | |
return .Unrecognized(from) | |
} | |
guard let eventNumber = Int(number) else { | |
return .Unrecognized(from) | |
} | |
guard let eventType = EventType.from(typeString: trans, operandString: transOp) else { | |
return .Unrecognized(from) | |
} | |
return Statement.StandardForm(eventNumber: eventNumber, | |
source: source, | |
channels: chanType, | |
eventType: eventType, sourceIn: sourceIn, sourceOut: sourceOut, recordIn: recIn, recordOut: recOut) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment