1use super::Protocol;
13use num_complex::Complex;
14use rustfft::FftPlanner;
15use std::f32::consts::PI;
16
17#[derive(Debug, Clone)]
19pub struct SyncCandidate {
20 pub freq_hz: f32,
22 pub dt_sec: f32,
24 pub score: f32,
26}
27
28#[derive(Copy, Clone, Debug)]
35pub struct SyncDims {
36 pub nfft1: usize,
38 pub nstep: usize,
40 pub nsps: usize,
42 pub nssy: usize,
44 pub nfos: usize,
46 pub nmax: usize,
48 pub nhsym: usize,
50 pub nh1: usize,
52 pub df: f32,
54 pub tstep: f32,
56 pub jstrt: i32,
59 pub jz: i32,
61 pub ds_spb: usize,
63 pub ds_rate: f32,
65}
66
67impl SyncDims {
68 #[inline]
69 pub const fn of<P: Protocol>() -> Self {
70 let nsps = P::NSPS as usize;
71 let nstep = nsps / P::NSTEP_PER_SYMBOL as usize;
72 let nfft1 = nsps * P::NFFT_PER_SYMBOL_FACTOR as usize;
73 let nmax = (P::T_SLOT_S * 12_000.0) as usize;
74 let ndown = P::NDOWN as usize;
75 Self {
76 nfft1,
77 nstep,
78 nsps,
79 nssy: P::NSTEP_PER_SYMBOL as usize,
80 nfos: P::NFFT_PER_SYMBOL_FACTOR as usize,
81 nmax,
82 nhsym: nmax / nstep - 3,
83 nh1: nfft1 / 2,
84 df: 12_000.0 / nfft1 as f32,
85 tstep: nstep as f32 / 12_000.0,
86 jstrt: (P::TX_START_OFFSET_S / (nstep as f32 / 12_000.0)) as i32,
87 jz: (2.5 / (nstep as f32 / 12_000.0)) as i32,
88 ds_spb: nsps / ndown,
89 ds_rate: 12_000.0 / ndown as f32,
90 }
91 }
92}
93
94pub struct Spectrogram {
100 pub n_freq: usize,
101 pub n_time: usize,
102 data: Vec<f32>,
103}
104
105impl Spectrogram {
106 #[inline]
107 fn get(&self, freq: usize, time: usize) -> f32 {
108 self.data[freq * self.n_time + time]
109 }
110}
111
112pub fn compute_spectra<P: Protocol>(audio: &[i16]) -> Spectrogram {
114 let d = SyncDims::of::<P>();
115 let fac = 1.0f32 / 300.0;
116 let mut planner = FftPlanner::<f32>::new();
117 let fft = planner.plan_fft_forward(d.nfft1);
118
119 let mut data = vec![0.0f32; d.nh1 * d.nhsym];
120 let mut buf = vec![Complex::new(0.0f32, 0.0); d.nfft1];
121
122 for j in 0..d.nhsym {
123 let ia = j * d.nstep;
124 for (k, c) in buf.iter_mut().enumerate() {
125 *c = if k < d.nsps {
126 let sample = if ia + k < audio.len() {
127 audio[ia + k] as f32 * fac
128 } else {
129 0.0
130 };
131 Complex::new(sample, 0.0)
132 } else {
133 Complex::new(0.0, 0.0)
134 };
135 }
136 fft.process(&mut buf);
137 for i in 0..d.nh1 {
138 data[i * d.nhsym + j] = buf[i].norm_sqr();
139 }
140 }
141
142 Spectrogram {
143 n_freq: d.nh1,
144 n_time: d.nhsym,
145 data,
146 }
147}
148
149pub fn coarse_sync<P: Protocol>(
155 audio: &[i16],
156 freq_min: f32,
157 freq_max: f32,
158 sync_min: f32,
159 freq_hint: Option<f32>,
160 max_cand: usize,
161) -> Vec<SyncCandidate> {
162 let d = SyncDims::of::<P>();
163 let s = compute_spectra::<P>(audio);
164 let ntones = P::NTONES as usize;
165 let pattern_len = P::SYNC_MODE.blocks()[0].pattern.len();
166
167 let ia = (freq_min / d.df).round() as usize;
169 let headroom = d.nfos * (ntones - 1) + 1;
170 let ib = ((freq_max / d.df).round() as usize).min(d.nh1.saturating_sub(headroom));
171 if ib < ia {
172 return Vec::new();
173 }
174
175 let n_freq = ib - ia + 1;
176 let n_lag = (2 * d.jz + 1) as usize;
177 let mut sync2d = vec![0.0f32; n_freq * n_lag];
178 let idx = |fi: usize, lag: i32| fi * n_lag + (lag + d.jz) as usize;
179
180 let num_blocks = P::SYNC_MODE.blocks().len();
184
185 for (fi, i) in (ia..=ib).enumerate() {
186 for lag in -d.jz..=d.jz {
187 let mut t_blocks = vec![0.0f32; num_blocks];
189 let mut t0_blocks = vec![0.0f32; num_blocks];
190
191 for (bk, block) in P::SYNC_MODE.blocks().iter().enumerate() {
192 let block_offset = d.nssy as i32 * block.start_symbol as i32;
193 for (n, &costas_n) in block.pattern.iter().enumerate() {
194 let m = lag + d.jstrt + block_offset + (d.nssy * n) as i32;
195 let tone_bin = i + d.nfos * costas_n as usize;
196 if m >= 0 && (m as usize) < d.nhsym && tone_bin < d.nh1 {
197 let m = m as usize;
198 t_blocks[bk] += s.get(tone_bin, m);
199 t0_blocks[bk] += (0..ntones)
201 .map(|k| s.get((i + d.nfos * k).min(d.nh1 - 1), m))
202 .sum::<f32>();
203 }
204 }
205 }
206
207 let t_all: f32 = t_blocks.iter().sum();
209 let t0_all: f32 = t0_blocks.iter().sum();
210 let t0_ref = (t0_all - t_all) / (ntones as f32 - 1.0);
213 let sync_all = if t0_ref > 0.0 { t_all / t0_ref } else { 0.0 };
214
215 let score = if num_blocks > 1 {
217 let t_tail: f32 = t_blocks[1..].iter().sum();
218 let t0_tail: f32 = t0_blocks[1..].iter().sum();
219 let t0_tail_ref = (t0_tail - t_tail) / (ntones as f32 - 1.0);
220 let sync_tail = if t0_tail_ref > 0.0 {
221 t_tail / t0_tail_ref
222 } else {
223 0.0
224 };
225 sync_all.max(sync_tail)
226 } else {
227 sync_all
228 };
229
230 sync2d[idx(fi, lag)] = score;
231 }
232 }
233
234 const MLAG: i32 = 10;
251
252 let mut red = vec![0.0f32; n_freq];
255 for fi in 0..n_freq {
256 red[fi] = (-d.jz..=d.jz)
257 .map(|lag| sync2d[idx(fi, lag)])
258 .fold(0.0f32, f32::max);
259 }
260
261 let pct = |xs: &[f32]| {
262 let mut sorted = xs.to_vec();
263 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
264 let pct_idx = (0.40 * n_freq as f32) as usize;
265 sorted[pct_idx.min(n_freq - 1)].max(f32::EPSILON)
266 };
267 let base = pct(&red);
268
269 let mut cands: Vec<SyncCandidate> = Vec::new();
270 for fi in 0..n_freq {
271 let i = ia + fi;
272 let freq_hz = i as f32 * d.df;
273
274 let mut peaks: Vec<(i32, f32)> = (-d.jz..=d.jz)
277 .filter_map(|lag| {
278 let raw = sync2d[idx(fi, lag)];
279 let norm = raw / base;
280 if norm.is_finite() && norm >= sync_min {
281 Some((lag, norm))
282 } else {
283 None
284 }
285 })
286 .collect();
287 peaks.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
288
289 let mut picked: Vec<i32> = Vec::new();
294 'outer: for (lag, score) in peaks {
295 for &pl in &picked {
296 if (lag - pl).abs() <= MLAG {
297 continue 'outer;
298 }
299 }
300 picked.push(lag);
301 cands.push(SyncCandidate {
302 freq_hz,
303 dt_sec: (lag as f32 - 0.5) * d.tstep,
304 score,
305 });
306 if picked.len() >= 8 {
309 break;
310 }
311 }
312 }
313 let _ = pattern_len; for i in 1..cands.len() {
317 for j in 0..i {
318 let fdiff = (cands[i].freq_hz - cands[j].freq_hz).abs();
319 let tdiff = (cands[i].dt_sec - cands[j].dt_sec).abs();
320 if fdiff < 4.0 && tdiff < 0.04 {
321 if cands[i].score >= cands[j].score {
322 cands[j].score = 0.0;
323 } else {
324 cands[i].score = 0.0;
325 }
326 }
327 }
328 }
329 cands.retain(|c| c.score >= sync_min);
330
331 if let Some(fhint) = freq_hint {
332 cands.sort_by(|a, b| {
333 let a_near = (a.freq_hz - fhint).abs() <= 10.0;
334 let b_near = (b.freq_hz - fhint).abs() <= 10.0;
335 match (a_near, b_near) {
336 (true, false) => std::cmp::Ordering::Less,
337 (false, true) => std::cmp::Ordering::Greater,
338 _ => b.score.partial_cmp(&a.score).unwrap(),
339 }
340 });
341 } else {
342 cands.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
343 }
344
345 cands.truncate(max_cand);
346 cands
347}
348
349pub fn make_costas_ref(pattern: &[u8], ds_spb: usize) -> Vec<Vec<Complex<f32>>> {
355 pattern
356 .iter()
357 .map(|&tone| {
358 let dphi = 2.0 * PI * tone as f32 / ds_spb as f32;
359 let mut waves = vec![Complex::new(0.0f32, 0.0); ds_spb];
360 let mut phi = 0.0f32;
361 for w in waves.iter_mut() {
362 *w = Complex::new(phi.cos(), phi.sin());
363 phi = (phi + dphi) % (2.0 * PI);
364 }
365 waves
366 })
367 .collect()
368}
369
370pub fn score_costas_block(
372 cd0: &[Complex<f32>],
373 csync: &[Vec<Complex<f32>>],
374 ds_spb: usize,
375 array_start: usize,
376) -> f32 {
377 let np2 = cd0.len();
378 csync
379 .iter()
380 .enumerate()
381 .map(|(k, ref_tone)| {
382 let start = array_start + k * ds_spb;
383 if start + ds_spb <= np2 {
384 cd0[start..start + ds_spb]
385 .iter()
386 .zip(ref_tone.iter())
387 .map(|(&s, &r)| s * r.conj())
388 .sum::<Complex<f32>>()
389 .norm_sqr()
390 } else {
391 0.0
392 }
393 })
394 .sum()
395}
396
397pub fn fine_sync_power<P: Protocol>(cd0: &[Complex<f32>], i0: usize) -> f32 {
399 fine_sync_power_per_block::<P>(cd0, i0).into_iter().sum()
400}
401
402pub fn fine_sync_power_per_block<P: Protocol>(cd0: &[Complex<f32>], i0: usize) -> Vec<f32> {
404 let d = SyncDims::of::<P>();
405 P::SYNC_MODE
406 .blocks()
407 .iter()
408 .map(|block| {
409 let csync = make_costas_ref(block.pattern, d.ds_spb);
410 let start = i0 + block.start_symbol as usize * d.ds_spb;
411 score_costas_block(cd0, &csync, d.ds_spb, start)
412 })
413 .collect()
414}
415
416pub fn parabolic_peak(y_neg: f32, y_0: f32, y_pos: f32) -> (f32, f32) {
418 let denom = y_neg - 2.0 * y_0 + y_pos;
419 if denom.abs() < f32::EPSILON {
420 return (0.0, y_0);
421 }
422 let offset = 0.5 * (y_neg - y_pos) / denom;
423 let peak = y_0 - 0.25 * (y_neg - y_pos) * offset;
424 (offset.clamp(-0.5, 0.5), peak)
425}
426
427pub fn refine_candidate<P: Protocol>(
433 cd0: &[Complex<f32>],
434 candidate: &SyncCandidate,
435 search_steps: i32,
436) -> SyncCandidate {
437 let d = SyncDims::of::<P>();
438 let nominal_i0 = ((candidate.dt_sec + P::TX_START_OFFSET_S) * d.ds_rate).round() as i32;
439 let (best_i0, best_score) = (-search_steps..=search_steps)
440 .map(|delta| {
441 let i0 = (nominal_i0 + delta).max(0) as usize;
442 let score = fine_sync_power::<P>(cd0, i0);
443 (i0, score)
444 })
445 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
446 .unwrap_or((0, 0.0));
447
448 let frac = if best_i0 >= 1 {
450 let y_neg = fine_sync_power::<P>(cd0, best_i0 - 1);
451 let y_pos = fine_sync_power::<P>(cd0, best_i0 + 1);
452 let (f, _) = parabolic_peak(y_neg, best_score, y_pos);
453 f
454 } else {
455 0.0
456 };
457
458 SyncCandidate {
459 freq_hz: candidate.freq_hz,
460 dt_sec: (best_i0 as f32 + frac) / d.ds_rate - P::TX_START_OFFSET_S,
461 score: best_score,
462 }
463}
464
465#[derive(Debug, Clone)]
467pub struct FineSyncDetail {
468 pub candidate: SyncCandidate,
469 pub per_block_scores: Vec<f32>,
471 pub drift_dt_sec: f32,
474}
475
476pub fn refine_candidate_double<P: Protocol>(
482 cd0: &[Complex<f32>],
483 candidate: &SyncCandidate,
484 search_steps: i32,
485) -> FineSyncDetail {
486 let d = SyncDims::of::<P>();
487 let blocks = P::SYNC_MODE.blocks();
488 let first = &blocks[0];
489 let last = &blocks[blocks.len() - 1];
490 let csync_first = make_costas_ref(first.pattern, d.ds_spb);
491 let csync_last = make_costas_ref(last.pattern, d.ds_spb);
492
493 let nominal_i0 = ((candidate.dt_sec + P::TX_START_OFFSET_S) * d.ds_rate).round() as i32;
494
495 let best_for = |pattern: &[u8], csync: &[Vec<Complex<f32>>], block_start: u32| {
496 let _ = pattern;
497 let (best_i0, _) = (-search_steps..=search_steps)
498 .map(|delta| {
499 let i0 = (nominal_i0 + delta).max(0) as usize;
500 let off = i0 + block_start as usize * d.ds_spb;
501 (i0, score_costas_block(cd0, csync, d.ds_spb, off))
502 })
503 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
504 .unwrap_or((nominal_i0.max(0) as usize, 0.0));
505 let frac = if best_i0 > 0 {
507 let off_neg = (best_i0 - 1) + block_start as usize * d.ds_spb;
508 let off_0 = best_i0 + block_start as usize * d.ds_spb;
509 let off_pos = (best_i0 + 1) + block_start as usize * d.ds_spb;
510 let (f, _) = parabolic_peak(
511 score_costas_block(cd0, csync, d.ds_spb, off_neg),
512 score_costas_block(cd0, csync, d.ds_spb, off_0),
513 score_costas_block(cd0, csync, d.ds_spb, off_pos),
514 );
515 f
516 } else {
517 0.0
518 };
519 (best_i0, frac)
520 };
521
522 let (best_i0_a, frac_a) = best_for(first.pattern, &csync_first, first.start_symbol);
523 let (best_i0_c, frac_c) = best_for(last.pattern, &csync_last, last.start_symbol);
524
525 let dt_a = best_i0_a as f32 / d.ds_rate + frac_a / d.ds_rate - P::TX_START_OFFSET_S;
526 let dt_c = best_i0_c as f32 / d.ds_rate + frac_c / d.ds_rate - P::TX_START_OFFSET_S;
527 let drift_dt_sec = dt_c - dt_a;
528
529 let avg_i0 = ((best_i0_a + best_i0_c) as f32 * 0.5).round() as usize;
530 let per_block_scores = fine_sync_power_per_block::<P>(cd0, avg_i0);
531 let total: f32 = per_block_scores.iter().sum();
532
533 FineSyncDetail {
534 candidate: SyncCandidate {
535 freq_hz: candidate.freq_hz,
536 dt_sec: (dt_a + dt_c) * 0.5,
537 score: total,
538 },
539 per_block_scores,
540 drift_dt_sec,
541 }
542}