use crate::dsp::{a_weighting, ema_tc};
use crate::filterbank::Tri;
pub struct SpectrumAnalyzer {
pub spec_pow_smooth: Vec<f32>,
pub filters: Vec<Tri>,
pub bars_y: Vec<f32>,
pub bars_v: Vec<f32>,
pub eq_ref: Vec<f32>,
pub db_low: f32,
pub db_high: f32,
sort_scratch: Vec<f32>,
pub bars_target: Vec<f32>,
flowed_scratch: Vec<f32>,
}
pub struct FlowSpringParams {
pub flow_k: f32,
pub spr_k: f32,
pub spr_zeta: f32,
}
impl SpectrumAnalyzer {
pub fn new(half_fft_size: usize) -> Self {
Self {
spec_pow_smooth: vec![0.0; half_fft_size],
filters: Vec::new(),
bars_y: Vec::new(),
bars_v: Vec::new(),
eq_ref: Vec::new(),
db_low: -60.0,
db_high: -20.0,
sort_scratch: Vec::new(),
bars_target: Vec::new(),
flowed_scratch: Vec::new(),
}
}
#[inline]
pub fn resize(&mut self, num_bars: usize) {
if self.bars_y.len() != num_bars {
self.bars_y = vec![0.0; num_bars];
self.bars_v = vec![0.0; num_bars];
self.eq_ref = vec![1e-6; num_bars];
self.bars_target = vec![0.0; num_bars];
self.sort_scratch = vec![0.0; num_bars];
self.flowed_scratch = vec![0.0; num_bars];
}
}
pub fn update_spectrum(
&mut self,
spec_pow: &[f32],
tau_spec: f32,
dt_s: f32,
) {
let len = self.spec_pow_smooth.len().min(spec_pow.len());
for (i, &pow) in spec_pow.iter().enumerate().take(len) {
let incoming = pow.max(1e-12);
let prev = self.spec_pow_smooth[i];
self.spec_pow_smooth[i] = if incoming >= prev {
incoming
} else {
ema_tc(prev, incoming, tau_spec, dt_s)
};
}
}
pub fn analyze_bands(&mut self, dt_s: f32, gate_open: bool) {
let filters_len = self.filters.len();
for (i, tri) in self.filters.iter().enumerate() {
let mut acc = 0.0f32;
for &(idx, wgt) in &tri.taps {
if idx < self.spec_pow_smooth.len() {
acc += self.spec_pow_smooth[idx] * wgt;
}
}
let amp = acc.sqrt();
let amp_weighted = amp * a_weighting(tri.center_hz);
self.eq_ref[i] =
ema_tc(self.eq_ref[i], amp_weighted, 6.0, dt_s)
.max(1e-9);
let rel = amp_weighted / self.eq_ref[i];
self.bars_target[i] = 20.0 * rel.max(1e-12).log10();
}
self.sort_scratch[..filters_len]
.copy_from_slice(&self.bars_target[..filters_len]);
self.update_db_range(filters_len, dt_s);
let low = self.db_low - 3.0;
let high = self.db_high + 6.0;
let range_inv = 1.0 / (high - low).max(12.0);
if gate_open {
for i in 0..filters_len {
let mut v = (self.bars_target[i] - low) * range_inv;
v = v.clamp(0.0, 1.0).powf(0.85);
self.bars_target[i] = 1.0 - (1.0 - v).powf(1.6);
}
} else {
for t in &mut self.bars_target[..filters_len] {
*t = 0.0;
}
}
}
fn update_db_range(&mut self, len: usize, dt_s: f32) {
if len == 0 {
return;
}
self.sort_scratch[..len].sort_by(|a, b| a.total_cmp(b));
let idx_low = ((len - 1) as f32 * 0.10).round() as usize;
let idx_high = ((len - 1) as f32 * 0.90).round() as usize;
let q10 = self.sort_scratch[idx_low];
let q90 = self.sort_scratch[idx_high];
self.db_low = ema_tc(self.db_low, q10, 0.30, dt_s);
self.db_high = ema_tc(self.db_high, q90, 0.50, dt_s);
}
pub fn apply_flow_and_spring(
&mut self,
params: &FlowSpringParams,
dt_s: f32,
gate_open: bool,
) {
let n = self.bars_y.len();
if n == 0 {
return;
}
if !gate_open {
let tau_silence = 0.22f32;
let a = (-dt_s / tau_silence).exp();
for i in 0..n {
self.bars_y[i] *= a;
self.bars_v[i] = 0.0;
if self.bars_y[i] < 0.001 {
self.bars_y[i] = 0.0;
}
}
return;
}
for i in 0..n {
let left = if i > 0 {
self.bars_y[i - 1]
} else {
self.bars_y[i]
};
let right = if i + 1 < n {
self.bars_y[i + 1]
} else {
self.bars_y[i]
};
let flow =
params.flow_k * (left + right - 2.0 * self.bars_y[i]);
self.flowed_scratch[i] =
(self.bars_target[i] + flow).clamp(0.0, 1.0);
}
let c = 2.0 * params.spr_k.sqrt() * params.spr_zeta;
for i in 0..n {
let a = params.spr_k
* (self.flowed_scratch[i] - self.bars_y[i])
- c * self.bars_v[i];
self.bars_v[i] += a * dt_s;
self.bars_y[i] = (self.bars_y[i] + self.bars_v[i] * dt_s)
.clamp(0.0, 1.0);
}
}
}