Skip to main content

oximedia_timecode/
tc_interpolate.rs

1#![allow(dead_code)]
2//! Timecode interpolation between known reference points.
3//!
4//! Provides frame-accurate timecode interpolation for situations where
5//! timecode values are only available at certain intervals (e.g., keyframes,
6//! LTC sync points) and intermediate values must be derived.
7
8use crate::{FrameRate, Timecode, TimecodeError};
9
10/// A known timecode reference point at a specific sample or frame position.
11#[derive(Debug, Clone, Copy, PartialEq)]
12pub struct TcRefPoint {
13    /// The timecode value at this reference point.
14    pub timecode: Timecode,
15    /// The absolute sample or frame position in the media stream.
16    pub position: u64,
17}
18
19impl TcRefPoint {
20    /// Creates a new timecode reference point.
21    pub fn new(timecode: Timecode, position: u64) -> Self {
22        Self { timecode, position }
23    }
24}
25
26/// Interpolation strategy for deriving intermediate timecodes.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum InterpolationMode {
29    /// Linear frame counting between reference points.
30    Linear,
31    /// Nearest reference point (snap to closest known value).
32    Nearest,
33    /// Forward-only: always use the preceding reference point and count forward.
34    ForwardOnly,
35}
36
37/// Timecode interpolator that derives frame-accurate timecodes from sparse reference points.
38#[derive(Debug, Clone)]
39pub struct TcInterpolator {
40    /// Sorted list of reference points (by position).
41    refs: Vec<TcRefPoint>,
42    /// Frame rate to use for interpolation.
43    frame_rate: FrameRate,
44    /// Interpolation mode.
45    mode: InterpolationMode,
46    /// Maximum allowable gap (in frames) before interpolation is considered unreliable.
47    max_gap: u64,
48}
49
50impl TcInterpolator {
51    /// Creates a new interpolator with the given frame rate and mode.
52    pub fn new(frame_rate: FrameRate, mode: InterpolationMode) -> Self {
53        Self {
54            refs: Vec::new(),
55            frame_rate,
56            mode,
57            max_gap: 300, // default: 10 seconds at 30fps
58        }
59    }
60
61    /// Sets the maximum allowable gap in frames.
62    pub fn with_max_gap(mut self, gap: u64) -> Self {
63        self.max_gap = gap;
64        self
65    }
66
67    /// Adds a reference point. Points are kept sorted by position.
68    pub fn add_ref(&mut self, point: TcRefPoint) {
69        let idx = self
70            .refs
71            .binary_search_by_key(&point.position, |r| r.position)
72            .unwrap_or_else(|i| i);
73        self.refs.insert(idx, point);
74    }
75
76    /// Returns the number of stored reference points.
77    pub fn ref_count(&self) -> usize {
78        self.refs.len()
79    }
80
81    /// Clears all reference points.
82    pub fn clear(&mut self) {
83        self.refs.clear();
84    }
85
86    /// Returns the frame rate used for interpolation.
87    pub fn frame_rate(&self) -> FrameRate {
88        self.frame_rate
89    }
90
91    /// Returns the interpolation mode.
92    pub fn mode(&self) -> InterpolationMode {
93        self.mode
94    }
95
96    /// Returns the maximum gap setting.
97    pub fn max_gap(&self) -> u64 {
98        self.max_gap
99    }
100
101    /// Interpolates the timecode at the given position.
102    ///
103    /// Returns `None` if there are no reference points or the position is out of range
104    /// for the chosen interpolation mode.
105    pub fn interpolate(&self, position: u64) -> Option<Result<Timecode, TimecodeError>> {
106        if self.refs.is_empty() {
107            return None;
108        }
109
110        match self.mode {
111            InterpolationMode::Linear => self.interpolate_linear(position),
112            InterpolationMode::Nearest => self.interpolate_nearest(position),
113            InterpolationMode::ForwardOnly => self.interpolate_forward(position),
114        }
115    }
116
117    /// Linear interpolation: find the bracketing reference points and count frames.
118    fn interpolate_linear(&self, position: u64) -> Option<Result<Timecode, TimecodeError>> {
119        // If exactly on a reference point, return it directly
120        if let Ok(idx) = self.refs.binary_search_by_key(&position, |r| r.position) {
121            return Some(Ok(self.refs[idx].timecode));
122        }
123
124        // Find the insertion point
125        let idx = self
126            .refs
127            .binary_search_by_key(&position, |r| r.position)
128            .unwrap_or_else(|i| i);
129
130        if idx == 0 {
131            // Before all reference points: extrapolate backward from first
132            let first = &self.refs[0];
133            let delta = first.position.saturating_sub(position);
134            if delta > self.max_gap {
135                return None;
136            }
137            let base_frames = first.timecode.to_frames();
138            let target_frames = base_frames.saturating_sub(delta);
139            Some(Timecode::from_frames(target_frames, self.frame_rate))
140        } else if idx >= self.refs.len() {
141            // After all reference points: extrapolate forward from last
142            let last = &self.refs[self.refs.len() - 1];
143            let delta = position.saturating_sub(last.position);
144            if delta > self.max_gap {
145                return None;
146            }
147            let base_frames = last.timecode.to_frames();
148            Some(Timecode::from_frames(base_frames + delta, self.frame_rate))
149        } else {
150            // Between two points: use the earlier one and count forward
151            let prev = &self.refs[idx - 1];
152            let delta = position - prev.position;
153            if delta > self.max_gap {
154                return None;
155            }
156            let base_frames = prev.timecode.to_frames();
157            Some(Timecode::from_frames(base_frames + delta, self.frame_rate))
158        }
159    }
160
161    /// Nearest-neighbor: snap to the closest reference point.
162    fn interpolate_nearest(&self, position: u64) -> Option<Result<Timecode, TimecodeError>> {
163        let idx = self
164            .refs
165            .binary_search_by_key(&position, |r| r.position)
166            .unwrap_or_else(|i| i);
167
168        let candidate = if idx == 0 {
169            &self.refs[0]
170        } else if idx >= self.refs.len() {
171            &self.refs[self.refs.len() - 1]
172        } else {
173            let dist_prev = position - self.refs[idx - 1].position;
174            let dist_next = self.refs[idx].position - position;
175            if dist_prev <= dist_next {
176                &self.refs[idx - 1]
177            } else {
178                &self.refs[idx]
179            }
180        };
181
182        let gap = position.abs_diff(candidate.position);
183
184        if gap > self.max_gap {
185            return None;
186        }
187
188        Some(Ok(candidate.timecode))
189    }
190
191    /// Forward-only: always use preceding reference and count frames forward.
192    fn interpolate_forward(&self, position: u64) -> Option<Result<Timecode, TimecodeError>> {
193        if let Ok(idx) = self.refs.binary_search_by_key(&position, |r| r.position) {
194            return Some(Ok(self.refs[idx].timecode));
195        }
196
197        let idx = self
198            .refs
199            .binary_search_by_key(&position, |r| r.position)
200            .unwrap_or_else(|i| i);
201
202        if idx == 0 {
203            return None; // no preceding reference
204        }
205
206        let prev = &self.refs[idx - 1];
207        let delta = position - prev.position;
208        if delta > self.max_gap {
209            return None;
210        }
211        let base_frames = prev.timecode.to_frames();
212        Some(Timecode::from_frames(base_frames + delta, self.frame_rate))
213    }
214
215    /// Checks whether the reference points are consistent
216    /// (i.e., timecode increments match positional deltas).
217    pub fn validate_consistency(&self) -> Vec<ConsistencyIssue> {
218        let mut issues = Vec::new();
219        for pair in self.refs.windows(2) {
220            let (a, b) = (&pair[0], &pair[1]);
221            let pos_delta = b.position - a.position;
222            let tc_delta = b
223                .timecode
224                .to_frames()
225                .saturating_sub(a.timecode.to_frames());
226            if pos_delta != tc_delta {
227                issues.push(ConsistencyIssue {
228                    position_a: a.position,
229                    position_b: b.position,
230                    expected_delta: pos_delta,
231                    actual_tc_delta: tc_delta,
232                });
233            }
234        }
235        issues
236    }
237}
238
239/// A consistency issue between two reference points.
240#[derive(Debug, Clone, PartialEq)]
241pub struct ConsistencyIssue {
242    /// Position of the first reference point.
243    pub position_a: u64,
244    /// Position of the second reference point.
245    pub position_b: u64,
246    /// Expected frame delta based on positional difference.
247    pub expected_delta: u64,
248    /// Actual timecode frame delta.
249    pub actual_tc_delta: u64,
250}
251
252/// Batch interpolation result.
253#[derive(Debug, Clone)]
254pub struct BatchInterpolationResult {
255    /// The position that was queried.
256    pub position: u64,
257    /// The interpolated timecode, or `None` if out of range.
258    pub timecode: Option<Timecode>,
259    /// Whether this result is considered reliable (within max_gap).
260    pub reliable: bool,
261}
262
263/// Performs batch interpolation for a sorted list of positions.
264pub fn batch_interpolate(
265    interp: &TcInterpolator,
266    positions: &[u64],
267) -> Vec<BatchInterpolationResult> {
268    positions
269        .iter()
270        .map(|&pos| {
271            let result = interp.interpolate(pos);
272            let (tc, reliable) = match result {
273                Some(Ok(tc)) => (Some(tc), true),
274                Some(Err(_)) => (None, false),
275                None => (None, false),
276            };
277            BatchInterpolationResult {
278                position: pos,
279                timecode: tc,
280                reliable,
281            }
282        })
283        .collect()
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    fn make_tc(h: u8, m: u8, s: u8, f: u8) -> Timecode {
291        Timecode::new(h, m, s, f, FrameRate::Fps25).expect("valid timecode")
292    }
293
294    #[test]
295    fn test_ref_point_creation() {
296        let tc = make_tc(1, 0, 0, 0);
297        let rp = TcRefPoint::new(tc, 90000);
298        assert_eq!(rp.position, 90000);
299        assert_eq!(rp.timecode, tc);
300    }
301
302    #[test]
303    fn test_interpolator_creation() {
304        let interp = TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Linear);
305        assert_eq!(interp.ref_count(), 0);
306        assert_eq!(interp.mode(), InterpolationMode::Linear);
307        assert_eq!(interp.max_gap(), 300);
308    }
309
310    #[test]
311    fn test_add_ref_sorted() {
312        let mut interp = TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Linear);
313        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 1, 0), 25));
314        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 0, 0), 0));
315        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 2, 0), 50));
316        assert_eq!(interp.ref_count(), 3);
317        // Verify sorting
318        assert_eq!(interp.refs[0].position, 0);
319        assert_eq!(interp.refs[1].position, 25);
320        assert_eq!(interp.refs[2].position, 50);
321    }
322
323    #[test]
324    fn test_interpolate_exact_match() {
325        let mut interp = TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Linear);
326        let tc = make_tc(0, 0, 1, 0);
327        interp.add_ref(TcRefPoint::new(tc, 25));
328        let result = interp
329            .interpolate(25)
330            .expect("interpolation should succeed")
331            .expect("interpolation should succeed");
332        assert_eq!(result, tc);
333    }
334
335    #[test]
336    fn test_interpolate_linear_between() {
337        let mut interp = TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Linear);
338        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 0, 0), 0));
339        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 2, 0), 50));
340        // Position 10 should be 10 frames from start => 00:00:00:10
341        let result = interp
342            .interpolate(10)
343            .expect("interpolation should succeed")
344            .expect("interpolation should succeed");
345        assert_eq!(result.hours, 0);
346        assert_eq!(result.minutes, 0);
347        assert_eq!(result.seconds, 0);
348        assert_eq!(result.frames, 10);
349    }
350
351    #[test]
352    fn test_interpolate_forward_extrapolation() {
353        let mut interp =
354            TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Linear).with_max_gap(500);
355        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 0, 0), 0));
356        // Position 30 => extrapolate forward => 00:00:01:05
357        let result = interp
358            .interpolate(30)
359            .expect("interpolation should succeed")
360            .expect("interpolation should succeed");
361        assert_eq!(result.seconds, 1);
362        assert_eq!(result.frames, 5);
363    }
364
365    #[test]
366    fn test_interpolate_empty() {
367        let interp = TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Linear);
368        assert!(interp.interpolate(10).is_none());
369    }
370
371    #[test]
372    fn test_interpolate_nearest() {
373        let mut interp = TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Nearest);
374        let tc0 = make_tc(0, 0, 0, 0);
375        let tc1 = make_tc(0, 0, 2, 0);
376        interp.add_ref(TcRefPoint::new(tc0, 0));
377        interp.add_ref(TcRefPoint::new(tc1, 50));
378        // Position 20 is closer to 0
379        let result = interp
380            .interpolate(20)
381            .expect("interpolation should succeed")
382            .expect("interpolation should succeed");
383        assert_eq!(result, tc0);
384        // Position 30 is closer to 50
385        let result = interp
386            .interpolate(30)
387            .expect("interpolation should succeed")
388            .expect("interpolation should succeed");
389        assert_eq!(result, tc1);
390    }
391
392    #[test]
393    fn test_interpolate_forward_only() {
394        let mut interp = TcInterpolator::new(FrameRate::Fps25, InterpolationMode::ForwardOnly);
395        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 0, 0), 10));
396        // Before first ref => None
397        assert!(interp.interpolate(5).is_none());
398        // After first ref => count forward
399        let result = interp
400            .interpolate(15)
401            .expect("interpolation should succeed")
402            .expect("interpolation should succeed");
403        assert_eq!(result.frames, 5);
404    }
405
406    #[test]
407    fn test_max_gap_exceeded() {
408        let mut interp =
409            TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Linear).with_max_gap(10);
410        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 0, 0), 0));
411        // Position 20 exceeds max_gap of 10
412        assert!(interp.interpolate(20).is_none());
413    }
414
415    #[test]
416    fn test_validate_consistency_ok() {
417        let mut interp = TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Linear);
418        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 0, 0), 0));
419        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 1, 0), 25));
420        let issues = interp.validate_consistency();
421        assert!(issues.is_empty());
422    }
423
424    #[test]
425    fn test_validate_consistency_mismatch() {
426        let mut interp = TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Linear);
427        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 0, 0), 0));
428        // Position delta = 30, but TC delta = 25 frames (1 second)
429        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 1, 0), 30));
430        let issues = interp.validate_consistency();
431        assert_eq!(issues.len(), 1);
432        assert_eq!(issues[0].expected_delta, 30);
433        assert_eq!(issues[0].actual_tc_delta, 25);
434    }
435
436    #[test]
437    fn test_batch_interpolate() {
438        let mut interp =
439            TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Linear).with_max_gap(500);
440        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 0, 0), 0));
441        let positions = vec![0, 5, 10, 25];
442        let results = batch_interpolate(&interp, &positions);
443        assert_eq!(results.len(), 4);
444        assert!(results[0].reliable);
445        assert_eq!(
446            results[0].timecode.expect("timecode should exist").frames,
447            0
448        );
449        assert_eq!(
450            results[2].timecode.expect("timecode should exist").frames,
451            10
452        );
453    }
454
455    #[test]
456    fn test_clear() {
457        let mut interp = TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Linear);
458        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 0, 0), 0));
459        assert_eq!(interp.ref_count(), 1);
460        interp.clear();
461        assert_eq!(interp.ref_count(), 0);
462    }
463}