use alloc::vec::Vec;
use num_complex::Complex32;
#[cfg(not(feature = "std"))]
use num_traits::Float;
use super::super::fft::default_planner;
use super::msk::FS_HZ;
const FILTER_CENTER_HZ: f32 = 1500.0;
const FILTER_PASSBAND_HALF_HZ: f32 = 900.0;
const FILTER_STOPBAND_HALF_HZ: f32 = 1_100.0;
const FILTER_ROLLOFF_RATE: f32 = core::f32::consts::PI / 200.0;
fn bandpass_gain(freq_hz: f32) -> f32 {
let f = (freq_hz - FILTER_CENTER_HZ).abs();
if f <= FILTER_PASSBAND_HALF_HZ {
1.0
} else if f <= FILTER_STOPBAND_HALF_HZ {
0.5 * (1.0 + (FILTER_ROLLOFF_RATE * (f - FILTER_PASSBAND_HALF_HZ)).cos())
} else {
0.0
}
}
pub fn analytic_signal(input: &[f32]) -> Vec<Complex32> {
let n = input.len();
let mut planner = default_planner();
let fwd = planner.plan_forward(n);
let inv = planner.plan_inverse(n);
let mut x: Vec<Complex32> = input.iter().map(|&v| Complex32::new(v, 0.0)).collect();
fwd.process(&mut x);
let df = FS_HZ / n as f32;
let nyquist = if n.is_multiple_of(2) {
Some(n / 2)
} else {
None
};
for (k, xv) in x.iter_mut().enumerate() {
if k == 0 || Some(k) == nyquist {
*xv *= bandpass_gain(k as f32 * df);
} else if k < n.div_ceil(2) {
*xv *= 2.0 * bandpass_gain(k as f32 * df);
} else {
*xv = Complex32::new(0.0, 0.0);
}
}
inv.process(&mut x);
let scale = 1.0 / n as f32;
for xv in x.iter_mut() {
*xv *= scale;
}
x
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn analytic_signal_of_pure_cosine_is_a_complex_exponential() {
let n = 256;
let k0 = 32;
let input: Vec<f32> = (0..n)
.map(|i| (2.0 * core::f32::consts::PI * k0 as f32 * i as f32 / n as f32).cos())
.collect();
let analytic = analytic_signal(&input);
for (i, a) in analytic.iter().enumerate() {
let phase = 2.0 * core::f32::consts::PI * k0 as f32 * i as f32 / n as f32;
let expected = Complex32::new(phase.cos(), phase.sin());
assert!(
(a - expected).norm() < 1e-3,
"sample {i}: got {a:?}, expected {expected:?}"
);
}
}
#[test]
fn analytic_signal_attenuates_out_of_band_tone() {
let n = 256;
let k0 = 64; let input: Vec<f32> = (0..n)
.map(|i| (2.0 * core::f32::consts::PI * k0 as f32 * i as f32 / n as f32).cos())
.collect();
let analytic = analytic_signal(&input);
let energy: f32 = analytic.iter().map(|a| a.norm_sqr()).sum();
assert!(energy < 1e-4, "expected near-zero energy, got {energy}");
}
#[test]
fn bandpass_gain_is_unity_at_center_and_zero_far_outside() {
assert!((bandpass_gain(FILTER_CENTER_HZ) - 1.0).abs() < 1e-6);
assert!((bandpass_gain(FILTER_CENTER_HZ - 500.0) - 1.0).abs() < 1e-6);
assert_eq!(bandpass_gain(FILTER_CENTER_HZ + 2000.0), 0.0);
assert_eq!(bandpass_gain(0.0), 0.0);
}
}