Skip to main content

oximedia_timecode/
timecode_generator.rs

1#![allow(dead_code)]
2//! Free-running timecode generator with configurable start time and frame rate.
3//!
4//! [`TimecodeGenerator`] provides an incrementing source of SMPTE timecodes
5//! suitable for free-running playout, offline conforming, or real-time capture
6//! scenarios.  It correctly handles drop-frame minute boundaries and midnight
7//! roll-over.
8
9use crate::{frame_rate_from_info, FrameRate, Timecode, TimecodeError};
10
11/// A free-running timecode generator.
12///
13/// The generator owns a current position expressed as a [`Timecode`] and
14/// advances it one frame at a time each time [`next`](TimecodeGenerator::next)
15/// is called.  The generator can be paused (`running = false`), reset to an
16/// arbitrary position, seeked, and fast-forwarded/rewound by an arbitrary
17/// number of frames.
18#[derive(Debug, Clone)]
19pub struct TimecodeGenerator {
20    /// Current timecode position (the value that will be returned by the next
21    /// call to [`next`](TimecodeGenerator::next)).
22    current: Timecode,
23    /// Whether the generator is advancing on each call to `next`.
24    pub running: bool,
25}
26
27impl TimecodeGenerator {
28    /// Create a new generator starting at `start` with the given `frame_rate`.
29    ///
30    /// The generator starts in the **running** state.
31    ///
32    /// # Errors
33    ///
34    /// Forwards any error from [`Timecode::new`] if `start` describes an
35    /// invalid timecode.
36    pub fn new(start: Timecode) -> Self {
37        Self {
38            current: start,
39            running: true,
40        }
41    }
42
43    /// Create a generator starting at midnight (`00:00:00:00`) for the given
44    /// frame rate.
45    ///
46    /// # Errors
47    ///
48    /// Returns an error if `frame_rate` cannot produce a valid midnight
49    /// timecode (should never occur for well-defined frame rates).
50    pub fn at_midnight(frame_rate: FrameRate) -> Result<Self, TimecodeError> {
51        let tc = Timecode::new(0, 0, 0, 0, frame_rate)?;
52        Ok(Self::new(tc))
53    }
54
55    /// Advance the generator by one frame (if running) and return the
56    /// timecode **before** the increment.
57    ///
58    /// If `running` is `false` the current position is returned without
59    /// advancing.
60    ///
61    /// Midnight roll-over is handled transparently; the generator continues
62    /// from `00:00:00:00` after `23:59:59:FF`.
63    pub fn next(&mut self) -> Timecode {
64        let out = self.current;
65        if self.running {
66            // Silently ignore increment errors (they should not occur for valid TC)
67            let _ = self.current.increment();
68        }
69        out
70    }
71
72    /// Return the current timecode position without advancing.
73    pub fn peek(&self) -> Timecode {
74        self.current
75    }
76
77    /// Reset the generator to midnight (`00:00:00:00`) for its current frame
78    /// rate.
79    ///
80    /// # Errors
81    ///
82    /// Returns an error if building the midnight timecode fails.
83    pub fn reset(&mut self) -> Result<(), TimecodeError> {
84        let rate = frame_rate_from_info(&self.current.frame_rate);
85        self.current = Timecode::new(0, 0, 0, 0, rate)?;
86        Ok(())
87    }
88
89    /// Seek to (reset to) an arbitrary timecode.
90    ///
91    /// The generator adopts the frame rate embedded in `tc`.
92    pub fn reset_to(&mut self, tc: Timecode) {
93        self.current = tc;
94    }
95
96    /// Seek to a specific timecode (alias of [`reset_to`](Self::reset_to)).
97    pub fn seek(&mut self, tc: Timecode) {
98        self.current = tc;
99    }
100
101    /// Skip forward (`n > 0`) or backward (`n < 0`) by `n` frames.
102    ///
103    /// The operation wraps around midnight boundaries correctly using the
104    /// modular arithmetic built into [`Timecode::from_frames`].
105    ///
106    /// # Errors
107    ///
108    /// Returns an error if the resulting frame count cannot be converted back
109    /// to a valid timecode.
110    pub fn skip_frames(&mut self, n: i64) -> Result<(), TimecodeError> {
111        let rate = frame_rate_from_info(&self.current.frame_rate);
112        let fps = self.current.frame_rate.fps as i64;
113        let frames_per_day = fps * 86_400;
114
115        let current_frames = self.current.to_frames() as i64;
116        // Modular arithmetic to handle both forward and backward wrapping
117        let new_frames = if frames_per_day > 0 {
118            ((current_frames + n).rem_euclid(frames_per_day)) as u64
119        } else {
120            (current_frames + n).max(0) as u64
121        };
122
123        self.current = Timecode::from_frames(new_frames, rate)?;
124        Ok(())
125    }
126
127    /// Start the generator (set `running = true`).
128    pub fn start(&mut self) {
129        self.running = true;
130    }
131
132    /// Stop the generator (set `running = false`).
133    pub fn stop(&mut self) {
134        self.running = false;
135    }
136
137    /// Return the frame rate of the current timecode.
138    pub fn frame_rate(&self) -> FrameRate {
139        frame_rate_from_info(&self.current.frame_rate)
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    fn make_gen_25() -> TimecodeGenerator {
148        TimecodeGenerator::at_midnight(FrameRate::Fps25).expect("midnight ok")
149    }
150
151    #[test]
152    fn test_generator_starts_at_midnight() {
153        let gen = make_gen_25();
154        let tc = gen.peek();
155        assert_eq!(tc.hours, 0);
156        assert_eq!(tc.minutes, 0);
157        assert_eq!(tc.seconds, 0);
158        assert_eq!(tc.frames, 0);
159    }
160
161    #[test]
162    fn test_next_increments() {
163        let mut gen = make_gen_25();
164        let tc0 = gen.next();
165        let tc1 = gen.next();
166        assert_eq!(tc0.to_frames() + 1, tc1.to_frames());
167    }
168
169    #[test]
170    fn test_next_returns_current_before_increment() {
171        let mut gen = make_gen_25();
172        let peek = gen.peek();
173        let got = gen.next();
174        assert_eq!(peek, got);
175    }
176
177    #[test]
178    fn test_stop_freezes_position() {
179        let mut gen = make_gen_25();
180        gen.stop();
181        let a = gen.next();
182        let b = gen.next();
183        assert_eq!(a, b);
184    }
185
186    #[test]
187    fn test_start_resumes_after_stop() {
188        let mut gen = make_gen_25();
189        gen.stop();
190        let _ = gen.next();
191        gen.start();
192        let before = gen.peek().to_frames();
193        let _ = gen.next();
194        let after = gen.peek().to_frames();
195        assert_eq!(after, before + 1);
196    }
197
198    #[test]
199    fn test_reset_to_midnight() {
200        let mut gen = make_gen_25();
201        // Advance a few frames
202        for _ in 0..100 {
203            let _ = gen.next();
204        }
205        gen.reset().expect("reset ok");
206        assert_eq!(gen.peek().to_frames(), 0);
207    }
208
209    #[test]
210    fn test_reset_to_arbitrary_tc() {
211        let mut gen = make_gen_25();
212        let target = Timecode::new(12, 34, 56, 10, FrameRate::Fps25).expect("valid");
213        gen.reset_to(target);
214        assert_eq!(gen.peek(), target);
215    }
216
217    #[test]
218    fn test_seek_alias() {
219        let mut gen = make_gen_25();
220        let target = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid");
221        gen.seek(target);
222        assert_eq!(gen.peek(), target);
223    }
224
225    #[test]
226    fn test_skip_forward() {
227        let mut gen = make_gen_25();
228        gen.skip_frames(100).expect("skip ok");
229        assert_eq!(gen.peek().to_frames(), 100);
230    }
231
232    #[test]
233    fn test_skip_backward_wraps() {
234        let mut gen = make_gen_25();
235        // Skipping backward from midnight wraps to end of day
236        gen.skip_frames(-1).expect("skip ok");
237        let frames_per_day = 25u64 * 86_400;
238        assert_eq!(gen.peek().to_frames(), frames_per_day - 1);
239    }
240
241    #[test]
242    fn test_skip_forward_wraps_midnight() {
243        let mut gen = make_gen_25();
244        let frames_per_day = 25i64 * 86_400;
245        // skip exactly one full day forward — should land back at midnight
246        gen.skip_frames(frames_per_day).expect("skip ok");
247        assert_eq!(gen.peek().to_frames(), 0);
248    }
249
250    #[test]
251    fn test_drop_frame_generator_next_at_minute_boundary() {
252        // Seek to just before 1-minute mark in 29.97 DF and verify next() skips frames 0+1
253        let start = Timecode::new(0, 0, 59, 29, FrameRate::Fps2997DF).expect("valid");
254        let mut gen = TimecodeGenerator::new(start);
255        let tc = gen.next(); // returns 00:00:59:29
256        assert_eq!(tc.frames, 29);
257        let next = gen.next(); // should advance to 00:01:00:02
258        assert_eq!(next.minutes, 1);
259        assert_eq!(next.seconds, 0);
260        assert_eq!(next.frames, 2);
261    }
262}