use crate::core_types::{NUM_BANDS, SAMPLE_RATE, FRAME_SAMPLES};
use crate::math::{sinf, cosf, sinhf, sqrtf, roundf, clampf};
pub const BAND_EDGES: [(f32, f32); NUM_BANDS] = [
(0.0, 500.0),
(500.0, 1000.0),
(1000.0, 2000.0),
(2000.0, 3000.0),
(3000.0, 4000.0),
];
#[derive(Debug, Clone, Copy)]
pub struct Biquad {
b0: f32, b1: f32, b2: f32,
a1: f32, a2: f32,
x1: f32, x2: f32,
y1: f32, y2: f32,
}
impl Biquad {
pub fn bandpass(center_hz: f32, bandwidth_hz: f32, sample_rate: f32) -> Self {
let w0 = 2.0 * core::f32::consts::PI * center_hz / sample_rate;
let bw = 2.0 * core::f32::consts::PI * bandwidth_hz / sample_rate;
let alpha = sinf(w0) * sinhf(bw / 2.0);
let b0 = alpha;
let b1 = 0.0;
let b2 = -alpha;
let a0 = 1.0 + alpha;
let a1 = -2.0 * cosf(w0);
let a2 = 1.0 - alpha;
Self {
b0: b0 / a0, b1: b1 / a0, b2: b2 / a0,
a1: a1 / a0, a2: a2 / a0,
x1: 0.0, x2: 0.0, y1: 0.0, y2: 0.0,
}
}
pub fn process(&mut self, x: f32) -> f32 {
let y = self.b0 * x + self.b1 * self.x1 + self.b2 * self.x2
- self.a1 * self.y1 - self.a2 * self.y2;
self.x2 = self.x1;
self.x1 = x;
self.y2 = self.y1;
self.y1 = y;
y
}
pub fn filter_buf(&mut self, input: &[f32], out: &mut [f32]) {
let n = input.len().min(out.len());
for i in 0..n {
out[i] = self.process(input[i]);
}
}
pub fn reset(&mut self) {
self.x1 = 0.0; self.x2 = 0.0;
self.y1 = 0.0; self.y2 = 0.0;
}
}
pub struct FilterBank {
filters: [Biquad; NUM_BANDS],
}
impl FilterBank {
pub fn new() -> Self {
let sr = SAMPLE_RATE as f32;
let mut filters = [Biquad::bandpass(1000.0, 500.0, sr); NUM_BANDS];
for i in 0..NUM_BANDS {
let (lo, hi) = BAND_EDGES[i];
let center = (lo + hi) / 2.0;
let bw = hi - lo;
let center = if center < 1.0 { bw / 2.0 } else { center };
filters[i] = Biquad::bandpass(center, bw, sr);
}
Self { filters }
}
pub fn analyze(&mut self, signal: &[f32]) -> [[f32; FRAME_SAMPLES]; NUM_BANDS] {
let mut bands = [[0.0f32; FRAME_SAMPLES]; NUM_BANDS];
let n = signal.len().min(FRAME_SAMPLES);
for b in 0..NUM_BANDS {
self.filters[b].filter_buf(&signal[..n], &mut bands[b][..n]);
}
bands
}
pub fn reset(&mut self) {
for f in &mut self.filters {
f.reset();
}
}
}
pub fn band_energies(bands: &[[f32; FRAME_SAMPLES]; NUM_BANDS], len: usize) -> [f32; NUM_BANDS] {
let mut energies = [0.0f32; NUM_BANDS];
let n = len.min(FRAME_SAMPLES);
for b in 0..NUM_BANDS {
let mut sum = 0.0f32;
for i in 0..n {
sum += bands[b][i] * bands[b][i];
}
energies[b] = sum / n as f32;
}
energies
}
pub fn bandpass_voicing_strength(
bands: &[[f32; FRAME_SAMPLES]; NUM_BANDS],
pitch_period: f32,
len: usize,
) -> [f32; NUM_BANDS] {
let mut voicing = [0.0f32; NUM_BANDS];
let n = len.min(FRAME_SAMPLES);
let period = roundf(pitch_period) as usize;
if period < 2 || period >= n {
return voicing; }
for b in 0..NUM_BANDS {
let mut num = 0.0f32;
let mut e0 = 0.0f32;
let mut e1 = 0.0f32;
for i in period..n {
num += bands[b][i] * bands[b][i - period];
e0 += bands[b][i] * bands[b][i];
e1 += bands[b][i - period] * bands[b][i - period];
}
let denom = sqrtf(e0 * e1);
if denom > 1e-10 {
voicing[b] = clampf(num / denom, 0.0, 1.0);
}
}
voicing
}
pub struct NoiseGen {
state: u32,
}
impl NoiseGen {
pub const fn new(seed: u32) -> Self {
Self { state: seed }
}
pub fn next(&mut self) -> f32 {
self.state = self.state.wrapping_mul(1103515245).wrapping_add(12345);
(self.state as i32 as f32) / (i32::MAX as f32)
}
}
pub fn mixed_excitation(
pitch_period: f32,
voicing: &[f32; NUM_BANDS],
jitter: f32,
output: &mut [f32],
pulse_phase: &mut f32,
noise: &mut NoiseGen,
) {
let n = output.len();
let mut pulse = [0.0f32; FRAME_SAMPLES];
let mut noise_buf = [0.0f32; FRAME_SAMPLES];
let len = n.min(FRAME_SAMPLES);
for i in 0..len {
noise_buf[i] = noise.next();
*pulse_phase += 1.0;
let jittered_period = pitch_period * (1.0 + jitter * (noise.next() * 0.1));
if *pulse_phase >= jittered_period {
*pulse_phase -= jittered_period;
pulse[i] = sqrtf(pitch_period); }
}
for i in 0..len {
let mut sample = 0.0f32;
for b in 0..NUM_BANDS {
let v = voicing[b];
let band_excitation = v * pulse[i] + (1.0 - v) * noise_buf[i];
sample += band_excitation;
}
output[i] = sample / NUM_BANDS as f32;
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
fn sine_wave(freq: f32, n: usize) -> Vec<f32> {
(0..n)
.map(|i| (2.0 * core::f32::consts::PI * freq * i as f32 / SAMPLE_RATE as f32).sin())
.collect()
}
#[test]
fn test_biquad_bandpass_passes_center() {
let mut bp = Biquad::bandpass(1000.0, 500.0, SAMPLE_RATE as f32);
let input = sine_wave(1000.0, 256);
let mut output = vec![0.0f32; 256];
bp.filter_buf(&input, &mut output);
let energy: f32 = output[50..].iter().map(|x| x * x).sum::<f32>() / 206.0;
assert!(energy > 0.01, "Bandpass should pass center freq, energy={}", energy);
}
#[test]
fn test_biquad_bandpass_rejects_far() {
let mut bp = Biquad::bandpass(1000.0, 500.0, SAMPLE_RATE as f32);
let input = sine_wave(3500.0, 256);
let mut output = vec![0.0f32; 256];
bp.filter_buf(&input, &mut output);
let energy_out: f32 = output[50..].iter().map(|x| x * x).sum::<f32>() / 206.0;
let energy_in: f32 = input[50..].iter().map(|x| x * x).sum::<f32>() / 206.0;
assert!(
energy_out < energy_in * 0.2,
"Bandpass should reject far freq: in={}, out={}",
energy_in, energy_out
);
}
#[test]
fn test_filter_bank_splits() {
let mut fb = FilterBank::new();
let signal = sine_wave(300.0, FRAME_SAMPLES);
let bands = fb.analyze(&signal);
let energies = band_energies(&bands, FRAME_SAMPLES);
assert!(
energies[0] > energies[2],
"Band 0 should dominate for 300Hz: {:?}",
energies
);
assert!(
energies[0] > energies[3],
"Band 0 should dominate for 300Hz: {:?}",
energies
);
}
#[test]
fn test_voicing_strength_voiced() {
let mut fb = FilterBank::new();
let signal = sine_wave(150.0, FRAME_SAMPLES);
let bands = fb.analyze(&signal);
let pitch_period = SAMPLE_RATE as f32 / 150.0;
let voicing = bandpass_voicing_strength(&bands, pitch_period, FRAME_SAMPLES);
let max_v = voicing.iter().cloned().fold(0.0f32, f32::max);
assert!(max_v > 0.5, "Periodic signal should show voicing: {:?}", voicing);
}
#[test]
fn test_voicing_strength_unvoiced() {
let mut fb = FilterBank::new();
let mut noise = [0.0f32; FRAME_SAMPLES];
let mut seed: u32 = 42;
for s in noise.iter_mut() {
seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
*s = (seed as i32 as f32) / (i32::MAX as f32);
}
let bands = fb.analyze(&noise);
let voicing = bandpass_voicing_strength(&bands, 80.0, FRAME_SAMPLES);
let avg: f32 = voicing.iter().sum::<f32>() / NUM_BANDS as f32;
assert!(avg < 0.5, "Noise should have low avg voicing: {}", avg);
}
#[test]
fn test_noise_gen_range() {
let mut ng = NoiseGen::new(12345);
for _ in 0..1000 {
let s = ng.next();
assert!(s >= -1.0 && s <= 1.0, "Noise out of range: {}", s);
}
}
#[test]
fn test_noise_gen_variance() {
let mut ng = NoiseGen::new(99);
let mut sum = 0.0f32;
let mut sum_sq = 0.0f32;
let n = 10000;
for _ in 0..n {
let s = ng.next();
sum += s;
sum_sq += s * s;
}
let mean = sum / n as f32;
let var = sum_sq / n as f32 - mean * mean;
assert!(mean.abs() < 0.1, "Noise mean should be ~0, got {}", mean);
assert!(var > 0.1 && var < 0.6, "Noise variance unexpected: {}", var);
}
#[test]
fn test_mixed_excitation_voiced() {
let mut phase = 0.0f32;
let mut ng = NoiseGen::new(7);
let mut output = [0.0f32; FRAME_SAMPLES];
let voicing = [1.0; NUM_BANDS];
mixed_excitation(80.0, &voicing, 0.0, &mut output, &mut phase, &mut ng);
let peak = output.iter().fold(0.0f32, |m, &s| m.max(s.abs()));
assert!(peak > 0.1, "Voiced excitation should have pulses, peak={}", peak);
}
#[test]
fn test_mixed_excitation_unvoiced() {
let mut phase = 0.0f32;
let mut ng = NoiseGen::new(7);
let mut output = [0.0f32; FRAME_SAMPLES];
let voicing = [0.0; NUM_BANDS];
mixed_excitation(80.0, &voicing, 0.0, &mut output, &mut phase, &mut ng);
let energy: f32 = output.iter().map(|x| x * x).sum::<f32>() / FRAME_SAMPLES as f32;
assert!(energy > 0.01, "Unvoiced should have noise energy, got {}", energy);
}
}