use alloc::string::String;
use alloc::vec::Vec;
#[cfg(not(feature = "std"))]
use num_traits::Float;
use crate::core::DecodeContext;
use crate::core::dsp::analytic_signal;
use crate::core::dsp::msk::NSPM;
use crate::msk144::sync::{msk144_sync, rotate_to_shift};
use crate::msk144::{frame_decode::decode_frame, spd::short_ping_decode};
const BLOCK_SIZE: usize = 7168;
const STEP_SIZE: usize = BLOCK_SIZE / 2;
const IAVPATTERNS: [[bool; 8]; 4] = [
[true, true, true, true, false, false, false, false],
[false, false, true, true, true, true, false, false],
[true, true, true, true, true, false, false, false],
[true, true, true, true, true, true, true, false],
];
const XMC: [f32; 4] = [2.0, 4.5, 2.5, 3.5];
#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
pub enum Depth {
Fast,
Normal,
#[default]
Deep,
}
impl Depth {
fn npat(self) -> usize {
match self {
Depth::Fast => 0,
Depth::Normal => 2,
Depth::Deep => 4,
}
}
}
#[derive(Clone, Debug)]
pub struct SlotDecode {
pub message: String,
pub snr_db: i32,
pub freq_hz: f32,
pub tsec: f32,
}
struct ScanState {
pnoise: f32,
msglast: String,
nsnrlast: i32,
}
impl Default for ScanState {
fn default() -> Self {
Self {
pnoise: -1.0,
msglast: String::new(),
nsnrlast: -99,
}
}
}
fn decode_block(
audio_i16: &[i16],
fc: f32,
ntol: f32,
depth: Depth,
ctx: &DecodeContext,
state: &mut ScanState,
) -> Option<SlotDecode> {
assert_eq!(audio_i16.len(), BLOCK_SIZE);
let d: Vec<f32> = audio_i16.iter().map(|&x| x as f32).collect();
let rms = (d.iter().map(|&x| x * x).sum::<f32>() / BLOCK_SIZE as f32).sqrt();
if rms < 1.0 {
return None; }
let fac = 1.0 / rms;
let d_norm: Vec<f32> = d.iter().map(|&x| x * fac).collect();
let cdat = analytic_signal(&d_norm);
let mut pow = [0.0f32; 8];
for (i, p) in pow.iter_mut().enumerate() {
let ib = i * NSPM;
let ie = ib + NSPM;
let energy: f32 = cdat[ib..ie].iter().map(|c| c.norm_sqr()).sum();
*p = energy * rms * rms;
}
let pmax = pow.iter().cloned().fold(f32::MIN, f32::max);
let pavg = pow.iter().sum::<f32>() / 8.0;
let cbig = &cdat[0..8 * NSPM];
if let Some(spd) = short_ping_decode(cbig, fc, ntol, ctx) {
return finish_decode(spd.message, pmax, state, spd.tdec_sec, spd.fest);
}
let tframe = NSPM as f32 / 12_000.0;
for iavg in 0..depth.npat() {
let navmask = IAVPATTERNS[iavg];
let navg = navmask.iter().filter(|&&b| b).count().max(1);
let deltaf = 10.0 / navg as f32;
let sync_result = msk144_sync(cbig, 8, ntol, deltaf, &navmask, 2, fc);
if !sync_result.success {
continue;
}
for peak in &sync_result.peaks {
for dither in 0..3 {
let ic0 = match dither {
0 => peak.shift,
1 => peak.shift.saturating_sub(1),
_ => (peak.shift + 1).min(NSPM - 1),
};
let aligned = rotate_to_shift(&sync_result.frame, ic0);
let softbits = crate::core::dsp::msk::matched_filter_softbits(
aligned.as_slice().try_into().expect("NSPM samples"),
);
if let Some(result) = decode_frame(&softbits, ctx) {
let tdec_sec = XMC[iavg] * tframe;
return finish_decode(result.message, pmax, state, tdec_sec, sync_result.fest);
}
}
}
}
if state.pnoise < 0.0 {
state.pnoise = pavg;
} else if pavg > state.pnoise {
state.pnoise = 0.9 * state.pnoise + 0.1 * pavg;
} else {
state.pnoise = pavg;
}
None
}
fn finish_decode(
message: String,
pmax: f32,
state: &mut ScanState,
tdec_sec: f32,
fest: f32,
) -> Option<SlotDecode> {
let snr0 = if state.pnoise > 0.0 {
10.0 * (pmax / state.pnoise - 1.0).log10()
} else {
0.0
};
let nsnr = (snr0.round() as i32).clamp(-8, 24);
let accept = message != state.msglast || nsnr > state.nsnrlast;
if !accept {
return None;
}
state.msglast = message.clone();
state.nsnrlast = nsnr;
Some(SlotDecode {
message,
snr_db: nsnr,
freq_hz: fest,
tsec: tdec_sec,
})
}
pub fn decode_slot(audio: &[i16], fc: f32, ntol: f32, depth: Depth) -> Vec<SlotDecode> {
let ctx = DecodeContext::default();
let mut state = ScanState::default();
let mut out = Vec::new();
if audio.len() < BLOCK_SIZE {
return out;
}
let mut position = 0usize;
while position + BLOCK_SIZE <= audio.len() {
let block = &audio[position..position + BLOCK_SIZE];
let tsec = position as f32 / 12_000.0;
if let Some(mut r) = decode_block(block, fc, ntol, depth, &ctx, &mut state) {
r.tsec += tsec;
out.push(r);
}
position += STEP_SIZE;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::FecCodec;
use crate::core::dsp::msk::build_bitseq;
use crate::fec::Ldpc128_90;
fn build_i4tone(bitseq_natural: &[u8; 144]) -> [u8; 144] {
let mut bp = [0i8; 144];
for i in 0..144 {
bp[i] = 2 * bitseq_natural[i] as i8 - 1;
}
let mut i4tone = [0i8; 144];
for i in 1..=72usize {
let b_2i_minus_1 = bp[2 * i - 2];
let b_2i = bp[2 * i - 1];
let b_wrap = bp[(2 * i) % 144];
i4tone[2 * i - 2] = (b_2i * b_2i_minus_1 + 1) / 2;
i4tone[2 * i - 1] = -((b_2i * b_wrap - 1) / 2);
}
let mut out = [0u8; 144];
for i in 0..144 {
out[i] = (-i4tone[i] + 1) as u8;
}
out
}
fn msk144sim_reference_audio(itone: &[u8], freq_hz: f32) -> Vec<f32> {
let twopi = 2.0 * core::f32::consts::PI;
let baud = 2000.0f32;
let dphi0 = twopi * (freq_hz - 0.25 * baud) / 12_000.0;
let dphi1 = twopi * (freq_hz + 0.25 * baud) / 12_000.0;
let mut phi = 0.0f32;
let mut out = Vec::with_capacity(itone.len() * 6);
for &tone in itone {
let dphi = if tone == 0 { dphi0 } else { dphi1 };
for _ in 0..6 {
out.push(phi.cos());
phi = (phi + dphi) % twopi;
}
}
out
}
fn pseudo_gaussian_noise_i16(n: usize, amp: f32, seed: u32) -> Vec<i16> {
let mut state = seed | 1;
let mut next_u32 = move || {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
state
};
let mut uniform = move || (next_u32() as f32 + 1.0) / (u32::MAX as f32 + 2.0);
(0..n)
.map(|_| {
let u1 = uniform();
let u2 = uniform();
let r = (-2.0 * u1.ln()).sqrt();
let theta = 2.0 * core::f32::consts::PI * u2;
(amp * r * theta.cos()).clamp(i16::MIN as f32, i16::MAX as f32) as i16
})
.collect()
}
fn build_codeword_bitseq(call1: &str, call2: &str, report: &str) -> [u8; 144] {
let msg77 = crate::msg::wsjt77::pack77(call1, call2, report).expect("valid message");
let mut info = [0u8; 90];
info[..77].copy_from_slice(&msg77);
let mut bytes = [0u8; 12];
for (i, &b) in info[..77].iter().enumerate() {
let byte_idx = i / 8;
let bit_pos = 7 - (i % 8);
bytes[byte_idx] |= (b & 1) << bit_pos;
}
let crc = crate::fec::ldpc_128_90::crc13(&bytes);
for i in 0..13 {
info[77 + i] = ((crc >> (12 - i)) & 1) as u8;
}
let mut codeword = [0u8; 128];
Ldpc128_90.encode(&info, &mut codeword);
build_bitseq(&codeword)
}
#[test]
fn decode_slot_recovers_a_real_message_from_a_15s_buffer() {
let bitseq = build_codeword_bitseq("K1ABC", "W9XYZ", "EN37");
let itone = build_i4tone(&bitseq);
const NREPS: usize = 20; let itone_repeated: Vec<u8> = itone.iter().copied().cycle().take(144 * NREPS).collect();
let fc_true = 1500.0f32;
let audio_f32 = msk144sim_reference_audio(&itone_repeated, fc_true);
let slot_len = 15 * 12_000;
let mut audio_i16 = pseudo_gaussian_noise_i16(slot_len, 300.0, 42);
let true_start = 3 * 12_000; const AMPLITUDE: f32 = 3000.0;
for (k, &s) in audio_f32.iter().enumerate() {
let v = audio_i16[true_start + k] as f32 + AMPLITUDE * s;
audio_i16[true_start + k] = v.clamp(i16::MIN as f32, i16::MAX as f32) as i16;
}
let decodes = decode_slot(&audio_i16, fc_true, 8.0, Depth::Deep);
assert!(!decodes.is_empty(), "expected at least one decode");
assert!(
decodes.iter().any(|d| d.message.contains("K1ABC")
&& d.message.contains("W9XYZ")
&& d.message.contains("EN37")),
"no decode matched the embedded message: {decodes:?}"
);
for d in &decodes {
assert!((-8..=24).contains(&d.snr_db), "snr_db out of range: {d:?}");
assert!(
d.tsec >= 0.0 && d.tsec <= 15.0,
"tsec out of slot range: {d:?}"
);
}
}
#[test]
fn decode_slot_returns_nothing_on_pure_noise() {
let audio_i16 = pseudo_gaussian_noise_i16(15 * 12_000, 300.0, 7);
let decodes = decode_slot(&audio_i16, 1500.0, 8.0, Depth::Fast);
assert!(
decodes.is_empty(),
"unexpected decodes from pure noise: {decodes:?}"
);
}
}