1#![allow(dead_code)]
7#![allow(clippy::cast_precision_loss)]
8#![allow(clippy::too_many_arguments)]
9
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum SequenceStrategy {
15 BackToBack,
17 FixedGap,
19 Overlap,
21}
22
23#[derive(Debug, Clone)]
25pub struct AutoSequenceConfig {
26 pub strategy: SequenceStrategy,
28 pub spacing: u64,
30 pub sort_by_timecode: bool,
32 pub target_track: u32,
34}
35
36impl AutoSequenceConfig {
37 #[must_use]
39 pub fn back_to_back(target_track: u32) -> Self {
40 Self {
41 strategy: SequenceStrategy::BackToBack,
42 spacing: 0,
43 sort_by_timecode: false,
44 target_track,
45 }
46 }
47
48 #[must_use]
50 pub fn with_gap(target_track: u32, gap: u64) -> Self {
51 Self {
52 strategy: SequenceStrategy::FixedGap,
53 spacing: gap,
54 sort_by_timecode: false,
55 target_track,
56 }
57 }
58
59 #[must_use]
61 pub fn with_overlap(target_track: u32, overlap: u64) -> Self {
62 Self {
63 strategy: SequenceStrategy::Overlap,
64 spacing: overlap,
65 sort_by_timecode: false,
66 target_track,
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct AutoClip {
74 pub id: u64,
76 pub source_in: u64,
78 pub source_out: u64,
80 pub source_timecode: Option<u64>,
82 pub audio_level_db: f64,
84}
85
86impl AutoClip {
87 #[must_use]
89 pub fn new(id: u64, source_in: u64, source_out: u64) -> Self {
90 Self {
91 id,
92 source_in,
93 source_out,
94 source_timecode: None,
95 audio_level_db: 0.0,
96 }
97 }
98
99 #[must_use]
101 pub fn duration(&self) -> u64 {
102 self.source_out.saturating_sub(self.source_in)
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct ClipPlacement {
109 pub clip_id: u64,
111 pub timeline_pos: u64,
113 pub duration: u64,
115 pub track: u32,
117}
118
119#[must_use]
121pub fn auto_sequence(clips: &[AutoClip], config: &AutoSequenceConfig) -> Vec<ClipPlacement> {
122 if clips.is_empty() {
123 return Vec::new();
124 }
125
126 let mut sorted: Vec<&AutoClip> = clips.iter().collect();
127 if config.sort_by_timecode {
128 sorted.sort_by_key(|c| c.source_timecode.unwrap_or(0));
129 }
130
131 let mut result = Vec::with_capacity(sorted.len());
132 let mut cursor: u64 = 0;
133
134 for clip in &sorted {
135 let dur = clip.duration();
136 result.push(ClipPlacement {
137 clip_id: clip.id,
138 timeline_pos: cursor,
139 duration: dur,
140 track: config.target_track,
141 });
142 match config.strategy {
143 SequenceStrategy::BackToBack => cursor += dur,
144 SequenceStrategy::FixedGap => cursor += dur + config.spacing,
145 SequenceStrategy::Overlap => cursor += dur.saturating_sub(config.spacing),
146 }
147 }
148 result
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum DuckMode {
154 DialogueOverMusic,
156 NarrationPriority,
158}
159
160#[derive(Debug, Clone)]
162pub struct AutoDuckConfig {
163 pub mode: DuckMode,
165 pub threshold_db: f64,
167 pub reduction_db: f64,
169 pub attack_ms: f64,
171 pub release_ms: f64,
173}
174
175impl Default for AutoDuckConfig {
176 fn default() -> Self {
177 Self {
178 mode: DuckMode::DialogueOverMusic,
179 threshold_db: -20.0,
180 reduction_db: -12.0,
181 attack_ms: 50.0,
182 release_ms: 200.0,
183 }
184 }
185}
186
187impl AutoDuckConfig {
188 #[must_use]
190 pub fn new(mode: DuckMode) -> Self {
191 Self {
192 mode,
193 ..Default::default()
194 }
195 }
196
197 #[must_use]
199 pub fn ducked_level(&self, input_db: f64) -> f64 {
200 if input_db > self.threshold_db {
201 input_db + self.reduction_db
202 } else {
203 input_db
204 }
205 }
206}
207
208#[derive(Debug, Clone, PartialEq, Eq)]
210pub struct TimelineGap {
211 pub start: u64,
213 pub end: u64,
215 pub track: u32,
217}
218
219impl TimelineGap {
220 #[must_use]
222 pub fn duration(&self) -> u64 {
223 self.end.saturating_sub(self.start)
224 }
225}
226
227#[must_use]
229pub fn detect_gaps(placements: &[ClipPlacement], track: u32) -> Vec<TimelineGap> {
230 let mut on_track: Vec<&ClipPlacement> =
231 placements.iter().filter(|p| p.track == track).collect();
232 on_track.sort_by_key(|p| p.timeline_pos);
233
234 let mut gaps = Vec::new();
235 for i in 1..on_track.len() {
236 let prev_end = on_track[i - 1].timeline_pos + on_track[i - 1].duration;
237 let curr_start = on_track[i].timeline_pos;
238 if curr_start > prev_end {
239 gaps.push(TimelineGap {
240 start: prev_end,
241 end: curr_start,
242 track,
243 });
244 }
245 }
246 gaps
247}
248
249#[must_use]
253pub fn ripple_close_gaps(placements: &[ClipPlacement], track: u32) -> HashMap<u64, u64> {
254 let mut on_track: Vec<&ClipPlacement> =
255 placements.iter().filter(|p| p.track == track).collect();
256 on_track.sort_by_key(|p| p.timeline_pos);
257
258 let mut result = HashMap::new();
259 let mut cursor: u64 = on_track.first().map_or(0, |p| p.timeline_pos);
260 for p in &on_track {
261 result.insert(p.clip_id, cursor);
262 cursor += p.duration;
263 }
264 result
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn test_auto_clip_duration() {
273 let c = AutoClip::new(1, 100, 500);
274 assert_eq!(c.duration(), 400);
275 }
276
277 #[test]
278 fn test_auto_clip_zero_duration() {
279 let c = AutoClip::new(1, 500, 500);
280 assert_eq!(c.duration(), 0);
281 }
282
283 #[test]
284 fn test_sequence_back_to_back() {
285 let clips = vec![AutoClip::new(1, 0, 100), AutoClip::new(2, 0, 200)];
286 let cfg = AutoSequenceConfig::back_to_back(0);
287 let result = auto_sequence(&clips, &cfg);
288 assert_eq!(result.len(), 2);
289 assert_eq!(result[0].timeline_pos, 0);
290 assert_eq!(result[0].duration, 100);
291 assert_eq!(result[1].timeline_pos, 100);
292 assert_eq!(result[1].duration, 200);
293 }
294
295 #[test]
296 fn test_sequence_fixed_gap() {
297 let clips = vec![AutoClip::new(1, 0, 100), AutoClip::new(2, 0, 100)];
298 let cfg = AutoSequenceConfig::with_gap(0, 50);
299 let result = auto_sequence(&clips, &cfg);
300 assert_eq!(result[0].timeline_pos, 0);
301 assert_eq!(result[1].timeline_pos, 150); }
303
304 #[test]
305 fn test_sequence_overlap() {
306 let clips = vec![AutoClip::new(1, 0, 100), AutoClip::new(2, 0, 100)];
307 let cfg = AutoSequenceConfig::with_overlap(0, 20);
308 let result = auto_sequence(&clips, &cfg);
309 assert_eq!(result[0].timeline_pos, 0);
310 assert_eq!(result[1].timeline_pos, 80); }
312
313 #[test]
314 fn test_sequence_empty() {
315 let cfg = AutoSequenceConfig::back_to_back(0);
316 let result = auto_sequence(&[], &cfg);
317 assert!(result.is_empty());
318 }
319
320 #[test]
321 fn test_sequence_sort_by_timecode() {
322 let mut c1 = AutoClip::new(1, 0, 100);
323 c1.source_timecode = Some(200);
324 let mut c2 = AutoClip::new(2, 0, 100);
325 c2.source_timecode = Some(100);
326 let mut cfg = AutoSequenceConfig::back_to_back(0);
327 cfg.sort_by_timecode = true;
328 let result = auto_sequence(&[c1, c2], &cfg);
329 assert_eq!(result[0].clip_id, 2); assert_eq!(result[1].clip_id, 1);
331 }
332
333 #[test]
334 fn test_duck_config_default() {
335 let cfg = AutoDuckConfig::default();
336 assert_eq!(cfg.mode, DuckMode::DialogueOverMusic);
337 assert!(cfg.threshold_db < 0.0);
338 }
339
340 #[test]
341 fn test_ducked_level_above_threshold() {
342 let cfg = AutoDuckConfig {
343 threshold_db: -20.0,
344 reduction_db: -12.0,
345 ..Default::default()
346 };
347 let ducked = cfg.ducked_level(-10.0);
348 assert!((ducked - (-22.0)).abs() < f64::EPSILON);
349 }
350
351 #[test]
352 fn test_ducked_level_below_threshold() {
353 let cfg = AutoDuckConfig::default();
354 let level = -30.0;
355 assert!((cfg.ducked_level(level) - level).abs() < f64::EPSILON);
356 }
357
358 #[test]
359 fn test_detect_gaps() {
360 let placements = vec![
361 ClipPlacement {
362 clip_id: 1,
363 timeline_pos: 0,
364 duration: 100,
365 track: 0,
366 },
367 ClipPlacement {
368 clip_id: 2,
369 timeline_pos: 150,
370 duration: 100,
371 track: 0,
372 },
373 ClipPlacement {
374 clip_id: 3,
375 timeline_pos: 250,
376 duration: 50,
377 track: 0,
378 },
379 ];
380 let gaps = detect_gaps(&placements, 0);
381 assert_eq!(gaps.len(), 1);
382 assert_eq!(gaps[0].start, 100);
383 assert_eq!(gaps[0].end, 150);
384 assert_eq!(gaps[0].duration(), 50);
385 }
386
387 #[test]
388 fn test_detect_no_gaps() {
389 let placements = vec![
390 ClipPlacement {
391 clip_id: 1,
392 timeline_pos: 0,
393 duration: 100,
394 track: 0,
395 },
396 ClipPlacement {
397 clip_id: 2,
398 timeline_pos: 100,
399 duration: 100,
400 track: 0,
401 },
402 ];
403 let gaps = detect_gaps(&placements, 0);
404 assert!(gaps.is_empty());
405 }
406
407 #[test]
408 fn test_ripple_close_gaps() {
409 let placements = vec![
410 ClipPlacement {
411 clip_id: 1,
412 timeline_pos: 0,
413 duration: 100,
414 track: 0,
415 },
416 ClipPlacement {
417 clip_id: 2,
418 timeline_pos: 200,
419 duration: 50,
420 track: 0,
421 },
422 ClipPlacement {
423 clip_id: 3,
424 timeline_pos: 400,
425 duration: 80,
426 track: 0,
427 },
428 ];
429 let new_pos = ripple_close_gaps(&placements, 0);
430 assert_eq!(*new_pos.get(&1).expect("get should succeed"), 0);
431 assert_eq!(*new_pos.get(&2).expect("get should succeed"), 100);
432 assert_eq!(*new_pos.get(&3).expect("get should succeed"), 150);
433 }
434}