use super::*;
use rustfft::num_complex::Complex;
#[derive(Clone, Debug)]
pub struct SpectralCompressor {
stft: STFT,
#[allow(dead_code)]
fft_size: usize,
sample_rate: f32,
threshold_db: f32, ratio: f32, attack: f32, release: f32, knee: f32,
attack_coeff: f32,
release_coeff: f32,
envelope: Vec<f32>,
enabled: bool,
}
impl SpectralCompressor {
pub fn new(
fft_size: usize,
hop_size: usize,
window_type: WindowType,
sample_rate: f32,
) -> Self {
assert!(fft_size.is_power_of_two(), "FFT size must be power of 2");
assert!(hop_size <= fft_size, "Hop size must be <= FFT size");
assert!(sample_rate > 0.0, "Sample rate must be positive");
let threshold_db = -20.0;
let ratio = 4.0;
let attack = 5.0; let release = 50.0; let knee = 6.0;
let hop_time = hop_size as f32 / sample_rate;
let attack_coeff = Self::calculate_coeff(attack, hop_time);
let release_coeff = Self::calculate_coeff(release, hop_time);
Self {
stft: STFT::new(fft_size, hop_size, window_type),
fft_size,
sample_rate,
threshold_db,
ratio,
attack,
release,
knee,
attack_coeff,
release_coeff,
envelope: vec![1.0; fft_size], enabled: true,
}
}
#[inline]
fn calculate_coeff(time_ms: f32, hop_time: f32) -> f32 {
let time_sec = time_ms / 1000.0;
(-hop_time / time_sec).exp()
}
pub fn set_threshold(&mut self, threshold_db: f32) {
self.threshold_db = threshold_db;
}
pub fn set_ratio(&mut self, ratio: f32) {
self.ratio = ratio.max(1.0);
}
pub fn set_attack(&mut self, attack_ms: f32) {
self.attack = attack_ms.max(0.1);
let hop_time = self.stft.hop_size as f32 / self.sample_rate;
self.attack_coeff = Self::calculate_coeff(self.attack, hop_time);
}
pub fn set_release(&mut self, release_ms: f32) {
self.release = release_ms.max(1.0);
let hop_time = self.stft.hop_size as f32 / self.sample_rate;
self.release_coeff = Self::calculate_coeff(self.release, hop_time);
}
pub fn set_knee(&mut self, knee_db: f32) {
self.knee = knee_db.max(0.0);
}
pub fn process(&mut self, output: &mut [f32], input: &[f32]) {
if !self.enabled {
output.copy_from_slice(input);
return;
}
self.stft.add_input(input);
let threshold_db = self.threshold_db;
let ratio = self.ratio;
let knee = self.knee;
let attack_coeff = self.attack_coeff;
let release_coeff = self.release_coeff;
let envelope = &mut self.envelope;
self.stft.process(output, |spectrum| {
Self::apply_compression_static(
spectrum,
envelope,
threshold_db,
ratio,
knee,
attack_coeff,
release_coeff,
);
});
}
pub fn threshold(&self) -> f32 {
self.threshold_db
}
pub fn ratio(&self) -> f32 {
self.ratio
}
pub fn attack(&self) -> f32 {
self.attack
}
pub fn release(&self) -> f32 {
self.release
}
pub fn knee(&self) -> f32 {
self.knee
}
#[inline]
fn apply_compression_static(
spectrum: &mut [Complex<f32>],
envelope: &mut [f32],
threshold_db: f32,
ratio: f32,
knee_db: f32,
attack_coeff: f32,
release_coeff: f32,
) {
let len = spectrum.len();
let mut magnitudes = vec![0.0; len];
ComplexOps::magnitude(&mut magnitudes, spectrum);
for i in 0..len {
let mag_db = if magnitudes[i] > 1e-10 {
20.0 * magnitudes[i].log10()
} else {
-100.0 };
let gain = if knee_db > 0.0 {
let knee_lower = threshold_db - knee_db / 2.0;
let knee_upper = threshold_db + knee_db / 2.0;
if mag_db < knee_lower {
1.0
} else if mag_db > knee_upper {
let over_db = mag_db - threshold_db;
let gain_reduction_db = over_db * (1.0 - 1.0 / ratio);
10.0_f32.powf(-gain_reduction_db / 20.0)
} else {
let knee_position = (mag_db - knee_lower) / knee_db; let over_db = mag_db - threshold_db;
let gain_reduction_db = over_db * (1.0 - 1.0 / ratio) * knee_position;
10.0_f32.powf(-gain_reduction_db / 20.0)
}
} else {
if mag_db >= threshold_db {
let over_db = mag_db - threshold_db;
let gain_reduction_db = over_db * (1.0 - 1.0 / ratio);
10.0_f32.powf(-gain_reduction_db / 20.0)
} else {
1.0
}
};
let current_env = envelope[i];
let coeff = if gain < current_env {
attack_coeff } else {
release_coeff };
envelope[i] = gain + coeff * (current_env - gain);
spectrum[i].re *= envelope[i];
spectrum[i].im *= envelope[i];
}
}
pub fn reset(&mut self) {
self.stft.reset();
self.envelope.fill(1.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_compressor_creation() {
let comp = SpectralCompressor::new(2048, 512, WindowType::Hann, 44100.0);
assert!(comp.is_enabled());
}
#[test]
#[should_panic(expected = "FFT size must be power of 2")]
fn test_spectral_compressor_requires_power_of_two() {
SpectralCompressor::new(1000, 250, WindowType::Hann, 44100.0);
}
#[test]
#[should_panic(expected = "Hop size must be <= FFT size")]
fn test_spectral_compressor_hop_validation() {
SpectralCompressor::new(512, 1024, WindowType::Hann, 44100.0);
}
#[test]
#[should_panic(expected = "Sample rate must be positive")]
fn test_spectral_compressor_sample_rate_validation() {
SpectralCompressor::new(512, 128, WindowType::Hann, 0.0);
}
#[test]
fn test_spectral_compressor_set_threshold() {
let mut comp = SpectralCompressor::new(512, 128, WindowType::Hann, 44100.0);
comp.set_threshold(-30.0);
assert_eq!(comp.threshold(), -30.0);
}
#[test]
fn test_spectral_compressor_set_ratio() {
let mut comp = SpectralCompressor::new(512, 128, WindowType::Hann, 44100.0);
comp.set_ratio(8.0);
assert_eq!(comp.ratio(), 8.0);
comp.set_ratio(0.5);
assert_eq!(comp.ratio(), 1.0);
}
#[test]
fn test_spectral_compressor_set_attack() {
let mut comp = SpectralCompressor::new(512, 128, WindowType::Hann, 44100.0);
comp.set_attack(10.0);
assert_eq!(comp.attack(), 10.0);
}
#[test]
fn test_spectral_compressor_set_release() {
let mut comp = SpectralCompressor::new(512, 128, WindowType::Hann, 44100.0);
comp.set_release(100.0);
assert_eq!(comp.release(), 100.0);
}
#[test]
fn test_spectral_compressor_set_knee() {
let mut comp = SpectralCompressor::new(512, 128, WindowType::Hann, 44100.0);
comp.set_knee(12.0);
assert_eq!(comp.knee(), 12.0);
}
#[test]
fn test_spectral_compressor_process_silent() {
let mut comp = SpectralCompressor::new(512, 128, WindowType::Hann, 44100.0);
let input = vec![0.0; 256];
let mut output = vec![0.0; 256];
comp.process(&mut output, &input);
for &sample in &output {
assert!(sample.abs() < 1e-6);
}
}
#[test]
fn test_spectral_compressor_process_with_compression() {
let mut comp = SpectralCompressor::new(2048, 512, WindowType::Hann, 44100.0);
comp.set_threshold(-40.0);
comp.set_ratio(4.0);
let mut input = vec![0.0; 2048];
for i in 0..2048 {
let t = i as f32 / 44100.0;
input[i] = (2.0 * std::f32::consts::PI * 440.0 * t).sin() * 0.5;
}
let mut output = vec![0.0; 2048];
comp.process(&mut output, &input);
let output_energy: f32 = output.iter().map(|x| x * x).sum();
assert!(output_energy > 0.0);
}
#[test]
fn test_spectral_compressor_reset() {
let mut comp = SpectralCompressor::new(512, 128, WindowType::Hann, 44100.0);
let input = vec![0.5; 256];
let mut output = vec![0.0; 256];
comp.process(&mut output, &input);
comp.reset();
comp.process(&mut output, &input);
}
#[test]
fn test_spectral_compressor_disabled() {
let mut comp = SpectralCompressor::new(512, 128, WindowType::Hann, 44100.0);
comp.set_enabled(false);
let input = vec![0.5; 256];
let mut output = vec![0.0; 256];
comp.process(&mut output, &input);
for i in 0..256 {
assert_eq!(output[i], input[i]);
}
}
#[test]
fn test_spectral_compressor_all_window_types() {
for window_type in [WindowType::Hann, WindowType::Hamming, WindowType::Blackman, WindowType::Rectangular] {
let mut comp = SpectralCompressor::new(512, 128, window_type, 44100.0);
let input = vec![0.0; 256];
let mut output = vec![0.0; 256];
comp.process(&mut output, &input);
assert_eq!(output.len(), 256);
}
}
#[test]
fn test_spectral_compressor_various_fft_sizes() {
for fft_size in [512, 1024, 2048, 4096] {
let hop_size = fft_size / 4;
let mut comp = SpectralCompressor::new(fft_size, hop_size, WindowType::Hann, 44100.0);
let input = vec![0.0; 512];
let mut output = vec![0.0; 512];
comp.process(&mut output, &input);
assert_eq!(output.len(), 512);
}
}
#[test]
fn test_spectral_compressor_enable_disable() {
let mut comp = SpectralCompressor::new(512, 128, WindowType::Hann, 44100.0);
assert!(comp.is_enabled());
comp.set_enabled(false);
assert!(!comp.is_enabled());
comp.set_enabled(true);
assert!(comp.is_enabled());
}
#[test]
fn test_spectral_compressor_soft_knee() {
let mut comp = SpectralCompressor::new(2048, 512, WindowType::Hann, 44100.0);
comp.set_threshold(-20.0);
comp.set_ratio(4.0);
comp.set_knee(6.0);
let mut input = vec![0.0; 2048];
for i in 0..2048 {
let t = i as f32 / 44100.0;
input[i] = (2.0 * std::f32::consts::PI * 1000.0 * t).sin() * 0.3;
}
let mut output = vec![0.0; 2048];
comp.process(&mut output, &input);
let output_energy: f32 = output.iter().map(|x| x * x).sum();
assert!(output_energy > 0.0);
}
#[test]
fn test_spectral_compressor_hard_knee() {
let mut comp = SpectralCompressor::new(2048, 512, WindowType::Hann, 44100.0);
comp.set_threshold(-20.0);
comp.set_ratio(4.0);
comp.set_knee(0.0);
let mut input = vec![0.0; 2048];
for i in 0..2048 {
let t = i as f32 / 44100.0;
input[i] = (2.0 * std::f32::consts::PI * 1000.0 * t).sin() * 0.3;
}
let mut output = vec![0.0; 2048];
comp.process(&mut output, &input);
let output_energy: f32 = output.iter().map(|x| x * x).sum();
assert!(output_energy > 0.0);
}
}