use crate::engine::ModulationParams;
use crate::engine::spectrogram;
use crate::engine::sync::refine_freq_hz_log_power;
use super::Jt65;
use super::sync_pattern::JT65_SYNC_POSITIONS;
pub type Spectrogram = spectrogram::Spectrogram;
const NSTEP_PER_SYMBOL: usize = 4;
fn build_spectrogram(audio: &[f32], sample_rate: u32) -> Spectrogram {
Spectrogram::build_for::<Jt65>(audio, sample_rate, NSTEP_PER_SYMBOL)
}
#[derive(Clone, Copy, Debug)]
pub struct SyncCandidate {
pub start_sample: usize,
pub freq_hz: f32,
pub score: f32,
}
pub const DEFAULT_SCORE_THRESHOLD: f32 = 0.1;
#[derive(Clone, Copy, Debug)]
pub struct SearchParams {
pub freq_min_hz: f32,
pub freq_max_hz: f32,
pub time_tolerance_sec: f32,
pub score_threshold: f32,
pub max_candidates: usize,
}
impl Default for SearchParams {
fn default() -> Self {
Self {
freq_min_hz: 1000.0,
freq_max_hz: 2000.0,
time_tolerance_sec: 7.62,
score_threshold: DEFAULT_SCORE_THRESHOLD,
max_candidates: 8,
}
}
}
const ROWS_PER_SYMBOL: usize = NSTEP_PER_SYMBOL;
fn sync_power_at_bin(spec: &Spectrogram, start_row: usize, bin: usize) -> f32 {
spectrogram::sync_power_at_bin(spec, start_row, bin, &JT65_SYNC_POSITIONS, ROWS_PER_SYMBOL)
}
pub fn score_candidate(spec: &Spectrogram, start_row: usize, base_bin: usize) -> f32 {
spectrogram::score_candidate(
spec,
start_row,
base_bin,
&JT65_SYNC_POSITIONS,
ROWS_PER_SYMBOL,
)
}
fn refine_freq_hz(spec: &Spectrogram, start_row: usize, base_bin: usize, df: f32) -> f32 {
refine_freq_hz_log_power(base_bin, spec.n_freq, df, |bin| {
sync_power_at_bin(spec, start_row, bin)
})
}
pub fn coarse_search(
audio: &[f32],
sample_rate: u32,
nominal_start_sample: usize,
params: &SearchParams,
) -> Vec<SyncCandidate> {
let spec = build_spectrogram(audio, sample_rate);
coarse_search_on_spec(&spec, sample_rate, nominal_start_sample, params)
}
pub fn coarse_search_on_spec(
spec: &Spectrogram,
sample_rate: u32,
nominal_start_sample: usize,
params: &SearchParams,
) -> Vec<SyncCandidate> {
if spec.n_time == 0 {
return Vec::new();
}
let nsps = (sample_rate as f32 * <Jt65 as ModulationParams>::SYMBOL_DT).round() as usize;
let df = sample_rate as f32 / nsps as f32;
let t_span_rows =
(params.time_tolerance_sec * sample_rate as f32 / spec.t_step.max(1) as f32).round() as i64;
let nominal_row = (nominal_start_sample / spec.t_step) as i64;
let row_min = (nominal_row - t_span_rows).max(0);
let row_max = nominal_row + t_span_rows;
let fmin_bin = (params.freq_min_hz / df).floor() as i64;
let fmax_bin = (params.freq_max_hz / df).ceil() as i64;
let mut out: Vec<SyncCandidate> = Vec::new();
for row in row_min..=row_max {
if row < 0 {
continue;
}
let row = row as usize;
if row + 125 * ROWS_PER_SYMBOL >= spec.n_time {
continue;
}
for fb in fmin_bin..=fmax_bin {
if fb < 0 || (fb as usize) + 66 > spec.n_freq {
continue;
}
let score = score_candidate(spec, row, fb as usize);
if score >= params.score_threshold {
out.push(SyncCandidate {
start_sample: row * spec.t_step,
freq_hz: refine_freq_hz(spec, row, fb as usize, df),
score,
});
}
}
}
out.sort_unstable_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
out.truncate(params.max_candidates);
out
}
#[cfg(test)]
mod tests {
use super::super::synthesize_standard;
use super::*;
#[test]
fn coarse_search_finds_clean_signal() {
let freq = 1270.0;
let audio = synthesize_standard("CQ", "K1ABC", "FN42", 12_000, freq, 0.3).expect("synth");
let cands = coarse_search(&audio, 12_000, 0, &SearchParams::default());
assert!(!cands.is_empty());
let best = cands[0];
assert!(
(best.freq_hz - 1270.0).abs() <= 4.0,
"best freq {} should be near 1270 Hz",
best.freq_hz
);
assert_eq!(best.start_sample, 0);
assert!(best.score > 0.5);
}
}