use std::f32::consts::PI;
use num_complex::Complex32;
use crate::STABILIZE_EVERY;
use crate::config::ResonatorConfig;
#[derive(Debug)]
pub struct Resonator {
freq: f32,
alpha: f32,
beta: f32,
z_re: f32,
z_im: f32,
w_re: f32,
w_im: f32,
r_re: f32,
r_im: f32,
rr_re: f32,
rr_im: f32,
sample_count: u64,
}
impl Resonator {
pub fn new(config: ResonatorConfig, sample_rate: f32) -> Self {
debug_assert!(
sample_rate.is_finite() && sample_rate > 0.0,
"sample_rate must be positive"
);
let ResonatorConfig { freq, alpha, beta } = config;
let phasor_angle = -2.0 * PI * freq / sample_rate;
Self {
freq,
alpha,
beta,
z_re: 1.0,
z_im: 0.0,
w_re: phasor_angle.cos(),
w_im: phasor_angle.sin(),
r_re: 0.0,
r_im: 0.0,
rr_re: 0.0,
rr_im: 0.0,
sample_count: 0,
}
}
#[inline]
pub fn process_sample(&mut self, sample: f32) {
self.r_re = (1.0 - self.alpha) * self.r_re + self.alpha * sample * self.z_re;
self.r_im = (1.0 - self.alpha) * self.r_im + self.alpha * sample * self.z_im;
self.rr_re = (1.0 - self.beta) * self.rr_re + self.beta * self.r_re;
self.rr_im = (1.0 - self.beta) * self.rr_im + self.beta * self.r_im;
let z_re = self.z_re;
let z_im = self.z_im;
self.z_re = z_re * self.w_re - z_im * self.w_im;
self.z_im = z_re * self.w_im + z_im * self.w_re;
self.sample_count += 1;
if self.sample_count.is_multiple_of(STABILIZE_EVERY) {
self.stabilize();
}
}
#[inline]
pub fn process_samples(&mut self, samples: &[f32]) {
for &s in samples {
self.process_sample(s);
}
}
fn stabilize(&mut self) {
let inv_mag = 1.0 / (self.z_re * self.z_re + self.z_im * self.z_im).sqrt();
self.z_re *= inv_mag;
self.z_im *= inv_mag;
}
pub fn reset(&mut self) {
self.z_re = 1.0;
self.z_im = 0.0;
self.r_re = 0.0;
self.r_im = 0.0;
self.rr_re = 0.0;
self.rr_im = 0.0;
self.sample_count = 0;
}
pub fn freq(&self) -> f32 {
self.freq
}
pub fn power(&self) -> f32 {
self.rr_re * self.rr_re + self.rr_im * self.rr_im
}
pub fn magnitude(&self) -> f32 {
self.power().sqrt()
}
pub fn phase(&self) -> f32 {
self.rr_im.atan2(self.rr_re)
}
pub fn complex(&self) -> Complex32 {
Complex32::new(self.rr_re, self.rr_im)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::heuristic_alpha;
#[test]
fn power_is_magnitude_squared() {
let mut r = Resonator::new(ResonatorConfig::new(440.0, 1.0, 1.0), 44100.0);
r.rr_re = 3.0;
r.rr_im = 4.0;
assert_eq!(r.power(), 25.0);
}
#[test]
fn magnitude_is_sqrt_of_power() {
let mut r = Resonator::new(ResonatorConfig::new(440.0, 1.0, 1.0), 44100.0);
r.rr_re = 3.0;
r.rr_im = 4.0;
assert_eq!(r.magnitude(), 5.0);
}
#[test]
fn phase_uses_atan2() {
let mut r = Resonator::new(ResonatorConfig::new(440.0, 1.0, 1.0), 44100.0);
r.rr_re = 1.0;
r.rr_im = 0.0;
assert_eq!(r.phase(), 0.0);
r.rr_re = 0.0;
r.rr_im = 1.0;
assert!((r.phase() - std::f32::consts::FRAC_PI_2).abs() < 1e-6);
r.rr_re = -1.0;
r.rr_im = 0.0;
assert!((r.phase() - PI).abs() < 1e-6);
}
#[test]
fn matched_sine_power_converges_near_one_quarter() {
let sr = 44100.0;
let freq = 440.0;
let alpha = heuristic_alpha(freq, sr);
let mut r = Resonator::new(ResonatorConfig::new(freq, alpha, alpha), sr);
let signal: Vec<f32> = (0..2 * sr as usize)
.map(|i| (2.0 * PI * freq * i as f32 / sr).cos())
.collect();
r.process_samples(&signal);
assert!(
(r.power() - 0.25).abs() < 0.01,
"power should be ~0.25, got {}",
r.power()
);
}
#[test]
fn reset_clears_state() {
let mut r = Resonator::new(ResonatorConfig::new(440.0, 0.01, 0.01), 44100.0);
r.process_samples(&vec![0.5; 1000]);
assert!(r.magnitude() > 0.0);
r.reset();
assert_eq!(r.complex(), Complex32::new(0.0, 0.0));
}
#[test]
fn stabilize_restores_unit_magnitude() {
let mut r = Resonator::new(ResonatorConfig::new(440.0, 1.0, 1.0), 44100.0);
r.z_re = 3.0;
r.z_im = 4.0;
r.stabilize();
assert!((r.z_re - 0.6).abs() < 1e-6);
assert!((r.z_im - 0.8).abs() < 1e-6);
}
}