use num_complex::Complex32;
pub const IF_RATE_HZ: u32 = 12_500;
const FLEN: usize = (IF_RATE_HZ as usize / 1200) + 1;
const MFLT_OVER: usize = 12;
const FLEN_OVERSAMPLED: usize = FLEN * MFLT_OVER + 1;
const PLL_GAIN: f32 = 38e-4;
const PLL_COEF: f32 = 0.52;
pub trait BitSink {
fn put_bit(&mut self, value: f32);
fn take_polarity_flip(&mut self) -> bool {
false
}
}
pub struct MskDemod {
msk_phi: f64,
msk_clk: f32,
msk_s: u32,
msk_df: f64,
inb: [Complex32; FLEN],
idx: usize,
pub(crate) lvl_sum: f64,
pub(crate) bit_count: i32,
h: [f32; FLEN_OVERSAMPLED],
}
impl MskDemod {
#[must_use]
pub fn new() -> Self {
let mut h = [0.0_f32; FLEN_OVERSAMPLED];
let center = (FLEN_OVERSAMPLED - 1) / 2;
#[allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss
)]
for (i, slot) in h.iter_mut().enumerate() {
let n = i as i32 - center as i32;
let arg =
2.0_f64 * core::f64::consts::PI * 600.0 / f64::from(IF_RATE_HZ) / MFLT_OVER as f64
* f64::from(n);
let c = arg.cos() as f32;
*slot = if c < 0.0 { 0.0 } else { c };
}
Self {
msk_phi: 0.0,
msk_clk: 0.0,
msk_s: 0,
msk_df: 0.0,
inb: [Complex32::new(0.0, 0.0); FLEN],
idx: 0,
lvl_sum: 0.0,
bit_count: 0,
h,
}
}
pub fn process<S: BitSink>(&mut self, samples: &[f32], sink: &mut S) {
let mut idx = self.idx;
let mut p = self.msk_phi;
for &in_sample in samples {
let s: f64 =
1800.0_f64 / f64::from(IF_RATE_HZ) * 2.0 * core::f64::consts::PI + self.msk_df;
p += s;
if p >= 2.0 * core::f64::consts::PI {
p -= 2.0 * core::f64::consts::PI;
}
#[allow(clippy::cast_possible_truncation)]
let cos_p = p.cos() as f32;
#[allow(clippy::cast_possible_truncation)]
let sin_p = p.sin() as f32;
self.inb[idx] = Complex32::new(in_sample * cos_p, -in_sample * sin_p);
idx = (idx + 1) % FLEN;
#[allow(clippy::cast_possible_truncation)]
{
self.msk_clk = (f64::from(self.msk_clk) + s) as f32;
}
let threshold = 3.0 * core::f64::consts::PI / 2.0 - s / 2.0;
if f64::from(self.msk_clk) >= threshold {
#[allow(clippy::cast_possible_truncation)]
{
self.msk_clk =
(f64::from(self.msk_clk) - 3.0 * core::f64::consts::PI / 2.0) as f32;
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
let mut o = (MFLT_OVER as f64 * (f64::from(self.msk_clk) / s + 0.5)) as usize;
if o > MFLT_OVER {
o = MFLT_OVER;
}
let mut v = Complex32::new(0.0, 0.0);
for j in 0..FLEN {
v += self.inb[(j + idx) % FLEN] * self.h[o];
o += MFLT_OVER;
}
let lvl = v.norm();
v /= lvl + 1e-8;
self.lvl_sum += f64::from(lvl) * f64::from(lvl) / 4.0;
self.bit_count += 1;
let vo: f32;
let dphi: f32;
if self.msk_s & 1 != 0 {
vo = v.im;
dphi = if vo >= 0.0 { -v.re } else { v.re };
} else {
vo = v.re;
dphi = if vo >= 0.0 { v.im } else { -v.im };
}
if self.msk_s & 2 != 0 {
sink.put_bit(-vo);
} else {
sink.put_bit(vo);
}
if sink.take_polarity_flip() {
self.msk_s ^= 2;
}
self.msk_s = self.msk_s.wrapping_add(1);
self.msk_df = f64::from(PLL_COEF) * self.msk_df
+ (1.0_f64 - f64::from(PLL_COEF)) * f64::from(PLL_GAIN) * f64::from(dphi);
}
}
self.idx = idx;
self.msk_phi = p;
}
pub fn toggle_polarity(&mut self) {
self.msk_s ^= 2;
}
}
impl Default for MskDemod {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
struct CapturingSink {
bits: Vec<bool>,
}
impl BitSink for CapturingSink {
fn put_bit(&mut self, value: f32) {
self.bits.push(value > 0.0);
}
}
#[test]
fn flen_constants_match_c_integer_division() {
assert_eq!(FLEN, 11);
assert_eq!(FLEN_OVERSAMPLED, 133);
}
#[test]
fn matched_filter_is_half_wave_clipped_cosine() {
let demod = MskDemod::new();
for &c in &demod.h {
assert!(c >= 0.0, "matched filter coefficient went negative");
}
let center = (FLEN_OVERSAMPLED - 1) / 2;
assert!(
(demod.h[center] - 1.0).abs() < 1e-6,
"center tap should be 1.0"
);
}
#[test]
fn demod_produces_no_bits_from_silence() {
let mut demod = MskDemod::new();
let mut sink = CapturingSink { bits: Vec::new() };
let silence = vec![0.0_f32; 12_500]; demod.process(&silence, &mut sink);
assert!(demod.lvl_sum.is_finite(), "lvl_sum became NaN/Inf");
assert!(demod.msk_df.is_finite(), "MskDf became NaN/Inf");
}
#[test]
fn demod_advances_phase_state() {
let mut demod = MskDemod::new();
let mut sink = CapturingSink { bits: Vec::new() };
let initial_phi = demod.msk_phi;
let initial_idx = demod.idx;
demod.process(&vec![0.0_f32; 1000], &mut sink);
assert!(
(demod.msk_phi - initial_phi).abs() > 0.0,
"VCO phase did not advance"
);
assert_eq!(demod.idx, (initial_idx + 1000) % FLEN);
}
}