use clap::Parser;
use std::path::PathBuf;
use voracious::{IqFormat, VorSource, WavIlsLocalizerSource, WavVorSource};
#[derive(Parser)]
#[command(name = "voracious")]
#[command(about = "VOR/ILS signal decoder for aviation navigation", long_about = None)]
struct Cli {
input: PathBuf,
#[arg(short, long)]
sample_rate: Option<u32>,
#[arg(short, long, default_value = "cf32")]
format: String,
#[arg(short, long)]
vor_freq: Option<f64>,
#[arg(long)]
ils_freq: Option<f64>,
#[arg(short, long)]
center_freq: Option<f64>,
#[arg(short, long, default_value = "3.0")]
window: f64,
#[arg(long, default_value = "15.0")]
morse_window: f64,
#[arg(long)]
debug_morse: bool,
#[arg(long)]
audio: bool,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
match (cli.vor_freq, cli.ils_freq) {
(Some(_), Some(_)) => {
eprintln!("Error: specify either --vor-freq or --ils-freq, not both");
std::process::exit(1);
}
(None, None) => {
eprintln!("Error: specify either --vor-freq or --ils-freq");
std::process::exit(1);
}
_ => {}
}
let input_str = cli.input.to_string_lossy();
let is_wav = input_str.ends_with(".wav") || input_str.ends_with(".WAV");
let is_device_uri = input_str.starts_with("rtlsdr://")
|| input_str.starts_with("airspy://")
|| input_str.starts_with("soapy://");
let audio_output = if cli.audio {
let is_file_source = !is_device_uri;
let buffer_size = if is_file_source {
voracious::audio::AUDIO_RATE * 4 } else {
voracious::audio::AUDIO_RATE * 2 };
match voracious::audio::AudioOutput::new(buffer_size) {
Some(audio) => {
eprintln!(
"Audio output enabled at {} Hz (mono)",
voracious::audio::AUDIO_RATE
);
Some(audio)
}
None => {
eprintln!(
"Warning: Could not initialize audio output (no soundcard or permission denied)"
);
None
}
}
} else {
None
};
if is_wav {
if let Some(ils_freq) = cli.ils_freq {
let source = WavIlsLocalizerSource::new(
&cli.input,
ils_freq,
cli.window,
cli.morse_window,
cli.debug_morse,
)?;
for result in source {
match result {
Ok(frame) => {
let json = serde_json::to_string(&frame)?;
println!("{}", json);
}
Err(e) => {
eprintln!("Error reading WAV: {}", e);
break;
}
}
}
return Ok(());
} else if let Some(vor_freq) = cli.vor_freq {
let source = WavVorSource::new(
&cli.input,
vor_freq,
cli.window,
cli.morse_window,
cli.debug_morse,
)?;
for result in source {
match result {
Ok(radial) => {
let json = serde_json::to_string(&radial)?;
println!("{}", json);
}
Err(e) => {
eprintln!("Error reading WAV: {}", e);
break;
}
}
}
return Ok(());
}
}
let inferred = infer_gqrx_metadata(&cli.input);
let sample_rate = cli
.sample_rate
.or_else(|| {
if is_device_uri {
infer_uri_u32_param(&input_str, &["rate", "sample_rate"])
} else {
None
}
})
.or_else(|| inferred.as_ref().map(|m| m.sample_rate_hz))
.unwrap_or(1_800_000);
let nav_freq = cli.vor_freq.or(cli.ils_freq).unwrap(); let center_freq = match cli
.center_freq
.or_else(|| {
if is_device_uri {
infer_uri_f64_param(&input_str, &["freq", "frequency"])
} else {
None
}
})
.or_else(|| inferred.as_ref().map(|m| m.center_freq_mhz))
{
Some(v) => v,
None if is_device_uri => nav_freq,
None => {
eprintln!(
"Missing --center-freq and could not infer from filename. \
Use --center-freq explicitly or provide a gqrx file named like gqrx_*_<centerHz>_<sampleRate>_fc.raw"
);
std::process::exit(1);
}
};
let iq_format = match cli.format.parse::<IqFormat>() {
Ok(v) => v,
Err(_) => {
eprintln!(
"Invalid format: {}. Use cu8, cs8, cs16, or cf32",
cli.format
);
std::process::exit(1);
}
};
if let Some(vor_freq) = cli.vor_freq {
let mut source = VorSource::new(
cli.input,
sample_rate,
iq_format,
vor_freq,
center_freq,
cli.window,
cli.morse_window,
cli.debug_morse,
)?;
if let Some(audio) = audio_output {
source.set_audio_output(audio);
}
for result in source {
match result {
Ok(radial) => {
let json = serde_json::to_string(&radial)?;
println!("{}", json);
}
Err(e) => {
eprintln!("Error reading samples: {}", e);
break;
}
}
}
} else {
let ils_freq = cli.ils_freq.unwrap(); use voracious::IlsLocalizerSource;
let mut source = IlsLocalizerSource::new(
cli.input,
sample_rate,
iq_format,
ils_freq,
center_freq,
cli.window,
cli.morse_window,
cli.debug_morse,
)?;
if let Some(audio) = audio_output {
source.set_audio_output(audio);
}
for result in source {
match result {
Ok(frame) => {
let json = serde_json::to_string(&frame)?;
println!("{}", json);
}
Err(e) => {
eprintln!("Error reading samples: {}", e);
break;
}
}
}
}
Ok(())
}
fn infer_uri_param<'a>(uri: &'a str, keys: &[&str]) -> Option<&'a str> {
let query = uri.split_once('?')?.1;
for param in query.split('&') {
let (k, v) = param.split_once('=')?;
if keys.contains(&k) {
return Some(v);
}
}
None
}
fn infer_uri_u32_param(uri: &str, keys: &[&str]) -> Option<u32> {
let raw = infer_uri_param(uri, keys)?;
desperado::parse_si_value::<u32>(raw).ok()
}
fn infer_uri_f64_param(uri: &str, keys: &[&str]) -> Option<f64> {
let raw = infer_uri_param(uri, keys)?;
desperado::parse_si_value::<f64>(raw)
.ok()
.map(|hz| hz / 1e6)
}
#[derive(Debug, Clone, Copy)]
struct GqrxMeta {
center_freq_mhz: f64,
sample_rate_hz: u32,
}
fn infer_gqrx_metadata(path: &std::path::Path) -> Option<GqrxMeta> {
let name = path.file_name()?.to_str()?;
if !name.starts_with("gqrx_") || !name.ends_with("_fc.raw") {
return None;
}
let stem = name.strip_suffix("_fc.raw")?;
let parts: Vec<&str> = stem.split('_').collect();
if parts.len() < 5 {
return None;
}
let center_hz: u64 = parts.get(parts.len() - 2)?.parse().ok()?;
let sample_rate_hz: u32 = parts.last()?.parse().ok()?;
Some(GqrxMeta {
center_freq_mhz: center_hz as f64 / 1e6,
sample_rate_hz,
})
}