#![allow(dead_code)]
use crate::{timecode_generator::TimecodeGenerator, FrameRate, Timecode, TimecodeError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JamSyncState {
WaitingForReference,
Locking,
Locked,
Holdover,
}
#[derive(Debug, Clone, Copy)]
pub struct JamSyncConfig {
pub lock_threshold: usize,
pub tolerance_frames: u64,
pub holdover_budget: u64,
}
impl Default for JamSyncConfig {
fn default() -> Self {
Self {
lock_threshold: 5,
tolerance_frames: 2,
holdover_budget: 25, }
}
}
pub struct JamSyncController {
state: JamSyncState,
config: JamSyncConfig,
generator: TimecodeGenerator,
frame_rate: FrameRate,
candidate_window: Vec<Timecode>,
last_reference: Option<Timecode>,
frames_since_ref: u64,
consecutive_count: usize,
}
impl JamSyncController {
pub fn new(frame_rate: FrameRate, config: JamSyncConfig) -> Result<Self, TimecodeError> {
let generator = TimecodeGenerator::at_midnight(frame_rate)?;
Ok(Self {
state: JamSyncState::WaitingForReference,
config,
generator,
frame_rate,
candidate_window: Vec::new(),
last_reference: None,
frames_since_ref: 0,
consecutive_count: 0,
})
}
pub fn with_default_config(frame_rate: FrameRate) -> Result<Self, TimecodeError> {
Self::new(frame_rate, JamSyncConfig::default())
}
pub fn state(&self) -> JamSyncState {
self.state
}
pub fn feed_reference(&mut self, tc: Timecode) {
self.frames_since_ref = 0;
match self.state {
JamSyncState::WaitingForReference => {
self.last_reference = Some(tc);
self.consecutive_count = 1;
self.state = JamSyncState::Locking;
}
JamSyncState::Locking => {
if self.is_sequential(tc) {
self.consecutive_count += 1;
if self.consecutive_count >= self.config.lock_threshold {
self.generator.reset_to(tc);
let _ = self.generator.next();
self.state = JamSyncState::Locked;
}
} else {
self.consecutive_count = 1;
}
self.last_reference = Some(tc);
}
JamSyncState::Locked => {
if !self.is_sequential(tc) {
self.generator.reset_to(tc);
let _ = self.generator.next();
}
self.last_reference = Some(tc);
}
JamSyncState::Holdover => {
self.generator.reset_to(tc);
let _ = self.generator.next();
self.last_reference = Some(tc);
self.consecutive_count = 1;
self.state = JamSyncState::Locked;
}
}
}
pub fn output(&mut self) -> Timecode {
self.frames_since_ref += 1;
match self.state {
JamSyncState::Locked => {
if self.frames_since_ref > self.config.holdover_budget {
self.state = JamSyncState::Holdover;
}
self.generator.next()
}
JamSyncState::Holdover => self.generator.next(),
_ => self.generator.peek(),
}
}
pub fn reset(&mut self) -> Result<(), TimecodeError> {
self.state = JamSyncState::WaitingForReference;
self.last_reference = None;
self.frames_since_ref = 0;
self.consecutive_count = 0;
self.candidate_window.clear();
self.generator.reset()
}
pub fn enter_holdover(&mut self) {
if self.state == JamSyncState::Locked {
self.state = JamSyncState::Holdover;
}
}
pub fn frames_since_reference(&self) -> u64 {
self.frames_since_ref
}
fn is_sequential(&self, incoming: Timecode) -> bool {
match self.last_reference {
None => false,
Some(last) => {
let expected = last.to_frames() + 1;
let actual = incoming.to_frames();
let diff = if actual >= expected {
actual - expected
} else {
expected - actual
};
diff <= self.config.tolerance_frames
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_ctrl() -> JamSyncController {
JamSyncController::with_default_config(FrameRate::Fps25).expect("ok")
}
fn seq(start: Timecode, n: usize) -> Vec<Timecode> {
let mut v = Vec::with_capacity(n);
let mut cur = start;
for _ in 0..n {
v.push(cur);
let _ = cur.increment();
}
v
}
#[test]
fn test_initial_state_is_waiting() {
let ctrl = make_ctrl();
assert_eq!(ctrl.state(), JamSyncState::WaitingForReference);
}
#[test]
fn test_first_feed_transitions_to_locking() {
let mut ctrl = make_ctrl();
let tc = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
ctrl.feed_reference(tc);
assert_eq!(ctrl.state(), JamSyncState::Locking);
}
#[test]
fn test_lock_acquired_after_threshold() {
let mut ctrl = make_ctrl();
let start = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
for tc in seq(start, ctrl.config.lock_threshold) {
ctrl.feed_reference(tc);
}
assert_eq!(ctrl.state(), JamSyncState::Locked);
}
#[test]
fn test_lock_not_acquired_before_threshold() {
let mut ctrl = make_ctrl();
let start = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
for tc in seq(start, ctrl.config.lock_threshold - 1) {
ctrl.feed_reference(tc);
}
assert_eq!(ctrl.state(), JamSyncState::Locking);
}
#[test]
fn test_non_sequential_resets_lock_count() {
let mut ctrl = make_ctrl();
let start = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
for tc in seq(start, ctrl.config.lock_threshold - 1) {
ctrl.feed_reference(tc);
}
let jump = Timecode::new(0, 0, 10, 0, FrameRate::Fps25).expect("valid");
ctrl.feed_reference(jump);
assert_eq!(ctrl.state(), JamSyncState::Locking);
assert_eq!(ctrl.consecutive_count, 1);
}
#[test]
fn test_output_tracks_reference_after_lock() {
let mut ctrl = make_ctrl();
let start = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid");
for tc in seq(start, ctrl.config.lock_threshold) {
ctrl.feed_reference(tc);
}
assert_eq!(ctrl.state(), JamSyncState::Locked);
let out = ctrl.output();
let expected_frames = start.to_frames() + ctrl.config.lock_threshold as u64;
assert_eq!(out.to_frames(), expected_frames);
}
#[test]
fn test_holdover_triggered_after_budget_exceeded() {
let budget = 5u64;
let config = JamSyncConfig {
lock_threshold: 3,
tolerance_frames: 2,
holdover_budget: budget,
};
let mut ctrl = JamSyncController::new(FrameRate::Fps25, config).expect("ok");
let start = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
for tc in seq(start, ctrl.config.lock_threshold) {
ctrl.feed_reference(tc);
}
assert_eq!(ctrl.state(), JamSyncState::Locked);
for _ in 0..=budget {
ctrl.output();
}
assert_eq!(ctrl.state(), JamSyncState::Holdover);
}
#[test]
fn test_holdover_keeps_advancing() {
let mut ctrl = make_ctrl();
let start = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
for tc in seq(start, ctrl.config.lock_threshold) {
ctrl.feed_reference(tc);
}
ctrl.enter_holdover();
assert_eq!(ctrl.state(), JamSyncState::Holdover);
let f0 = ctrl.output().to_frames();
let f1 = ctrl.output().to_frames();
assert_eq!(f1, f0 + 1, "generator must keep advancing in holdover");
}
#[test]
fn test_re_lock_from_holdover() {
let mut ctrl = make_ctrl();
let start = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
for tc in seq(start, ctrl.config.lock_threshold) {
ctrl.feed_reference(tc);
}
ctrl.enter_holdover();
let new_ref = Timecode::new(0, 1, 0, 0, FrameRate::Fps25).expect("valid");
ctrl.feed_reference(new_ref);
assert_eq!(ctrl.state(), JamSyncState::Locked);
}
#[test]
fn test_reset_returns_to_waiting() {
let mut ctrl = make_ctrl();
let start = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
for tc in seq(start, ctrl.config.lock_threshold) {
ctrl.feed_reference(tc);
}
ctrl.reset().expect("reset ok");
assert_eq!(ctrl.state(), JamSyncState::WaitingForReference);
}
#[test]
fn test_output_frozen_while_locking() {
let mut ctrl = make_ctrl();
let tc = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
ctrl.feed_reference(tc);
assert_eq!(ctrl.state(), JamSyncState::Locking);
let o1 = ctrl.output();
let o2 = ctrl.output();
assert_eq!(o1, o2, "output must be frozen during locking");
}
#[test]
fn test_frames_since_reference_counter() {
let mut ctrl = make_ctrl();
let start = Timecode::new(0, 0, 0, 0, FrameRate::Fps25).expect("valid");
for tc in seq(start, ctrl.config.lock_threshold) {
ctrl.feed_reference(tc);
}
for _ in 0..3 {
ctrl.output();
}
assert_eq!(ctrl.frames_since_reference(), 3);
let new_tc = Timecode::new(0, 0, 5, 0, FrameRate::Fps25).expect("valid");
ctrl.feed_reference(new_tc);
assert_eq!(ctrl.frames_since_reference(), 0);
}
}