use num_complex::Complex32;
use crate::engine::dsp::msk::{NSPM, build_bitseq, synth_frame};
pub fn synth_codeword_frame(codeword: &[u8; 128]) -> [Complex32; NSPM] {
let bitseq = build_bitseq(codeword);
synth_frame(&bitseq)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::dsp::msk::{build_bitseq, matched_filter_softbits};
use crate::engine::{FecCodec, FecOpts};
use crate::fec::Ldpc128_90;
use crate::fec::ldpc_128_90::check_crc13;
fn build_info_with_crc(pattern: impl Fn(usize) -> u8) -> [u8; 90] {
let mut info = [0u8; 90];
for i in 0..77 {
info[i] = pattern(i);
}
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;
}
info
}
fn softbits_to_data_llr(softbits: &[f32; 144]) -> [f32; 128] {
let mean: f32 = softbits.iter().sum::<f32>() / 144.0;
let var: f32 = softbits
.iter()
.map(|s| (s - mean) * (s - mean))
.sum::<f32>()
/ 144.0;
let sigma_sig = var.sqrt();
const SIGMA: f32 = 0.60;
let mut llr = [0.0f32; 128];
for (i, v) in llr[0..48].iter_mut().enumerate() {
*v = 2.0 * (softbits[8 + i] / sigma_sig) / (SIGMA * SIGMA);
}
for (i, v) in llr[48..128].iter_mut().enumerate() {
*v = 2.0 * (softbits[64 + i] / sigma_sig) / (SIGMA * SIGMA);
}
llr
}
#[test]
fn clean_frame_roundtrips_through_ldpc() {
let info = build_info_with_crc(|i| ((i * 7 + 3) & 1) as u8);
assert!(check_crc13(&info));
let mut codeword = [0u8; 128];
Ldpc128_90.encode(&info, &mut codeword);
let frame = synth_codeword_frame(&codeword);
let softbits = matched_filter_softbits(&frame);
let bitseq = build_bitseq(&codeword);
for i in 0..144 {
assert_eq!(
softbits[i] > 0.0,
bitseq[i] == 1,
"softbit {i} sign mismatch"
);
}
let llr = softbits_to_data_llr(&softbits);
let opts = FecOpts {
verify_info: Some(check_crc13),
..Default::default()
};
let r = Ldpc128_90
.decode_soft(&llr, &opts)
.expect("BP converges on clean synth frame");
assert_eq!(&r.info[..77], &info[..77]);
}
#[test]
fn phase_rotated_frame_still_decodes() {
let info = build_info_with_crc(|i| ((i * 3 + 5) & 1) as u8);
let mut codeword = [0u8; 128];
Ldpc128_90.encode(&info, &mut codeword);
let mut frame = synth_codeword_frame(&codeword);
let phase = 0.7_f32; let rot = Complex32::new(phase.cos(), phase.sin());
for c in frame.iter_mut() {
*c *= rot;
}
let softbits = matched_filter_softbits(&frame);
let bitseq = build_bitseq(&codeword);
for i in 0..144 {
assert_eq!(
softbits[i] > 0.0,
bitseq[i] == 1,
"softbit {i} sign mismatch under phase rotation"
);
}
let llr = softbits_to_data_llr(&softbits);
let opts = FecOpts {
verify_info: Some(check_crc13),
..Default::default()
};
let r = Ldpc128_90
.decode_soft(&llr, &opts)
.expect("BP converges under constant phase rotation");
assert_eq!(&r.info[..77], &info[..77]);
}
#[test]
fn mildly_perturbed_frame_still_decodes() {
let info = build_info_with_crc(|i| ((i * 5 + 1) & 1) as u8);
let mut codeword = [0u8; 128];
Ldpc128_90.encode(&info, &mut codeword);
let mut frame = synth_codeword_frame(&codeword);
for (k, c) in frame.iter_mut().enumerate() {
if k % 7 == 0 {
let n = (k as f32 * 12.9898).sin() * 43_758.547;
let pseudo = n - n.floor() - 0.5; *c += Complex32::new(0.15 * pseudo, -0.15 * pseudo);
}
}
let softbits = matched_filter_softbits(&frame);
let llr = softbits_to_data_llr(&softbits);
let opts = FecOpts {
verify_info: Some(check_crc13),
..Default::default()
};
let r = Ldpc128_90
.decode_soft(&llr, &opts)
.expect("BP converges under mild perturbation");
assert_eq!(&r.info[..77], &info[..77]);
}
}