use num_complex::Complex32;
use std::f32::consts::PI;
use crate::core::{FecCodec, FecOpts};
use crate::fec::Ldpc240_101;
use super::framing::{INFO_BYTES_PER_BLOCK, UnpackError, unpack as unpack_frame};
use super::interleaver::deinterleave_llr;
use super::puncture::{Mode, de_puncture_llr};
use super::sync_pattern::{
PILOT_QPSK_POINT, PILOT_SYMBOL_INTERVAL, PREAMBLE_LEN, UVPACKET_PREAMBLE_BPSK_BITS,
};
const K_LDPC: usize = 101;
const SAMPLE_RATE_HZ: f32 = 12_000.0;
const NSPS: usize = 10;
const RRC_SPAN_SYMS: usize = 6;
const RRC_ALPHA: f32 = 0.5;
const RRC_LEN: usize = RRC_SPAN_SYMS * NSPS + 1;
const SYM_PEAK_OFFSET: usize = RRC_LEN - 1;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum DecodeError {
Truncated,
FecFailed,
Crc(UnpackError),
LayoutMismatch {
wanted_mode: Mode,
got_mode: Mode,
wanted_blocks: u8,
got_blocks: u8,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DecodedFrame {
pub app_type: u8,
pub sequence: u8,
pub mode: Mode,
pub block_count: u8,
pub payload: Vec<u8>,
}
fn default_fec_opts() -> FecOpts<'static> {
FecOpts {
bp_max_iter: 50,
osd_depth: 2,
ap_mask: None,
verify_info: None,
}
}
pub fn decode_known_layout(
audio: &[f32],
sample_offset: usize,
audio_centre_hz: f32,
mode: Mode,
n_blocks: u8,
) -> Result<DecodedFrame, DecodeError> {
decode_known_layout_with_opts(
audio,
sample_offset,
audio_centre_hz,
mode,
n_blocks,
&default_fec_opts(),
)
}
pub fn decode_known_layout_with_opts(
audio: &[f32],
sample_offset: usize,
audio_centre_hz: f32,
mode: Mode,
n_blocks: u8,
fec_opts: &FecOpts,
) -> Result<DecodedFrame, DecodeError> {
let n_blocks_u = n_blocks as usize;
let block_ch_bits = mode.ch_bits_per_block();
debug_assert!(block_ch_bits.is_multiple_of(2));
let n_data_syms = n_blocks_u * block_ch_bits / 2;
let n_pilots = n_data_syms.div_ceil(PILOT_SYMBOL_INTERVAL - 1);
let total_syms = PREAMBLE_LEN + n_pilots + n_data_syms;
let needed_samples = total_syms * NSPS + RRC_LEN;
if sample_offset + needed_samples > audio.len() {
return Err(DecodeError::Truncated);
}
let slice = &audio[sample_offset..sample_offset + needed_samples];
let mf_out = downconvert_and_matched_filter(slice, audio_centre_hz);
let radius = NSPS as isize;
let base = SYM_PEAK_OFFSET as isize;
let mut best_off = SYM_PEAK_OFFSET;
let mut best_corr = Complex32::new(0.0, 0.0);
let mut best_mag2 = -1.0_f32;
for jitter in -radius..=radius {
let off = base + jitter;
if off < 0 {
continue;
}
let off = off as usize;
let last = off + (PREAMBLE_LEN - 1) * NSPS;
if last >= mf_out.len() {
continue;
}
let c = preamble_correlation(&mf_out, off);
let mag2 = c.norm_sqr();
if mag2 > best_mag2 {
best_mag2 = mag2;
best_corr = c;
best_off = off;
}
}
let frac_off: f32 = {
let need_minus = best_off > 0 && (best_off - 1) + (PREAMBLE_LEN - 1) * NSPS < mf_out.len();
let need_plus = (best_off + 1) + (PREAMBLE_LEN - 1) * NSPS < mf_out.len();
if need_minus && need_plus {
let m_minus = preamble_correlation(&mf_out, best_off - 1).norm_sqr();
let m_plus = preamble_correlation(&mf_out, best_off + 1).norm_sqr();
let m_zero = best_mag2;
let denom = 2.0 * (m_plus - 2.0 * m_zero + m_minus);
if denom.abs() > 1e-9 {
((m_minus - m_plus) / denom).clamp(-0.5, 0.5)
} else {
0.0
}
} else {
0.0
}
};
if frac_off.abs() > 1e-3 {
let mut acc = Complex32::new(0.0, 0.0);
for (i, &b) in UVPACKET_PREAMBLE_BPSK_BITS.iter().enumerate() {
let s = if b { -1.0_f32 } else { 1.0 };
acc += sample_mf_lerp(&mf_out, best_off as f32 + frac_off + (i * NSPS) as f32) * s;
}
best_corr = acc;
}
let mut symbols: Vec<Complex32> = Vec::with_capacity(total_syms);
for i in 0..total_syms {
let pos = best_off as f32 + frac_off + (i * NSPS) as f32;
if pos < 0.0 || pos as usize + 1 >= mf_out.len() {
return Err(DecodeError::Truncated);
}
symbols.push(sample_mf_lerp(&mf_out, pos));
}
let pilot_ref = qpsk_constellation_point(PILOT_QPSK_POINT);
let mut anchor_idx: Vec<usize> = Vec::with_capacity(n_pilots + 1);
let mut anchor_phase: Vec<f32> = Vec::with_capacity(n_pilots + 1);
let preamble_centre = (PREAMBLE_LEN - 1) / 2;
anchor_idx.push(preamble_centre);
anchor_phase.push(best_corr.arg());
for k in 0..n_pilots {
let sym_pos = PREAMBLE_LEN + k * PILOT_SYMBOL_INTERVAL;
if sym_pos >= total_syms {
break;
}
let r = symbols[sym_pos];
let phase = (r * pilot_ref.conj()).arg();
let prev = *anchor_phase.last().unwrap();
let unwrapped = unwrap_phase(prev, phase);
anchor_idx.push(sym_pos);
anchor_phase.push(unwrapped);
}
let lms_coeffs: Option<[f32; 3]> = if anchor_idx.len() >= 3 {
let n_total = total_syms.max(1) as f32;
let mut a_mat = [[0.0_f32; 3]; 3];
let mut a_vec = [0.0_f32; 3];
let weights: Vec<f32> = (0..anchor_idx.len())
.map(|k| {
if k == 0 {
(PREAMBLE_LEN as f32).sqrt()
} else {
1.0
}
})
.collect();
for k in 0..anchor_idx.len() {
let t = anchor_idx[k] as f32 / n_total;
let w = weights[k];
let row = [1.0, t, t * t];
for i in 0..3 {
for j in 0..3 {
a_mat[i][j] += w * row[i] * row[j];
}
a_vec[i] += w * row[i] * anchor_phase[k];
}
}
solve_3x3(&a_mat, &a_vec)
} else {
None
};
let total_syms_f = total_syms.max(1) as f32;
let block_data_syms = block_ch_bits / 2;
let mut block_dd_correction = vec![0.0_f32; n_blocks_u];
{
let mut data_running = 0_usize;
let mut block_resid = vec![Complex32::new(0.0, 0.0); n_blocks_u];
for i in PREAMBLE_LEN..total_syms {
let rel = i - PREAMBLE_LEN;
if rel.is_multiple_of(PILOT_SYMBOL_INTERVAL) {
continue;
}
let phase = lms_phase(lms_coeffs, total_syms_f, &anchor_idx, &anchor_phase, i);
let derot = symbols[i] * Complex32::from_polar(1.0, -phase);
let candidates = [
Complex32::new(1.0, 0.0),
Complex32::new(0.0, 1.0),
Complex32::new(-1.0, 0.0),
Complex32::new(0.0, -1.0),
];
let mut best_c = candidates[0];
let mut best_d2 = f32::INFINITY;
for &c in &candidates {
let d2 = (derot - c).norm_sqr();
if d2 < best_d2 {
best_d2 = d2;
best_c = c;
}
}
let r = derot * best_c.conj();
let block_idx = data_running / block_data_syms;
if block_idx < n_blocks_u {
block_resid[block_idx] += r;
}
data_running += 1;
if data_running >= n_data_syms {
break;
}
}
for b in 0..n_blocks_u {
let mag = block_resid[b].norm();
let n_per_block = block_data_syms as f32;
if mag > 0.25 * n_per_block {
block_dd_correction[b] = block_resid[b].arg();
}
}
}
let mut a_acc = 0.0_f32;
let mut a_norm = 0.0_f32;
for (i, &b) in UVPACKET_PREAMBLE_BPSK_BITS.iter().enumerate() {
let expected = Complex32::new(if b { -1.0 } else { 1.0 }, 0.0);
let phase = lms_phase(lms_coeffs, total_syms_f, &anchor_idx, &anchor_phase, i);
let derot = symbols[i] * Complex32::from_polar(1.0, -phase);
a_acc += derot.re * expected.re + derot.im * expected.im;
a_norm += expected.norm_sqr();
}
for k in 0..n_pilots {
let sym_pos = PREAMBLE_LEN + k * PILOT_SYMBOL_INTERVAL;
if sym_pos >= total_syms {
break;
}
let phase = lms_phase(
lms_coeffs,
total_syms_f,
&anchor_idx,
&anchor_phase,
sym_pos,
);
let derot = symbols[sym_pos] * Complex32::from_polar(1.0, -phase);
a_acc += derot.re * pilot_ref.re + derot.im * pilot_ref.im;
a_norm += pilot_ref.norm_sqr();
}
let amplitude = if a_norm > 0.0 { a_acc / a_norm } else { 1.0 };
let amplitude = amplitude.max(1e-6);
let mut data_mag_sq = 0.0_f32;
let mut data_seen = 0_usize;
{
let mut data_running = 0_usize;
for i in PREAMBLE_LEN..total_syms {
let rel = i - PREAMBLE_LEN;
if rel.is_multiple_of(PILOT_SYMBOL_INTERVAL) {
continue;
}
let block_idx = data_running / block_data_syms;
let phase = lms_phase(lms_coeffs, total_syms_f, &anchor_idx, &anchor_phase, i)
+ block_dd_correction.get(block_idx).copied().unwrap_or(0.0);
let derot = symbols[i] * Complex32::from_polar(1.0, -phase);
data_mag_sq += derot.norm_sqr();
data_seen += 1;
data_running += 1;
if data_running >= n_data_syms {
break;
}
}
}
let sigma_sq_n = if data_seen > 0 {
let mean_mag_sq = data_mag_sq / data_seen as f32;
((mean_mag_sq - amplitude * amplitude) / 2.0).max(1e-6)
} else {
1.0
};
let llr_scale = amplitude / sigma_sq_n;
let mut llrs_channel: Vec<f32> = Vec::with_capacity(n_blocks_u * block_ch_bits);
let mut data_count = 0usize;
let mut data_running = 0_usize;
for i in 0..total_syms {
if i < PREAMBLE_LEN {
continue;
}
let rel = i - PREAMBLE_LEN;
if rel.is_multiple_of(PILOT_SYMBOL_INTERVAL) {
continue;
}
let block_idx = data_running / block_data_syms;
let phase = lms_phase(lms_coeffs, total_syms_f, &anchor_idx, &anchor_phase, i)
+ block_dd_correction.get(block_idx).copied().unwrap_or(0.0);
let derot = symbols[i] * Complex32::from_polar(1.0, -phase);
let (llr_b1, llr_b0) = qpsk_llrs(derot);
llrs_channel.push(llr_b1 * llr_scale);
llrs_channel.push(llr_b0 * llr_scale);
data_count += 1;
data_running += 1;
if data_count >= n_data_syms {
break;
}
}
debug_assert_eq!(llrs_channel.len(), n_blocks_u * block_ch_bits);
let llrs_per_block = deinterleave_llr(&llrs_channel, n_blocks_u);
let fec = Ldpc240_101;
let mut decoded_info: Vec<Vec<u8>> = Vec::with_capacity(n_blocks_u);
for block_llrs in &llrs_per_block {
let full_llrs = de_puncture_llr(block_llrs, mode);
let result = fec
.decode_soft(&full_llrs, fec_opts)
.ok_or(DecodeError::FecFailed)?;
decoded_info.push(result.info);
}
let mut frame_data = Vec::with_capacity(n_blocks_u * INFO_BYTES_PER_BLOCK);
for block_info in &decoded_info {
debug_assert_eq!(block_info.len(), K_LDPC);
for byte_idx in 0..INFO_BYTES_PER_BLOCK {
let mut byte = 0u8;
for bit_idx in 0..8 {
if block_info[byte_idx * 8 + bit_idx] != 0 {
byte |= 1 << (7 - bit_idx);
}
}
frame_data.push(byte);
}
}
let (header, payload) = unpack_frame(&frame_data).map_err(DecodeError::Crc)?;
if header.mode != mode || header.block_count as usize != n_blocks_u {
return Err(DecodeError::LayoutMismatch {
wanted_mode: mode,
got_mode: header.mode,
wanted_blocks: n_blocks,
got_blocks: header.block_count,
});
}
Ok(DecodedFrame {
app_type: header.app_type,
sequence: header.sequence,
mode: header.mode,
block_count: header.block_count,
payload: payload.to_vec(),
})
}
fn qpsk_constellation_point(idx: u8) -> Complex32 {
match idx & 0x3 {
0 => Complex32::new(1.0, 0.0),
1 => Complex32::new(0.0, 1.0),
2 => Complex32::new(-1.0, 0.0),
3 => Complex32::new(0.0, -1.0),
_ => unreachable!(),
}
}
fn qpsk_llrs(r: Complex32) -> (f32, f32) {
let re = r.re;
let im = r.im;
let llr_b1 = -(re + im);
let llr_b0 = im.max(-re) - re.max(-im);
(llr_b1, llr_b0)
}
fn downconvert_and_matched_filter(audio: &[f32], audio_centre_hz: f32) -> Vec<Complex32> {
let two_pi_fc_dt = 2.0 * PI * audio_centre_hz / SAMPLE_RATE_HZ;
let mut bb: Vec<Complex32> = Vec::with_capacity(audio.len());
for (n, &s) in audio.iter().enumerate() {
let phase = two_pi_fc_dt * n as f32;
let (sin, cos) = phase.sin_cos();
bb.push(Complex32::new(2.0 * s * cos, -2.0 * s * sin));
}
let rrc = rrc_pulse(RRC_ALPHA, RRC_SPAN_SYMS, NSPS);
let n_out = bb.len() + rrc.len() - 1;
let mut out = vec![Complex32::new(0.0, 0.0); n_out];
for (i, &x) in bb.iter().enumerate() {
for (j, &h) in rrc.iter().enumerate() {
out[i + j] += x * h;
}
}
out
}
fn rrc_pulse(alpha: f32, span_syms: usize, samples_per_sym: usize) -> Vec<f32> {
let n = span_syms * samples_per_sym;
let mut h = vec![0.0_f32; n + 1];
let center = n as f32 / 2.0;
for (i, h_i) in h.iter_mut().enumerate() {
let t = (i as f32 - center) / samples_per_sym as f32;
*h_i = if t.abs() < 1e-6 {
1.0 - alpha + 4.0 * alpha / PI
} else if (t.abs() - 1.0 / (4.0 * alpha)).abs() < 1e-6 {
(alpha / 2.0_f32.sqrt())
* ((1.0 + 2.0 / PI) * (PI / (4.0 * alpha)).sin()
+ (1.0 - 2.0 / PI) * (PI / (4.0 * alpha)).cos())
} else {
let pi_t = PI * t;
let four_at = 4.0 * alpha * t;
((pi_t * (1.0 - alpha)).sin() + four_at * (pi_t * (1.0 + alpha)).cos())
/ (pi_t * (1.0 - four_at * four_at))
};
}
let norm: f32 = h.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 0.0 {
for x in h.iter_mut() {
*x /= norm;
}
}
h
}
fn preamble_correlation(mf_out: &[Complex32], offset: usize) -> Complex32 {
let mut acc = Complex32::new(0.0, 0.0);
for (i, &b) in UVPACKET_PREAMBLE_BPSK_BITS.iter().enumerate() {
let pos = offset + i * NSPS;
let s = if b { -1.0_f32 } else { 1.0_f32 };
acc += mf_out[pos] * s;
}
acc
}
fn unwrap_phase(prev: f32, new: f32) -> f32 {
let mut delta = new - prev;
while delta > PI {
delta -= 2.0 * PI;
}
while delta < -PI {
delta += 2.0 * PI;
}
prev + delta
}
fn sample_mf_lerp(mf_out: &[Complex32], pos: f32) -> Complex32 {
let p_int = pos.floor() as usize;
let alpha = pos - p_int as f32;
mf_out[p_int] * (1.0 - alpha) + mf_out[p_int + 1] * alpha
}
fn solve_3x3(a: &[[f32; 3]; 3], b: &[f32; 3]) -> Option<[f32; 3]> {
let det = a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
- a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
+ a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0]);
if det.abs() < 1e-9 {
return None;
}
let det_x = b[0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
- a[0][1] * (b[1] * a[2][2] - a[1][2] * b[2])
+ a[0][2] * (b[1] * a[2][1] - a[1][1] * b[2]);
let det_y = a[0][0] * (b[1] * a[2][2] - a[1][2] * b[2])
- b[0] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
+ a[0][2] * (a[1][0] * b[2] - b[1] * a[2][0]);
let det_z = a[0][0] * (a[1][1] * b[2] - b[1] * a[2][1])
- a[0][1] * (a[1][0] * b[2] - b[1] * a[2][0])
+ b[0] * (a[1][0] * a[2][1] - a[1][1] * a[2][0]);
Some([det_x / det, det_y / det, det_z / det])
}
fn lms_phase(
coeffs: Option<[f32; 3]>,
total_syms_f: f32,
anchor_idx: &[usize],
anchor_phase: &[f32],
sym_idx: usize,
) -> f32 {
if let Some(c) = coeffs {
let t = sym_idx as f32 / total_syms_f;
c[0] + c[1] * t + c[2] * t * t
} else {
interp_phase(anchor_idx, anchor_phase, sym_idx)
}
}
fn interp_phase(anchor_idx: &[usize], anchor_phase: &[f32], sym_idx: usize) -> f32 {
debug_assert_eq!(anchor_idx.len(), anchor_phase.len());
debug_assert!(!anchor_idx.is_empty());
if sym_idx <= anchor_idx[0] {
return anchor_phase[0];
}
let last = anchor_idx.len() - 1;
if sym_idx >= anchor_idx[last] {
return anchor_phase[last];
}
let mut k = 0;
while k + 1 < anchor_idx.len() && anchor_idx[k + 1] < sym_idx {
k += 1;
}
let i0 = anchor_idx[k] as f32;
let i1 = anchor_idx[k + 1] as f32;
let p0 = anchor_phase[k];
let p1 = anchor_phase[k + 1];
let t = (sym_idx as f32 - i0) / (i1 - i0);
p0 + t * (p1 - p0)
}
const PREAMBLE_PEAK_REL_THRESHOLD: f32 = 0.5;
pub fn decode(audio: &[f32], audio_centre_hz: f32) -> Vec<DecodedFrame> {
if audio.len() < PREAMBLE_LEN * NSPS + RRC_LEN {
return Vec::new();
}
let mf_out = downconvert_and_matched_filter(audio, audio_centre_hz);
let max_corr_offset = mf_out.len().saturating_sub((PREAMBLE_LEN - 1) * NSPS + 1);
if max_corr_offset == 0 {
return Vec::new();
}
let mut scores = vec![0.0f32; max_corr_offset];
for (offset, slot) in scores.iter_mut().enumerate() {
let c = preamble_correlation(&mf_out, offset);
*slot = c.norm_sqr();
}
let global_max = scores.iter().cloned().fold(0.0f32, f32::max);
if global_max <= 0.0 {
return Vec::new();
}
let threshold = global_max * PREAMBLE_PEAK_REL_THRESHOLD;
let mut peaks: Vec<(usize, f32)> = scores
.iter()
.enumerate()
.filter(|(_, s)| **s >= threshold)
.map(|(i, &s)| (i, s))
.collect();
peaks.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
let mut picked: Vec<usize> = Vec::new();
for (offset, _) in peaks {
if picked.iter().all(|&p| offset.abs_diff(p) > NSPS) {
picked.push(offset);
}
}
picked.sort_unstable();
let mut frames: Vec<DecodedFrame> = Vec::new();
let mut consumed_until: usize = 0;
for mf_off in picked {
let Some(audio_off) = mf_off.checked_sub(SYM_PEAK_OFFSET) else {
continue;
};
if audio_off < consumed_until {
continue;
}
let mut decoded: Option<DecodedFrame> = None;
let mut consumed_end = audio_off;
'outer: for mode in [Mode::Robust, Mode::Standard, Mode::Fast, Mode::Express] {
for n_blocks in (1u8..=32).rev() {
let needed = needed_samples_for(mode, n_blocks);
if audio_off + needed > audio.len() {
continue;
}
if let Ok(f) =
decode_known_layout(audio, audio_off, audio_centre_hz, mode, n_blocks)
{
decoded = Some(f);
consumed_end = audio_off + needed;
break 'outer;
}
}
}
if let Some(f) = decoded {
frames.push(f);
consumed_until = consumed_end;
}
}
frames
}
fn needed_samples_for(mode: Mode, n_blocks: u8) -> usize {
let block_ch_bits = mode.ch_bits_per_block();
let n_data_syms = (n_blocks as usize) * block_ch_bits / 2;
let n_pilots = n_data_syms.div_ceil(PILOT_SYMBOL_INTERVAL - 1);
let total_syms = PREAMBLE_LEN + n_pilots + n_data_syms;
total_syms * NSPS + RRC_LEN
}
#[cfg(test)]
mod tests {
use super::*;
use crate::uvpacket::AUDIO_CENTRE_HZ;
use crate::uvpacket::framing::{FrameHeader, HEADER_BYTES};
use crate::uvpacket::tx::encode;
fn header_for(mode: Mode, n_blocks: u8, app_type: u8, seq: u8) -> FrameHeader {
FrameHeader {
mode,
block_count: n_blocks,
app_type,
sequence: seq,
}
}
#[test]
fn roundtrip_clean_channel_all_modes() {
for mode in [Mode::Robust, Mode::Standard, Mode::Fast, Mode::Express] {
let n_blocks = 4u8;
let header = header_for(mode, n_blocks, 1, 7);
let payload: Vec<u8> = (0..40).map(|i| (i ^ 0x5A) as u8).collect();
let audio = encode(&header, &payload, AUDIO_CENTRE_HZ).unwrap();
let decoded = decode_known_layout(&audio, 0, AUDIO_CENTRE_HZ, mode, n_blocks)
.unwrap_or_else(|e| panic!("{mode:?}: {e:?}"));
assert_eq!(decoded.app_type, 1);
assert_eq!(decoded.sequence, 7);
assert_eq!(decoded.mode, mode);
assert_eq!(decoded.block_count, n_blocks);
assert_eq!(&decoded.payload[..payload.len()], &payload[..]);
for &b in &decoded.payload[payload.len()..] {
assert_eq!(b, 0, "{mode:?} non-zero padding byte");
}
}
}
#[test]
fn roundtrip_qsl_size_standard() {
let header = header_for(Mode::Standard, 19, 1, 0);
let payload: Vec<u8> = (0..214).map(|i| (i ^ 0xAA) as u8).collect();
let audio = encode(&header, &payload, AUDIO_CENTRE_HZ).unwrap();
let decoded = decode_known_layout(&audio, 0, AUDIO_CENTRE_HZ, Mode::Standard, 19).unwrap();
assert_eq!(&decoded.payload[..214], &payload[..]);
}
#[test]
fn roundtrip_max_blocks_robust() {
let header = header_for(Mode::Robust, 32, 5, 31);
let payload: Vec<u8> = (0..(32 * INFO_BYTES_PER_BLOCK - HEADER_BYTES))
.map(|i| ((i * 31) & 0xFF) as u8)
.collect();
let audio = encode(&header, &payload, AUDIO_CENTRE_HZ).unwrap();
let decoded = decode_known_layout(&audio, 0, AUDIO_CENTRE_HZ, Mode::Robust, 32).unwrap();
assert_eq!(&decoded.payload[..payload.len()], &payload[..]);
}
#[test]
fn roundtrip_single_block_all_modes() {
for mode in [Mode::Robust, Mode::Standard, Mode::Fast, Mode::Express] {
let header = header_for(mode, 1, 0, 0);
let payload: Vec<u8> = vec![0xC3; INFO_BYTES_PER_BLOCK - HEADER_BYTES];
let audio = encode(&header, &payload, AUDIO_CENTRE_HZ).unwrap();
let decoded = decode_known_layout(&audio, 0, AUDIO_CENTRE_HZ, mode, 1)
.unwrap_or_else(|e| panic!("{mode:?}: {e:?}"));
assert_eq!(&decoded.payload[..payload.len()], &payload[..]);
}
}
#[test]
fn truncated_audio_is_reported() {
let header = header_for(Mode::Robust, 4, 1, 0);
let audio = encode(&header, b"hi", AUDIO_CENTRE_HZ).unwrap();
let short = &audio[..audio.len() / 2];
let err = decode_known_layout(short, 0, AUDIO_CENTRE_HZ, Mode::Robust, 4).unwrap_err();
assert_eq!(err, DecodeError::Truncated);
}
#[test]
fn wrong_mode_rejects() {
let header = header_for(Mode::Robust, 4, 1, 0);
let audio = encode(&header, b"abc", AUDIO_CENTRE_HZ).unwrap();
let err = decode_known_layout(&audio, 0, AUDIO_CENTRE_HZ, Mode::Standard, 4).unwrap_err();
assert!(
matches!(
err,
DecodeError::FecFailed | DecodeError::Crc(_) | DecodeError::LayoutMismatch { .. }
),
"expected FecFailed / Crc / LayoutMismatch, got {err:?}",
);
}
#[test]
fn auto_detect_each_mode_at_buffer_start() {
for mode in [Mode::Robust, Mode::Standard] {
let header = header_for(mode, 4, 2, 11);
let payload: Vec<u8> = (0..16).map(|i| (i ^ 0xC3) as u8).collect();
let audio = encode(&header, &payload, AUDIO_CENTRE_HZ).unwrap();
let frames = decode(&audio, AUDIO_CENTRE_HZ);
assert_eq!(frames.len(), 1, "{mode:?}: got {} frames", frames.len());
let f = &frames[0];
assert_eq!(f.mode, mode);
assert_eq!(f.block_count, 4);
assert_eq!(f.app_type, 2);
assert_eq!(f.sequence, 11);
assert_eq!(&f.payload[..payload.len()], &payload[..]);
}
}
#[test]
#[ignore = "Phase 2: Fast-mode LDPC convergence on high-zero-density payloads"]
fn auto_detect_fast_mode_high_zero_density() {
let header = header_for(Mode::Fast, 4, 2, 11);
let payload: Vec<u8> = (0..16).map(|i| (i ^ 0xC3) as u8).collect();
let audio = encode(&header, &payload, AUDIO_CENTRE_HZ).unwrap();
let frames = decode(&audio, AUDIO_CENTRE_HZ);
assert_eq!(frames.len(), 1, "got {} frames", frames.len());
}
#[test]
#[ignore = "Phase 2: Express-mode LDPC convergence on high-zero-density payloads"]
fn auto_detect_express_mode_high_zero_density() {
let header = header_for(Mode::Express, 4, 2, 11);
let payload: Vec<u8> = (0..16).map(|i| (i ^ 0xC3) as u8).collect();
let audio = encode(&header, &payload, AUDIO_CENTRE_HZ).unwrap();
let frames = decode(&audio, AUDIO_CENTRE_HZ);
assert_eq!(frames.len(), 1, "got {} frames", frames.len());
}
#[test]
fn auto_detect_with_leading_and_trailing_silence() {
let header = header_for(Mode::Standard, 6, 3, 4);
let payload: Vec<u8> = vec![0x77; 32];
let frame_audio = encode(&header, &payload, AUDIO_CENTRE_HZ).unwrap();
let lead = vec![0.0f32; 500];
let tail = vec![0.0f32; 800];
let mut audio = Vec::with_capacity(lead.len() + frame_audio.len() + tail.len());
audio.extend_from_slice(&lead);
audio.extend_from_slice(&frame_audio);
audio.extend_from_slice(&tail);
let frames = decode(&audio, AUDIO_CENTRE_HZ);
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].mode, Mode::Standard);
assert_eq!(frames[0].block_count, 6);
assert_eq!(&frames[0].payload[..payload.len()], &payload[..]);
}
#[test]
fn auto_detect_two_back_to_back_frames() {
let h1 = header_for(Mode::Robust, 3, 1, 5);
let p1: Vec<u8> = vec![0xAA; 20];
let h2 = header_for(Mode::Fast, 5, 4, 6);
let p2: Vec<u8> = vec![0xBB; 40];
let a1 = encode(&h1, &p1, AUDIO_CENTRE_HZ).unwrap();
let a2 = encode(&h2, &p2, AUDIO_CENTRE_HZ).unwrap();
let gap = vec![0.0f32; 1000];
let mut audio = Vec::with_capacity(a1.len() + gap.len() + a2.len());
audio.extend_from_slice(&a1);
audio.extend_from_slice(&gap);
audio.extend_from_slice(&a2);
let frames = decode(&audio, AUDIO_CENTRE_HZ);
assert_eq!(frames.len(), 2, "got {} frames", frames.len());
let by_seq: std::collections::HashMap<u8, &DecodedFrame> =
frames.iter().map(|f| (f.sequence, f)).collect();
let f1 = by_seq.get(&5).expect("first frame missing");
let f2 = by_seq.get(&6).expect("second frame missing");
assert_eq!(f1.mode, Mode::Robust);
assert_eq!(f1.block_count, 3);
assert_eq!(&f1.payload[..p1.len()], &p1[..]);
assert_eq!(f2.mode, Mode::Fast);
assert_eq!(f2.block_count, 5);
assert_eq!(&f2.payload[..p2.len()], &p2[..]);
}
#[test]
fn auto_detect_empty_audio() {
let frames = decode(&[], AUDIO_CENTRE_HZ);
assert!(frames.is_empty());
let frames = decode(&vec![0.0f32; 5000], AUDIO_CENTRE_HZ);
assert!(frames.is_empty());
}
#[test]
fn qpsk_llr_clean_constellation_points() {
let (b1, b0) = qpsk_llrs(Complex32::new(1.0, 0.0));
assert!(b1 < 0.0 && b0 < 0.0, "+1+0j → ({b1}, {b0})");
let (b1, b0) = qpsk_llrs(Complex32::new(0.0, 1.0));
assert!(b1 < 0.0 && b0 > 0.0, "0+1j → ({b1}, {b0})");
let (b1, b0) = qpsk_llrs(Complex32::new(-1.0, 0.0));
assert!(b1 > 0.0 && b0 > 0.0, "-1+0j → ({b1}, {b0})");
let (b1, b0) = qpsk_llrs(Complex32::new(0.0, -1.0));
assert!(b1 > 0.0 && b0 < 0.0, "0-1j → ({b1}, {b0})");
}
}