mfsk_core/wspr/
spectrogram.rs1use crate::core::ModulationParams;
15use num_complex::Complex;
16use rustfft::FftPlanner;
17
18use super::Wspr;
19
20pub struct Spectrogram {
22 pub mags_sqr: Vec<f32>,
24 pub n_time: usize,
25 pub n_freq: usize,
26 pub t_step: usize,
28 pub nsps: usize,
30 pub df: f32,
32 pub noise_per_bin: f32,
34}
35
36impl Spectrogram {
37 pub fn build(audio: &[f32], sample_rate: u32) -> Self {
40 let nsps = (sample_rate as f32 * <Wspr as ModulationParams>::SYMBOL_DT).round() as usize;
41 let t_step = nsps / 4;
42 let n_freq = nsps / 2;
43 if audio.len() < nsps || t_step == 0 {
44 return Self {
45 mags_sqr: Vec::new(),
46 n_time: 0,
47 n_freq: 0,
48 t_step: 0,
49 nsps,
50 df: sample_rate as f32 / nsps as f32,
51 noise_per_bin: 1.0,
52 };
53 }
54 let n_time = (audio.len() - nsps) / t_step + 1;
55
56 let mut mags_sqr = vec![0f32; n_time * n_freq];
57 let mut planner = FftPlanner::<f32>::new();
58 let fft = planner.plan_fft_forward(nsps);
59 let mut scratch = vec![Complex::new(0f32, 0f32); fft.get_inplace_scratch_len()];
60 let mut buf: Vec<Complex<f32>> = vec![Complex::new(0f32, 0f32); nsps];
61
62 for t in 0..n_time {
63 let start = t * t_step;
64 for (slot, &s) in buf.iter_mut().zip(&audio[start..start + nsps]) {
65 *slot = Complex::new(s, 0.0);
66 }
67 fft.process_with_scratch(&mut buf, &mut scratch);
68 let row = &mut mags_sqr[t * n_freq..(t + 1) * n_freq];
69 for (slot, c) in row.iter_mut().zip(buf.iter().take(n_freq)) {
70 *slot = c.norm_sqr();
71 }
72 }
73
74 let mut sorted = mags_sqr.clone();
78 sorted.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
79 let keep = (sorted.len() as f32 * 0.95) as usize;
80 let noise_per_bin = if keep > 0 {
81 sorted[..keep].iter().sum::<f32>() / keep as f32
82 } else {
83 1.0
84 };
85
86 Self {
87 mags_sqr,
88 n_time,
89 n_freq,
90 t_step,
91 nsps,
92 df: sample_rate as f32 / nsps as f32,
93 noise_per_bin: noise_per_bin.max(1e-6),
94 }
95 }
96
97 #[inline]
98 pub fn get(&self, t: usize, f: usize) -> f32 {
99 self.mags_sqr[t * self.n_freq + f]
100 }
101}
102
103pub fn score_candidate(spec: &Spectrogram, t_row: usize, base_bin: usize) -> f32 {
110 use super::WSPR_SYNC_VECTOR;
111 const ROWS_PER_SYMBOL: usize = 4;
112 let last_row = t_row + 161 * ROWS_PER_SYMBOL;
113 if last_row >= spec.n_time || base_bin + 4 > spec.n_freq {
114 return 0.0;
115 }
116 let mut sync_pwr = 0.0f32;
117 let mut off_pwr = 0.0f32;
118 for i in 0..162 {
119 let t = t_row + i * ROWS_PER_SYMBOL;
120 let m0 = spec.get(t, base_bin);
121 let m1 = spec.get(t, base_bin + 1);
122 let m2 = spec.get(t, base_bin + 2);
123 let m3 = spec.get(t, base_bin + 3);
124 if WSPR_SYNC_VECTOR[i] == 0 {
125 sync_pwr += m0 + m2;
126 off_pwr += m1 + m3;
127 } else {
128 sync_pwr += m1 + m3;
129 off_pwr += m0 + m2;
130 }
131 }
132 let noise_floor = spec.noise_per_bin * 162.0;
133 let denom = sync_pwr + off_pwr + noise_floor;
134 if denom > 0.0 {
135 (sync_pwr - off_pwr) / denom
136 } else {
137 0.0
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::super::synthesize_type1;
144 use super::*;
145
146 #[test]
147 fn spec_matches_direct_demod() {
148 let freq = 1500.0;
153 let audio = synthesize_type1("K1ABC", "FN42", 37, 12_000, freq, 0.3).expect("synth");
154 let spec = Spectrogram::build(&audio, 12_000);
155 assert!(spec.n_time > 0);
156 assert_eq!(spec.n_freq, 4096);
157
158 let true_bin = 1024;
159 let true_t = 0usize;
160 let best_score = score_candidate(&spec, true_t, true_bin);
161 for dt in [-2i32, -1, 1, 2] {
163 if let Ok(t) = (true_t as i32 + dt).try_into() {
164 let s = score_candidate(&spec, t, true_bin);
165 assert!(s < best_score, "dt={} scored {} >= {}", dt, s, best_score);
166 }
167 }
168 for df in [-2i32, -1, 1, 2] {
169 let b = (true_bin as i32 + df) as usize;
170 let s = score_candidate(&spec, true_t, b);
171 assert!(s < best_score, "df={} scored {} >= {}", df, s, best_score);
172 }
173 }
174}