use crate::engine::ModulationParams;
use num_complex::Complex;
use rustfft::FftPlanner;
use super::Jt65;
use super::gray::inv_gray6;
use super::interleave::deinterleave;
use super::sync_pattern::JT65_NPRC;
type DemodWithRunnerup = (
[u8; 63],
[f32; 63],
[u8; 63],
[f32; 63],
[[f32; 64]; 63],
f32,
);
pub fn demodulate_aligned(
audio: &[f32],
sample_rate: u32,
start_sample: usize,
base_freq_hz: f32,
) -> Option<[u8; 63]> {
let nsps = (sample_rate as f32 * <Jt65 as ModulationParams>::SYMBOL_DT).round() as usize;
let df = sample_rate as f32 / nsps as f32; let base_bin = (base_freq_hz / df).round() as usize;
if start_sample + 126 * nsps > audio.len() || base_bin + 66 >= nsps / 2 {
return None;
}
let mut planner = FftPlanner::<f32>::new();
let fft = planner.plan_fft_forward(nsps);
let mut scratch = vec![Complex::new(0f32, 0f32); fft.get_inplace_scratch_len()];
let mut buf: Vec<Complex<f32>> = vec![Complex::new(0f32, 0f32); nsps];
let (syms, _conf, _second_sym, _rel, _raw_pwr, _snr_db) =
demodulate_aligned_with_confidence_inner(
audio,
sample_rate,
start_sample,
base_freq_hz,
nsps,
base_bin,
&mut buf,
&mut scratch,
&*fft,
)?;
Some(syms)
}
pub fn demodulate_aligned_with_confidence(
audio: &[f32],
sample_rate: u32,
start_sample: usize,
base_freq_hz: f32,
) -> Option<([u8; 63], [f32; 63])> {
let (syms, conf, _snr_db) =
demodulate_aligned_with_confidence_and_snr(audio, sample_rate, start_sample, base_freq_hz)?;
Some((syms, conf))
}
pub fn demodulate_aligned_with_runnerup(
audio: &[f32],
sample_rate: u32,
start_sample: usize,
base_freq_hz: f32,
) -> Option<DemodWithRunnerup> {
let nsps = (sample_rate as f32 * <Jt65 as ModulationParams>::SYMBOL_DT).round() as usize;
let df = sample_rate as f32 / nsps as f32;
let base_bin = (base_freq_hz / df).round() as usize;
if start_sample + 126 * nsps > audio.len() || base_bin + 66 >= nsps / 2 {
return None;
}
let mut planner = FftPlanner::<f32>::new();
let fft = planner.plan_fft_forward(nsps);
let mut scratch = vec![Complex::new(0f32, 0f32); fft.get_inplace_scratch_len()];
let mut buf: Vec<Complex<f32>> = vec![Complex::new(0f32, 0f32); nsps];
demodulate_aligned_with_confidence_inner(
audio,
sample_rate,
start_sample,
base_freq_hz,
nsps,
base_bin,
&mut buf,
&mut scratch,
&*fft,
)
}
pub fn demodulate_aligned_with_confidence_and_snr(
audio: &[f32],
sample_rate: u32,
start_sample: usize,
base_freq_hz: f32,
) -> Option<([u8; 63], [f32; 63], f32)> {
let nsps = (sample_rate as f32 * <Jt65 as ModulationParams>::SYMBOL_DT).round() as usize;
let df = sample_rate as f32 / nsps as f32;
let base_bin = (base_freq_hz / df).round() as usize;
if start_sample + 126 * nsps > audio.len() || base_bin + 66 >= nsps / 2 {
return None;
}
let mut planner = FftPlanner::<f32>::new();
let fft = planner.plan_fft_forward(nsps);
let mut scratch = vec![Complex::new(0f32, 0f32); fft.get_inplace_scratch_len()];
let mut buf: Vec<Complex<f32>> = vec![Complex::new(0f32, 0f32); nsps];
let (syms, conf, _second_sym, _rel, _raw_pwr, snr_db) =
demodulate_aligned_with_confidence_inner(
audio,
sample_rate,
start_sample,
base_freq_hz,
nsps,
base_bin,
&mut buf,
&mut scratch,
&*fft,
)?;
Some((syms, conf, snr_db))
}
fn demodulate_aligned_with_confidence_inner(
audio: &[f32],
sample_rate: u32,
start_sample: usize,
base_freq_hz: f32,
nsps: usize,
base_bin: usize,
buf: &mut [Complex<f32>],
scratch: &mut [Complex<f32>],
fft: &dyn rustfft::Fft<f32>,
) -> Option<DemodWithRunnerup> {
let mut symbols = [0u8; 63];
let mut conf = [0f32; 63];
let mut rel = [0f32; 63];
let mut second_tone_sym = [0u8; 63];
let mut raw_pwr = [[0f32; 64]; 63];
let mut xsig_sum = 0.0f32;
let mut xnoi_sum = 0.0f32;
let mut k = 0usize;
let residual_hz = base_freq_hz - base_bin as f32 * (sample_rate as f32 / nsps as f32);
let dphi = -core::f32::consts::TAU * residual_hz / sample_rate as f32;
let mut phase = 0.0f32;
for sym_idx in 0..126 {
let sym_start = start_sample + sym_idx * nsps;
for (slot, &s) in buf.iter_mut().zip(&audio[sym_start..sym_start + nsps]) {
*slot = Complex::new(s, 0.0) * Complex::new(phase.cos(), phase.sin());
phase += dphi;
if phase > core::f32::consts::PI {
phase -= core::f32::consts::TAU;
} else if phase < -core::f32::consts::PI {
phase += core::f32::consts::TAU;
}
}
fft.process_with_scratch(buf, scratch);
if JT65_NPRC[sym_idx] == 1 {
continue;
}
let mut best_tone = 0u8;
let mut best_pwr = f32::NEG_INFINITY;
let mut second_tone = 0u8;
let mut second_pwr = f32::NEG_INFINITY;
let mut total_pwr = 0.0f32;
for tone in 0u8..64 {
let bin = base_bin + 2 + tone as usize;
let p = buf[bin].norm_sqr();
raw_pwr[k][tone as usize] = p;
total_pwr += p;
if p > best_pwr {
second_pwr = best_pwr;
second_tone = best_tone;
best_pwr = p;
best_tone = tone;
} else if p > second_pwr {
second_pwr = p;
second_tone = tone;
}
}
symbols[k] = inv_gray6(best_tone);
second_tone_sym[k] = inv_gray6(second_tone);
conf[k] = if best_pwr > 0.0 {
((best_pwr - second_pwr.max(0.0)) / best_pwr).clamp(0.0, 1.0)
} else {
0.0
};
rel[k] = if total_pwr > 0.0 {
(best_pwr / total_pwr).clamp(0.0, 1.0)
} else {
0.0
};
xsig_sum += best_pwr;
xnoi_sum += (total_pwr - best_pwr) / 63.0;
k += 1;
}
debug_assert_eq!(k, 63);
deinterleave(&mut symbols);
deinterleave(&mut second_tone_sym);
let mut conf_perm = [0f32; 63];
let mut rel_perm = [0f32; 63];
for i in 0..7 {
for j in 0..9 {
conf_perm[j * 7 + i] = conf[i * 9 + j];
rel_perm[j * 7 + i] = rel[i * 9 + j];
}
}
const SNR_FLOOR_DB: f32 = -30.0;
const SNR_CEIL_DB: f32 = -1.0;
let snr_db = if xnoi_sum < f32::EPSILON {
if xsig_sum < f32::EPSILON {
SNR_FLOOR_DB
} else {
SNR_CEIL_DB
}
} else {
let ratio = xsig_sum / xnoi_sum - 1.0;
if ratio <= 0.001 {
SNR_FLOOR_DB
} else {
let bw_offset_db =
10.0 * (2500.0 / <Jt65 as ModulationParams>::TONE_SPACING_HZ).log10();
(10.0 * ratio.log10() - bw_offset_db).clamp(SNR_FLOOR_DB, SNR_CEIL_DB)
}
};
Some((
symbols,
conf_perm,
second_tone_sym,
rel_perm,
raw_pwr,
snr_db,
))
}
#[cfg(test)]
mod tests {
use super::super::tx::synthesize_standard;
use super::*;
use crate::engine::{DecodeContext, MessageCodec};
use crate::fec::Rs63_12;
use crate::msg::{Jt72Codec, Jt72Message};
#[test]
fn synth_decode_roundtrip_cq_k1abc_fn42() {
let freq = 1270.0;
let audio =
synthesize_standard("CQ", "K1ABC", "FN42", 12_000, freq, 0.3).expect("pack+synth");
let received = demodulate_aligned(&audio, 12_000, 0, freq).expect("demod");
let rs = Rs63_12::new();
let (info, nerr) = rs.decode_jt65(&received).expect("clean decode");
assert_eq!(nerr, 0, "clean synth should have zero errors");
let mut payload = [0u8; 72];
for (i, bit) in payload.iter_mut().enumerate() {
let word = info[i / 6];
let shift = 5 - (i % 6);
*bit = (word >> shift) & 1;
}
let msg = Jt72Codec::default()
.unpack(&payload, &DecodeContext::default())
.expect("unpack");
match msg {
Jt72Message::Standard {
call1,
call2,
grid_or_report,
} => {
assert_eq!(call1, "CQ");
assert_eq!(call2, "K1ABC");
assert_eq!(grid_or_report, "FN42");
}
other => panic!("expected Standard, got {:?}", other),
}
}
}