use num_complex::Complex32 as C32;
use crate::core::{Block, WorkReport};
#[inline]
pub fn rms(x: &[f32]) -> f32 {
if x.is_empty() { return 0.0; }
let s: f32 = x.iter().map(|v| v*v).sum();
(s / (x.len() as f32)).sqrt()
}
pub fn hann(n: usize) -> Vec<f32> {
(0..n).map(|k| 0.5 - 0.5 * (core::f32::consts::TAU * k as f32 / n as f32).cos()).collect()
}
pub fn tone(fs: f32, f_hz: f32, n: usize, amp: f32) -> Vec<f32> {
(0..n)
.map(|k| amp * (core::f32::consts::TAU * f_hz * (k as f32) / fs).sin())
.collect()
}
pub fn gen_complex_tone(fs: f32, f_hz: f32, n: usize) -> Vec<C32> {
(0..n)
.map(|k| {
let ph = core::f32::consts::TAU * f_hz * (k as f32) / fs;
C32::new(ph.cos(), ph.sin())
})
.collect()
}
pub fn snr_db_at(fs: f32, f_hz: f32, x: &[f32]) -> f32 {
let n = x.len().max(1);
let w = hann(n);
let two_pi = core::f32::consts::TAU;
let mut re = 0.0f32;
let mut im = 0.0f32;
for (k, (&xi, &wi)) in x.iter().zip(w.iter()).enumerate() {
let ph = two_pi * f_hz * (k as f32) / fs;
re += wi * xi * ph.cos();
im += wi * xi * ph.sin();
}
let sig = (re*re + im*im).sqrt() / (w.iter().sum::<f32>() + 1e-12);
let p_total: f32 = x.iter().map(|v| v*v).sum::<f32>() / (n as f32);
let p_sig = sig * sig;
let p_noise = (p_total - p_sig).max(1e-12);
10.0 * (p_sig / p_noise).log10()
}
pub fn measure<F: FnMut() -> usize>(mut f: F, n: usize) -> (f32, f32) {
let t0 = std::time::Instant::now();
let _ = f();
let dt = t0.elapsed().as_secs_f32();
let msps = (n as f32) / dt / 1e6;
(msps, dt)
}
#[inline]
pub fn run_block<B: Block>(blk: &mut B, input: &[B::In], output: &mut [B::Out]) -> WorkReport {
blk.process(input, output)
}
#[inline]
pub fn run_block_vec<B: Block>(blk: &mut B, input: &[B::In]) -> (Vec<B::Out>, WorkReport)
where
B::Out: Default + Copy,
{
let mut out = vec![B::Out::default(); input.len()];
let wr = blk.process(input, &mut out);
(out, wr)
}
use rustfft::{FftPlanner, num_complex::Complex};
pub fn power_spectrum(samples: &[f32], fs: f32) -> (Vec<f32>, f32) {
let n = samples.len().next_power_of_two().min(4096).max(64);
let bin_hz = fs / n as f32;
let mut buf: Vec<Complex<f32>> = (0..n)
.map(|i| {
let s = if i < samples.len() { samples[i] } else { 0.0 };
let w = 0.5 - 0.5 * (core::f32::consts::TAU * i as f32 / n as f32).cos();
Complex { re: s * w, im: 0.0 }
})
.collect();
FftPlanner::new().plan_fft_forward(n).process(&mut buf);
let scale = 1.0 / n as f32;
let bins = n / 2 + 1;
let power_db: Vec<f32> = buf[..bins]
.iter()
.map(|c| {
let mag_sq = (c.re * c.re + c.im * c.im) * scale * scale;
10.0 * (mag_sq + 1e-12_f32).log10()
})
.collect();
(power_db, bin_hz)
}
pub fn spectrum_snr_db(samples: &[f32], fs: f32, carrier_hz: f32) -> f32 {
let (power_db, bin_hz) = power_spectrum(samples, fs);
let n_bins = power_db.len();
if n_bins < 3 { return 0.0; }
let peak_bin = ((carrier_hz / bin_hz).round() as usize).min(n_bins - 1);
let search_r = 3_usize;
let lo = peak_bin.saturating_sub(search_r);
let hi = (peak_bin + search_r).min(n_bins - 1);
let sig_bin = (lo..=hi)
.max_by(|&a, &b| power_db[a].partial_cmp(&power_db[b]).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(peak_bin);
let sig_db = power_db[sig_bin];
let guard = 10_usize;
let mut noise_bins: Vec<f32> = power_db
.iter()
.enumerate()
.filter(|&(i, _)| i > 0 && (i as isize - sig_bin as isize).unsigned_abs() >= guard)
.map(|(_, &v)| v)
.collect();
if noise_bins.is_empty() { return 0.0; }
noise_bins.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let noise_db = noise_bins[noise_bins.len() / 2];
sig_db - noise_db
}
pub fn spectrum_bw_hz(samples: &[f32], fs: f32, carrier_hz: f32, _threshold_db: f32) -> f32 {
let search_hz = 4_000.0_f32;
let carrier_drop_db = 35.0_f32;
let carrier_guard_bins = 3_usize;
let (power_db, bin_hz) = power_spectrum(samples, fs);
let n_bins = power_db.len();
if n_bins < 3 { return bin_hz; }
let nominal_bin = ((carrier_hz / bin_hz).round() as usize).min(n_bins - 1);
let cr = 3_usize;
let c_lo = nominal_bin.saturating_sub(cr);
let c_hi = (nominal_bin + cr).min(n_bins - 1);
let carrier_bin = (c_lo..=c_hi)
.max_by(|&a, &b| power_db[a].partial_cmp(&power_db[b]).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(nominal_bin);
let cutoff = power_db[carrier_bin] - carrier_drop_db;
let search_bins = (search_hz / bin_hz).ceil() as usize;
let lsb_lo = carrier_bin.saturating_sub(search_bins);
let lsb_hi = carrier_bin.saturating_sub(carrier_guard_bins);
let left_edge = if lsb_lo < lsb_hi {
(lsb_lo..=lsb_hi).find(|&i| power_db[i] >= cutoff).unwrap_or(carrier_bin)
} else {
carrier_bin
};
let usb_lo = (carrier_bin + carrier_guard_bins).min(n_bins - 1);
let usb_hi = (carrier_bin + search_bins).min(n_bins - 1);
let right_edge = if usb_lo < usb_hi {
(usb_lo..=usb_hi).rfind(|&i| power_db[i] >= cutoff).unwrap_or(carrier_bin)
} else {
carrier_bin
};
((right_edge.max(left_edge) - left_edge + 1) as f32) * bin_hz
}
pub fn best_sync(
results: &[crate::sync::psk31_sync::Psk31SyncResult],
carrier_hz: f32,
baud: f32,
) -> Option<(f32, usize)> {
results
.iter()
.filter(|r| (r.carrier_hz - carrier_hz).abs() <= 2.0 * baud)
.min_by(|a, b| {
let da = (a.carrier_hz - carrier_hz).abs();
let db = (b.carrier_hz - carrier_hz).abs();
a.time_sym.cmp(&b.time_sym)
.then(da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal))
})
.map(|r| (r.carrier_hz, r.time_sym))
}
pub const SIGNAL_THRESHOLD: f32 = 0.1;
pub const PSK31_BW_HZ: f32 = 62.5;
#[inline(always)]
pub fn atan2_approx(y: f32, x: f32) -> f32 {
let ax = x.abs();
let ay = y.abs();
let (mn, mx) = if ax < ay { (ax, ay) } else { (ay, ax) };
let r = mn / (mx + f32::EPSILON);
let r2 = r * r;
let phi = r * (std::f32::consts::FRAC_PI_4 + r2 * (-0.2447 + r2 * 0.0663));
let phi = if ax < ay { std::f32::consts::FRAC_PI_2 - phi } else { phi };
if x < 0.0 {
(std::f32::consts::PI - phi) * if y < 0.0 { -1.0 } else { 1.0 }
} else {
phi * if y < 0.0 { -1.0 } else { 1.0 }
}
}