124 lines
4.2 KiB
Swift
124 lines
4.2 KiB
Swift
import Foundation
|
|
import ForgeColor
|
|
import ForgeGrade
|
|
import ForgeLibrary
|
|
|
|
/// Post-handoff writers: ALE, CSV, CMX3600 EDL with ASC CDL comments.
|
|
public enum PostExport {
|
|
|
|
private static func f6(_ v: Float) -> String {
|
|
String(format: "%.6f", v)
|
|
}
|
|
|
|
/// First CDL node from a shot's grade snapshot, if any.
|
|
static func cdl(from shot: Shot) -> CDL? {
|
|
guard let data = shot.gradeSnapshot,
|
|
let stack = try? GradeSnapshot.decode(data) else { return nil }
|
|
for node in stack.nodes {
|
|
if case .cdl(let c) = node.kind, node.isEnabled {
|
|
return c
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
static func ascSOP(_ c: CDL) -> String {
|
|
"(\(f6(c.slope.x)) \(f6(c.slope.y)) \(f6(c.slope.z)))"
|
|
+ "(\(f6(c.offset.x)) \(f6(c.offset.y)) \(f6(c.offset.z)))"
|
|
+ "(\(f6(c.power.x)) \(f6(c.power.y)) \(f6(c.power.z)))"
|
|
}
|
|
|
|
// MARK: ALE
|
|
|
|
public static func writeALE(shots: [Shot], fps: Int) -> String {
|
|
var out = """
|
|
Heading
|
|
FIELD_DELIM\tTABS
|
|
VIDEO_FORMAT\t1080
|
|
FPS\t\(fps)
|
|
|
|
Column
|
|
Name\tStart\tEnd\tCamera\tASC_SOP\tASC_SAT\tEI\tWB\tTint\tFPS
|
|
|
|
Data
|
|
|
|
"""
|
|
for s in shots {
|
|
let c = cdl(from: s)
|
|
var row = [String]()
|
|
row.append(s.clipName)
|
|
row.append(s.timecodeIn ?? "")
|
|
row.append(s.timecodeOut ?? "")
|
|
row.append(s.cameraSlot)
|
|
row.append(c.map(ascSOP) ?? "")
|
|
row.append(c.map { f6($0.saturation) } ?? "")
|
|
row.append(s.exposureIndex.map(String.init) ?? "")
|
|
row.append(s.whiteBalance.map(String.init) ?? "")
|
|
row.append(s.tint.map(String.init) ?? "")
|
|
row.append(s.fps.map { String(format: "%g", $0) } ?? "")
|
|
out += row.joined(separator: "\t") + "\n"
|
|
}
|
|
return out
|
|
}
|
|
|
|
// MARK: CSV
|
|
|
|
private static func csvEscape(_ s: String) -> String {
|
|
if s.contains(",") || s.contains("\"") || s.contains("\n") {
|
|
return "\"" + s.replacingOccurrences(of: "\"", with: "\"\"") + "\""
|
|
}
|
|
return s
|
|
}
|
|
|
|
public static func writeCSV(shots: [Shot]) -> String {
|
|
var out = "clip_name,camera_slot,tc_in,tc_out,ei,wb,tint,fps,asc_sop,asc_sat\n"
|
|
for s in shots {
|
|
let c = cdl(from: s)
|
|
var row = [String]()
|
|
row.append(s.clipName)
|
|
row.append(s.cameraSlot)
|
|
row.append(s.timecodeIn ?? "")
|
|
row.append(s.timecodeOut ?? "")
|
|
row.append(s.exposureIndex.map(String.init) ?? "")
|
|
row.append(s.whiteBalance.map(String.init) ?? "")
|
|
row.append(s.tint.map(String.init) ?? "")
|
|
row.append(s.fps.map { String(format: "%g", $0) } ?? "")
|
|
row.append(c.map(ascSOP) ?? "")
|
|
row.append(c.map { f6($0.saturation) } ?? "")
|
|
out += row.map(csvEscape).joined(separator: ",") + "\n"
|
|
}
|
|
return out
|
|
}
|
|
|
|
// MARK: EDL (CMX3600 + CDL comments)
|
|
|
|
public static func writeEDL(shots: [Shot], title: String, fps: Int) -> String {
|
|
var out = "TITLE: \(title)\nFCM: NON-DROP FRAME\n\n"
|
|
var recordTC = Timecode.parse("01:00:00:00", fps: fps)!
|
|
var event = 1
|
|
|
|
for s in shots {
|
|
guard let srcIn = s.timecodeIn.flatMap({ Timecode.parse($0, fps: fps) }),
|
|
let srcOut = s.timecodeOut.flatMap({ Timecode.parse($0, fps: fps) }),
|
|
srcOut.frames > srcIn.frames else { continue }
|
|
let duration = srcOut - srcIn
|
|
let recIn = recordTC
|
|
let recOut = recordTC + duration
|
|
|
|
// Reel column: clip name truncated to CMX-friendly 8+ (modern EDLs allow long names).
|
|
out += String(format: "%03d %@ V C %@ %@ %@ %@\n",
|
|
event, s.clipName, srcIn.description, srcOut.description,
|
|
recIn.description, recOut.description)
|
|
out += "* FROM CLIP NAME: \(s.clipName)\n"
|
|
if let c = cdl(from: s) {
|
|
out += "* ASC_SOP \(ascSOP(c))\n"
|
|
out += "* ASC_SAT \(f6(c.saturation))\n"
|
|
}
|
|
out += "\n"
|
|
|
|
recordTC = recOut
|
|
event += 1
|
|
}
|
|
return out
|
|
}
|
|
}
|