1use crate::{FrameRate, Timecode, TimecodeError};
11
12pub struct TimecodeSynchronizer {
14 frame_rate: FrameRate,
16 current_timecode: Option<Timecode>,
18 ltc_state: SourceState,
20 vitc_state: SourceState,
22 jam_sync: Option<JamSyncState>,
24 drift_corrector: DriftCorrector,
26 strategy: ReconciliationStrategy,
28}
29
30impl TimecodeSynchronizer {
31 pub fn new(frame_rate: FrameRate, strategy: ReconciliationStrategy) -> Self {
33 TimecodeSynchronizer {
34 frame_rate,
35 current_timecode: None,
36 ltc_state: SourceState::new("LTC"),
37 vitc_state: SourceState::new("VITC"),
38 jam_sync: None,
39 drift_corrector: DriftCorrector::new(frame_rate),
40 strategy,
41 }
42 }
43
44 pub fn update_ltc(&mut self, timecode: Timecode) -> Result<(), TimecodeError> {
46 self.ltc_state.update(timecode);
47 self.reconcile()
48 }
49
50 pub fn update_vitc(&mut self, timecode: Timecode) -> Result<(), TimecodeError> {
52 self.vitc_state.update(timecode);
53 self.reconcile()
54 }
55
56 pub fn get_timecode(&self) -> Option<Timecode> {
58 self.current_timecode
59 }
60
61 pub fn jam_sync(&mut self, reference: Timecode) {
63 self.jam_sync = Some(JamSyncState::new(reference, self.frame_rate));
64 self.current_timecode = Some(reference);
65 }
66
67 pub fn disable_jam_sync(&mut self) {
69 self.jam_sync = None;
70 }
71
72 pub fn is_jam_synced(&self) -> bool {
74 self.jam_sync.is_some()
75 }
76
77 fn reconcile(&mut self) -> Result<(), TimecodeError> {
79 let ltc_tc = self.ltc_state.last_timecode();
80 let vitc_tc = self.vitc_state.last_timecode();
81
82 let new_timecode = match self.strategy {
83 ReconciliationStrategy::PreferLtc => ltc_tc.or(vitc_tc),
84 ReconciliationStrategy::PreferVitc => vitc_tc.or(ltc_tc),
85 ReconciliationStrategy::CrossCheck => self.cross_check_timecodes(ltc_tc, vitc_tc),
86 ReconciliationStrategy::MostRecent => self.select_most_recent(ltc_tc, vitc_tc),
87 };
88
89 if let Some(tc) = new_timecode {
90 let corrected = self.drift_corrector.correct(tc)?;
92 self.current_timecode = Some(corrected);
93 }
94
95 Ok(())
96 }
97
98 fn cross_check_timecodes(
100 &self,
101 ltc: Option<Timecode>,
102 vitc: Option<Timecode>,
103 ) -> Option<Timecode> {
104 match (ltc, vitc) {
105 (Some(ltc_tc), Some(vitc_tc)) => {
106 let ltc_frames = ltc_tc.to_frames();
108 let vitc_frames = vitc_tc.to_frames();
109 let diff = (ltc_frames as i64 - vitc_frames as i64).abs();
110
111 if diff <= 2 {
112 Some(ltc_tc)
114 } else {
115 if self.ltc_state.is_reliable() {
117 Some(ltc_tc)
118 } else {
119 Some(vitc_tc)
120 }
121 }
122 }
123 (Some(tc), None) | (None, Some(tc)) => Some(tc),
124 (None, None) => None,
125 }
126 }
127
128 fn select_most_recent(
130 &self,
131 ltc: Option<Timecode>,
132 vitc: Option<Timecode>,
133 ) -> Option<Timecode> {
134 let ltc_age = self.ltc_state.age_ms();
135 let vitc_age = self.vitc_state.age_ms();
136
137 match (ltc, vitc) {
138 (Some(ltc_tc), Some(_vitc_tc)) => {
139 if ltc_age < vitc_age {
140 Some(ltc_tc)
141 } else {
142 Some(_vitc_tc)
143 }
144 }
145 (Some(tc), None) | (None, Some(tc)) => Some(tc),
146 (None, None) => None,
147 }
148 }
149
150 pub fn reset(&mut self) {
152 self.current_timecode = None;
153 self.ltc_state.reset();
154 self.vitc_state.reset();
155 self.jam_sync = None;
156 self.drift_corrector.reset();
157 }
158
159 pub fn status(&self) -> SyncStatus {
161 SyncStatus {
162 is_synchronized: self.current_timecode.is_some(),
163 ltc_available: self.ltc_state.is_available(),
164 vitc_available: self.vitc_state.is_available(),
165 jam_sync_active: self.jam_sync.is_some(),
166 drift_ppm: self.drift_corrector.drift_ppm(),
167 }
168 }
169}
170
171struct SourceState {
173 #[allow(dead_code)]
175 name: String,
176 last_timecode: Option<Timecode>,
178 last_update_ms: u64,
180 reliability: f32,
182 #[allow(dead_code)]
184 error_count: u32,
185}
186
187impl SourceState {
188 fn new(name: &str) -> Self {
189 SourceState {
190 name: name.to_string(),
191 last_timecode: None,
192 last_update_ms: 0,
193 reliability: 0.0,
194 error_count: 0,
195 }
196 }
197
198 fn update(&mut self, timecode: Timecode) {
199 if let Some(last) = self.last_timecode {
201 let expected_frames = last.to_frames() + 1;
202 let actual_frames = timecode.to_frames();
203
204 if (expected_frames as i64 - actual_frames as i64).abs() > 5 {
205 self.error_count += 1;
206 self.reliability = (self.reliability - 0.1).max(0.0);
207 } else {
208 self.reliability = (self.reliability + 0.1).min(1.0);
209 }
210 }
211
212 self.last_timecode = Some(timecode);
213 self.last_update_ms = current_time_ms();
214 }
215
216 fn last_timecode(&self) -> Option<Timecode> {
217 self.last_timecode
218 }
219
220 fn is_available(&self) -> bool {
221 self.last_timecode.is_some() && self.age_ms() < 1000
222 }
223
224 fn is_reliable(&self) -> bool {
225 self.reliability > 0.7
226 }
227
228 fn age_ms(&self) -> u64 {
229 current_time_ms().saturating_sub(self.last_update_ms)
230 }
231
232 fn reset(&mut self) {
233 self.last_timecode = None;
234 self.last_update_ms = 0;
235 self.reliability = 0.0;
236 self.error_count = 0;
237 }
238}
239
240#[allow(dead_code)]
242struct JamSyncState {
243 reference: Timecode,
245 frame_rate: FrameRate,
247 start_time_ms: u64,
249 accumulated_frames: u64,
251}
252
253impl JamSyncState {
254 fn new(reference: Timecode, frame_rate: FrameRate) -> Self {
255 JamSyncState {
256 reference,
257 frame_rate,
258 start_time_ms: current_time_ms(),
259 accumulated_frames: 0,
260 }
261 }
262
263 #[allow(dead_code)]
265 fn generate_current(&mut self) -> Result<Timecode, TimecodeError> {
266 let elapsed_ms = current_time_ms().saturating_sub(self.start_time_ms);
267 let fps = self.frame_rate.as_float();
268 let frames = ((elapsed_ms as f64 / 1000.0) * fps) as u64;
269
270 let total_frames = self.reference.to_frames() + frames;
271 Timecode::from_frames(total_frames, self.frame_rate)
272 }
273}
274
275struct DriftCorrector {
277 frame_rate: FrameRate,
279 reference_frames: u64,
281 actual_frames: u64,
283 drift_ppm: f32,
285 history: Vec<(u64, u64)>, }
288
289impl DriftCorrector {
290 fn new(frame_rate: FrameRate) -> Self {
291 DriftCorrector {
292 frame_rate,
293 reference_frames: 0,
294 actual_frames: 0,
295 drift_ppm: 0.0,
296 history: Vec::new(),
297 }
298 }
299
300 fn correct(&mut self, timecode: Timecode) -> Result<Timecode, TimecodeError> {
302 let frames = timecode.to_frames();
303 let timestamp = current_time_ms();
304
305 self.history.push((timestamp, frames));
307 if self.history.len() > 100 {
308 self.history.remove(0);
309 }
310
311 if self.history.len() >= 10 {
313 self.calculate_drift();
314 }
315
316 if self.drift_ppm.abs() > 100.0 {
318 let correction_frames = (frames as f32 * self.drift_ppm / 1_000_000.0) as i64;
319 let corrected_frames = (frames as i64 + correction_frames).max(0) as u64;
320 Timecode::from_frames(corrected_frames, self.frame_rate)
321 } else {
322 Ok(timecode)
323 }
324 }
325
326 fn calculate_drift(&mut self) {
328 if self.history.len() < 2 {
329 return;
330 }
331
332 let (first_time, first_frames) = self.history[0];
333 let (last_time, last_frames) = *self.history.last().unwrap();
334
335 let time_diff_ms = last_time.saturating_sub(first_time);
336 let frame_diff = last_frames.saturating_sub(first_frames);
337
338 if time_diff_ms > 0 {
339 let expected_frames = (time_diff_ms as f64 / 1000.0) * self.frame_rate.as_float();
340 let drift = (frame_diff as f64 - expected_frames) / expected_frames;
341 self.drift_ppm = (drift * 1_000_000.0) as f32;
342 }
343 }
344
345 fn drift_ppm(&self) -> f32 {
346 self.drift_ppm
347 }
348
349 fn reset(&mut self) {
350 self.reference_frames = 0;
351 self.actual_frames = 0;
352 self.drift_ppm = 0.0;
353 self.history.clear();
354 }
355}
356
357#[derive(Debug, Clone, Copy, PartialEq, Eq)]
359pub enum ReconciliationStrategy {
360 PreferLtc,
362 PreferVitc,
364 CrossCheck,
366 MostRecent,
368}
369
370#[derive(Debug, Clone)]
372pub struct SyncStatus {
373 pub is_synchronized: bool,
375 pub ltc_available: bool,
377 pub vitc_available: bool,
379 pub jam_sync_active: bool,
381 pub drift_ppm: f32,
383}
384
385pub struct GenlockSynchronizer {
387 frame_rate: FrameRate,
389 reference_phase: f32,
391 current_phase: f32,
393 phase_error: f32,
395 locked: bool,
397}
398
399impl GenlockSynchronizer {
400 pub fn new(frame_rate: FrameRate) -> Self {
402 GenlockSynchronizer {
403 frame_rate,
404 reference_phase: 0.0,
405 current_phase: 0.0,
406 phase_error: 0.0,
407 locked: false,
408 }
409 }
410
411 pub fn update_reference(&mut self, phase: f32) {
413 self.reference_phase = phase;
414 self.calculate_phase_error();
415 }
416
417 pub fn update_timecode(&mut self, timecode: &Timecode) {
419 let frames = timecode.frames as f32;
420 let fps = self.frame_rate.frames_per_second() as f32;
421 self.current_phase = frames / fps;
422 self.calculate_phase_error();
423 }
424
425 fn calculate_phase_error(&mut self) {
427 self.phase_error = self.current_phase - self.reference_phase;
428
429 while self.phase_error > 0.5 {
431 self.phase_error -= 1.0;
432 }
433 while self.phase_error < -0.5 {
434 self.phase_error += 1.0;
435 }
436
437 self.locked = self.phase_error.abs() < 0.01;
439 }
440
441 pub fn is_locked(&self) -> bool {
443 self.locked
444 }
445
446 pub fn phase_error(&self) -> f32 {
448 self.phase_error
449 }
450
451 pub fn correction_frames(&self) -> i32 {
453 let fps = self.frame_rate.frames_per_second() as f32;
454 (self.phase_error * fps) as i32
455 }
456
457 pub fn reset(&mut self) {
459 self.reference_phase = 0.0;
460 self.current_phase = 0.0;
461 self.phase_error = 0.0;
462 self.locked = false;
463 }
464}
465
466pub struct TimecodeAggregator {
468 sources: Vec<TimecodeSource>,
470 strategy: VotingStrategy,
472}
473
474impl TimecodeAggregator {
475 pub fn new(strategy: VotingStrategy) -> Self {
477 TimecodeAggregator {
478 sources: Vec::new(),
479 strategy,
480 }
481 }
482
483 pub fn add_source(&mut self, name: String, timecode: Timecode, confidence: f32) {
485 self.sources.push(TimecodeSource {
486 name,
487 timecode,
488 confidence,
489 });
490 }
491
492 pub fn clear_sources(&mut self) {
494 self.sources.clear();
495 }
496
497 pub fn aggregate(&self) -> Option<Timecode> {
499 if self.sources.is_empty() {
500 return None;
501 }
502
503 match self.strategy {
504 VotingStrategy::Unanimous => self.unanimous_vote(),
505 VotingStrategy::Majority => self.majority_vote(),
506 VotingStrategy::HighestConfidence => self.highest_confidence(),
507 VotingStrategy::WeightedAverage => self.weighted_average(),
508 }
509 }
510
511 fn unanimous_vote(&self) -> Option<Timecode> {
513 if self.sources.is_empty() {
514 return None;
515 }
516
517 let first = &self.sources[0].timecode;
518 for source in &self.sources[1..] {
519 if source.timecode.to_frames() != first.to_frames() {
520 return None;
521 }
522 }
523
524 Some(*first)
525 }
526
527 fn majority_vote(&self) -> Option<Timecode> {
529 if self.sources.is_empty() {
530 return None;
531 }
532
533 let mut counts: Vec<(u64, usize)> = Vec::new();
535 for source in &self.sources {
536 let frames = source.timecode.to_frames();
537 if let Some(entry) = counts.iter_mut().find(|(f, _)| *f == frames) {
538 entry.1 += 1;
539 } else {
540 counts.push((frames, 1));
541 }
542 }
543
544 counts.sort_by(|a, b| b.1.cmp(&a.1));
546 if let Some((frames, _)) = counts.first() {
547 self.sources
549 .iter()
550 .find(|s| s.timecode.to_frames() == *frames)
551 .map(|s| s.timecode)
552 } else {
553 None
554 }
555 }
556
557 fn highest_confidence(&self) -> Option<Timecode> {
559 self.sources
560 .iter()
561 .max_by(|a, b| a.confidence.partial_cmp(&b.confidence).unwrap())
562 .map(|s| s.timecode)
563 }
564
565 fn weighted_average(&self) -> Option<Timecode> {
567 if self.sources.is_empty() {
568 return None;
569 }
570
571 let total_weight: f32 = self.sources.iter().map(|s| s.confidence).sum();
572 if total_weight == 0.0 {
573 return None;
574 }
575
576 let weighted_frames: f64 = self
577 .sources
578 .iter()
579 .map(|s| s.timecode.to_frames() as f64 * s.confidence as f64)
580 .sum();
581
582 let avg_frames = (weighted_frames / total_weight as f64) as u64;
583
584 Timecode::from_frames(avg_frames, FrameRate::Fps25).ok()
586 }
587}
588
589#[derive(Debug, Clone)]
591struct TimecodeSource {
592 #[allow(dead_code)]
593 name: String,
594 timecode: Timecode,
595 confidence: f32,
596}
597
598#[derive(Debug, Clone, Copy, PartialEq, Eq)]
600pub enum VotingStrategy {
601 Unanimous,
603 Majority,
605 HighestConfidence,
607 WeightedAverage,
609}
610
611fn current_time_ms() -> u64 {
613 0
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621
622 #[test]
623 fn test_synchronizer_creation() {
624 let sync = TimecodeSynchronizer::new(FrameRate::Fps25, ReconciliationStrategy::PreferLtc);
625 assert!(sync.get_timecode().is_none());
626 }
627
628 #[test]
629 fn test_jam_sync() {
630 let mut sync =
631 TimecodeSynchronizer::new(FrameRate::Fps25, ReconciliationStrategy::PreferLtc);
632 let tc = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).unwrap();
633
634 sync.jam_sync(tc);
635 assert!(sync.is_jam_synced());
636 }
637
638 #[test]
639 fn test_genlock() {
640 let mut genlock = GenlockSynchronizer::new(FrameRate::Fps25);
641 genlock.update_reference(0.0);
642
643 let tc = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).unwrap();
644 genlock.update_timecode(&tc);
645
646 assert!(genlock.is_locked());
648
649 genlock.update_reference(0.5);
651 let tc2 = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).unwrap();
652 genlock.update_timecode(&tc2);
653 assert!(!genlock.is_locked());
655 }
656
657 #[test]
658 fn test_aggregator() {
659 let mut agg = TimecodeAggregator::new(VotingStrategy::HighestConfidence);
660
661 let tc1 = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).unwrap();
662 let tc2 = Timecode::new(1, 0, 0, 1, FrameRate::Fps25).unwrap();
663
664 agg.add_source("LTC".to_string(), tc1, 0.8);
665 agg.add_source("VITC".to_string(), tc2, 0.9);
666
667 let result = agg.aggregate();
668 assert_eq!(result, Some(tc2));
669 }
670}