Skip to main content

oximedia_edit/
multitrack.rs

1//! Multi-track edit operations.
2//!
3//! Provides track management including locking, visibility, muting, and soloing
4//! for video, audio, title, and effect tracks.
5
6#![allow(dead_code)]
7#![allow(clippy::module_name_repetitions)]
8
9/// The type of media carried by a track.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum TrackType {
12    /// A video track.
13    Video,
14    /// An audio track.
15    Audio,
16    /// A title (graphics/subtitle) track.
17    Title,
18    /// An effect (adjustment) track.
19    Effect,
20}
21
22impl TrackType {
23    /// Returns `true` when the track carries both audio and video content
24    /// (i.e., it is a Video or Audio track).
25    #[must_use]
26    pub fn is_av(self) -> bool {
27        matches!(self, TrackType::Video | TrackType::Audio)
28    }
29}
30
31/// Per-track edit-lock state.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct TrackLock {
34    /// Unique track identifier.
35    pub track_id: u32,
36    /// When `true`, edits to this track are blocked.
37    pub locked: bool,
38    /// When `true`, sync-lock prevents this track from drifting during ripple edits.
39    pub sync_locked: bool,
40}
41
42impl TrackLock {
43    /// Create a new unlocked `TrackLock` for `track_id`.
44    #[must_use]
45    pub fn new(track_id: u32) -> Self {
46        Self {
47            track_id,
48            locked: false,
49            sync_locked: false,
50        }
51    }
52
53    /// Returns `true` when the track can be edited (not hard-locked).
54    #[must_use]
55    pub fn can_edit(&self) -> bool {
56        !self.locked
57    }
58}
59
60/// Per-track visibility and monitoring state.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct TrackVisibility {
63    /// Unique track identifier.
64    pub track_id: u32,
65    /// Whether the track content is visible in the viewer.
66    pub visible: bool,
67    /// When `true`, only solo tracks are rendered.
68    pub solo: bool,
69    /// When `true`, audio output from this track is silenced.
70    pub muted: bool,
71}
72
73impl TrackVisibility {
74    /// Create a new `TrackVisibility` with sensible defaults (visible, not solo, not muted).
75    #[must_use]
76    pub fn new(track_id: u32) -> Self {
77        Self {
78            track_id,
79            visible: true,
80            solo: false,
81            muted: false,
82        }
83    }
84
85    /// Returns `true` when this track's audio is audible.
86    ///
87    /// A track is audible when:
88    /// - it is not muted, **and**
89    /// - either no track is soloed, or this track is the soloed one.
90    #[must_use]
91    pub fn is_audible(&self, any_solo: bool) -> bool {
92        if self.muted {
93            return false;
94        }
95        if any_solo {
96            return self.solo;
97        }
98        true
99    }
100}
101
102/// Complete multi-track configuration for a timeline.
103#[derive(Debug, Default)]
104pub struct MultitrackConfig {
105    /// Ordered list of `(track_id, TrackType)` pairs.
106    pub tracks: Vec<(u32, TrackType)>,
107    /// Lock states, one entry per track.
108    pub locks: Vec<TrackLock>,
109    /// Visibility states, one entry per track.
110    pub visibility: Vec<TrackVisibility>,
111}
112
113impl MultitrackConfig {
114    /// Create an empty configuration.
115    #[must_use]
116    pub fn new() -> Self {
117        Self::default()
118    }
119
120    /// Add a new track and initialise its lock/visibility state.
121    pub fn add_track(&mut self, track_id: u32, track_type: TrackType) {
122        self.tracks.push((track_id, track_type));
123        self.locks.push(TrackLock::new(track_id));
124        self.visibility.push(TrackVisibility::new(track_id));
125    }
126
127    /// Lock or unlock a track by ID.  Does nothing if the track is not found.
128    pub fn lock_track(&mut self, track_id: u32, locked: bool) {
129        if let Some(lock) = self.locks.iter_mut().find(|l| l.track_id == track_id) {
130            lock.locked = locked;
131        }
132    }
133
134    /// Mute or unmute a track by ID.  Does nothing if the track is not found.
135    pub fn mute_track(&mut self, track_id: u32, muted: bool) {
136        if let Some(vis) = self.visibility.iter_mut().find(|v| v.track_id == track_id) {
137            vis.muted = muted;
138        }
139    }
140
141    /// Solo or un-solo a track by ID.  Does nothing if the track is not found.
142    pub fn solo_track(&mut self, track_id: u32, solo: bool) {
143        if let Some(vis) = self.visibility.iter_mut().find(|v| v.track_id == track_id) {
144            vis.solo = solo;
145        }
146    }
147
148    /// Return the IDs of all tracks whose audio is currently audible.
149    #[must_use]
150    pub fn audible_tracks(&self) -> Vec<u32> {
151        let any_solo = self.visibility.iter().any(|v| v.solo);
152        self.visibility
153            .iter()
154            .filter(|v| v.is_audible(any_solo))
155            .map(|v| v.track_id)
156            .collect()
157    }
158
159    /// Return the total number of tracks.
160    #[must_use]
161    pub fn track_count(&self) -> usize {
162        self.tracks.len()
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    // ----- TrackType tests -----
171
172    #[test]
173    fn test_track_type_video_is_av() {
174        assert!(TrackType::Video.is_av());
175    }
176
177    #[test]
178    fn test_track_type_audio_is_av() {
179        assert!(TrackType::Audio.is_av());
180    }
181
182    #[test]
183    fn test_track_type_title_not_av() {
184        assert!(!TrackType::Title.is_av());
185    }
186
187    #[test]
188    fn test_track_type_effect_not_av() {
189        assert!(!TrackType::Effect.is_av());
190    }
191
192    // ----- TrackLock tests -----
193
194    #[test]
195    fn test_track_lock_new_unlocked() {
196        let lock = TrackLock::new(1);
197        assert!(!lock.locked);
198        assert!(lock.can_edit());
199    }
200
201    #[test]
202    fn test_track_lock_locked_cannot_edit() {
203        let mut lock = TrackLock::new(2);
204        lock.locked = true;
205        assert!(!lock.can_edit());
206    }
207
208    // ----- TrackVisibility tests -----
209
210    #[test]
211    fn test_track_visibility_defaults() {
212        let vis = TrackVisibility::new(1);
213        assert!(vis.visible);
214        assert!(!vis.solo);
215        assert!(!vis.muted);
216    }
217
218    #[test]
219    fn test_is_audible_no_solo_not_muted() {
220        let vis = TrackVisibility::new(1);
221        assert!(vis.is_audible(false));
222    }
223
224    #[test]
225    fn test_is_audible_muted() {
226        let mut vis = TrackVisibility::new(1);
227        vis.muted = true;
228        assert!(!vis.is_audible(false));
229    }
230
231    #[test]
232    fn test_is_audible_solo_mode_not_soloed() {
233        let vis = TrackVisibility::new(1); // solo = false
234        assert!(!vis.is_audible(true));
235    }
236
237    #[test]
238    fn test_is_audible_solo_mode_is_soloed() {
239        let mut vis = TrackVisibility::new(1);
240        vis.solo = true;
241        assert!(vis.is_audible(true));
242    }
243
244    // ----- MultitrackConfig tests -----
245
246    #[test]
247    fn test_multitrack_add_track() {
248        let mut cfg = MultitrackConfig::new();
249        cfg.add_track(1, TrackType::Video);
250        cfg.add_track(2, TrackType::Audio);
251        assert_eq!(cfg.track_count(), 2);
252    }
253
254    #[test]
255    fn test_multitrack_lock_track() {
256        let mut cfg = MultitrackConfig::new();
257        cfg.add_track(1, TrackType::Video);
258        cfg.lock_track(1, true);
259        assert!(!cfg.locks[0].can_edit());
260    }
261
262    #[test]
263    fn test_multitrack_mute_track() {
264        let mut cfg = MultitrackConfig::new();
265        cfg.add_track(1, TrackType::Audio);
266        cfg.mute_track(1, true);
267        let audible = cfg.audible_tracks();
268        assert!(!audible.contains(&1));
269    }
270
271    #[test]
272    fn test_multitrack_solo_track() {
273        let mut cfg = MultitrackConfig::new();
274        cfg.add_track(1, TrackType::Audio);
275        cfg.add_track(2, TrackType::Audio);
276        cfg.solo_track(1, true);
277        let audible = cfg.audible_tracks();
278        // Only track 1 should be audible because it's the soloed track
279        assert!(audible.contains(&1));
280        assert!(!audible.contains(&2));
281    }
282
283    #[test]
284    fn test_multitrack_audible_tracks_all_active() {
285        let mut cfg = MultitrackConfig::new();
286        cfg.add_track(1, TrackType::Audio);
287        cfg.add_track(2, TrackType::Audio);
288        let audible = cfg.audible_tracks();
289        assert_eq!(audible.len(), 2);
290    }
291}