Skip to main content

oximedia_edl/
lib.rs

1//! OxiMedia EDL - CMX 3600 Edit Decision List parser and generator.
2//!
3//! This crate provides comprehensive support for EDL (Edit Decision List) files,
4//! with a focus on the CMX 3600 format and related formats.
5//!
6//! # Features
7//!
8//! - CMX 3600, CMX 3400, GVG, and Sony BVE-9000 format support
9//! - Event types: Cut, Dissolve, Wipe, Key
10//! - Timecode support: Drop-frame, Non-drop-frame (24, 25, 30, 60 fps)
11//! - Reel names and source references
12//! - Motion effects (speed changes, reverse playback, freeze frames)
13//! - Audio channel mapping and routing
14//! - EDL validation and compliance checking
15//! - Format conversion and optimization
16//!
17//! # Example: Parsing an EDL
18//!
19//! ```
20//! use oximedia_edl::{parse_edl, Edl};
21//!
22//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
23//! let edl_text = r#"
24//! TITLE: Example EDL
25//! FCM: DROP FRAME
26//!
27//! 001  AX       V     C        01:00:00:00 01:00:05:00 01:00:00:00 01:00:05:00
28//! * FROM CLIP NAME: shot001.mov
29//! "#;
30//!
31//! let edl = parse_edl(edl_text)?;
32//! assert_eq!(edl.title, Some("Example EDL".to_string()));
33//! assert_eq!(edl.events.len(), 1);
34//! # Ok(())
35//! # }
36//! ```
37//!
38//! # Example: Generating an EDL
39//!
40//! ```
41//! use oximedia_edl::{Edl, EdlFormat, EdlGenerator};
42//! use oximedia_edl::event::{EdlEvent, EditType, TrackType};
43//! use oximedia_edl::timecode::{EdlFrameRate, EdlTimecode};
44//!
45//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
46//! let mut edl = Edl::new(EdlFormat::Cmx3600);
47//! edl.set_title("My EDL".to_string());
48//! edl.set_frame_rate(EdlFrameRate::Fps25);
49//!
50//! let tc1 = EdlTimecode::new(1, 0, 0, 0, EdlFrameRate::Fps25)?;
51//! let tc2 = EdlTimecode::new(1, 0, 5, 0, EdlFrameRate::Fps25)?;
52//!
53//! let event = EdlEvent::new(
54//!     1,
55//!     "A001".to_string(),
56//!     TrackType::Video,
57//!     EditType::Cut,
58//!     tc1,
59//!     tc2,
60//!     tc1,
61//!     tc2,
62//! );
63//!
64//! edl.add_event(event)?;
65//!
66//! let generator = EdlGenerator::new();
67//! let output = generator.generate(&edl)?;
68//! assert!(output.contains("TITLE: My EDL"));
69//! # Ok(())
70//! # }
71//! ```
72//!
73//! # Example: Validating an EDL
74//!
75//! ```
76//! use oximedia_edl::{Edl, EdlFormat, EdlValidator};
77//! use oximedia_edl::validator::ValidationLevel;
78//!
79//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
80//! let edl = Edl::new(EdlFormat::Cmx3600);
81//! let validator = EdlValidator::strict();
82//! let report = validator.validate(&edl)?;
83//! # Ok(())
84//! # }
85//! ```
86
87#![warn(missing_docs)]
88#![allow(
89    clippy::module_name_repetitions,
90    clippy::cast_possible_truncation,
91    clippy::cast_precision_loss,
92    clippy::cast_sign_loss,
93    dead_code,
94    clippy::pedantic
95)]
96
97pub mod aaf;
98pub mod ale;
99pub mod audio;
100pub mod batch_export;
101pub mod cmx3600;
102pub mod conform_report;
103pub mod consolidate;
104pub mod converter;
105pub mod diff;
106pub mod edl_ascii_timeline;
107pub mod edl_changelist;
108pub mod edl_comments;
109pub mod edl_compare;
110pub mod edl_duration;
111pub mod edl_event;
112pub mod edl_filter;
113pub mod edl_merge;
114pub mod edl_sanitize;
115pub mod edl_statistics;
116pub mod edl_timeline;
117pub mod edl_validator;
118pub mod error;
119pub mod event;
120pub mod event_list;
121pub mod fcpxml;
122pub mod filter;
123pub mod frame_count;
124pub mod fuzz_tests;
125pub mod generator;
126pub mod lazy_parser;
127pub mod metadata;
128pub mod motion;
129pub mod multicam;
130pub mod optimizer;
131pub mod otio;
132pub mod parser;
133pub mod parser_opt;
134pub mod reel;
135pub mod reel_map;
136pub mod reel_registry;
137pub mod roundtrip;
138pub mod subframe;
139pub mod tc_cache;
140pub mod tc_list;
141pub mod timecode;
142pub mod to_timeline;
143pub mod transition_events;
144pub mod validator;
145pub mod version_history;
146
147pub use ale::{parse_ale_records, AleRecord};
148pub use batch_export::BatchEdlExporter;
149pub use error::{EdlError, EdlResult};
150pub use generator::EdlGenerator;
151pub use parser::{parse_edl, EdlParser};
152pub use validator::EdlValidator;
153
154use crate::event::EdlEvent;
155use crate::reel::ReelTable;
156use crate::timecode::EdlFrameRate;
157use std::path::PathBuf;
158
159/// EDL format identifier.
160#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
161pub enum EdlFormat {
162    /// CMX 3600 format (most common).
163    Cmx3600,
164    /// CMX 3400 format (older).
165    Cmx3400,
166    /// CMX 340 format.
167    Cmx340,
168    /// GVG (Grass Valley Group) format.
169    Gvg,
170    /// Sony BVE-9000 format.
171    SonyBve9000,
172}
173
174impl EdlFormat {
175    /// Get the format name as a string.
176    #[must_use]
177    pub const fn as_str(&self) -> &'static str {
178        match self {
179            Self::Cmx3600 => "CMX 3600",
180            Self::Cmx3400 => "CMX 3400",
181            Self::Cmx340 => "CMX 340",
182            Self::Gvg => "GVG",
183            Self::SonyBve9000 => "Sony BVE-9000",
184        }
185    }
186}
187
188impl std::fmt::Display for EdlFormat {
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        write!(f, "{}", self.as_str())
191    }
192}
193
194/// Main EDL structure containing all events and metadata.
195#[derive(Debug, Clone)]
196pub struct Edl {
197    /// EDL format.
198    pub format: EdlFormat,
199
200    /// Optional title of the EDL.
201    pub title: Option<String>,
202
203    /// Frame rate for timecodes.
204    pub frame_rate: EdlFrameRate,
205
206    /// List of events in the EDL.
207    pub events: Vec<EdlEvent>,
208
209    /// Reel table with source information.
210    pub reel_table: ReelTable,
211
212    /// Optional source file path (for reference).
213    pub source_file: Option<PathBuf>,
214
215    /// Optional comments not associated with specific events.
216    pub global_comments: Vec<String>,
217}
218
219impl Edl {
220    /// Create a new empty EDL with the specified format.
221    #[must_use]
222    pub fn new(format: EdlFormat) -> Self {
223        Self {
224            format,
225            title: None,
226            frame_rate: EdlFrameRate::Fps2997NDF,
227            events: Vec::new(),
228            reel_table: ReelTable::new(),
229            source_file: None,
230            global_comments: Vec::new(),
231        }
232    }
233
234    /// Create a new CMX 3600 EDL (most common format).
235    #[must_use]
236    pub fn cmx3600() -> Self {
237        Self::new(EdlFormat::Cmx3600)
238    }
239
240    /// Set the EDL title.
241    pub fn set_title(&mut self, title: String) {
242        self.title = Some(title);
243    }
244
245    /// Set the frame rate.
246    pub fn set_frame_rate(&mut self, frame_rate: EdlFrameRate) {
247        self.frame_rate = frame_rate;
248    }
249
250    /// Set the source file path.
251    pub fn set_source_file(&mut self, path: PathBuf) {
252        self.source_file = Some(path);
253    }
254
255    /// Add an event to the EDL.
256    ///
257    /// # Errors
258    ///
259    /// Returns an error if the event is invalid.
260    pub fn add_event(&mut self, event: EdlEvent) -> EdlResult<()> {
261        event.validate()?;
262        self.events.push(event);
263        Ok(())
264    }
265
266    /// Add a global comment.
267    pub fn add_global_comment(&mut self, comment: String) {
268        self.global_comments.push(comment);
269    }
270
271    /// Get an event by number.
272    #[must_use]
273    pub fn get_event(&self, number: u32) -> Option<&EdlEvent> {
274        self.events.iter().find(|e| e.number == number)
275    }
276
277    /// Get a mutable event by number.
278    pub fn get_event_mut(&mut self, number: u32) -> Option<&mut EdlEvent> {
279        self.events.iter_mut().find(|e| e.number == number)
280    }
281
282    /// Remove an event by number.
283    pub fn remove_event(&mut self, number: u32) -> Option<EdlEvent> {
284        if let Some(index) = self.events.iter().position(|e| e.number == number) {
285            Some(self.events.remove(index))
286        } else {
287            None
288        }
289    }
290
291    /// Get the number of events.
292    #[must_use]
293    pub fn event_count(&self) -> usize {
294        self.events.len()
295    }
296
297    /// Get the total duration of the EDL in frames.
298    #[must_use]
299    pub fn total_duration_frames(&self) -> u64 {
300        self.events.iter().map(|e| e.duration_frames()).sum()
301    }
302
303    /// Get the total duration in seconds.
304    #[must_use]
305    pub fn total_duration_seconds(&self) -> f64 {
306        self.total_duration_frames() as f64 / self.frame_rate.fps() as f64
307    }
308
309    /// Sort events by record in timecode.
310    pub fn sort_events(&mut self) {
311        self.events.sort_by_key(|e| e.record_in.to_frames());
312    }
313
314    /// Renumber events sequentially starting from 1.
315    pub fn renumber_events(&mut self) {
316        for (i, event) in self.events.iter_mut().enumerate() {
317            event.number = (i + 1) as u32;
318        }
319    }
320
321    /// Validate the entire EDL.
322    ///
323    /// # Errors
324    ///
325    /// Returns an error if the EDL is invalid.
326    pub fn validate(&self) -> EdlResult<()> {
327        let validator = EdlValidator::default();
328        validator.validate(self)?;
329        Ok(())
330    }
331
332    /// Generate the EDL as a string.
333    ///
334    /// # Errors
335    ///
336    /// Returns an error if generation fails.
337    pub fn to_string_format(&self) -> EdlResult<String> {
338        let generator = EdlGenerator::new();
339        generator.generate(self)
340    }
341
342    /// Parse an EDL from a string.
343    ///
344    /// # Errors
345    ///
346    /// Returns an error if parsing fails.
347    #[allow(clippy::should_implement_trait)]
348    pub fn from_str(input: &str) -> EdlResult<Self> {
349        parse_edl(input)
350    }
351
352    /// Load an EDL from a file.
353    ///
354    /// # Errors
355    ///
356    /// Returns an error if the file cannot be read or parsed.
357    pub fn from_file(path: &std::path::Path) -> EdlResult<Self> {
358        let content = std::fs::read_to_string(path)?;
359        let mut edl = parse_edl(&content)?;
360        edl.set_source_file(path.to_path_buf());
361        Ok(edl)
362    }
363
364    /// Save the EDL to a file.
365    ///
366    /// # Errors
367    ///
368    /// Returns an error if the file cannot be written.
369    pub fn to_file(&self, path: &std::path::Path) -> EdlResult<()> {
370        let generator = EdlGenerator::new();
371        generator.generate_to_file(self, path)
372    }
373}
374
375impl Default for Edl {
376    fn default() -> Self {
377        Self::cmx3600()
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384    use crate::event::{EditType, TrackType};
385    use crate::timecode::EdlTimecode;
386
387    #[test]
388    fn test_create_edl() {
389        let edl = Edl::new(EdlFormat::Cmx3600);
390        assert_eq!(edl.format, EdlFormat::Cmx3600);
391        assert_eq!(edl.events.len(), 0);
392    }
393
394    #[test]
395    fn test_add_event() {
396        let mut edl = Edl::new(EdlFormat::Cmx3600);
397        edl.set_frame_rate(EdlFrameRate::Fps25);
398
399        let tc1 = EdlTimecode::new(1, 0, 0, 0, EdlFrameRate::Fps25).expect("failed to create");
400        let tc2 = EdlTimecode::new(1, 0, 5, 0, EdlFrameRate::Fps25).expect("failed to create");
401
402        let event = EdlEvent::new(
403            1,
404            "A001".to_string(),
405            TrackType::Video,
406            EditType::Cut,
407            tc1,
408            tc2,
409            tc1,
410            tc2,
411        );
412
413        edl.add_event(event).expect("add_event should succeed");
414        assert_eq!(edl.events.len(), 1);
415    }
416
417    #[test]
418    fn test_get_event() {
419        let mut edl = Edl::new(EdlFormat::Cmx3600);
420        edl.set_frame_rate(EdlFrameRate::Fps25);
421
422        let tc1 = EdlTimecode::new(1, 0, 0, 0, EdlFrameRate::Fps25).expect("failed to create");
423        let tc2 = EdlTimecode::new(1, 0, 5, 0, EdlFrameRate::Fps25).expect("failed to create");
424
425        let event = EdlEvent::new(
426            1,
427            "A001".to_string(),
428            TrackType::Video,
429            EditType::Cut,
430            tc1,
431            tc2,
432            tc1,
433            tc2,
434        );
435
436        edl.add_event(event).expect("add_event should succeed");
437
438        let retrieved = edl.get_event(1).expect("get_event should succeed");
439        assert_eq!(retrieved.number, 1);
440    }
441
442    #[test]
443    fn test_remove_event() {
444        let mut edl = Edl::new(EdlFormat::Cmx3600);
445        edl.set_frame_rate(EdlFrameRate::Fps25);
446
447        let tc1 = EdlTimecode::new(1, 0, 0, 0, EdlFrameRate::Fps25).expect("failed to create");
448        let tc2 = EdlTimecode::new(1, 0, 5, 0, EdlFrameRate::Fps25).expect("failed to create");
449
450        let event = EdlEvent::new(
451            1,
452            "A001".to_string(),
453            TrackType::Video,
454            EditType::Cut,
455            tc1,
456            tc2,
457            tc1,
458            tc2,
459        );
460
461        edl.add_event(event).expect("add_event should succeed");
462        assert_eq!(edl.events.len(), 1);
463
464        edl.remove_event(1);
465        assert_eq!(edl.events.len(), 0);
466    }
467
468    #[test]
469    fn test_renumber_events() {
470        let mut edl = Edl::new(EdlFormat::Cmx3600);
471        edl.set_frame_rate(EdlFrameRate::Fps25);
472
473        let tc1 = EdlTimecode::new(1, 0, 0, 0, EdlFrameRate::Fps25).expect("failed to create");
474        let tc2 = EdlTimecode::new(1, 0, 5, 0, EdlFrameRate::Fps25).expect("failed to create");
475
476        let event1 = EdlEvent::new(
477            10,
478            "A001".to_string(),
479            TrackType::Video,
480            EditType::Cut,
481            tc1,
482            tc2,
483            tc1,
484            tc2,
485        );
486
487        let event2 = EdlEvent::new(
488            20,
489            "A002".to_string(),
490            TrackType::Video,
491            EditType::Cut,
492            tc1,
493            tc2,
494            tc1,
495            tc2,
496        );
497
498        edl.add_event(event1).expect("add_event should succeed");
499        edl.add_event(event2).expect("add_event should succeed");
500
501        edl.renumber_events();
502
503        assert_eq!(edl.events[0].number, 1);
504        assert_eq!(edl.events[1].number, 2);
505    }
506
507    #[test]
508    fn test_total_duration() {
509        let mut edl = Edl::new(EdlFormat::Cmx3600);
510        edl.set_frame_rate(EdlFrameRate::Fps25);
511
512        let tc1 = EdlTimecode::new(1, 0, 0, 0, EdlFrameRate::Fps25).expect("failed to create");
513        let tc2 = EdlTimecode::new(1, 0, 5, 0, EdlFrameRate::Fps25).expect("failed to create");
514
515        let event = EdlEvent::new(
516            1,
517            "A001".to_string(),
518            TrackType::Video,
519            EditType::Cut,
520            tc1,
521            tc2,
522            tc1,
523            tc2,
524        );
525
526        edl.add_event(event).expect("add_event should succeed");
527
528        let duration_frames = edl.total_duration_frames();
529        assert_eq!(duration_frames, 125); // 5 seconds * 25 fps
530
531        let duration_seconds = edl.total_duration_seconds();
532        assert!((duration_seconds - 5.0).abs() < f64::EPSILON);
533    }
534
535    #[test]
536    fn test_edl_format_display() {
537        assert_eq!(EdlFormat::Cmx3600.to_string(), "CMX 3600");
538        assert_eq!(EdlFormat::Cmx3400.to_string(), "CMX 3400");
539        assert_eq!(EdlFormat::Gvg.to_string(), "GVG");
540    }
541
542    #[test]
543    fn test_parse_and_generate_roundtrip() {
544        let edl_text = r#"TITLE: Test EDL
545FCM: NON-DROP FRAME
546
547001  AX       V     C        01:00:00:00 01:00:05:00 01:00:00:00 01:00:05:00
548
549"#;
550
551        let edl = parse_edl(edl_text).expect("operation should succeed");
552        let generated = edl.to_string_format().expect("formatting should succeed");
553
554        // Parse the generated EDL again
555        let edl2 = parse_edl(&generated).expect("operation should succeed");
556
557        assert_eq!(edl.title, edl2.title);
558        assert_eq!(edl.events.len(), edl2.events.len());
559        assert_eq!(edl.events[0].number, edl2.events[0].number);
560    }
561
562    /// Requirement: test_cmx3600_roundtrip — creates an Edl with 3 events,
563    /// serialises to CMX-3600 format, re-parses, and verifies event count and
564    /// reel names are preserved.
565    #[test]
566    fn test_cmx3600_roundtrip() {
567        let mut edl = Edl::new(EdlFormat::Cmx3600);
568        edl.set_title("Roundtrip Test EDL".to_string());
569        edl.set_frame_rate(EdlFrameRate::Fps25);
570
571        let reels = ["ALPHA", "BETA", "GAMMA"];
572        for (i, reel) in reels.iter().enumerate() {
573            let n = i as u8;
574            let tc_in = EdlTimecode::new(1, 0, n * 5, 0, EdlFrameRate::Fps25).expect("valid tc_in");
575            let tc_out =
576                EdlTimecode::new(1, 0, n * 5 + 5, 0, EdlFrameRate::Fps25).expect("valid tc_out");
577            let event = EdlEvent::new(
578                (i + 1) as u32,
579                reel.to_string(),
580                TrackType::Video,
581                EditType::Cut,
582                tc_in,
583                tc_out,
584                tc_in,
585                tc_out,
586            );
587            edl.add_event(event).expect("add_event should succeed");
588        }
589
590        assert_eq!(
591            edl.events.len(),
592            3,
593            "EDL should have 3 events before serialisation"
594        );
595
596        // Serialise to CMX-3600 format string.
597        let cmx_string = edl
598            .to_string_format()
599            .expect("serialisation should succeed");
600        assert!(
601            cmx_string.contains("TITLE: Roundtrip Test EDL"),
602            "generated text should contain the title"
603        );
604
605        // Re-parse the generated string.
606        let edl2 = parse_edl(&cmx_string).expect("re-parse should succeed");
607
608        // Verify event count.
609        assert_eq!(
610            edl2.events.len(),
611            3,
612            "re-parsed EDL should have 3 events; generated text:\n{cmx_string}"
613        );
614
615        // Verify reel names are preserved in order.
616        for (orig_event, reparsed_event) in edl.events.iter().zip(edl2.events.iter()) {
617            assert_eq!(
618                orig_event.reel, reparsed_event.reel,
619                "reel name should survive the roundtrip"
620            );
621        }
622    }
623}