use voracious::decoders::{calculate_radial, calculate_radial_vortrack, decode_morse_ident};
const AUDIO_RATE: f64 = 1_800_000.0 / 38.0;
const MORSE_WINDOW_SAMPLES: usize = (AUDIO_RATE as usize) * 15;
#[cfg(feature = "test-fixtures")]
mod fixture_helpers {
use super::*;
use std::path::Path;
pub fn load_f32(path: &Path) -> Vec<f64> {
let bytes =
std::fs::read(path).unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
assert!(
bytes.len().is_multiple_of(4),
"file length not a multiple of 4"
);
bytes
.chunks_exact(4)
.map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64)
.collect()
}
pub fn fixture_path(stem: &str, suffix: &str) -> std::path::PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/data")
.join(format!("{stem}_{suffix}.f32"))
}
pub fn decode_morse_sliding(audio: &[f64]) -> (Option<String>, Vec<String>) {
let step = MORSE_WINDOW_SAMPLES / 2;
let mut all_tokens: Vec<String> = Vec::new();
let mut best_ident: Option<String> = None;
let mut start = 0;
while start + MORSE_WINDOW_SAMPLES <= audio.len() {
let window = &audio[start..start + MORSE_WINDOW_SAMPLES];
let (ident, tokens, _) = decode_morse_ident(window, AUDIO_RATE);
all_tokens.extend(tokens);
if best_ident.is_none() {
best_ident = ident;
}
start += step;
}
(best_ident, all_tokens)
}
}
#[cfg(feature = "test-fixtures")]
use fixture_helpers::*;
#[cfg(feature = "test-fixtures")]
const KLO_STEM: &str = "gqrx_20250925_144051_114647000_1800000_fc";
#[cfg(feature = "test-fixtures")]
const ARL_STEM: &str = "gqrx_20251107_182558_116000000_1800000_fc";
#[test]
#[ignore]
#[cfg(feature = "test-fixtures")]
fn test_klo_vortrack_radial_in_range() {
let audio = load_f32(&fixture_path(KLO_STEM, "audio"));
let radial = calculate_radial_vortrack(&audio, AUDIO_RATE)
.expect("calculate_radial_vortrack should return a value for KLO");
assert!(
(115.0..=125.0).contains(&radial),
"KLO vortrack radial {radial:.1}° out of expected range 115–125°"
);
}
#[test]
#[ignore]
#[cfg(feature = "test-fixtures")]
fn test_klo_vortrack_windowed_consistent() {
let audio = load_f32(&fixture_path(KLO_STEM, "audio"));
let full_radial =
calculate_radial_vortrack(&audio, AUDIO_RATE).expect("full signal radial failed");
let window = (AUDIO_RATE * 3.0) as usize;
let mut radials: Vec<f64> = Vec::new();
for i in 0..3 {
let start = i * window;
let end = (start + window).min(audio.len());
if let Some(r) = calculate_radial_vortrack(&audio[start..end], AUDIO_RATE) {
radials.push(r);
}
}
assert!(!radials.is_empty(), "no windowed radials computed");
for r in &radials {
let diff = ((r - full_radial + 540.0) % 360.0) - 180.0;
assert!(
diff.abs() < 5.0,
"KLO windowed radial {r:.1}° deviates {diff:.1}° from full-signal {full_radial:.1}°"
);
}
}
#[test]
#[ignore]
#[cfg(feature = "test-fixtures")]
fn test_klo_morse_ident() {
let audio = load_f32(&fixture_path(KLO_STEM, "audio"));
let (ident, tokens) = decode_morse_sliding(&audio);
if tokens.is_empty() {
eprintln!("KLO: Morse decoder returned no tokens — skipping (known issue, see TODO)");
return;
}
assert_eq!(
ident.as_deref(),
Some("KLO"),
"KLO: expected ident 'KLO', got {ident:?} (tokens: {tokens:?})"
);
}
#[test]
#[ignore]
#[cfg(feature = "test-fixtures")]
fn test_arl_vortrack_radial_in_range() {
let audio = load_f32(&fixture_path(ARL_STEM, "audio"));
let radial = calculate_radial_vortrack(&audio, AUDIO_RATE)
.expect("calculate_radial_vortrack should return a value for ARL");
assert!(
(110.0..=120.0).contains(&radial),
"ARL vortrack radial {radial:.1}° out of expected range 110–120°"
);
}
#[test]
#[ignore]
#[cfg(feature = "test-fixtures")]
fn test_arl_vortrack_windowed_consistent() {
let audio = load_f32(&fixture_path(ARL_STEM, "audio"));
let full_radial =
calculate_radial_vortrack(&audio, AUDIO_RATE).expect("full signal radial failed");
let window = (AUDIO_RATE * 3.0) as usize;
let mut radials: Vec<f64> = Vec::new();
for i in 0..5 {
let start = i * window;
let end = (start + window).min(audio.len());
if let Some(r) = calculate_radial_vortrack(&audio[start..end], AUDIO_RATE) {
radials.push(r);
}
}
assert!(!radials.is_empty(), "no windowed radials computed");
for r in &radials {
let diff = ((r - full_radial + 540.0) % 360.0) - 180.0;
assert!(
diff.abs() < 5.0,
"ARL windowed radial {r:.1}° deviates {diff:.1}° from full-signal {full_radial:.1}°"
);
}
}
#[test]
#[ignore]
#[cfg(feature = "test-fixtures")]
fn test_arl_morse_ident() {
let audio = load_f32(&fixture_path(ARL_STEM, "audio"));
let (ident, tokens) = decode_morse_sliding(&audio);
assert!(
!tokens.is_empty(),
"ARL: expected Morse tokens but got none"
);
assert_eq!(
ident.as_deref(),
Some("ARL"),
"ARL: expected ident 'ARL', got {ident:?} (tokens: {tokens:?})"
);
}
#[test]
fn test_vortrack_returns_none_for_short_input() {
let short = vec![0.0f64; (AUDIO_RATE * 0.5) as usize];
assert!(
calculate_radial_vortrack(&short, AUDIO_RATE).is_none(),
"expected None for input shorter than 1 second"
);
}
#[test]
fn test_calculate_radial_returns_none_for_short_input() {
let short = vec![0.0f64; (AUDIO_RATE * 0.5) as usize];
assert!(
calculate_radial(&short, &short, AUDIO_RATE).is_none(),
"expected None for input shorter than 1 second"
);
}
#[test]
fn test_morse_returns_none_for_silence() {
let silence = vec![0.0f64; MORSE_WINDOW_SAMPLES];
let (ident, tokens, _) = decode_morse_ident(&silence, AUDIO_RATE);
assert!(
ident.is_none(),
"expected no ident from silence, got {ident:?}"
);
assert!(tokens.is_empty(), "expected no tokens from silence");
}