Skip to content

Instantly share code, notes, and snippets.

@iluvcapra
Last active June 30, 2018 20:43
Show Gist options
  • Save iluvcapra/eb622baf9ea67c7d00d4ed81a5295761 to your computer and use it in GitHub Desktop.
Save iluvcapra/eb622baf9ea67c7d00d4ed81a5295761 to your computer and use it in GitHub Desktop.
CMX3600 EDL parser, also auto-detects file32 form
//
// 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