1#![allow(dead_code)]
2use crate::{FrameRate, Timecode, TimecodeError};
9
10#[derive(Debug, Clone, PartialEq)]
12pub struct SubtitleCue {
13 pub id: u32,
15 pub tc_in: Timecode,
17 pub tc_out: Timecode,
19 pub text: String,
21}
22
23impl SubtitleCue {
24 pub fn new(id: u32, tc_in: Timecode, tc_out: Timecode, text: String) -> Self {
26 Self {
27 id,
28 tc_in,
29 tc_out,
30 text,
31 }
32 }
33
34 pub fn duration_frames(&self) -> u64 {
36 let f_in = self.tc_in.to_frames();
37 let f_out = self.tc_out.to_frames();
38 f_out.saturating_sub(f_in)
39 }
40
41 #[allow(clippy::cast_precision_loss)]
43 pub fn duration_secs(&self) -> f64 {
44 let fps = self.tc_in.frame_rate.fps as f64;
45 self.duration_frames() as f64 / fps
46 }
47
48 pub fn overlaps(&self, other: &SubtitleCue) -> bool {
50 let a_in = self.tc_in.to_frames();
51 let a_out = self.tc_out.to_frames();
52 let b_in = other.tc_in.to_frames();
53 let b_out = other.tc_out.to_frames();
54 a_in < b_out && b_in < a_out
55 }
56}
57
58pub fn apply_frame_offset(
60 cues: &[SubtitleCue],
61 offset: i64,
62 rate: FrameRate,
63) -> Result<Vec<SubtitleCue>, TimecodeError> {
64 let mut result = Vec::with_capacity(cues.len());
65 for cue in cues {
66 let f_in = cue.tc_in.to_frames() as i64 + offset;
67 let f_out = cue.tc_out.to_frames() as i64 + offset;
68 if f_in < 0 || f_out < 0 {
69 return Err(TimecodeError::InvalidFrames);
70 }
71 let new_in = Timecode::from_frames(f_in as u64, rate)?;
72 let new_out = Timecode::from_frames(f_out as u64, rate)?;
73 result.push(SubtitleCue::new(cue.id, new_in, new_out, cue.text.clone()));
74 }
75 Ok(result)
76}
77
78#[allow(
87 clippy::cast_precision_loss,
88 clippy::cast_possible_truncation,
89 clippy::cast_sign_loss
90)]
91pub fn apply_linear_stretch(
92 cues: &[SubtitleCue],
93 anchor_frame: u64,
94 factor: f64,
95 rate: FrameRate,
96) -> Result<Vec<SubtitleCue>, TimecodeError> {
97 let mut result = Vec::with_capacity(cues.len());
98 let anchor = anchor_frame as f64;
99 for cue in cues {
100 let f_in = cue.tc_in.to_frames() as f64;
101 let f_out = cue.tc_out.to_frames() as f64;
102 let new_in = anchor + (f_in - anchor) * factor;
103 let new_out = anchor + (f_out - anchor) * factor;
104 if new_in < 0.0 || new_out < 0.0 {
105 return Err(TimecodeError::InvalidFrames);
106 }
107 let tc_in = Timecode::from_frames(new_in.round() as u64, rate)?;
108 let tc_out = Timecode::from_frames(new_out.round() as u64, rate)?;
109 result.push(SubtitleCue::new(cue.id, tc_in, tc_out, cue.text.clone()));
110 }
111 Ok(result)
112}
113
114#[allow(clippy::cast_precision_loss)]
119pub fn compute_average_drift(cues: &[SubtitleCue], reference_in_frames: &[u64]) -> Option<f64> {
120 if cues.is_empty() || cues.len() != reference_in_frames.len() {
121 return None;
122 }
123 let total: f64 = cues
124 .iter()
125 .zip(reference_in_frames.iter())
126 .map(|(cue, &ref_f)| cue.tc_in.to_frames() as f64 - ref_f as f64)
127 .sum();
128 Some(total / cues.len() as f64)
129}
130
131pub fn sort_cues_by_in(cues: &mut [SubtitleCue]) {
133 cues.sort_by_key(|c| c.tc_in.to_frames());
134}
135
136pub fn find_overlaps(cues: &[SubtitleCue]) -> Vec<(usize, usize)> {
138 let mut overlaps = Vec::new();
139 for i in 0..cues.len() {
140 for j in (i + 1)..cues.len() {
141 if cues[i].overlaps(&cues[j]) {
142 overlaps.push((i, j));
143 }
144 }
145 }
146 overlaps
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 fn tc(h: u8, m: u8, s: u8, f: u8) -> Timecode {
154 Timecode::new(h, m, s, f, FrameRate::Fps25).expect("valid timecode")
155 }
156
157 fn make_cue(id: u32, s_in: u8, f_in: u8, s_out: u8, f_out: u8) -> SubtitleCue {
158 SubtitleCue::new(
159 id,
160 tc(0, 0, s_in, f_in),
161 tc(0, 0, s_out, f_out),
162 format!("cue {id}"),
163 )
164 }
165
166 #[test]
167 fn test_cue_duration_frames() {
168 let cue = make_cue(1, 0, 0, 1, 0);
169 assert_eq!(cue.duration_frames(), 25);
170 }
171
172 #[test]
173 fn test_cue_duration_secs() {
174 let cue = make_cue(1, 0, 0, 2, 0);
175 let d = cue.duration_secs();
176 assert!((d - 2.0).abs() < 1e-9);
177 }
178
179 #[test]
180 fn test_cue_overlap() {
181 let a = make_cue(1, 0, 0, 2, 0); let b = make_cue(2, 1, 0, 3, 0); assert!(a.overlaps(&b));
184 }
185
186 #[test]
187 fn test_cue_no_overlap() {
188 let a = make_cue(1, 0, 0, 1, 0); let b = make_cue(2, 1, 0, 2, 0); assert!(!a.overlaps(&b));
191 }
192
193 #[test]
194 fn test_apply_frame_offset_positive() {
195 let cues = vec![make_cue(1, 0, 0, 1, 0)];
196 let shifted =
197 apply_frame_offset(&cues, 10, FrameRate::Fps25).expect("frame offset should succeed");
198 assert_eq!(shifted[0].tc_in.to_frames(), 10);
199 assert_eq!(shifted[0].tc_out.to_frames(), 35);
200 }
201
202 #[test]
203 fn test_apply_frame_offset_negative_clamp() {
204 let cues = vec![make_cue(1, 0, 0, 1, 0)];
205 let result = apply_frame_offset(&cues, -100, FrameRate::Fps25);
206 assert!(result.is_err());
207 }
208
209 #[test]
210 fn test_linear_stretch_identity() {
211 let cues = vec![make_cue(1, 1, 0, 2, 0)];
212 let stretched = apply_linear_stretch(&cues, 0, 1.0, FrameRate::Fps25)
213 .expect("linear stretch should succeed");
214 assert_eq!(stretched[0].tc_in.to_frames(), cues[0].tc_in.to_frames());
215 assert_eq!(stretched[0].tc_out.to_frames(), cues[0].tc_out.to_frames());
216 }
217
218 #[test]
219 fn test_linear_stretch_double() {
220 let cues = vec![make_cue(1, 1, 0, 2, 0)]; let stretched = apply_linear_stretch(&cues, 0, 2.0, FrameRate::Fps25)
222 .expect("linear stretch should succeed");
223 assert_eq!(stretched[0].tc_in.to_frames(), 50);
224 assert_eq!(stretched[0].tc_out.to_frames(), 100);
225 }
226
227 #[test]
228 fn test_compute_average_drift_zero() {
229 let cues = vec![make_cue(1, 1, 0, 2, 0)];
230 let refs = vec![25];
231 let drift = compute_average_drift(&cues, &refs).expect("drift computation should succeed");
232 assert!((drift).abs() < 1e-9);
233 }
234
235 #[test]
236 fn test_compute_average_drift_some() {
237 let cues = vec![make_cue(1, 1, 0, 2, 0), make_cue(2, 2, 0, 3, 0)];
238 let refs = vec![20, 45];
239 let drift = compute_average_drift(&cues, &refs).expect("drift computation should succeed");
240 assert!((drift - 5.0).abs() < 1e-9);
242 }
243
244 #[test]
245 fn test_sort_cues_by_in() {
246 let mut cues = vec![make_cue(2, 2, 0, 3, 0), make_cue(1, 0, 0, 1, 0)];
247 sort_cues_by_in(&mut cues);
248 assert_eq!(cues[0].id, 1);
249 assert_eq!(cues[1].id, 2);
250 }
251
252 #[test]
253 fn test_find_overlaps() {
254 let cues = vec![
255 make_cue(1, 0, 0, 2, 0),
256 make_cue(2, 1, 0, 3, 0),
257 make_cue(3, 5, 0, 6, 0),
258 ];
259 let overlaps = find_overlaps(&cues);
260 assert_eq!(overlaps.len(), 1);
261 assert_eq!(overlaps[0], (0, 1));
262 }
263
264 #[test]
265 fn test_compute_average_drift_empty() {
266 let drift = compute_average_drift(&[], &[]);
267 assert!(drift.is_none());
268 }
269}