1#![allow(dead_code)]
7#![allow(clippy::cast_precision_loss)]
8#![allow(clippy::too_many_arguments)]
9
10#[derive(Debug, Clone)]
12pub struct PllState {
13 pub phase_error: f64,
15 pub freq_error: f64,
17 pub filter_state: f64,
19 pub kp: f64,
21 pub ki: f64,
23 pub correction: f64,
25}
26
27impl PllState {
28 #[must_use]
30 pub fn new(kp: f64, ki: f64) -> Self {
31 Self {
32 phase_error: 0.0,
33 freq_error: 0.0,
34 filter_state: 0.0,
35 kp,
36 ki,
37 correction: 0.0,
38 }
39 }
40
41 pub fn update(&mut self, measured_phase: f64, expected_phase: f64) -> f64 {
43 self.phase_error = measured_phase - expected_phase;
44 self.filter_state += self.ki * self.phase_error;
45 self.correction = self.kp * self.phase_error + self.filter_state;
46 self.freq_error = self.correction;
47 self.correction
48 }
49
50 pub fn reset(&mut self) {
52 self.phase_error = 0.0;
53 self.freq_error = 0.0;
54 self.filter_state = 0.0;
55 self.correction = 0.0;
56 }
57
58 #[must_use]
60 pub fn is_locked(&self, threshold: f64) -> bool {
61 self.phase_error.abs() < threshold && self.freq_error.abs() < threshold * 0.01
62 }
63}
64
65impl Default for PllState {
66 fn default() -> Self {
67 Self::new(0.1, 0.001)
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct DriftRateEstimator {
74 measurements: Vec<(f64, f64)>,
76 max_window: usize,
78 estimated_ppm: f64,
80}
81
82impl DriftRateEstimator {
83 #[must_use]
85 pub fn new(max_window: usize) -> Self {
86 Self {
87 measurements: Vec::new(),
88 max_window,
89 estimated_ppm: 0.0,
90 }
91 }
92
93 pub fn add_measurement(&mut self, time: f64, offset: f64) {
95 self.measurements.push((time, offset));
96 if self.measurements.len() > self.max_window {
97 self.measurements.remove(0);
98 }
99 if self.measurements.len() >= 2 {
100 self.estimated_ppm = self.compute_drift_ppm();
101 }
102 }
103
104 fn compute_drift_ppm(&self) -> f64 {
106 let n = self.measurements.len() as f64;
107 let sum_t: f64 = self.measurements.iter().map(|(t, _)| t).sum();
108 let sum_o: f64 = self.measurements.iter().map(|(_, o)| o).sum();
109 let sum_t2: f64 = self.measurements.iter().map(|(t, _)| t * t).sum();
110 let sum_to: f64 = self.measurements.iter().map(|(t, o)| t * o).sum();
111
112 let denom = n * sum_t2 - sum_t * sum_t;
113 if denom.abs() < f64::EPSILON {
114 return 0.0;
115 }
116 let slope = (n * sum_to - sum_t * sum_o) / denom;
117 slope / 48.0 }
120
121 #[must_use]
123 pub fn drift_ppm(&self) -> f64 {
124 self.estimated_ppm
125 }
126
127 #[must_use]
129 pub fn drift_samples_per_second(&self, sample_rate: u32) -> f64 {
130 self.estimated_ppm * f64::from(sample_rate) / 1_000_000.0
131 }
132
133 pub fn clear(&mut self) {
135 self.measurements.clear();
136 self.estimated_ppm = 0.0;
137 }
138
139 #[must_use]
141 pub fn len(&self) -> usize {
142 self.measurements.len()
143 }
144
145 #[must_use]
147 pub fn is_empty(&self) -> bool {
148 self.measurements.is_empty()
149 }
150}
151
152#[derive(Debug, Clone, Copy)]
154pub struct SampleAdjustment {
155 pub delta: i64,
157 pub fractional: f64,
159 pub cumulative_drift: f64,
161}
162
163impl SampleAdjustment {
164 #[must_use]
166 pub fn new(delta: i64, fractional: f64, cumulative_drift: f64) -> Self {
167 Self {
168 delta,
169 fractional,
170 cumulative_drift,
171 }
172 }
173
174 #[must_use]
176 pub fn needs_adjustment(&self) -> bool {
177 self.delta != 0
178 }
179}
180
181#[derive(Debug, Clone)]
183pub struct DriftCorrector {
184 pll: PllState,
186 estimator: DriftRateEstimator,
188 accumulator: f64,
190 total_samples: u64,
192 sample_rate: u32,
194}
195
196impl DriftCorrector {
197 #[must_use]
199 pub fn new(sample_rate: u32) -> Self {
200 Self {
201 pll: PllState::new(0.05, 0.0005),
202 estimator: DriftRateEstimator::new(100),
203 accumulator: 0.0,
204 total_samples: 0,
205 sample_rate,
206 }
207 }
208
209 pub fn process_block(&mut self, block_size: usize, measured_offset: f64) -> SampleAdjustment {
211 let time = self.total_samples as f64 / f64::from(self.sample_rate);
212 self.estimator.add_measurement(time, measured_offset);
213
214 let drift_rate = self.estimator.drift_samples_per_second(self.sample_rate);
215 let block_drift = drift_rate * block_size as f64 / f64::from(self.sample_rate);
216
217 self.accumulator += block_drift;
218 let delta = self.accumulator.trunc() as i64;
219 self.accumulator -= delta as f64;
220
221 self.total_samples += block_size as u64;
222
223 SampleAdjustment::new(delta, self.accumulator, measured_offset + block_drift)
224 }
225
226 pub fn update_pll(&mut self, measured: f64, expected: f64) -> f64 {
228 self.pll.update(measured, expected)
229 }
230
231 pub fn reset(&mut self) {
233 self.pll.reset();
234 self.estimator.clear();
235 self.accumulator = 0.0;
236 self.total_samples = 0;
237 }
238
239 #[must_use]
241 pub fn drift_ppm(&self) -> f64 {
242 self.estimator.drift_ppm()
243 }
244
245 #[must_use]
247 pub fn is_locked(&self) -> bool {
248 self.pll.is_locked(1.0)
249 }
250}
251
252#[derive(Debug, Clone)]
270pub struct GenlockDriftEstimator {
271 camera_count: usize,
273 observations: Vec<Vec<(f64, f64)>>,
275 max_observations: usize,
277}
278
279#[derive(Debug, Clone)]
281pub struct GenlockDriftResult {
282 pub camera_a: usize,
284 pub camera_b: usize,
286 pub drift_ppm: f64,
288 pub offset_at_zero: f64,
290 pub r_squared: f64,
292 pub rms_residual: f64,
294 pub num_observations: usize,
296}
297
298impl GenlockDriftEstimator {
299 #[must_use]
303 pub fn new(camera_count: usize, max_observations: usize) -> Self {
304 let num_pairs = if camera_count >= 2 {
305 camera_count * (camera_count - 1) / 2
306 } else {
307 0
308 };
309 Self {
310 camera_count,
311 observations: vec![Vec::new(); num_pairs],
312 max_observations,
313 }
314 }
315
316 pub fn add_observation(
321 &mut self,
322 cam_a: usize,
323 cam_b: usize,
324 time_seconds: f64,
325 offset_samples: f64,
326 ) -> bool {
327 if let Some(pair_idx) = self.pair_index(cam_a, cam_b) {
328 let obs = &mut self.observations[pair_idx];
329 obs.push((time_seconds, offset_samples));
330 if obs.len() > self.max_observations {
331 obs.remove(0);
332 }
333 true
334 } else {
335 false
336 }
337 }
338
339 pub fn estimate_pair(&self, cam_a: usize, cam_b: usize) -> Option<GenlockDriftResult> {
343 let pair_idx = self.pair_index(cam_a, cam_b)?;
344 let obs = &self.observations[pair_idx];
345 if obs.len() < 2 {
346 return None;
347 }
348
349 let (slope, intercept, r_sq, rms) = linear_regression_with_stats(obs);
350
351 let drift_ppm = slope / 48.0; Some(GenlockDriftResult {
358 camera_a: cam_a.min(cam_b),
359 camera_b: cam_a.max(cam_b),
360 drift_ppm,
361 offset_at_zero: intercept,
362 r_squared: r_sq,
363 rms_residual: rms,
364 num_observations: obs.len(),
365 })
366 }
367
368 pub fn estimate_all(&self) -> Vec<GenlockDriftResult> {
370 let mut results = Vec::new();
371 for a in 0..self.camera_count {
372 for b in (a + 1)..self.camera_count {
373 if let Some(result) = self.estimate_pair(a, b) {
374 results.push(result);
375 }
376 }
377 }
378 results
379 }
380
381 pub fn predicted_offset(&self, cam_a: usize, cam_b: usize, time_seconds: f64) -> Option<f64> {
385 let pair_idx = self.pair_index(cam_a, cam_b)?;
386 let obs = &self.observations[pair_idx];
387 if obs.len() < 2 {
388 return None;
389 }
390 let (slope, intercept, _, _) = linear_regression_with_stats(obs);
391 Some(slope * time_seconds + intercept)
392 }
393
394 pub fn observation_count(&self, cam_a: usize, cam_b: usize) -> usize {
396 self.pair_index(cam_a, cam_b)
397 .map(|i| self.observations[i].len())
398 .unwrap_or(0)
399 }
400
401 #[must_use]
403 pub fn camera_count(&self) -> usize {
404 self.camera_count
405 }
406
407 pub fn clear(&mut self) {
409 for obs in &mut self.observations {
410 obs.clear();
411 }
412 }
413
414 fn pair_index(&self, cam_a: usize, cam_b: usize) -> Option<usize> {
418 if cam_a >= self.camera_count || cam_b >= self.camera_count || cam_a == cam_b {
419 return None;
420 }
421 let (lo, hi) = if cam_a < cam_b {
422 (cam_a, cam_b)
423 } else {
424 (cam_b, cam_a)
425 };
426 let idx = lo * (2 * self.camera_count - lo - 1) / 2 + (hi - lo - 1);
429 if idx < self.observations.len() {
430 Some(idx)
431 } else {
432 None
433 }
434 }
435}
436
437fn linear_regression_with_stats(data: &[(f64, f64)]) -> (f64, f64, f64, f64) {
440 let n = data.len() as f64;
441 if n < 2.0 {
442 return (0.0, 0.0, 0.0, 0.0);
443 }
444
445 let sum_x: f64 = data.iter().map(|(x, _)| x).sum();
446 let sum_y: f64 = data.iter().map(|(_, y)| y).sum();
447 let sum_xx: f64 = data.iter().map(|(x, _)| x * x).sum();
448 let sum_xy: f64 = data.iter().map(|(x, y)| x * y).sum();
449
450 let denom = n * sum_xx - sum_x * sum_x;
451 if denom.abs() < f64::EPSILON {
452 let mean_y = sum_y / n;
453 return (0.0, mean_y, 0.0, 0.0);
454 }
455
456 let slope = (n * sum_xy - sum_x * sum_y) / denom;
457 let intercept = (sum_y - slope * sum_x) / n;
458
459 let mean_y = sum_y / n;
461 let ss_tot: f64 = data.iter().map(|(_, y)| (y - mean_y).powi(2)).sum();
462 let ss_res: f64 = data
463 .iter()
464 .map(|(x, y)| {
465 let pred = slope * x + intercept;
466 (y - pred).powi(2)
467 })
468 .sum();
469
470 let r_sq = if ss_tot > 1e-15 {
471 1.0 - ss_res / ss_tot
472 } else {
473 1.0
474 };
475
476 let rms = (ss_res / n).sqrt();
477
478 (slope, intercept, r_sq, rms)
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484
485 #[test]
486 fn test_pll_state_creation() {
487 let pll = PllState::new(0.1, 0.001);
488 assert_eq!(pll.kp, 0.1);
489 assert_eq!(pll.ki, 0.001);
490 assert_eq!(pll.phase_error, 0.0);
491 assert_eq!(pll.correction, 0.0);
492 }
493
494 #[test]
495 fn test_pll_default() {
496 let pll = PllState::default();
497 assert_eq!(pll.kp, 0.1);
498 assert_eq!(pll.ki, 0.001);
499 }
500
501 #[test]
502 fn test_pll_update_convergence() {
503 let mut pll = PllState::new(0.5, 0.01);
504 let mut phase = 10.0_f64;
505 for _ in 0..50 {
506 let correction = pll.update(phase, 0.0);
507 phase -= correction;
508 }
509 assert!(phase.abs() < 1.0, "PLL should converge: {phase}");
510 }
511
512 #[test]
513 fn test_pll_reset() {
514 let mut pll = PllState::new(0.1, 0.001);
515 pll.update(5.0, 0.0);
516 assert_ne!(pll.phase_error, 0.0);
517 pll.reset();
518 assert_eq!(pll.phase_error, 0.0);
519 assert_eq!(pll.filter_state, 0.0);
520 assert_eq!(pll.correction, 0.0);
521 }
522
523 #[test]
524 fn test_pll_lock_detection() {
525 let pll = PllState::new(0.1, 0.001);
526 assert!(pll.is_locked(1.0));
528 }
529
530 #[test]
531 fn test_drift_rate_estimator_creation() {
532 let est = DriftRateEstimator::new(50);
533 assert_eq!(est.max_window, 50);
534 assert_eq!(est.drift_ppm(), 0.0);
535 assert!(est.is_empty());
536 }
537
538 #[test]
539 fn test_drift_rate_single_measurement() {
540 let mut est = DriftRateEstimator::new(100);
541 est.add_measurement(1.0, 5.0);
542 assert_eq!(est.len(), 1);
543 assert_eq!(est.drift_ppm(), 0.0);
545 }
546
547 #[test]
548 fn test_drift_rate_two_measurements() {
549 let mut est = DriftRateEstimator::new(100);
550 est.add_measurement(0.0, 0.0);
551 est.add_measurement(1.0, 48.0); assert_ne!(est.drift_ppm(), 0.0);
553 }
554
555 #[test]
556 fn test_drift_rate_window_limit() {
557 let mut est = DriftRateEstimator::new(5);
558 for i in 0..10 {
559 est.add_measurement(i as f64, i as f64 * 2.0);
560 }
561 assert_eq!(est.len(), 5);
562 }
563
564 #[test]
565 fn test_drift_rate_clear() {
566 let mut est = DriftRateEstimator::new(100);
567 est.add_measurement(0.0, 0.0);
568 est.add_measurement(1.0, 10.0);
569 est.clear();
570 assert!(est.is_empty());
571 assert_eq!(est.drift_ppm(), 0.0);
572 }
573
574 #[test]
575 fn test_sample_adjustment_creation() {
576 let adj = SampleAdjustment::new(2, 0.3, 15.7);
577 assert_eq!(adj.delta, 2);
578 assert!((adj.fractional - 0.3).abs() < f64::EPSILON);
579 assert!(adj.needs_adjustment());
580 }
581
582 #[test]
583 fn test_sample_adjustment_no_adjustment() {
584 let adj = SampleAdjustment::new(0, 0.5, 0.5);
585 assert!(!adj.needs_adjustment());
586 }
587
588 #[test]
589 fn test_drift_corrector_creation() {
590 let dc = DriftCorrector::new(48000);
591 assert_eq!(dc.sample_rate, 48000);
592 assert_eq!(dc.total_samples, 0);
593 }
594
595 #[test]
596 fn test_drift_corrector_process_block() {
597 let mut dc = DriftCorrector::new(48000);
598 dc.estimator.add_measurement(0.0, 0.0);
600 dc.estimator.add_measurement(1.0, 48.0);
601 let adj = dc.process_block(480, 48.0);
602 assert_eq!(adj.delta.abs() <= 1, true); }
605
606 #[test]
607 fn test_drift_corrector_reset() {
608 let mut dc = DriftCorrector::new(48000);
609 dc.estimator.add_measurement(0.0, 0.0);
610 dc.estimator.add_measurement(1.0, 100.0);
611 dc.total_samples = 1000;
612 dc.reset();
613 assert_eq!(dc.total_samples, 0);
614 assert_eq!(dc.accumulator, 0.0);
615 assert!(dc.estimator.is_empty());
616 }
617
618 #[test]
619 fn test_drift_corrector_pll_update() {
620 let mut dc = DriftCorrector::new(48000);
621 let correction = dc.update_pll(10.0, 0.0);
622 assert!(correction.abs() > 0.0);
623 }
624
625 #[test]
628 fn test_genlock_creation() {
629 let est = GenlockDriftEstimator::new(4, 1000);
630 assert_eq!(est.camera_count(), 4);
631 assert_eq!(est.observations.len(), 6);
633 }
634
635 #[test]
636 fn test_genlock_add_observation() {
637 let mut est = GenlockDriftEstimator::new(3, 100);
638 assert!(est.add_observation(0, 1, 0.0, 0.0));
639 assert!(est.add_observation(0, 1, 1.0, 48.0));
640 assert_eq!(est.observation_count(0, 1), 2);
641 assert_eq!(est.observation_count(1, 0), 2);
643 }
644
645 #[test]
646 fn test_genlock_invalid_pair() {
647 let mut est = GenlockDriftEstimator::new(2, 100);
648 assert!(!est.add_observation(0, 0, 0.0, 0.0)); assert!(!est.add_observation(0, 5, 0.0, 0.0)); }
651
652 #[test]
653 fn test_genlock_linear_drift() {
654 let mut est = GenlockDriftEstimator::new(2, 1000);
655 for i in 0..100 {
657 let t = i as f64;
658 est.add_observation(0, 1, t, t * 10.0);
659 }
660 let result = est.estimate_pair(0, 1).expect("should have enough data");
661 assert!(
663 (result.drift_ppm - 10.0 / 48.0).abs() < 0.01,
664 "drift_ppm={}",
665 result.drift_ppm
666 );
667 assert!(result.r_squared > 0.999, "r²={}", result.r_squared);
668 assert!(result.rms_residual < 0.01, "rms={}", result.rms_residual);
669 }
670
671 #[test]
672 fn test_genlock_predicted_offset() {
673 let mut est = GenlockDriftEstimator::new(2, 1000);
674 est.add_observation(0, 1, 0.0, 0.0);
675 est.add_observation(0, 1, 10.0, 100.0);
676 let pred = est.predicted_offset(0, 1, 20.0).expect("should predict");
678 assert!((pred - 200.0).abs() < 1.0, "pred={pred}");
679 }
680
681 #[test]
682 fn test_genlock_insufficient_data() {
683 let mut est = GenlockDriftEstimator::new(2, 100);
684 est.add_observation(0, 1, 0.0, 0.0);
685 assert!(est.estimate_pair(0, 1).is_none());
686 assert!(est.predicted_offset(0, 1, 5.0).is_none());
687 }
688
689 #[test]
690 fn test_genlock_estimate_all() {
691 let mut est = GenlockDriftEstimator::new(3, 100);
692 est.add_observation(0, 1, 0.0, 0.0);
694 est.add_observation(0, 1, 1.0, 5.0);
695 let results = est.estimate_all();
696 assert_eq!(results.len(), 1); assert_eq!(results[0].camera_a, 0);
698 assert_eq!(results[0].camera_b, 1);
699 }
700
701 #[test]
702 fn test_genlock_clear() {
703 let mut est = GenlockDriftEstimator::new(2, 100);
704 est.add_observation(0, 1, 0.0, 0.0);
705 est.add_observation(0, 1, 1.0, 10.0);
706 assert_eq!(est.observation_count(0, 1), 2);
707 est.clear();
708 assert_eq!(est.observation_count(0, 1), 0);
709 }
710
711 #[test]
712 fn test_genlock_max_observations() {
713 let mut est = GenlockDriftEstimator::new(2, 5);
714 for i in 0..10 {
715 est.add_observation(0, 1, i as f64, i as f64 * 2.0);
716 }
717 assert_eq!(est.observation_count(0, 1), 5);
718 }
719
720 #[test]
723 fn test_linear_regression_perfect_line() {
724 let data: Vec<(f64, f64)> = (0..10).map(|i| (i as f64, 3.0 * i as f64 + 2.0)).collect();
725 let (slope, intercept, r_sq, rms) = linear_regression_with_stats(&data);
726 assert!((slope - 3.0).abs() < 1e-10);
727 assert!((intercept - 2.0).abs() < 1e-10);
728 assert!((r_sq - 1.0).abs() < 1e-10);
729 assert!(rms < 1e-10);
730 }
731
732 #[test]
733 fn test_linear_regression_constant() {
734 let data: Vec<(f64, f64)> = (0..5).map(|i| (i as f64, 7.0)).collect();
735 let (slope, intercept, _, _) = linear_regression_with_stats(&data);
736 assert!(slope.abs() < 1e-10);
737 assert!((intercept - 7.0).abs() < 1e-10);
738 }
739
740 #[test]
741 fn test_linear_regression_single_point() {
742 let data = vec![(1.0, 2.0)];
743 let (slope, _, _, _) = linear_regression_with_stats(&data);
744 assert_eq!(slope, 0.0);
745 }
746}