use num_complex::Complex32;
#[cfg(not(feature = "std"))]
use num_traits::Float;
pub const NSPM: usize = 864;
pub const FS_HZ: f32 = 12_000.0;
pub const S8: [u8; 8] = [0, 1, 1, 1, 0, 0, 1, 0];
pub fn half_sine_pulse() -> [f32; 12] {
let mut pp = [0.0f32; 12];
for (i, p) in pp.iter_mut().enumerate() {
*p = (i as f32 * core::f32::consts::PI / 12.0).sin();
}
pp
}
pub fn bipolar_sync_word() -> [i8; 8] {
let mut s8 = [0i8; 8];
for (i, s) in s8.iter_mut().enumerate() {
*s = 2 * S8[i] as i8 - 1;
}
s8
}
pub fn sync_waveform() -> [Complex32; 42] {
let pp = half_sine_pulse();
let s8 = bipolar_sync_word();
let mut cbi = [0.0f32; 42];
let mut cbq = [0.0f32; 42];
for i in 0..12 {
cbi[i] = pp[i] * s8[1] as f32;
cbi[12 + i] = pp[i] * s8[3] as f32;
cbi[24 + i] = pp[i] * s8[5] as f32;
}
for i in 0..6 {
cbi[36 + i] = pp[i] * s8[7] as f32;
}
for i in 0..6 {
cbq[i] = pp[6 + i] * s8[0] as f32;
}
for i in 0..12 {
cbq[6 + i] = pp[i] * s8[2] as f32;
cbq[18 + i] = pp[i] * s8[4] as f32;
cbq[30 + i] = pp[i] * s8[6] as f32;
}
let mut cb = [Complex32::new(0.0, 0.0); 42];
for i in 0..42 {
cb[i] = Complex32::new(cbi[i], cbq[i]);
}
cb
}
pub fn build_bitseq(codeword: &[u8; 128]) -> [u8; 144] {
let mut bitseq = [0u8; 144];
bitseq[0..8].copy_from_slice(&S8);
bitseq[8..56].copy_from_slice(&codeword[0..48]);
bitseq[56..64].copy_from_slice(&S8);
bitseq[64..144].copy_from_slice(&codeword[48..128]);
bitseq
}
pub fn synth_frame(bitseq: &[u8; 144]) -> [Complex32; NSPM] {
let pp = half_sine_pulse();
let mut bp = [0i8; 144];
for i in 0..144 {
bp[i] = 2 * bitseq[i] as i8 - 1;
}
let mut xi = [0.0f32; NSPM];
let mut xq = [0.0f32; NSPM];
for k in 0..6 {
xq[k] = bp[0] as f32 * pp[6 + k];
}
for i in 1..=71usize {
let is0 = (i - 1) * 12 + 6; let bit = bp[2 * i] as f32; for k in 0..12 {
xq[is0 + k] = bit * pp[k];
}
}
for k in 0..6 {
xq[858 + k] = bp[0] as f32 * pp[k];
}
for i in 1..=72usize {
let is0 = (i - 1) * 12; let bit = bp[2 * i - 1] as f32; for k in 0..12 {
xi[is0 + k] = bit * pp[k];
}
}
let mut c = [Complex32::new(0.0, 0.0); NSPM];
for k in 0..NSPM {
c[k] = Complex32::new(xi[k], xq[k]);
}
c
}
pub fn matched_filter_softbits(c: &[Complex32; NSPM]) -> [f32; 144] {
let pp = half_sine_pulse();
let cb = sync_waveform();
let mut cca = Complex32::new(0.0, 0.0);
let mut ccb = Complex32::new(0.0, 0.0);
for k in 0..42 {
cca += c[k] * cb[k].conj();
ccb += c[336 + k] * cb[k].conj();
}
let s = cca + ccb;
let phase0 = s.im.atan2(s.re);
let cfac = Complex32::new(phase0.cos(), phase0.sin());
let mut cr = [Complex32::new(0.0, 0.0); NSPM];
for k in 0..NSPM {
cr[k] = c[k] * cfac.conj();
}
let mut softbits = [0.0f32; 144];
softbits[0] = (0..6).map(|k| cr[k].im * pp[6 + k]).sum::<f32>()
+ (0..6).map(|k| cr[858 + k].im * pp[k]).sum::<f32>();
softbits[1] = (0..12).map(|k| cr[k].re * pp[k]).sum::<f32>();
for i in 2..=72usize {
let start_q = (i - 1) * 12 - 6; softbits[2 * i - 2] = (0..12).map(|k| cr[start_q + k].im * pp[k]).sum::<f32>();
let start_i = (i - 1) * 12; softbits[2 * i - 1] = (0..12).map(|k| cr[start_i + k].re * pp[k]).sum::<f32>();
}
softbits
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn half_sine_pulse_matches_analytic_sin() {
let pp = half_sine_pulse();
for (i, &p) in pp.iter().enumerate() {
let expected = (i as f32 * core::f32::consts::PI / 12.0).sin();
assert!(
(p - expected).abs() < 1e-6,
"pp[{i}] = {p}, expected {expected}"
);
}
assert!((pp[0] - 0.0).abs() < 1e-6);
assert!((pp[6] - 1.0).abs() < 1e-6);
}
#[test]
fn bipolar_sync_word_matches_fortran() {
assert_eq!(bipolar_sync_word(), [-1, 1, 1, 1, -1, -1, 1, -1]);
}
#[test]
fn sync_waveform_matches_fortran_layout() {
let pp = half_sine_pulse();
let cb = sync_waveform();
for i in 0..12 {
assert!((cb[i].re - pp[i]).abs() < 1e-6, "cb[{i}].re");
assert!((cb[12 + i].re - pp[i]).abs() < 1e-6, "cb[{}].re", 12 + i);
assert!((cb[24 + i].re + pp[i]).abs() < 1e-6, "cb[{}].re", 24 + i);
}
for i in 0..6 {
assert!((cb[36 + i].re + pp[i]).abs() < 1e-6, "cb[{}].re", 36 + i);
}
for i in 0..6 {
assert!((cb[i].im + pp[6 + i]).abs() < 1e-6, "cb[{i}].im");
}
for i in 0..12 {
assert!((cb[6 + i].im - pp[i]).abs() < 1e-6, "cb[{}].im", 6 + i);
assert!((cb[18 + i].im + pp[i]).abs() < 1e-6, "cb[{}].im", 18 + i);
assert!((cb[30 + i].im - pp[i]).abs() < 1e-6, "cb[{}].im", 30 + i);
}
}
#[test]
fn frame_constants_match_wsjtx() {
assert_eq!(NSPM, 864);
assert_eq!(FS_HZ, 12_000.0);
assert_eq!(S8, [0, 1, 1, 1, 0, 0, 1, 0]);
}
#[test]
fn synth_frame_wraps_bit_one_across_boundary() {
let pp = half_sine_pulse();
let mut bitseq = [1u8; 144]; bitseq[0] = 0;
let c = synth_frame(&bitseq);
for k in 0..6 {
assert!(
(c[k].im + pp[6 + k]).abs() < 1e-6,
"c[{k}].im (start wrap), got {}",
c[k].im
);
assert!(
(c[858 + k].im + pp[k]).abs() < 1e-6,
"c[{}].im (end wrap), got {}",
858 + k,
c[858 + k].im
);
}
bitseq[0] = 1;
let c2 = synth_frame(&bitseq);
for k in 0..6 {
assert!(
(c2[k].im - pp[6 + k]).abs() < 1e-6,
"c2[{k}].im (start wrap)"
);
assert!(
(c2[858 + k].im - pp[k]).abs() < 1e-6,
"c2[{}].im (end wrap)",
858 + k
);
}
for k in 6..858 {
assert!(
(c[k].im - c2[k].im).abs() < 1e-6,
"c[{k}].im should not depend on bit 0"
);
}
for k in 0..NSPM {
assert!(
(c[k].re - c2[k].re).abs() < 1e-6,
"c[{k}].re (I rail) should not depend on bit 0"
);
}
}
}