use crate::core::{FrameLayout, ModulationParams, Protocol, ProtocolId, SyncMode};
use crate::fec::Rs63_12;
use crate::msg::Jt72Codec;
pub mod gray;
pub mod interleave;
pub mod rx;
pub mod search;
pub mod sync_pattern;
pub mod tx;
pub use gray::{gray6, inv_gray6};
pub use interleave::{deinterleave, interleave};
pub use rx::{demodulate_aligned, demodulate_aligned_with_confidence};
pub use sync_pattern::{JT65_DATA_POSITIONS, JT65_NPRC, JT65_SYNC_BLOCKS, JT65_SYNC_POSITIONS};
pub use tx::{encode_channel_symbols, synthesize_audio, synthesize_standard};
pub fn decode_at(
audio: &[f32],
sample_rate: u32,
start_sample: usize,
base_freq_hz: f32,
) -> Option<crate::msg::Jt72Message> {
use crate::core::{DecodeContext, MessageCodec};
let received = rx::demodulate_aligned(audio, sample_rate, start_sample, base_freq_hz)?;
let rs = Rs63_12::new();
let (info, _nerr) = rs.decode_jt65(&received)?;
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;
}
crate::msg::Jt72Codec::default().unpack(&payload, &DecodeContext::default())
}
pub fn decode_at_with_erasures(
audio: &[f32],
sample_rate: u32,
start_sample: usize,
base_freq_hz: f32,
attempts: &[usize],
) -> Option<crate::msg::Jt72Message> {
use crate::core::{DecodeContext, MessageCodec};
let (symbols, conf) =
rx::demodulate_aligned_with_confidence(audio, sample_rate, start_sample, base_freq_hz)?;
let mut order: Vec<usize> = (0..63).collect();
order.sort_by(|&a, &b| {
conf[a]
.partial_cmp(&conf[b])
.unwrap_or(std::cmp::Ordering::Equal)
});
let rs = Rs63_12::new();
let codec = crate::msg::Jt72Codec::default();
let ctx = DecodeContext::default();
for &n_eras in attempts {
let n_eras = n_eras.min(51); let eras: Vec<u32> = order.iter().take(n_eras).map(|&i| i as u32).collect();
let mut sent = [0u8; 63];
sent.copy_from_slice(&symbols);
if let Some((info, _nerr)) = rs.decode_jt65_erasures(&sent, &eras) {
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;
}
if let Some(msg) = codec.unpack(&payload, &ctx) {
return Some(msg);
}
}
}
None
}
#[derive(Clone, Debug)]
pub struct Jt65Decode {
pub message: crate::msg::Jt72Message,
pub freq_hz: f32,
pub start_sample: usize,
}
pub fn decode_scan(
audio: &[f32],
sample_rate: u32,
nominal_start_sample: usize,
params: &search::SearchParams,
) -> Vec<Jt65Decode> {
use crate::core::ModulationParams;
let nsps = (sample_rate as f32 * <Jt65 as ModulationParams>::SYMBOL_DT).round() as usize;
let cands = search::coarse_search(audio, sample_rate, nominal_start_sample, params);
let mut seen: Vec<Jt65Decode> = Vec::new();
for c in cands {
let Some(msg) = decode_at(audio, sample_rate, c.start_sample, c.freq_hz) else {
continue;
};
let dup = seen.iter().any(|prev| {
prev.message == msg
&& (prev.freq_hz - c.freq_hz).abs() <= 2.0
&& (prev.start_sample as i64 - c.start_sample as i64).abs() <= nsps as i64
});
if !dup {
seen.push(Jt65Decode {
message: msg,
freq_hz: c.freq_hz,
start_sample: c.start_sample,
});
}
}
seen
}
pub fn decode_scan_default(audio: &[f32], sample_rate: u32) -> Vec<Jt65Decode> {
decode_scan(audio, sample_rate, 0, &search::SearchParams::default())
}
#[derive(Copy, Clone, Debug, Default)]
pub struct Jt65;
impl ModulationParams for Jt65 {
const NTONES: u32 = 66;
const BITS_PER_SYMBOL: u32 = 6;
const NSPS: u32 = 4460;
const SYMBOL_DT: f32 = 4460.0 / 12_000.0;
const TONE_SPACING_HZ: f32 = 12_000.0 / 4460.0; const GRAY_MAP: &'static [u8] = &IDENTITY_66;
const GFSK_BT: f32 = 0.0; const GFSK_HMOD: f32 = 1.0;
const NFFT_PER_SYMBOL_FACTOR: u32 = 2;
const NSTEP_PER_SYMBOL: u32 = 2;
const NDOWN: u32 = 4;
}
const IDENTITY_66: [u8; 66] = {
let mut m = [0u8; 66];
let mut i = 0usize;
while i < 66 {
m[i] = i as u8;
i += 1;
}
m
};
impl FrameLayout for Jt65 {
const N_DATA: u32 = 63;
const N_SYNC: u32 = 63;
const N_SYMBOLS: u32 = 126;
const N_RAMP: u32 = 0;
const SYNC_MODE: SyncMode = SyncMode::Block(&JT65_SYNC_BLOCKS);
const T_SLOT_S: f32 = 60.0;
const TX_START_OFFSET_S: f32 = 0.0;
}
impl Protocol for Jt65 {
type Fec = Rs63_12;
type Msg = Jt72Codec;
const ID: ProtocolId = ProtocolId::Jt65;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::msg::Jt72Message;
#[test]
fn erasure_assisted_decode_recovers_under_moderate_noise() {
let freq = 1270.0;
let audio = synthesize_standard("CQ", "K1ABC", "FN42", 12_000, freq, 0.3).expect("synth");
let msg = decode_at_with_erasures(&audio, 12_000, 0, freq, &[0, 8, 16, 24, 32])
.expect("erasure-aware path must decode clean synth");
assert!(matches!(
msg,
Jt72Message::Standard { ref call1, ref call2, ref grid_or_report }
if call1 == "CQ" && call2 == "K1ABC" && grid_or_report == "FN42"
));
}
#[test]
fn jt65_trait_surface() {
assert_eq!(<Jt65 as ModulationParams>::NTONES, 66);
assert_eq!(<Jt65 as ModulationParams>::BITS_PER_SYMBOL, 6);
assert_eq!(<Jt65 as ModulationParams>::NSPS, 4460);
assert_eq!(<Jt65 as FrameLayout>::N_SYMBOLS, 126);
assert_eq!(<Jt65 as FrameLayout>::N_DATA, 63);
assert_eq!(<Jt65 as FrameLayout>::N_SYNC, 63);
match <Jt65 as FrameLayout>::SYNC_MODE {
SyncMode::Block(blocks) => {
assert_eq!(blocks.len(), 63);
for b in blocks {
assert_eq!(b.pattern, &[0u8]);
}
}
SyncMode::Interleaved { .. } => panic!("JT65 must use Block sync"),
}
let _fec = Rs63_12::default();
}
}