use std::f64::consts::PI;
use std::io;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use chrono::NaiveDateTime;
use hound::WavReader;
use num_complex::Complex;
use rustfft::FftPlanner;
use crate::audio::AudioOutput;
use crate::decoders::{
VOR_MORSE_AUDIO_BPF_HIGH, VOR_MORSE_AUDIO_BPF_LOW, VOR_MORSE_AUDIO_BPF_ORDER, VorProcessor,
VorRadial, calculate_radial, metrics,
};
use crate::sources::common::{
extract_vor_30hz_modulation, extract_vor_reference_subcarrier_envelope,
extract_vor_variable_subcarrier_envelope,
};
use desperado::dsp::filters::ButterworthFilter;
pub struct WavVorSource {
samples: Vec<f64>,
sample_rate: f64,
timestamp_base: f64,
vor_frequency: f64,
window_samples: usize,
var_30hz_full: Vec<f64>,
ref_30hz_full: Vec<f64>,
morse_bpf: ButterworthFilter,
pos: usize,
processor: VorProcessor,
audio_output: Option<AudioOutput>,
}
impl WavVorSource {
pub fn new<P: AsRef<Path>>(
path: P,
vor_freq_mhz: f64,
window_seconds: f64,
morse_window_seconds: f64,
debug_morse: bool,
) -> Result<Self, io::Error> {
let path_ref = path.as_ref();
let (raw_samples, sample_rate) = read_wav_mono(path_ref)?;
let max_amp = raw_samples.iter().map(|x| x.abs()).fold(0.0f64, f64::max);
let normalized_samples: Vec<f64> = if max_amp > 1e-9 {
raw_samples.iter().map(|x| x / max_amp).collect()
} else {
raw_samples
};
let (samples, _carrier) = demod_real_if(&normalized_samples, sample_rate);
let timestamp_base = parse_wav_start_unix(path_ref).unwrap_or_else(unix_now_seconds);
let window_samples = (window_seconds * sample_rate).round() as usize;
let env_var = extract_vor_variable_subcarrier_envelope(&samples);
let env_ref = extract_vor_reference_subcarrier_envelope(&samples);
let var_mean = env_var.iter().sum::<f64>() / env_var.len() as f64;
let var_centered: Vec<f64> = env_var.iter().map(|x| x - var_mean).collect();
let var_30hz_full = extract_vor_30hz_modulation(&var_centered);
let ref_mean = env_ref.iter().sum::<f64>() / env_ref.len() as f64;
let ref_centered: Vec<f64> = env_ref.iter().map(|x| x - ref_mean).collect();
let ref_30hz_full = extract_vor_30hz_modulation(&ref_centered);
let morse_bpf = ButterworthFilter::bandpass(
VOR_MORSE_AUDIO_BPF_LOW,
VOR_MORSE_AUDIO_BPF_HIGH,
sample_rate,
VOR_MORSE_AUDIO_BPF_ORDER,
);
let processor = VorProcessor::new(
window_seconds,
morse_window_seconds,
sample_rate,
debug_morse,
);
Ok(Self {
samples,
sample_rate,
timestamp_base,
vor_frequency: vor_freq_mhz,
window_samples,
var_30hz_full,
ref_30hz_full,
morse_bpf,
pos: 0,
processor,
audio_output: None,
})
}
pub fn set_audio_output(&mut self, audio: AudioOutput) {
self.audio_output = Some(audio);
}
}
impl Iterator for WavVorSource {
type Item = Result<VorRadial, io::Error>;
fn next(&mut self) -> Option<Self::Item> {
if self.pos >= self.samples.len() {
return None;
}
let audio_rate = self.sample_rate;
let end = (self.pos + self.window_samples).min(self.samples.len());
let chunk = self.samples[self.pos..end].to_vec();
if let Some(ref audio_out) = self.audio_output {
let filtered_audio = self.morse_bpf.filter(&chunk);
for sample in filtered_audio {
let clamped = sample.clamp(-1.0, 1.0) as f32;
let _ = audio_out.send(clamped);
}
}
self.pos = end;
if chunk.is_empty() {
return None;
}
let var_30 = self.var_30hz_full[..end.min(self.var_30hz_full.len())].to_vec();
let ref_30 = self.ref_30hz_full[..end.min(self.ref_30hz_full.len())].to_vec();
self.processor.accumulate(&chunk, &var_30, &ref_30);
let elapsed = self.pos as f64 / self.sample_rate;
let timestamp = self.timestamp_base + elapsed;
if let Some(radial_deg) = calculate_radial(&var_30, &ref_30, audio_rate) {
let signal_quality = metrics::compute_signal_quality(
None,
&chunk,
&var_30,
&ref_30,
audio_rate,
self.pos,
&[], );
let vor_radial = VorRadial::new(
metrics::round_decimals(timestamp, 5),
metrics::round_decimals(radial_deg, 2),
self.vor_frequency,
)
.with_quality(signal_quality);
Some(Ok(vor_radial))
} else {
self.next()
}
}
}
fn read_wav_mono<P: AsRef<Path>>(path: P) -> Result<(Vec<f64>, f64), io::Error> {
let reader = WavReader::open(&path)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
let spec = reader.spec();
if spec.channels != 1 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Expected mono WAV, got {} channels", spec.channels),
));
}
let sample_rate = spec.sample_rate as f64;
let samples: Result<Vec<f64>, _> = reader
.into_samples::<i16>()
.map(|s| {
s.map(|sample| sample as f64 / 32768.0)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))
})
.collect();
let samples = samples?;
Ok((samples, sample_rate))
}
fn parse_wav_start_unix<P: AsRef<Path>>(path: P) -> Option<f64> {
let filename = path.as_ref().file_name()?.to_string_lossy();
let name_without_ext = filename.split('.').next()?;
let parts: Vec<&str> = name_without_ext.split('_').collect();
if parts.len() < 3 {
return None;
}
let date_part = parts[1];
let time_part = parts[2];
let datetime_str = format!("{} {}", date_part, time_part.replace('-', ":"));
let dt = NaiveDateTime::parse_from_str(&datetime_str, "%Y-%m-%d %H:%M:%S").ok()?;
Some(dt.and_utc().timestamp() as f64)
}
fn unix_now_seconds() -> f64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs_f64())
.unwrap_or(0.0)
}
fn demod_real_if(samples: &[f64], sample_rate: f64) -> (Vec<f64>, Option<f64>) {
let fft_len = samples.len().min(1 << 17).next_power_of_two();
let mut planner = FftPlanner::<f64>::new();
let fft = planner.plan_fft_forward(fft_len);
let mut spectrum: Vec<Complex<f64>> = samples[..fft_len]
.iter()
.map(|&s| Complex::new(s, 0.0))
.collect();
fft.process(&mut spectrum);
let num_bins = fft_len / 2;
let power: Vec<f64> = spectrum[..num_bins].iter().map(|c| c.norm_sqr()).collect();
let bin_hz = sample_rate / fft_len as f64;
let min_bin = (200.0 / bin_hz).ceil() as usize;
let (peak_bin, &peak_power) = power[min_bin..]
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.map(|(i, p)| (i + min_bin, p))
.unwrap_or((min_bin, &0.0));
let mut sorted_power = power.clone();
sorted_power.sort_by(|a, b| a.partial_cmp(b).unwrap());
let median_power = sorted_power[sorted_power.len() / 2];
let threshold_linear = median_power * 100.0; if peak_power < threshold_linear || median_power == 0.0 {
return (samples.to_vec(), None);
}
let lo = peak_bin.saturating_sub(2);
let hi = (peak_bin + 3).min(num_bins);
let (weighted_freq, weight_sum) =
power[lo..hi]
.iter()
.enumerate()
.fold((0.0_f64, 0.0_f64), |(wf, ws), (i, &p)| {
let freq = (lo + i) as f64 * bin_hz;
(wf + freq * p, ws + p)
});
let carrier_freq = if weight_sum > 0.0 {
weighted_freq / weight_sum
} else {
peak_bin as f64 * bin_hz
};
let q = 30.0_f64;
let w0 = 2.0 * PI * carrier_freq / sample_rate;
let alpha = w0.sin() / (2.0 * q);
let cos_w0 = w0.cos();
let b0 = 1.0;
let b1 = -2.0 * cos_w0;
let b2 = 1.0;
let a1 = -2.0 * cos_w0;
let a2 = 1.0 - alpha;
let mut filtered = vec![0.0; samples.len()];
let mut y1 = 0.0;
let mut y2 = 0.0;
let mut x1 = 0.0;
let mut x2 = 0.0;
for (i, &x) in samples.iter().enumerate() {
let y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
filtered[i] = y;
x2 = x1;
x1 = x;
y2 = y1;
y1 = y;
}
let reversed: Vec<f64> = filtered.iter().rev().cloned().collect();
let mut backward = vec![0.0; samples.len()];
y1 = 0.0;
y2 = 0.0;
x1 = 0.0;
x2 = 0.0;
for (i, &x) in reversed.iter().enumerate() {
let y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
backward[i] = y;
x2 = x1;
x1 = x;
y2 = y1;
y1 = y;
}
backward.reverse();
(backward, Some(carrier_freq))
}