1#![allow(dead_code)]
6#![allow(clippy::cast_precision_loss)]
7
8use crate::{FrameRate, Timecode, TimecodeError};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
12pub struct FrameOffset {
13 frames: u64,
14}
15
16impl FrameOffset {
17 pub fn new(frames: u64) -> Self {
19 Self { frames }
20 }
21
22 pub fn as_frames(&self) -> u64 {
24 self.frames
25 }
26
27 pub fn add_frames(self, n: u64) -> Self {
29 Self {
30 frames: self.frames + n,
31 }
32 }
33
34 pub fn sub_frames(self, n: u64) -> Self {
36 Self {
37 frames: self.frames.saturating_sub(n),
38 }
39 }
40
41 pub fn diff(self, other: FrameOffset) -> i64 {
43 self.frames as i64 - other.frames as i64
44 }
45
46 pub fn to_timecode(self, frame_rate: FrameRate) -> Result<Timecode, TimecodeError> {
48 Timecode::from_frames(self.frames, frame_rate)
49 }
50
51 pub fn to_seconds(self, frame_rate: FrameRate) -> f64 {
53 let (num, den) = frame_rate.as_rational();
54 self.frames as f64 * den as f64 / num as f64
55 }
56
57 pub fn from_seconds(seconds: f64, frame_rate: FrameRate) -> Self {
59 let (num, den) = frame_rate.as_rational();
60 let frames = (seconds * num as f64 / den as f64).round() as u64;
61 Self { frames }
62 }
63
64 pub fn from_timecode(tc: &Timecode) -> Self {
66 Self {
67 frames: tc.to_frames(),
68 }
69 }
70}
71
72impl From<u64> for FrameOffset {
73 fn from(n: u64) -> Self {
74 Self::new(n)
75 }
76}
77
78#[derive(Debug, Clone)]
82pub struct CrossRateConverter {
83 src_rate: FrameRate,
84 dst_rate: FrameRate,
85}
86
87impl CrossRateConverter {
88 pub fn new(src_rate: FrameRate, dst_rate: FrameRate) -> Self {
90 Self { src_rate, dst_rate }
91 }
92
93 pub fn convert(&self, offset: FrameOffset) -> FrameOffset {
95 let (src_num, src_den) = self.src_rate.as_rational();
97 let (dst_num, dst_den) = self.dst_rate.as_rational();
98 let numerator = offset.frames as u128 * src_den as u128 * dst_num as u128;
101 let denominator = src_num as u128 * dst_den as u128;
102 let dst_frames = (numerator + denominator / 2) / denominator;
103 FrameOffset::new(dst_frames as u64)
104 }
105
106 pub fn seconds_to_offset(&self, seconds: f64) -> FrameOffset {
108 FrameOffset::from_seconds(seconds, self.dst_rate)
109 }
110}
111
112#[derive(Debug, Clone)]
114pub struct OffsetTable {
115 frame_rate: FrameRate,
116 entries: Vec<OffsetEntry>,
117}
118
119#[derive(Debug, Clone, Copy)]
121pub struct OffsetEntry {
122 pub src_frame: u64,
124 pub dst_frame: u64,
126 pub edit_type: EditType,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum EditType {
133 Continuous,
135 Cut,
137 Dissolve,
139 Discontinuity,
141}
142
143impl OffsetTable {
144 pub fn new(frame_rate: FrameRate) -> Self {
146 Self {
147 frame_rate,
148 entries: Vec::new(),
149 }
150 }
151
152 pub fn add_entry(&mut self, src_frame: u64, dst_frame: u64, edit_type: EditType) {
154 self.entries.push(OffsetEntry {
155 src_frame,
156 dst_frame,
157 edit_type,
158 });
159 self.entries.sort_by_key(|e| e.src_frame);
160 }
161
162 pub fn lookup(&self, src_frame: u64) -> Option<&OffsetEntry> {
164 let pos = self.entries.partition_point(|e| e.src_frame <= src_frame);
166 if pos == 0 {
167 None
168 } else {
169 Some(&self.entries[pos - 1])
170 }
171 }
172
173 pub fn len(&self) -> usize {
175 self.entries.len()
176 }
177
178 pub fn is_empty(&self) -> bool {
180 self.entries.is_empty()
181 }
182
183 pub fn frame_rate(&self) -> FrameRate {
185 self.frame_rate
186 }
187
188 pub fn offset_at(&self, src_frame: u64) -> Option<i64> {
190 let entry = self.lookup(src_frame)?;
191 Some(entry.dst_frame as i64 - entry.src_frame as i64)
192 }
193}
194
195pub fn frame_duration(start: FrameOffset, end: FrameOffset, frame_rate: FrameRate) -> f64 {
197 let frames = end.as_frames().saturating_sub(start.as_frames());
198 let (num, den) = frame_rate.as_rational();
199 frames as f64 * den as f64 / num as f64
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_frame_offset_arithmetic() {
208 let a = FrameOffset::new(100);
209 let b = a.add_frames(50);
210 assert_eq!(b.as_frames(), 150);
211 let c = b.sub_frames(30);
212 assert_eq!(c.as_frames(), 120);
213 }
214
215 #[test]
216 fn test_frame_offset_diff() {
217 let a = FrameOffset::new(200);
218 let b = FrameOffset::new(150);
219 assert_eq!(a.diff(b), 50);
220 assert_eq!(b.diff(a), -50);
221 }
222
223 #[test]
224 fn test_frame_offset_to_seconds_25fps() {
225 let offset = FrameOffset::new(25);
226 let secs = offset.to_seconds(FrameRate::Fps25);
227 assert!((secs - 1.0).abs() < 1e-9);
228 }
229
230 #[test]
231 fn test_frame_offset_from_seconds_25fps() {
232 let offset = FrameOffset::from_seconds(2.0, FrameRate::Fps25);
233 assert_eq!(offset.as_frames(), 50);
234 }
235
236 #[test]
237 fn test_frame_offset_from_timecode() {
238 let tc = Timecode::new(0, 0, 1, 0, FrameRate::Fps25).unwrap();
239 let offset = FrameOffset::from_timecode(&tc);
240 assert_eq!(offset.as_frames(), 25);
241 }
242
243 #[test]
244 fn test_frame_offset_to_timecode() {
245 let offset = FrameOffset::new(25);
246 let tc = offset.to_timecode(FrameRate::Fps25).unwrap();
247 assert_eq!(tc.seconds, 1);
248 assert_eq!(tc.frames, 0);
249 }
250
251 #[test]
252 fn test_cross_rate_same_rate() {
253 let conv = CrossRateConverter::new(FrameRate::Fps25, FrameRate::Fps25);
254 let offset = FrameOffset::new(100);
255 let converted = conv.convert(offset);
256 assert_eq!(converted.as_frames(), 100);
257 }
258
259 #[test]
260 fn test_cross_rate_25_to_50() {
261 let conv = CrossRateConverter::new(FrameRate::Fps25, FrameRate::Fps50);
262 let offset = FrameOffset::new(25);
263 let converted = conv.convert(offset);
264 assert_eq!(converted.as_frames(), 50);
265 }
266
267 #[test]
268 fn test_cross_rate_50_to_25() {
269 let conv = CrossRateConverter::new(FrameRate::Fps50, FrameRate::Fps25);
270 let offset = FrameOffset::new(50);
271 let converted = conv.convert(offset);
272 assert_eq!(converted.as_frames(), 25);
273 }
274
275 #[test]
276 fn test_offset_table_lookup() {
277 let mut table = OffsetTable::new(FrameRate::Fps25);
278 table.add_entry(0, 0, EditType::Continuous);
279 table.add_entry(100, 200, EditType::Cut);
280 table.add_entry(300, 400, EditType::Dissolve);
281
282 assert!(table.lookup(0).is_some());
283 let entry = table.lookup(150).unwrap();
284 assert_eq!(entry.src_frame, 100);
285 assert_eq!(entry.dst_frame, 200);
286 }
287
288 #[test]
289 fn test_offset_table_offset_at() {
290 let mut table = OffsetTable::new(FrameRate::Fps25);
291 table.add_entry(0, 10, EditType::Continuous);
292 assert_eq!(table.offset_at(0), Some(10));
293 assert_eq!(table.offset_at(50), Some(10));
294 }
295
296 #[test]
297 fn test_offset_table_empty_lookup() {
298 let table = OffsetTable::new(FrameRate::Fps25);
299 assert!(table.lookup(0).is_none());
300 assert!(table.is_empty());
301 }
302
303 #[test]
304 fn test_frame_duration() {
305 let start = FrameOffset::new(0);
306 let end = FrameOffset::new(25);
307 let dur = frame_duration(start, end, FrameRate::Fps25);
308 assert!((dur - 1.0).abs() < 1e-9);
309 }
310
311 #[test]
312 fn test_frame_offset_sub_saturate() {
313 let a = FrameOffset::new(5);
314 let b = a.sub_frames(100);
315 assert_eq!(b.as_frames(), 0);
316 }
317}