use crate::synthesis::spectral::{WindowType, STFT};
#[derive(Clone)]
pub struct SpectralDynamics {
stft: STFT,
#[allow(dead_code)]
fft_size: usize,
sample_rate: f32,
threshold: f32, ratio: f32, attack: f32, release: f32, knee: f32, mix: f32, enabled: bool,
envelope: Vec<f32>,
}
impl SpectralDynamics {
pub fn new(fft_size: usize, hop_size: usize, window: WindowType, sample_rate: f32) -> Self {
let num_bins = fft_size / 2 + 1;
Self {
stft: STFT::new(fft_size, hop_size, window),
fft_size,
sample_rate,
threshold: -20.0,
ratio: 4.0,
attack: 5.0,
release: 50.0,
knee: 6.0,
mix: 1.0,
enabled: true,
envelope: vec![0.0; num_bins],
}
}
pub fn threshold(&self) -> f32 { self.threshold }
pub fn set_threshold(&mut self, threshold: f32) { self.threshold = threshold; }
pub fn ratio(&self) -> f32 { self.ratio }
pub fn set_ratio(&mut self, ratio: f32) { self.ratio = ratio.max(0.1); }
pub fn attack(&self) -> f32 { self.attack }
pub fn set_attack(&mut self, attack: f32) { self.attack = attack.max(0.1); }
pub fn release(&self) -> f32 { self.release }
pub fn set_release(&mut self, release: f32) { self.release = release.max(1.0); }
pub fn knee(&self) -> f32 { self.knee }
pub fn set_knee(&mut self, knee: f32) { self.knee = knee.max(0.0); }
pub fn mix(&self) -> f32 { self.mix }
pub fn set_mix(&mut self, mix: f32) { self.mix = mix.clamp(0.0, 1.0); }
pub fn gentle() -> Self {
let mut s = Self::new(2048, 512, WindowType::Hann, 44100.0);
s.threshold = -15.0; s.ratio = 2.0; s.attack = 10.0; s.release = 100.0; s.knee = 6.0; s.mix = 0.7;
s
}
pub fn moderate() -> Self {
let mut s = Self::new(2048, 512, WindowType::Hann, 44100.0);
s.threshold = -20.0; s.ratio = 4.0; s.attack = 5.0; s.release = 50.0; s.knee = 6.0; s.mix = 1.0;
s
}
pub fn aggressive() -> Self {
let mut s = Self::new(2048, 512, WindowType::Hann, 44100.0);
s.threshold = -25.0; s.ratio = 8.0; s.attack = 1.0; s.release = 20.0; s.knee = 2.0; s.mix = 1.0;
s
}
pub fn expander() -> Self {
let mut s = Self::new(2048, 512, WindowType::Hann, 44100.0);
s.threshold = -30.0; s.ratio = 0.5; s.attack = 1.0; s.release = 50.0; s.knee = 0.0; s.mix = 1.0;
s
}
pub fn gate_like() -> Self {
let mut s = Self::new(2048, 512, WindowType::Hann, 44100.0);
s.threshold = -40.0; s.ratio = 0.1; s.attack = 0.5; s.release = 20.0; s.knee = 0.0; s.mix = 1.0;
s
}
pub fn process(&mut self, output: &mut [f32], input: &[f32]) {
if !self.enabled {
output.copy_from_slice(input);
return;
}
self.stft.add_input(input);
self.stft.process(output, |spectrum| {
let attack_coeff = (-1000.0 / (self.attack * self.sample_rate)).exp();
let release_coeff = (-1000.0 / (self.release * self.sample_rate)).exp();
let max_bins = spectrum.len().min(self.envelope.len());
for (i, bin) in spectrum.iter_mut().enumerate().take(max_bins) {
let magnitude = (bin.re * bin.re + bin.im * bin.im).sqrt();
let magnitude_db = 20.0 * magnitude.max(1e-10).log10();
let target = magnitude_db;
let coeff = if target > self.envelope[i] { attack_coeff } else { release_coeff };
self.envelope[i] = target + coeff * (self.envelope[i] - target);
let over_threshold = self.envelope[i] - self.threshold;
let gain_reduction = if over_threshold > self.knee {
over_threshold * (1.0 - 1.0 / self.ratio)
} else if over_threshold > 0.0 {
let knee_ratio = over_threshold / self.knee;
knee_ratio * knee_ratio * 0.5 * (1.0 - 1.0 / self.ratio) * self.knee
} else {
0.0
};
let gain_db = -gain_reduction;
let gain = 10.0_f32.powf(gain_db / 20.0);
let final_gain = 1.0 + self.mix * (gain - 1.0);
bin.re *= final_gain;
bin.im *= final_gain;
}
});
}
pub fn reset(&mut self) {
self.stft.reset();
self.envelope.fill(0.0);
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spectral_dynamics_creation() {
let dynamics = SpectralDynamics::new(2048, 512, WindowType::Hann, 44100.0);
assert!(dynamics.is_enabled());
assert_eq!(dynamics.threshold(), -20.0);
assert_eq!(dynamics.ratio(), 4.0);
assert_eq!(dynamics.attack(), 5.0);
assert_eq!(dynamics.release(), 50.0);
assert_eq!(dynamics.knee(), 6.0);
assert_eq!(dynamics.mix(), 1.0);
}
#[test]
fn test_set_threshold() {
let mut dynamics = SpectralDynamics::new(512, 128, WindowType::Hann, 44100.0);
dynamics.set_threshold(-30.0);
assert_eq!(dynamics.threshold(), -30.0);
}
#[test]
fn test_set_ratio() {
let mut dynamics = SpectralDynamics::new(512, 128, WindowType::Hann, 44100.0);
dynamics.set_ratio(8.0);
assert_eq!(dynamics.ratio(), 8.0);
dynamics.set_ratio(0.05);
assert_eq!(dynamics.ratio(), 0.1);
}
#[test]
fn test_set_attack() {
let mut dynamics = SpectralDynamics::new(512, 128, WindowType::Hann, 44100.0);
dynamics.set_attack(10.0);
assert_eq!(dynamics.attack(), 10.0);
dynamics.set_attack(0.05);
assert_eq!(dynamics.attack(), 0.1);
}
#[test]
fn test_set_release() {
let mut dynamics = SpectralDynamics::new(512, 128, WindowType::Hann, 44100.0);
dynamics.set_release(100.0);
assert_eq!(dynamics.release(), 100.0);
dynamics.set_release(0.5);
assert_eq!(dynamics.release(), 1.0);
}
#[test]
fn test_set_knee() {
let mut dynamics = SpectralDynamics::new(512, 128, WindowType::Hann, 44100.0);
dynamics.set_knee(12.0);
assert_eq!(dynamics.knee(), 12.0);
dynamics.set_knee(-5.0);
assert_eq!(dynamics.knee(), 0.0);
}
#[test]
fn test_set_mix() {
let mut dynamics = SpectralDynamics::new(512, 128, WindowType::Hann, 44100.0);
dynamics.set_mix(0.5);
assert_eq!(dynamics.mix(), 0.5);
dynamics.set_mix(-0.5);
assert_eq!(dynamics.mix(), 0.0);
dynamics.set_mix(1.5);
assert_eq!(dynamics.mix(), 1.0);
}
#[test]
fn test_enable_disable() {
let mut dynamics = SpectralDynamics::new(512, 128, WindowType::Hann, 44100.0);
assert!(dynamics.is_enabled());
dynamics.set_enabled(false);
assert!(!dynamics.is_enabled());
dynamics.set_enabled(true);
assert!(dynamics.is_enabled());
}
#[test]
fn test_process_disabled() {
let mut dynamics = SpectralDynamics::new(512, 128, WindowType::Hann, 44100.0);
dynamics.set_enabled(false);
let input = vec![0.1; 512];
let mut output = vec![0.0; 512];
dynamics.process(&mut output, &input);
assert_eq!(output, input);
}
#[test]
fn test_process_basic() {
let mut dynamics = SpectralDynamics::new(512, 128, WindowType::Hann, 44100.0);
let input = vec![0.1; 512];
let mut output = vec![0.0; 512];
dynamics.process(&mut output, &input);
}
#[test]
fn test_reset() {
let mut dynamics = SpectralDynamics::new(512, 128, WindowType::Hann, 44100.0);
let input = vec![0.1; 512];
let mut output = vec![0.0; 512];
dynamics.process(&mut output, &input);
dynamics.reset();
dynamics.process(&mut output, &input);
}
#[test]
fn test_preset_gentle() {
let dynamics = SpectralDynamics::gentle();
assert_eq!(dynamics.threshold(), -15.0);
assert_eq!(dynamics.ratio(), 2.0);
assert_eq!(dynamics.attack(), 10.0);
assert_eq!(dynamics.release(), 100.0);
assert_eq!(dynamics.knee(), 6.0);
assert_eq!(dynamics.mix(), 0.7);
assert!(dynamics.is_enabled());
}
#[test]
fn test_preset_moderate() {
let dynamics = SpectralDynamics::moderate();
assert_eq!(dynamics.threshold(), -20.0);
assert_eq!(dynamics.ratio(), 4.0);
assert_eq!(dynamics.attack(), 5.0);
assert_eq!(dynamics.release(), 50.0);
assert_eq!(dynamics.knee(), 6.0);
assert_eq!(dynamics.mix(), 1.0);
assert!(dynamics.is_enabled());
}
#[test]
fn test_preset_aggressive() {
let dynamics = SpectralDynamics::aggressive();
assert_eq!(dynamics.threshold(), -25.0);
assert_eq!(dynamics.ratio(), 8.0);
assert_eq!(dynamics.attack(), 1.0);
assert_eq!(dynamics.release(), 20.0);
assert_eq!(dynamics.knee(), 2.0);
assert_eq!(dynamics.mix(), 1.0);
assert!(dynamics.is_enabled());
}
#[test]
fn test_preset_expander() {
let dynamics = SpectralDynamics::expander();
assert_eq!(dynamics.threshold(), -30.0);
assert_eq!(dynamics.ratio(), 0.5);
assert_eq!(dynamics.attack(), 1.0);
assert_eq!(dynamics.release(), 50.0);
assert_eq!(dynamics.knee(), 0.0);
assert_eq!(dynamics.mix(), 1.0);
assert!(dynamics.is_enabled());
}
#[test]
fn test_preset_gate_like() {
let dynamics = SpectralDynamics::gate_like();
assert_eq!(dynamics.threshold(), -40.0);
assert_eq!(dynamics.ratio(), 0.1);
assert_eq!(dynamics.attack(), 0.5);
assert_eq!(dynamics.release(), 20.0);
assert_eq!(dynamics.knee(), 0.0);
assert_eq!(dynamics.mix(), 1.0);
assert!(dynamics.is_enabled());
}
}