use crate::{FrameRate, Timecode, TimecodeError};
pub struct TimecodeSynchronizer {
frame_rate: FrameRate,
current_timecode: Option<Timecode>,
ltc_state: SourceState,
vitc_state: SourceState,
jam_sync: Option<JamSyncState>,
drift_corrector: DriftCorrector,
strategy: ReconciliationStrategy,
}
impl TimecodeSynchronizer {
pub fn new(frame_rate: FrameRate, strategy: ReconciliationStrategy) -> Self {
TimecodeSynchronizer {
frame_rate,
current_timecode: None,
ltc_state: SourceState::new("LTC"),
vitc_state: SourceState::new("VITC"),
jam_sync: None,
drift_corrector: DriftCorrector::new(frame_rate),
strategy,
}
}
pub fn update_ltc(&mut self, timecode: Timecode) -> Result<(), TimecodeError> {
self.ltc_state.update(timecode);
self.reconcile()
}
pub fn update_vitc(&mut self, timecode: Timecode) -> Result<(), TimecodeError> {
self.vitc_state.update(timecode);
self.reconcile()
}
pub fn get_timecode(&self) -> Option<Timecode> {
self.current_timecode
}
pub fn jam_sync(&mut self, reference: Timecode) {
self.jam_sync = Some(JamSyncState::new(reference, self.frame_rate));
self.current_timecode = Some(reference);
}
pub fn disable_jam_sync(&mut self) {
self.jam_sync = None;
}
pub fn is_jam_synced(&self) -> bool {
self.jam_sync.is_some()
}
fn reconcile(&mut self) -> Result<(), TimecodeError> {
let ltc_tc = self.ltc_state.last_timecode();
let vitc_tc = self.vitc_state.last_timecode();
let new_timecode = match self.strategy {
ReconciliationStrategy::PreferLtc => ltc_tc.or(vitc_tc),
ReconciliationStrategy::PreferVitc => vitc_tc.or(ltc_tc),
ReconciliationStrategy::CrossCheck => self.cross_check_timecodes(ltc_tc, vitc_tc),
ReconciliationStrategy::MostRecent => self.select_most_recent(ltc_tc, vitc_tc),
};
if let Some(tc) = new_timecode {
let corrected = self.drift_corrector.correct(tc)?;
self.current_timecode = Some(corrected);
}
Ok(())
}
fn cross_check_timecodes(
&self,
ltc: Option<Timecode>,
vitc: Option<Timecode>,
) -> Option<Timecode> {
match (ltc, vitc) {
(Some(ltc_tc), Some(vitc_tc)) => {
let ltc_frames = ltc_tc.to_frames();
let vitc_frames = vitc_tc.to_frames();
let diff = (ltc_frames as i64 - vitc_frames as i64).abs();
if diff <= 2 {
Some(ltc_tc)
} else {
if self.ltc_state.is_reliable() {
Some(ltc_tc)
} else {
Some(vitc_tc)
}
}
}
(Some(tc), None) | (None, Some(tc)) => Some(tc),
(None, None) => None,
}
}
fn select_most_recent(
&self,
ltc: Option<Timecode>,
vitc: Option<Timecode>,
) -> Option<Timecode> {
let ltc_age = self.ltc_state.age_ms();
let vitc_age = self.vitc_state.age_ms();
match (ltc, vitc) {
(Some(ltc_tc), Some(_vitc_tc)) => {
if ltc_age < vitc_age {
Some(ltc_tc)
} else {
Some(_vitc_tc)
}
}
(Some(tc), None) | (None, Some(tc)) => Some(tc),
(None, None) => None,
}
}
pub fn reset(&mut self) {
self.current_timecode = None;
self.ltc_state.reset();
self.vitc_state.reset();
self.jam_sync = None;
self.drift_corrector.reset();
}
pub fn status(&self) -> SyncStatus {
SyncStatus {
is_synchronized: self.current_timecode.is_some(),
ltc_available: self.ltc_state.is_available(),
vitc_available: self.vitc_state.is_available(),
jam_sync_active: self.jam_sync.is_some(),
drift_ppm: self.drift_corrector.drift_ppm(),
}
}
}
struct SourceState {
#[allow(dead_code)]
name: String,
last_timecode: Option<Timecode>,
last_update_ms: u64,
reliability: f32,
#[allow(dead_code)]
error_count: u32,
}
impl SourceState {
fn new(name: &str) -> Self {
SourceState {
name: name.to_string(),
last_timecode: None,
last_update_ms: 0,
reliability: 0.0,
error_count: 0,
}
}
fn update(&mut self, timecode: Timecode) {
if let Some(last) = self.last_timecode {
let expected_frames = last.to_frames() + 1;
let actual_frames = timecode.to_frames();
if (expected_frames as i64 - actual_frames as i64).abs() > 5 {
self.error_count += 1;
self.reliability = (self.reliability - 0.1).max(0.0);
} else {
self.reliability = (self.reliability + 0.1).min(1.0);
}
}
self.last_timecode = Some(timecode);
self.last_update_ms = current_time_ms();
}
fn last_timecode(&self) -> Option<Timecode> {
self.last_timecode
}
fn is_available(&self) -> bool {
self.last_timecode.is_some() && self.age_ms() < 1000
}
fn is_reliable(&self) -> bool {
self.reliability > 0.7
}
fn age_ms(&self) -> u64 {
current_time_ms().saturating_sub(self.last_update_ms)
}
fn reset(&mut self) {
self.last_timecode = None;
self.last_update_ms = 0;
self.reliability = 0.0;
self.error_count = 0;
}
}
#[allow(dead_code)]
struct JamSyncState {
reference: Timecode,
frame_rate: FrameRate,
start_time_ms: u64,
accumulated_frames: u64,
}
impl JamSyncState {
fn new(reference: Timecode, frame_rate: FrameRate) -> Self {
JamSyncState {
reference,
frame_rate,
start_time_ms: current_time_ms(),
accumulated_frames: 0,
}
}
#[allow(dead_code)]
fn generate_current(&mut self) -> Result<Timecode, TimecodeError> {
let elapsed_ms = current_time_ms().saturating_sub(self.start_time_ms);
let fps = self.frame_rate.as_float();
let frames = ((elapsed_ms as f64 / 1000.0) * fps) as u64;
let total_frames = self.reference.to_frames() + frames;
Timecode::from_frames(total_frames, self.frame_rate)
}
}
struct DriftCorrector {
frame_rate: FrameRate,
reference_frames: u64,
actual_frames: u64,
drift_ppm: f32,
history: Vec<(u64, u64)>, }
impl DriftCorrector {
fn new(frame_rate: FrameRate) -> Self {
DriftCorrector {
frame_rate,
reference_frames: 0,
actual_frames: 0,
drift_ppm: 0.0,
history: Vec::new(),
}
}
fn correct(&mut self, timecode: Timecode) -> Result<Timecode, TimecodeError> {
let frames = timecode.to_frames();
let timestamp = current_time_ms();
self.history.push((timestamp, frames));
if self.history.len() > 100 {
self.history.remove(0);
}
if self.history.len() >= 10 {
self.calculate_drift();
}
if self.drift_ppm.abs() > 100.0 {
let correction_frames = (frames as f32 * self.drift_ppm / 1_000_000.0) as i64;
let corrected_frames = (frames as i64 + correction_frames).max(0) as u64;
Timecode::from_frames(corrected_frames, self.frame_rate)
} else {
Ok(timecode)
}
}
fn calculate_drift(&mut self) {
if self.history.len() < 2 {
return;
}
let (first_time, first_frames) = self.history[0];
let (last_time, last_frames) = match self.history.last() {
Some(v) => *v,
None => return,
};
let time_diff_ms = last_time.saturating_sub(first_time);
let frame_diff = last_frames.saturating_sub(first_frames);
if time_diff_ms > 0 {
let expected_frames = (time_diff_ms as f64 / 1000.0) * self.frame_rate.as_float();
let drift = (frame_diff as f64 - expected_frames) / expected_frames;
self.drift_ppm = (drift * 1_000_000.0) as f32;
}
}
fn drift_ppm(&self) -> f32 {
self.drift_ppm
}
fn reset(&mut self) {
self.reference_frames = 0;
self.actual_frames = 0;
self.drift_ppm = 0.0;
self.history.clear();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReconciliationStrategy {
PreferLtc,
PreferVitc,
CrossCheck,
MostRecent,
}
#[derive(Debug, Clone)]
pub struct SyncStatus {
pub is_synchronized: bool,
pub ltc_available: bool,
pub vitc_available: bool,
pub jam_sync_active: bool,
pub drift_ppm: f32,
}
pub struct GenlockSynchronizer {
frame_rate: FrameRate,
reference_phase: f32,
current_phase: f32,
phase_error: f32,
locked: bool,
}
impl GenlockSynchronizer {
pub fn new(frame_rate: FrameRate) -> Self {
GenlockSynchronizer {
frame_rate,
reference_phase: 0.0,
current_phase: 0.0,
phase_error: 0.0,
locked: false,
}
}
pub fn update_reference(&mut self, phase: f32) {
self.reference_phase = phase;
self.calculate_phase_error();
}
pub fn update_timecode(&mut self, timecode: &Timecode) {
let frames = timecode.frames as f32;
let fps = self.frame_rate.frames_per_second() as f32;
self.current_phase = frames / fps;
self.calculate_phase_error();
}
fn calculate_phase_error(&mut self) {
self.phase_error = self.current_phase - self.reference_phase;
while self.phase_error > 0.5 {
self.phase_error -= 1.0;
}
while self.phase_error < -0.5 {
self.phase_error += 1.0;
}
self.locked = self.phase_error.abs() < 0.01;
}
pub fn is_locked(&self) -> bool {
self.locked
}
pub fn phase_error(&self) -> f32 {
self.phase_error
}
pub fn correction_frames(&self) -> i32 {
let fps = self.frame_rate.frames_per_second() as f32;
(self.phase_error * fps) as i32
}
pub fn reset(&mut self) {
self.reference_phase = 0.0;
self.current_phase = 0.0;
self.phase_error = 0.0;
self.locked = false;
}
}
pub struct TimecodeAggregator {
sources: Vec<TimecodeSource>,
strategy: VotingStrategy,
}
impl TimecodeAggregator {
pub fn new(strategy: VotingStrategy) -> Self {
TimecodeAggregator {
sources: Vec::new(),
strategy,
}
}
pub fn add_source(&mut self, name: String, timecode: Timecode, confidence: f32) {
self.sources.push(TimecodeSource {
name,
timecode,
confidence,
});
}
pub fn clear_sources(&mut self) {
self.sources.clear();
}
pub fn aggregate(&self) -> Option<Timecode> {
if self.sources.is_empty() {
return None;
}
match self.strategy {
VotingStrategy::Unanimous => self.unanimous_vote(),
VotingStrategy::Majority => self.majority_vote(),
VotingStrategy::HighestConfidence => self.highest_confidence(),
VotingStrategy::WeightedAverage => self.weighted_average(),
}
}
fn unanimous_vote(&self) -> Option<Timecode> {
if self.sources.is_empty() {
return None;
}
let first = &self.sources[0].timecode;
for source in &self.sources[1..] {
if source.timecode.to_frames() != first.to_frames() {
return None;
}
}
Some(*first)
}
fn majority_vote(&self) -> Option<Timecode> {
if self.sources.is_empty() {
return None;
}
let mut counts: Vec<(u64, usize)> = Vec::new();
for source in &self.sources {
let frames = source.timecode.to_frames();
if let Some(entry) = counts.iter_mut().find(|(f, _)| *f == frames) {
entry.1 += 1;
} else {
counts.push((frames, 1));
}
}
counts.sort_by(|a, b| b.1.cmp(&a.1));
if let Some((frames, _)) = counts.first() {
self.sources
.iter()
.find(|s| s.timecode.to_frames() == *frames)
.map(|s| s.timecode)
} else {
None
}
}
fn highest_confidence(&self) -> Option<Timecode> {
self.sources
.iter()
.max_by(|a, b| {
a.confidence
.partial_cmp(&b.confidence)
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|s| s.timecode)
}
fn weighted_average(&self) -> Option<Timecode> {
if self.sources.is_empty() {
return None;
}
let total_weight: f32 = self.sources.iter().map(|s| s.confidence).sum();
if total_weight == 0.0 {
return None;
}
let weighted_frames: f64 = self
.sources
.iter()
.map(|s| s.timecode.to_frames() as f64 * s.confidence as f64)
.sum();
let avg_frames = (weighted_frames / total_weight as f64) as u64;
Timecode::from_frames(avg_frames, FrameRate::Fps25).ok()
}
}
#[derive(Debug, Clone)]
struct TimecodeSource {
#[allow(dead_code)]
name: String,
timecode: Timecode,
confidence: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VotingStrategy {
Unanimous,
Majority,
HighestConfidence,
WeightedAverage,
}
fn current_time_ms() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_synchronizer_creation() {
let sync = TimecodeSynchronizer::new(FrameRate::Fps25, ReconciliationStrategy::PreferLtc);
assert!(sync.get_timecode().is_none());
}
#[test]
fn test_jam_sync() {
let mut sync =
TimecodeSynchronizer::new(FrameRate::Fps25, ReconciliationStrategy::PreferLtc);
let tc = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid timecode");
sync.jam_sync(tc);
assert!(sync.is_jam_synced());
}
#[test]
fn test_genlock() {
let mut genlock = GenlockSynchronizer::new(FrameRate::Fps25);
genlock.update_reference(0.0);
let tc = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid timecode");
genlock.update_timecode(&tc);
assert!(genlock.is_locked());
genlock.update_reference(0.5);
let tc2 = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid timecode");
genlock.update_timecode(&tc2);
assert!(!genlock.is_locked());
}
#[test]
fn test_aggregator() {
let mut agg = TimecodeAggregator::new(VotingStrategy::HighestConfidence);
let tc1 = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid timecode");
let tc2 = Timecode::new(1, 0, 0, 1, FrameRate::Fps25).expect("valid timecode");
agg.add_source("LTC".to_string(), tc1, 0.8);
agg.add_source("VITC".to_string(), tc2, 0.9);
let result = agg.aggregate();
assert_eq!(result, Some(tc2));
}
}