Skip to main content

oxihuman_export/
edl_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! CMX 3600 EDL (Edit Decision List) export.
6
7/// An EDL event entry.
8#[derive(Debug, Clone)]
9pub struct EdlEvent {
10    pub event_num: u32,
11    pub reel: String,
12    pub channels: String,
13    pub transition: String,
14    pub src_in: String,
15    pub src_out: String,
16    pub rec_in: String,
17    pub rec_out: String,
18}
19
20/// A CMX 3600 EDL export.
21#[derive(Debug, Clone)]
22pub struct EdlExport {
23    pub title: String,
24    pub fcm: String, /* Frame Count Mode: "NON-DROP FRAME" or "DROP FRAME" */
25    pub events: Vec<EdlEvent>,
26}
27
28/// SMPTE timecode from frame count at 24fps.
29pub fn frames_to_timecode(frames: u32, fps: u32) -> String {
30    let fps = fps.max(1);
31    let f = frames % fps;
32    let s = (frames / fps) % 60;
33    let m = (frames / fps / 60) % 60;
34    let h = frames / fps / 3600;
35    format!("{:02}:{:02}:{:02}:{:02}", h, m, s, f)
36}
37
38/// Create a new EDL export.
39pub fn new_edl_export(title: &str, drop_frame: bool) -> EdlExport {
40    EdlExport {
41        title: title.to_string(),
42        fcm: if drop_frame {
43            "DROP FRAME".to_string()
44        } else {
45            "NON-DROP FRAME".to_string()
46        },
47        events: Vec::new(),
48    }
49}
50
51/// Add an event to the EDL.
52#[allow(clippy::too_many_arguments)]
53pub fn edl_add_event(
54    export: &mut EdlExport,
55    reel: &str,
56    channels: &str,
57    transition: &str,
58    src_in: &str,
59    src_out: &str,
60    rec_in: &str,
61    rec_out: &str,
62) {
63    let event_num = export.events.len() as u32 + 1;
64    export.events.push(EdlEvent {
65        event_num,
66        reel: reel.to_string(),
67        channels: channels.to_string(),
68        transition: transition.to_string(),
69        src_in: src_in.to_string(),
70        src_out: src_out.to_string(),
71        rec_in: rec_in.to_string(),
72        rec_out: rec_out.to_string(),
73    });
74}
75
76/// Return the event count.
77pub fn edl_event_count(export: &EdlExport) -> usize {
78    export.events.len()
79}
80
81/// Serialize the EDL to a string in CMX 3600 format.
82pub fn edl_to_string(export: &EdlExport) -> String {
83    let mut out = format!("TITLE: {}\nFCM: {}\n\n", export.title, export.fcm);
84    for ev in &export.events {
85        out.push_str(&format!(
86            "{:03}  {}  {}  {}  {}  {}  {}  {}\n",
87            ev.event_num,
88            ev.reel,
89            ev.channels,
90            ev.transition,
91            ev.src_in,
92            ev.src_out,
93            ev.rec_in,
94            ev.rec_out,
95        ));
96    }
97    out
98}
99
100/// Estimate the EDL file size.
101pub fn edl_size_bytes(export: &EdlExport) -> usize {
102    edl_to_string(export).len()
103}
104
105/// Find events for a specific reel.
106pub fn events_for_reel<'a>(export: &'a EdlExport, reel: &str) -> Vec<&'a EdlEvent> {
107    export.events.iter().filter(|e| e.reel == reel).collect()
108}
109
110/// Validate the EDL.
111pub fn validate_edl(export: &EdlExport) -> bool {
112    !export.title.is_empty()
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    fn sample_edl() -> EdlExport {
120        let mut exp = new_edl_export("MY_EDIT", false);
121        edl_add_event(
122            &mut exp,
123            "A001",
124            "V",
125            "C",
126            "01:00:00:00",
127            "01:00:04:00",
128            "01:00:00:00",
129            "01:00:04:00",
130        );
131        edl_add_event(
132            &mut exp,
133            "B002",
134            "V",
135            "C",
136            "01:00:04:00",
137            "01:00:08:00",
138            "01:00:04:00",
139            "01:00:08:00",
140        );
141        exp
142    }
143
144    #[test]
145    fn test_event_count() {
146        assert_eq!(edl_event_count(&sample_edl()), 2);
147    }
148
149    #[test]
150    fn test_to_string_title() {
151        assert!(edl_to_string(&sample_edl()).contains("MY_EDIT"));
152    }
153
154    #[test]
155    fn test_to_string_event() {
156        assert!(edl_to_string(&sample_edl()).contains("A001"));
157    }
158
159    #[test]
160    fn test_validate() {
161        assert!(validate_edl(&sample_edl()));
162    }
163
164    #[test]
165    fn test_events_for_reel() {
166        let exp = sample_edl();
167        assert_eq!(events_for_reel(&exp, "A001").len(), 1);
168        assert_eq!(events_for_reel(&exp, "B002").len(), 1);
169        assert_eq!(events_for_reel(&exp, "C003").len(), 0);
170    }
171
172    #[test]
173    fn test_frames_to_timecode() {
174        let tc = frames_to_timecode(0, 24);
175        assert_eq!(tc, "00:00:00:00");
176    }
177
178    #[test]
179    fn test_frames_to_timecode_24fps() {
180        /* 24 frames = 1 second at 24fps */
181        let tc = frames_to_timecode(24, 24);
182        assert_eq!(tc, "00:00:01:00");
183    }
184
185    #[test]
186    fn test_edl_size_positive() {
187        assert!(edl_size_bytes(&sample_edl()) > 0);
188    }
189
190    #[test]
191    fn test_fcm_drop_frame() {
192        let exp = new_edl_export("X", true);
193        assert!(exp.fcm.contains("DROP FRAME"));
194    }
195}