use num_complex::Complex32 as C32;
use crate::codec::ldpc::N;
use crate::modulate::ft8::{
FT8_TONE_SPACING_HZ, FT8_SAMPLES_PER_SYM, FT8_TOTAL_SYMS,
FT8_TONES,
};
use crate::sync::waterfall::{Waterfall, compute_waterfall};
use crate::sync::costas::{Candidate, find_candidates};
const FT8_COSTAS: [u8; 7] = [3, 1, 4, 0, 6, 5, 2];
const FT8_SYNC_POS: [i32; 3] = [0, 36, 72];
const FT8_GRAY8: [usize; 8] = [0, 1, 3, 2, 5, 6, 4, 7];
pub struct Ft8SyncResult {
pub time_sym: i32,
pub freq_bin: usize,
pub score: f32,
pub llr: [f32; N],
}
pub fn ft8_sync(
iq: &[C32],
fs: f32,
base_hz: f32,
max_hz: f32,
t_min: i32,
t_max: i32,
max_cand: usize,
) -> Vec<Ft8SyncResult> {
let num_tones_frame = FT8_TONES;
let freq_range = (max_hz - base_hz).max(0.0);
let num_bins = (freq_range / FT8_TONE_SPACING_HZ).ceil() as usize + num_tones_frame + 1;
let wf_syms = (t_max + FT8_TOTAL_SYMS as i32 - t_min).max(1) as usize;
let wf_sample_start = if t_min >= 0 {
t_min as usize * FT8_SAMPLES_PER_SYM
} else {
0 };
let sym_offset_adj = if t_min < 0 { -t_min } else { 0 };
let wf = compute_waterfall(
iq,
fs,
base_hz,
FT8_TONE_SPACING_HZ,
FT8_SAMPLES_PER_SYM,
wf_syms,
num_bins,
wf_sample_start,
);
let wf_t_min = 0i32;
let wf_t_max = (wf_syms as i32 - FT8_TOTAL_SYMS as i32).max(0);
let sync_pos_adjusted: Vec<i32> = FT8_SYNC_POS.iter().map(|&p| p).collect();
let candidates = find_candidates(
&wf,
&FT8_COSTAS,
&sync_pos_adjusted,
num_tones_frame,
wf_t_min,
wf_t_max,
max_cand,
);
let mut results = Vec::with_capacity(candidates.len());
for cand in candidates {
let llr = extract_ft8_llr(&wf, &cand);
let llr_norm = normalise_llr(llr);
results.push(Ft8SyncResult {
time_sym: cand.time_sym - sym_offset_adj,
freq_bin: cand.freq_bin,
score: cand.score,
llr: llr_norm,
});
}
results
}
fn extract_ft8_llr(wf: &Waterfall, cand: &Candidate) -> [f32; N] {
let mut llr = [0.0f32; N];
let mut llr_idx = 0usize;
let data_ranges: &[(usize, usize)] = &[(7, 36), (43, 72)];
for &(range_start, range_end) in data_ranges {
for data_sym in range_start..range_end {
let wf_sym = cand.time_sym + data_sym as i32;
if wf_sym < 0 || wf_sym >= wf.num_syms as i32 {
llr_idx += 3;
continue;
}
let wf_sym = wf_sym as usize;
let mut s = [f32::NEG_INFINITY; 8];
for j in 0..8 {
let bin = cand.freq_bin + j;
if bin < wf.num_tones {
s[j] = wf.get(wf_sym, bin);
}
}
let s2 = [
s[FT8_GRAY8[0]], s[FT8_GRAY8[1]], s[FT8_GRAY8[2]], s[FT8_GRAY8[3]],
s[FT8_GRAY8[4]], s[FT8_GRAY8[5]], s[FT8_GRAY8[6]], s[FT8_GRAY8[7]],
];
llr[llr_idx] = max4(s2[4], s2[5], s2[6], s2[7])
- max4(s2[0], s2[1], s2[2], s2[3]);
llr[llr_idx + 1] = max4(s2[2], s2[3], s2[6], s2[7])
- max4(s2[0], s2[1], s2[4], s2[5]);
llr[llr_idx + 2] = max4(s2[1], s2[3], s2[5], s2[7])
- max4(s2[0], s2[2], s2[4], s2[6]);
llr[llr_idx] = -llr[llr_idx];
llr[llr_idx + 1] = -llr[llr_idx + 1];
llr[llr_idx + 2] = -llr[llr_idx + 2];
llr_idx += 3;
}
}
llr
}
fn normalise_llr(mut llr: [f32; N]) -> [f32; N] {
let variance: f32 = llr.iter().map(|x| x * x).sum::<f32>() / N as f32;
if variance > 1e-10 {
let scale = (24.0 / variance).sqrt();
for v in llr.iter_mut() {
*v *= scale;
}
}
llr
}
#[inline]
fn max4(a: f32, b: f32, c: f32, d: f32) -> f32 {
a.max(b).max(c.max(d))
}