1use crate::msg::WsprMessage;
8
9use super::search::{SearchParams, coarse_search};
10use super::{decode_from_deinterleaved_llrs, demodulate_aligned};
11
12#[derive(Clone, Debug)]
14pub struct WsprDecode {
15 pub message: WsprMessage,
17 pub freq_hz: f32,
19 pub start_sample: usize,
21}
22
23pub fn decode_at(
26 audio: &[f32],
27 sample_rate: u32,
28 start_sample: usize,
29 freq_hz: f32,
30) -> Option<WsprDecode> {
31 let mut llrs = demodulate_aligned(audio, sample_rate, start_sample, freq_hz);
32 deinterleave_llrs(&mut llrs);
33 let message = decode_from_deinterleaved_llrs(&llrs)?;
34 Some(WsprDecode {
35 message,
36 freq_hz,
37 start_sample,
38 })
39}
40
41pub fn decode_scan(
48 audio: &[f32],
49 sample_rate: u32,
50 nominal_start_sample: usize,
51 params: &SearchParams,
52) -> Vec<WsprDecode> {
53 let cands = coarse_search(audio, sample_rate, nominal_start_sample, params);
54 let mut seen: Vec<WsprDecode> = Vec::new();
55 const FREQ_DEDUP_HZ: f32 = 5.0;
56 const TIME_DEDUP_SAMPLES: i64 = 8192; for c in cands {
58 let Some(d) = decode_at(audio, sample_rate, c.start_sample, c.freq_hz) else {
59 continue;
60 };
61 let dup = seen.iter().any(|prev| {
62 prev.message == d.message
63 && (prev.freq_hz - d.freq_hz).abs() <= FREQ_DEDUP_HZ
64 && (prev.start_sample as i64 - d.start_sample as i64).abs() <= TIME_DEDUP_SAMPLES
65 });
66 if !dup {
67 seen.push(d);
68 }
69 }
70 seen
71}
72
73pub fn decode_scan_default(audio: &[f32], sample_rate: u32) -> Vec<WsprDecode> {
75 decode_scan(audio, sample_rate, 0, &SearchParams::default())
76}
77
78fn deinterleave_llrs(llrs: &mut [f32; 162]) {
81 let mut tmp = [0f32; 162];
82 let mut p = 0u8;
83 let mut i = 0u8;
84 while p < 162 {
85 let i64 = i as u64;
87 let j = ((((i64 * 0x8020_0802u64) & 0x0884_4221_10u64).wrapping_mul(0x0101_0101_01u64))
88 >> 32) as u8 as usize;
89 if j < 162 {
90 tmp[p as usize] = llrs[j];
91 p += 1;
92 }
93 i = i.wrapping_add(1);
94 }
95 *llrs = tmp;
96}
97
98#[cfg(test)]
99mod tests {
100 use super::super::search::SearchParams;
101 use super::super::synthesize_type1;
102 use super::*;
103 use crate::msg::WsprMessage;
104
105 #[test]
106 fn synth_decode_roundtrip_k1abc_fn42_37() {
107 let freq = 1500.0;
108 let audio =
109 synthesize_type1("K1ABC", "FN42", 37, 12_000, freq, 0.3).expect("valid message");
110 let r = decode_at(&audio, 12_000, 0, freq).expect("decode");
111 assert_eq!(
112 r.message,
113 WsprMessage::Type1 {
114 callsign: "K1ABC".into(),
115 grid: "FN42".into(),
116 power_dbm: 37,
117 }
118 );
119 }
120
121 #[test]
122 fn scan_recovers_message_without_freq_hint() {
123 let freq = 1500.0;
124 let audio = synthesize_type1("K1ABC", "FN42", 37, 12_000, freq, 0.3).expect("synth");
125 let decodes = decode_scan(
126 &audio,
127 12_000,
128 0,
129 &SearchParams {
130 freq_min_hz: 1450.0,
131 freq_max_hz: 1550.0,
132 ..SearchParams::default()
133 },
134 );
135 assert!(!decodes.is_empty(), "at least one decode");
136 let d = decodes.into_iter().next().unwrap();
137 assert_eq!(
138 d.message,
139 WsprMessage::Type1 {
140 callsign: "K1ABC".into(),
141 grid: "FN42".into(),
142 power_dbm: 37,
143 }
144 );
145 assert!((d.freq_hz - 1500.0).abs() <= 2.0);
146 }
147
148 #[test]
149 fn survives_moderate_awgn() {
150 use std::f32::consts::PI;
151
152 let freq = 1500.0;
153 let mut audio =
154 synthesize_type1("K9AN", "EN50", 33, 12_000, freq, 0.5).expect("valid message");
155
156 let mut seed: u32 = 0x1234_5678;
160 for (i, s) in audio.iter_mut().enumerate() {
161 seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12345);
163 let rnd = ((seed >> 16) as f32 / 32768.0 - 1.0) * 0.10;
164 let off = 0.05 * (2.0 * PI * 2345.7 * i as f32 / 12_000.0).sin();
165 *s += rnd + off;
166 }
167
168 let r = decode_at(&audio, 12_000, 0, freq).expect("decode under noise");
169 assert_eq!(
170 r.message,
171 WsprMessage::Type1 {
172 callsign: "K9AN".into(),
173 grid: "EN50".into(),
174 power_dbm: 33,
175 }
176 );
177 }
178}