use num_complex::Complex32 as C32;
use crate::modulate::psk31::{Bpsk31Mod, Qpsk31Mod, psk31_sps};
use crate::demodulate::psk31::{Bpsk31Demod, Bpsk31Decider, Qpsk31Demod, Qpsk31Decider};
use crate::codec::varicode::VaricodeDecoder;
use crate::core::Block;
#[test]
fn roundtrip_bpsk31_bits_noiseless() {
let bits_in: Vec<u8> = (0..128).map(|i| ((i * 7 + 3) & 1) as u8).collect();
let fs = 8000.0;
let mut modulator = Bpsk31Mod::new(fs, 0.0, 1.0);
let iq = modulator.modulate_bits(&bits_in);
let mut demod = Bpsk31Demod::new(fs, 0.0, 1.0);
let mut soft = vec![0.0f32; bits_in.len() + 4];
let wr = demod.process(&iq, &mut soft);
soft.truncate(wr.out_written);
let mut decider = Bpsk31Decider::new();
let mut bits_out = vec![0u8; soft.len()];
let dr = decider.process(&soft, &mut bits_out);
bits_out.truncate(dr.out_written);
let skip = 1;
let n = bits_in.len().min(bits_out.len()).saturating_sub(skip);
let errors: usize = (0..n)
.filter(|&i| bits_in[i + skip] != bits_out[i + skip])
.count();
assert_eq!(errors, 0, "BER non-zero: {} errors in {} bits", errors, n);
}
#[test]
fn roundtrip_bpsk31_text() {
let text = b"HELLO TEST";
let fs = 8000.0;
let mut modulator = Bpsk31Mod::new(fs, 0.0, 1.0);
let iq = modulator.modulate_text(text, 32, 64);
let mut demod = Bpsk31Demod::new(fs, 0.0, 1.0);
let mut soft = vec![0.0f32; iq.len()];
let wr = demod.process(&iq, &mut soft);
soft.truncate(wr.out_written);
let mut bits = vec![0u8; soft.len()];
let dr = Bpsk31Decider::new().process(&soft, &mut bits);
bits.truncate(dr.out_written);
let mut vdec = VaricodeDecoder::new();
for &b in &bits {
vdec.push_bit(b);
}
vdec.push_bit(0);
vdec.push_bit(0);
let mut decoded = Vec::new();
while let Some(c) = vdec.pop_char() {
decoded.push(c);
}
let decoded_str = String::from_utf8_lossy(&decoded);
let expected = String::from_utf8_lossy(text);
assert!(
decoded_str.contains(expected.as_ref()),
"decoded '{}' does not contain '{}'",
decoded_str,
expected
);
}
#[test]
fn roundtrip_bpsk31_pulse_zero_crossing() {
let fs = 8000.0;
let sps = psk31_sps(fs);
let mut modulator = Bpsk31Mod::new(fs, 0.0, 1.0);
let iq = modulator.modulate_bits(&[1u8, 0u8]);
let mid = sps + sps / 2;
let sample = iq[mid];
let amplitude = (sample.re * sample.re + sample.im * sample.im).sqrt();
assert!(
amplitude < 0.15,
"expected near-zero at phase-flip midpoint, got amplitude {}",
amplitude
);
}
#[test]
fn roundtrip_bpsk31_no_flip_constant() {
let fs = 8000.0;
let sps = psk31_sps(fs);
let mut modulator = Bpsk31Mod::new(fs, 0.0, 1.0);
let iq = modulator.modulate_bits(&[1u8; 4]);
let amp: Vec<f32> = iq[sps..3*sps]
.iter()
.map(|s| (s.re * s.re + s.im * s.im).sqrt())
.collect();
let mean = amp.iter().sum::<f32>() / amp.len() as f32;
let var = amp.iter().map(|a| (a - mean) * (a - mean)).sum::<f32>() / amp.len() as f32;
assert!(
var < 0.01,
"expected constant amplitude for no-flip symbols, variance = {}",
var
);
}
#[test]
fn roundtrip_qpsk31_bits_noiseless() {
let bits_in: Vec<u8> = (0..64).map(|i| (i & 1) as u8).collect();
let fs = 8000.0;
let mut modulator = Qpsk31Mod::new(fs, 0.0, 1.0);
let iq = modulator.modulate_bits(&bits_in);
let mut demod = Qpsk31Demod::new(fs, 0.0, 1.0);
let mut soft = vec![0.0f32; bits_in.len() * 4]; let wr = demod.process(&iq, &mut soft);
soft.truncate(wr.out_written);
let mut decider = Qpsk31Decider::new();
decider.process(&soft, &mut vec![]);
let mut bits_out = Vec::new();
decider.flush(&mut bits_out);
let skip = 5;
let n = bits_in.len().min(bits_out.len()).saturating_sub(skip);
let errors: usize = (0..n)
.filter(|&i| bits_in[i + skip] != bits_out[i + skip])
.count();
assert_eq!(errors, 0, "BER non-zero: {} errors in {} bits", errors, n);
}
#[test]
fn roundtrip_qpsk31_text() {
let text = b"HELLO TEST";
let fs = 8000.0;
let mut modulator = Qpsk31Mod::new(fs, 0.0, 1.0);
let iq = modulator.modulate_text(text, 32, 64);
let mut demod = Qpsk31Demod::new(fs, 0.0, 1.0);
let mut soft = vec![0.0f32; iq.len()];
let wr = demod.process(&iq, &mut soft);
soft.truncate(wr.out_written);
let mut decider = Qpsk31Decider::new();
decider.process(&soft, &mut vec![]);
let mut decoded_bits = Vec::new();
decider.flush(&mut decoded_bits);
let mut vdec = VaricodeDecoder::new();
for &b in &decoded_bits {
vdec.push_bit(b);
}
vdec.push_bit(0);
vdec.push_bit(0);
let mut decoded = Vec::new();
while let Some(c) = vdec.pop_char() {
decoded.push(c);
}
let decoded_str = String::from_utf8_lossy(&decoded);
let expected = String::from_utf8_lossy(text);
assert!(
decoded_str.contains(expected.as_ref()),
"QPSK31 decoded '{}' does not contain '{}'",
decoded_str,
expected
);
}
#[test]
fn roundtrip_psk31_sync_finds_bpsk31() {
use crate::sync::psk31_sync::psk31_sync;
use crate::modulate::psk31::PSK31_BAUD;
let fs = 8000.0;
let base_hz = 900.0;
let carrier_hz = base_hz + 3.0 * PSK31_BAUD;
let text = b"CQ CQ";
let mut modulator = Bpsk31Mod::new(fs, carrier_hz, 1.0);
let iq = modulator.modulate_text(text, 64, 32);
let total_samples = (fs * 4.0) as usize;
let mut buf = vec![C32::new(0.0, 0.0); total_samples];
for (i, s) in iq.iter().enumerate() {
if i < total_samples {
buf[i] = *s;
}
}
let results = psk31_sync(&buf, fs, base_hz, base_hz + 200.0, 4, 3.0, 256, 5);
assert!(!results.is_empty(), "psk31_sync should find the BPSK31 carrier");
let best = &results[0];
assert!(
(best.carrier_hz - carrier_hz).abs() < 40.0,
"carrier_hz {} too far from expected {}",
best.carrier_hz,
carrier_hz
);
assert!(!best.soft_bits.is_empty(), "soft_bits should not be empty");
}
#[test]
fn roundtrip_psk31_sync_finds_qpsk31() {
use crate::sync::psk31_sync::psk31_sync;
use crate::modulate::psk31::PSK31_BAUD;
let fs = 8000.0;
let base_hz = 1400.0;
let carrier_hz = base_hz + 2.0 * PSK31_BAUD;
let text = b"TEST";
let mut modulator = Qpsk31Mod::new(fs, carrier_hz, 1.0);
let iq = modulator.modulate_text(text, 64, 32);
let total_samples = (fs * 4.0) as usize;
let mut buf = vec![C32::new(0.0, 0.0); total_samples];
for (i, s) in iq.iter().enumerate() {
if i < total_samples {
buf[i] = *s;
}
}
let results = psk31_sync(&buf, fs, base_hz, base_hz + 200.0, 4, 3.0, 256, 5);
assert!(!results.is_empty(), "psk31_sync should find the QPSK31 carrier");
let best = &results[0];
assert!(
(best.carrier_hz - carrier_hz).abs() < 40.0,
"carrier_hz {} too far from expected {}",
best.carrier_hz,
carrier_hz
);
}