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).unwrap()
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.interpolate(25).unwrap().unwrap();
329        assert_eq!(result, tc);
330    }
331
332    #[test]
333    fn test_interpolate_linear_between() {
334        let mut interp = TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Linear);
335        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 0, 0), 0));
336        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 2, 0), 50));
337        // Position 10 should be 10 frames from start => 00:00:00:10
338        let result = interp.interpolate(10).unwrap().unwrap();
339        assert_eq!(result.hours, 0);
340        assert_eq!(result.minutes, 0);
341        assert_eq!(result.seconds, 0);
342        assert_eq!(result.frames, 10);
343    }
344
345    #[test]
346    fn test_interpolate_forward_extrapolation() {
347        let mut interp =
348            TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Linear).with_max_gap(500);
349        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 0, 0), 0));
350        // Position 30 => extrapolate forward => 00:00:01:05
351        let result = interp.interpolate(30).unwrap().unwrap();
352        assert_eq!(result.seconds, 1);
353        assert_eq!(result.frames, 5);
354    }
355
356    #[test]
357    fn test_interpolate_empty() {
358        let interp = TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Linear);
359        assert!(interp.interpolate(10).is_none());
360    }
361
362    #[test]
363    fn test_interpolate_nearest() {
364        let mut interp = TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Nearest);
365        let tc0 = make_tc(0, 0, 0, 0);
366        let tc1 = make_tc(0, 0, 2, 0);
367        interp.add_ref(TcRefPoint::new(tc0, 0));
368        interp.add_ref(TcRefPoint::new(tc1, 50));
369        // Position 20 is closer to 0
370        let result = interp.interpolate(20).unwrap().unwrap();
371        assert_eq!(result, tc0);
372        // Position 30 is closer to 50
373        let result = interp.interpolate(30).unwrap().unwrap();
374        assert_eq!(result, tc1);
375    }
376
377    #[test]
378    fn test_interpolate_forward_only() {
379        let mut interp = TcInterpolator::new(FrameRate::Fps25, InterpolationMode::ForwardOnly);
380        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 0, 0), 10));
381        // Before first ref => None
382        assert!(interp.interpolate(5).is_none());
383        // After first ref => count forward
384        let result = interp.interpolate(15).unwrap().unwrap();
385        assert_eq!(result.frames, 5);
386    }
387
388    #[test]
389    fn test_max_gap_exceeded() {
390        let mut interp =
391            TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Linear).with_max_gap(10);
392        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 0, 0), 0));
393        // Position 20 exceeds max_gap of 10
394        assert!(interp.interpolate(20).is_none());
395    }
396
397    #[test]
398    fn test_validate_consistency_ok() {
399        let mut interp = TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Linear);
400        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 0, 0), 0));
401        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 1, 0), 25));
402        let issues = interp.validate_consistency();
403        assert!(issues.is_empty());
404    }
405
406    #[test]
407    fn test_validate_consistency_mismatch() {
408        let mut interp = TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Linear);
409        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 0, 0), 0));
410        // Position delta = 30, but TC delta = 25 frames (1 second)
411        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 1, 0), 30));
412        let issues = interp.validate_consistency();
413        assert_eq!(issues.len(), 1);
414        assert_eq!(issues[0].expected_delta, 30);
415        assert_eq!(issues[0].actual_tc_delta, 25);
416    }
417
418    #[test]
419    fn test_batch_interpolate() {
420        let mut interp =
421            TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Linear).with_max_gap(500);
422        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 0, 0), 0));
423        let positions = vec![0, 5, 10, 25];
424        let results = batch_interpolate(&interp, &positions);
425        assert_eq!(results.len(), 4);
426        assert!(results[0].reliable);
427        assert_eq!(results[0].timecode.unwrap().frames, 0);
428        assert_eq!(results[2].timecode.unwrap().frames, 10);
429    }
430
431    #[test]
432    fn test_clear() {
433        let mut interp = TcInterpolator::new(FrameRate::Fps25, InterpolationMode::Linear);
434        interp.add_ref(TcRefPoint::new(make_tc(0, 0, 0, 0), 0));
435        assert_eq!(interp.ref_count(), 1);
436        interp.clear();
437        assert_eq!(interp.ref_count(), 0);
438    }
439}