1#![allow(dead_code)]
2use crate::{timecode_generator::TimecodeGenerator, FrameRate, Timecode, TimecodeError};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum JamSyncState {
25 WaitingForReference,
27 Locking,
30 Locked,
32 Holdover,
35}
36
37#[derive(Debug, Clone, Copy)]
39pub struct JamSyncConfig {
40 pub lock_threshold: usize,
43 pub tolerance_frames: u64,
46 pub holdover_budget: u64,
49}
50
51impl Default for JamSyncConfig {
52 fn default() -> Self {
53 Self {
54 lock_threshold: 5,
55 tolerance_frames: 2,
56 holdover_budget: 25, }
58 }
59}
60
61pub struct JamSyncController {
81 state: JamSyncState,
83 config: JamSyncConfig,
85 generator: TimecodeGenerator,
87 frame_rate: FrameRate,
89 candidate_window: Vec<Timecode>,
91 last_reference: Option<Timecode>,
93 frames_since_ref: u64,
95 consecutive_count: usize,
97}
98
99impl JamSyncController {
100 pub fn new(frame_rate: FrameRate, config: JamSyncConfig) -> Result<Self, TimecodeError> {
108 let generator = TimecodeGenerator::at_midnight(frame_rate)?;
109 Ok(Self {
110 state: JamSyncState::WaitingForReference,
111 config,
112 generator,
113 frame_rate,
114 candidate_window: Vec::new(),
115 last_reference: None,
116 frames_since_ref: 0,
117 consecutive_count: 0,
118 })
119 }
120
121 pub fn with_default_config(frame_rate: FrameRate) -> Result<Self, TimecodeError> {
127 Self::new(frame_rate, JamSyncConfig::default())
128 }
129
130 pub fn state(&self) -> JamSyncState {
132 self.state
133 }
134
135 pub fn feed_reference(&mut self, tc: Timecode) {
144 self.frames_since_ref = 0;
145
146 match self.state {
147 JamSyncState::WaitingForReference => {
148 self.last_reference = Some(tc);
149 self.consecutive_count = 1;
150 self.state = JamSyncState::Locking;
151 }
152
153 JamSyncState::Locking => {
154 if self.is_sequential(tc) {
155 self.consecutive_count += 1;
156 if self.consecutive_count >= self.config.lock_threshold {
157 self.generator.reset_to(tc);
159 let _ = self.generator.next();
161 self.state = JamSyncState::Locked;
162 }
163 } else {
164 self.consecutive_count = 1;
166 }
167 self.last_reference = Some(tc);
168 }
169
170 JamSyncState::Locked => {
171 if !self.is_sequential(tc) {
173 self.generator.reset_to(tc);
175 let _ = self.generator.next();
176 }
177 self.last_reference = Some(tc);
178 }
179
180 JamSyncState::Holdover => {
181 self.generator.reset_to(tc);
183 let _ = self.generator.next();
184 self.last_reference = Some(tc);
185 self.consecutive_count = 1;
186 self.state = JamSyncState::Locked;
187 }
188 }
189 }
190
191 pub fn output(&mut self) -> Timecode {
201 self.frames_since_ref += 1;
203
204 match self.state {
205 JamSyncState::Locked => {
206 if self.frames_since_ref > self.config.holdover_budget {
207 self.state = JamSyncState::Holdover;
208 }
209 self.generator.next()
210 }
211 JamSyncState::Holdover => self.generator.next(),
212 _ => self.generator.peek(),
214 }
215 }
216
217 pub fn reset(&mut self) -> Result<(), TimecodeError> {
225 self.state = JamSyncState::WaitingForReference;
226 self.last_reference = None;
227 self.frames_since_ref = 0;
228 self.consecutive_count = 0;
229 self.candidate_window.clear();
230 self.generator.reset()
231 }
232
233 pub fn enter_holdover(&mut self) {
236 if self.state == JamSyncState::Locked {
237 self.state = JamSyncState::Holdover;
238 }
239 }
240
241 pub fn frames_since_reference(&self) -> u64 {
243 self.frames_since_ref
244 }
245
246 fn is_sequential(&self, incoming: Timecode) -> bool {
254 match self.last_reference {
255 None => false,
256 Some(last) => {
257 let expected = last.to_frames() + 1;
258 let actual = incoming.to_frames();
259 let diff = if actual >= expected {
261 actual - expected
262 } else {
263 expected - actual
264 };
265 diff <= self.config.tolerance_frames
266 }
267 }
268 }
269}
270
271#[cfg(test)]
276mod tests {
277 use super::*;
278
279 fn make_ctrl() -> JamSyncController {
280 JamSyncController::with_default_config(FrameRate::Fps25).expect("ok")
281 }
282
283 fn seq(start: Timecode, n: usize) -> Vec<Timecode> {
285 let mut v = Vec::with_capacity(n);
286 let mut cur = start;
287 for _ in 0..n {
288 v.push(cur);
289 let _ = cur.increment();
290 }
291 v
292 }
293
294 #[test]
295 fn test_initial_state_is_waiting() {
296 let ctrl = make_ctrl();
297 assert_eq!(ctrl.state(), JamSyncState::WaitingForReference);
298 }
299
300 #[test]
301 fn test_first_feed_transitions_to_locking() {
302 let mut ctrl = make_ctrl();
303 let tc = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
304 ctrl.feed_reference(tc);
305 assert_eq!(ctrl.state(), JamSyncState::Locking);
306 }
307
308 #[test]
309 fn test_lock_acquired_after_threshold() {
310 let mut ctrl = make_ctrl();
311 let start = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
312 for tc in seq(start, ctrl.config.lock_threshold) {
313 ctrl.feed_reference(tc);
314 }
315 assert_eq!(ctrl.state(), JamSyncState::Locked);
316 }
317
318 #[test]
319 fn test_lock_not_acquired_before_threshold() {
320 let mut ctrl = make_ctrl();
321 let start = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
322 for tc in seq(start, ctrl.config.lock_threshold - 1) {
324 ctrl.feed_reference(tc);
325 }
326 assert_eq!(ctrl.state(), JamSyncState::Locking);
327 }
328
329 #[test]
330 fn test_non_sequential_resets_lock_count() {
331 let mut ctrl = make_ctrl();
332 let start = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
333 for tc in seq(start, ctrl.config.lock_threshold - 1) {
335 ctrl.feed_reference(tc);
336 }
337 let jump = Timecode::new(0, 0, 10, 0, FrameRate::Fps25).expect("valid");
339 ctrl.feed_reference(jump);
340 assert_eq!(ctrl.state(), JamSyncState::Locking);
341 assert_eq!(ctrl.consecutive_count, 1);
343 }
344
345 #[test]
346 fn test_output_tracks_reference_after_lock() {
347 let mut ctrl = make_ctrl();
348 let start = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid");
349 for tc in seq(start, ctrl.config.lock_threshold) {
350 ctrl.feed_reference(tc);
351 }
352 assert_eq!(ctrl.state(), JamSyncState::Locked);
353 let out = ctrl.output();
355 let expected_frames = start.to_frames() + ctrl.config.lock_threshold as u64;
356 assert_eq!(out.to_frames(), expected_frames);
357 }
358
359 #[test]
360 fn test_holdover_triggered_after_budget_exceeded() {
361 let budget = 5u64;
362 let config = JamSyncConfig {
363 lock_threshold: 3,
364 tolerance_frames: 2,
365 holdover_budget: budget,
366 };
367 let mut ctrl = JamSyncController::new(FrameRate::Fps25, config).expect("ok");
368 let start = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
369 for tc in seq(start, ctrl.config.lock_threshold) {
370 ctrl.feed_reference(tc);
371 }
372 assert_eq!(ctrl.state(), JamSyncState::Locked);
373 for _ in 0..=budget {
375 ctrl.output();
376 }
377 assert_eq!(ctrl.state(), JamSyncState::Holdover);
378 }
379
380 #[test]
381 fn test_holdover_keeps_advancing() {
382 let mut ctrl = make_ctrl();
383 let start = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
384 for tc in seq(start, ctrl.config.lock_threshold) {
385 ctrl.feed_reference(tc);
386 }
387 ctrl.enter_holdover();
388 assert_eq!(ctrl.state(), JamSyncState::Holdover);
389 let f0 = ctrl.output().to_frames();
390 let f1 = ctrl.output().to_frames();
391 assert_eq!(f1, f0 + 1, "generator must keep advancing in holdover");
392 }
393
394 #[test]
395 fn test_re_lock_from_holdover() {
396 let mut ctrl = make_ctrl();
397 let start = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
398 for tc in seq(start, ctrl.config.lock_threshold) {
399 ctrl.feed_reference(tc);
400 }
401 ctrl.enter_holdover();
402 let new_ref = Timecode::new(0, 1, 0, 0, FrameRate::Fps25).expect("valid");
404 ctrl.feed_reference(new_ref);
405 assert_eq!(ctrl.state(), JamSyncState::Locked);
406 }
407
408 #[test]
409 fn test_reset_returns_to_waiting() {
410 let mut ctrl = make_ctrl();
411 let start = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
412 for tc in seq(start, ctrl.config.lock_threshold) {
413 ctrl.feed_reference(tc);
414 }
415 ctrl.reset().expect("reset ok");
416 assert_eq!(ctrl.state(), JamSyncState::WaitingForReference);
417 }
418
419 #[test]
420 fn test_output_frozen_while_locking() {
421 let mut ctrl = make_ctrl();
422 let tc = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
423 ctrl.feed_reference(tc);
424 assert_eq!(ctrl.state(), JamSyncState::Locking);
425 let o1 = ctrl.output();
427 let o2 = ctrl.output();
428 assert_eq!(o1, o2, "output must be frozen during locking");
429 }
430
431 #[test]
432 fn test_frames_since_reference_counter() {
433 let mut ctrl = make_ctrl();
434 let start = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
435 for tc in seq(start, ctrl.config.lock_threshold) {
436 ctrl.feed_reference(tc);
437 }
438 for _ in 0..3 {
440 ctrl.output();
441 }
442 assert_eq!(ctrl.frames_since_reference(), 3);
443 let new_tc = Timecode::new(0, 0, 5, 0, FrameRate::Fps25).expect("valid");
445 ctrl.feed_reference(new_tc);
446 assert_eq!(ctrl.frames_since_reference(), 0);
447 }
448}