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) = match self.history.last() {
334 Some(v) => *v,
335 None => return,
336 };
337
338 let time_diff_ms = last_time.saturating_sub(first_time);
339 let frame_diff = last_frames.saturating_sub(first_frames);
340
341 if time_diff_ms > 0 {
342 let expected_frames = (time_diff_ms as f64 / 1000.0) * self.frame_rate.as_float();
343 let drift = (frame_diff as f64 - expected_frames) / expected_frames;
344 self.drift_ppm = (drift * 1_000_000.0) as f32;
345 }
346 }
347
348 fn drift_ppm(&self) -> f32 {
349 self.drift_ppm
350 }
351
352 fn reset(&mut self) {
353 self.reference_frames = 0;
354 self.actual_frames = 0;
355 self.drift_ppm = 0.0;
356 self.history.clear();
357 }
358}
359
360#[derive(Debug, Clone, Copy, PartialEq, Eq)]
362pub enum ReconciliationStrategy {
363 PreferLtc,
365 PreferVitc,
367 CrossCheck,
369 MostRecent,
371}
372
373#[derive(Debug, Clone)]
375pub struct SyncStatus {
376 pub is_synchronized: bool,
378 pub ltc_available: bool,
380 pub vitc_available: bool,
382 pub jam_sync_active: bool,
384 pub drift_ppm: f32,
386}
387
388pub struct GenlockSynchronizer {
390 frame_rate: FrameRate,
392 reference_phase: f32,
394 current_phase: f32,
396 phase_error: f32,
398 locked: bool,
400}
401
402impl GenlockSynchronizer {
403 pub fn new(frame_rate: FrameRate) -> Self {
405 GenlockSynchronizer {
406 frame_rate,
407 reference_phase: 0.0,
408 current_phase: 0.0,
409 phase_error: 0.0,
410 locked: false,
411 }
412 }
413
414 pub fn update_reference(&mut self, phase: f32) {
416 self.reference_phase = phase;
417 self.calculate_phase_error();
418 }
419
420 pub fn update_timecode(&mut self, timecode: &Timecode) {
422 let frames = timecode.frames as f32;
423 let fps = self.frame_rate.frames_per_second() as f32;
424 self.current_phase = frames / fps;
425 self.calculate_phase_error();
426 }
427
428 fn calculate_phase_error(&mut self) {
430 self.phase_error = self.current_phase - self.reference_phase;
431
432 while self.phase_error > 0.5 {
434 self.phase_error -= 1.0;
435 }
436 while self.phase_error < -0.5 {
437 self.phase_error += 1.0;
438 }
439
440 self.locked = self.phase_error.abs() < 0.01;
442 }
443
444 pub fn is_locked(&self) -> bool {
446 self.locked
447 }
448
449 pub fn phase_error(&self) -> f32 {
451 self.phase_error
452 }
453
454 pub fn correction_frames(&self) -> i32 {
456 let fps = self.frame_rate.frames_per_second() as f32;
457 (self.phase_error * fps) as i32
458 }
459
460 pub fn reset(&mut self) {
462 self.reference_phase = 0.0;
463 self.current_phase = 0.0;
464 self.phase_error = 0.0;
465 self.locked = false;
466 }
467}
468
469pub struct TimecodeAggregator {
471 sources: Vec<TimecodeSource>,
473 strategy: VotingStrategy,
475}
476
477impl TimecodeAggregator {
478 pub fn new(strategy: VotingStrategy) -> Self {
480 TimecodeAggregator {
481 sources: Vec::new(),
482 strategy,
483 }
484 }
485
486 pub fn add_source(&mut self, name: String, timecode: Timecode, confidence: f32) {
488 self.sources.push(TimecodeSource {
489 name,
490 timecode,
491 confidence,
492 });
493 }
494
495 pub fn clear_sources(&mut self) {
497 self.sources.clear();
498 }
499
500 pub fn aggregate(&self) -> Option<Timecode> {
502 if self.sources.is_empty() {
503 return None;
504 }
505
506 match self.strategy {
507 VotingStrategy::Unanimous => self.unanimous_vote(),
508 VotingStrategy::Majority => self.majority_vote(),
509 VotingStrategy::HighestConfidence => self.highest_confidence(),
510 VotingStrategy::WeightedAverage => self.weighted_average(),
511 }
512 }
513
514 fn unanimous_vote(&self) -> Option<Timecode> {
516 if self.sources.is_empty() {
517 return None;
518 }
519
520 let first = &self.sources[0].timecode;
521 for source in &self.sources[1..] {
522 if source.timecode.to_frames() != first.to_frames() {
523 return None;
524 }
525 }
526
527 Some(*first)
528 }
529
530 fn majority_vote(&self) -> Option<Timecode> {
532 if self.sources.is_empty() {
533 return None;
534 }
535
536 let mut counts: Vec<(u64, usize)> = Vec::new();
538 for source in &self.sources {
539 let frames = source.timecode.to_frames();
540 if let Some(entry) = counts.iter_mut().find(|(f, _)| *f == frames) {
541 entry.1 += 1;
542 } else {
543 counts.push((frames, 1));
544 }
545 }
546
547 counts.sort_by(|a, b| b.1.cmp(&a.1));
549 if let Some((frames, _)) = counts.first() {
550 self.sources
552 .iter()
553 .find(|s| s.timecode.to_frames() == *frames)
554 .map(|s| s.timecode)
555 } else {
556 None
557 }
558 }
559
560 fn highest_confidence(&self) -> Option<Timecode> {
562 self.sources
563 .iter()
564 .max_by(|a, b| {
565 a.confidence
566 .partial_cmp(&b.confidence)
567 .unwrap_or(std::cmp::Ordering::Equal)
568 })
569 .map(|s| s.timecode)
570 }
571
572 fn weighted_average(&self) -> Option<Timecode> {
574 if self.sources.is_empty() {
575 return None;
576 }
577
578 let total_weight: f32 = self.sources.iter().map(|s| s.confidence).sum();
579 if total_weight == 0.0 {
580 return None;
581 }
582
583 let weighted_frames: f64 = self
584 .sources
585 .iter()
586 .map(|s| s.timecode.to_frames() as f64 * s.confidence as f64)
587 .sum();
588
589 let avg_frames = (weighted_frames / total_weight as f64) as u64;
590
591 Timecode::from_frames(avg_frames, FrameRate::Fps25).ok()
593 }
594}
595
596#[derive(Debug, Clone)]
598struct TimecodeSource {
599 #[allow(dead_code)]
600 name: String,
601 timecode: Timecode,
602 confidence: f32,
603}
604
605#[derive(Debug, Clone, Copy, PartialEq, Eq)]
607pub enum VotingStrategy {
608 Unanimous,
610 Majority,
612 HighestConfidence,
614 WeightedAverage,
616}
617
618fn current_time_ms() -> u64 {
620 use std::time::{SystemTime, UNIX_EPOCH};
621 SystemTime::now()
622 .duration_since(UNIX_EPOCH)
623 .map(|d| d.as_millis() as u64)
624 .unwrap_or(0)
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630
631 #[test]
632 fn test_synchronizer_creation() {
633 let sync = TimecodeSynchronizer::new(FrameRate::Fps25, ReconciliationStrategy::PreferLtc);
634 assert!(sync.get_timecode().is_none());
635 }
636
637 #[test]
638 fn test_jam_sync() {
639 let mut sync =
640 TimecodeSynchronizer::new(FrameRate::Fps25, ReconciliationStrategy::PreferLtc);
641 let tc = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid timecode");
642
643 sync.jam_sync(tc);
644 assert!(sync.is_jam_synced());
645 }
646
647 #[test]
648 fn test_genlock() {
649 let mut genlock = GenlockSynchronizer::new(FrameRate::Fps25);
650 genlock.update_reference(0.0);
651
652 let tc = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid timecode");
653 genlock.update_timecode(&tc);
654
655 assert!(genlock.is_locked());
657
658 genlock.update_reference(0.5);
660 let tc2 = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid timecode");
661 genlock.update_timecode(&tc2);
662 assert!(!genlock.is_locked());
664 }
665
666 #[test]
667 fn test_aggregator() {
668 let mut agg = TimecodeAggregator::new(VotingStrategy::HighestConfidence);
669
670 let tc1 = Timecode::new(1, 0, 0, 0, FrameRate::Fps25).expect("valid timecode");
671 let tc2 = Timecode::new(1, 0, 0, 1, FrameRate::Fps25).expect("valid timecode");
672
673 agg.add_source("LTC".to_string(), tc1, 0.8);
674 agg.add_source("VITC".to_string(), tc2, 0.9);
675
676 let result = agg.aggregate();
677 assert_eq!(result, Some(tc2));
678 }
679}