1use alloc::vec::Vec;
9
10use super::Ft8;
11use num_complex::Complex;
12
13pub use crate::core::sync::{
14 FineSyncDetail as GenericFineSyncDetail, SyncCandidate, make_costas_ref, parabolic_peak,
15 score_costas_block,
16};
17
18#[derive(Debug, Clone)]
21pub struct FineSyncDetail {
22 pub candidate: SyncCandidate,
23 pub score_a: f32,
24 pub score_b: f32,
25 pub score_c: f32,
26 pub drift_dt_sec: f32,
27}
28
29impl From<GenericFineSyncDetail> for FineSyncDetail {
30 fn from(g: GenericFineSyncDetail) -> Self {
31 let mut it = g.per_block_scores.into_iter();
32 Self {
33 candidate: g.candidate,
34 score_a: it.next().unwrap_or(0.0),
35 score_b: it.next().unwrap_or(0.0),
36 score_c: it.next().unwrap_or(0.0),
37 drift_dt_sec: g.drift_dt_sec,
38 }
39 }
40}
41
42#[inline]
43pub fn coarse_sync(
44 audio: &[i16],
45 freq_min: f32,
46 freq_max: f32,
47 sync_min: f32,
48 freq_hint: Option<f32>,
49 max_cand: usize,
50) -> Vec<SyncCandidate> {
51 crate::core::sync::coarse_sync::<Ft8>(audio, freq_min, freq_max, sync_min, freq_hint, max_cand)
52}
53
54#[inline]
55pub fn compute_spectra(audio: &[i16]) -> crate::core::sync::Spectrogram {
56 crate::core::sync::compute_spectra::<Ft8>(audio)
57}
58
59#[inline]
60pub fn fine_sync_power(cd0: &[Complex<f32>], i0: usize) -> f32 {
61 crate::core::sync::fine_sync_power::<Ft8>(cd0, i0)
62}
63
64#[inline]
66pub fn fine_sync_power_split(cd0: &[Complex<f32>], i0: usize) -> (f32, f32, f32) {
67 let scores = crate::core::sync::fine_sync_power_per_block::<Ft8>(cd0, i0);
68 (
69 scores.first().copied().unwrap_or(0.0),
70 scores.get(1).copied().unwrap_or(0.0),
71 scores.get(2).copied().unwrap_or(0.0),
72 )
73}
74
75#[inline]
76pub fn refine_candidate(
77 cd0: &[Complex<f32>],
78 candidate: &SyncCandidate,
79 search_steps: i32,
80) -> SyncCandidate {
81 crate::core::sync::refine_candidate::<Ft8>(cd0, candidate, search_steps)
82}
83
84#[inline]
85pub fn refine_candidate_double(
86 cd0: &[Complex<f32>],
87 candidate: &SyncCandidate,
88 search_steps: i32,
89) -> FineSyncDetail {
90 crate::core::sync::refine_candidate_double::<Ft8>(cd0, candidate, search_steps).into()
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn parabolic_peak_at_center() {
99 let (offset, _) = parabolic_peak(1.0, 2.0, 1.0);
100 assert!(offset.abs() < 1e-6);
101 }
102
103 #[test]
104 fn parabolic_peak_offset_right() {
105 let (offset, _) = parabolic_peak(0.5, 1.5, 2.0);
106 assert!(offset > 0.0);
107 }
108
109 #[test]
110 fn fine_sync_silence_is_zero() {
111 let cd0 = vec![Complex::new(0.0f32, 0.0); 3200];
112 let sync = fine_sync_power(&cd0, 0);
113 assert_eq!(sync, 0.0);
114 }
115
116 #[test]
117 fn coarse_sync_on_silence_returns_empty_or_low() {
118 let audio = vec![0i16; 15 * 12000];
119 let cands = coarse_sync(&audio, 200.0, 2800.0, 1.0, None, 100);
120 assert!(cands.len() <= 100);
121 }
122
123 #[test]
124 fn fine_sync_split_silence_is_zero() {
125 let cd0 = vec![Complex::new(0.0f32, 0.0); 3200];
126 let (sa, sb, sc) = fine_sync_power_split(&cd0, 0);
127 assert_eq!(sa, 0.0);
128 assert_eq!(sb, 0.0);
129 assert_eq!(sc, 0.0);
130 }
131
132 #[test]
133 fn fine_sync_split_sum_equals_total() {
134 let mut cd0 = vec![Complex::new(0.0f32, 0.0); 3200];
135 for (i, c) in cd0.iter_mut().enumerate() {
136 let t = i as f32 / 200.0;
137 c.re = (2.0 * std::f32::consts::PI * 50.0 * t).cos() * 100.0;
138 }
139 let total = fine_sync_power(&cd0, 100);
140 let (sa, sb, sc) = fine_sync_power_split(&cd0, 100);
141 let diff = (total - (sa + sb + sc)).abs();
142 assert!(diff < 1e-3);
143 }
144
145 #[test]
146 fn refine_candidate_double_silence_no_panic() {
147 let cd0 = vec![Complex::new(0.0f32, 0.0); 3200];
148 let cand = SyncCandidate {
149 freq_hz: 1000.0,
150 dt_sec: 0.0,
151 score: 1.0,
152 };
153 let detail = refine_candidate_double(&cd0, &cand, 5);
154 assert!(detail.drift_dt_sec.is_finite());
155 }
156}