#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum KneeMode {
Hard,
Soft(f32),
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct CompressorConfig {
pub threshold_db: f32,
pub ratio: f32,
pub attack_secs: f32,
pub release_secs: f32,
pub makeup_gain_db: f32,
pub knee: KneeMode,
pub sample_rate: f32,
}
impl Default for CompressorConfig {
fn default() -> Self {
Self {
threshold_db: -20.0,
ratio: 4.0,
attack_secs: 0.01,
release_secs: 0.1,
makeup_gain_db: 0.0,
knee: KneeMode::Hard,
sample_rate: 48_000.0,
}
}
}
#[allow(dead_code)]
pub struct Compressor {
config: CompressorConfig,
envelope: f32,
attack_coeff: f32,
release_coeff: f32,
last_gain_reduction_db: f32,
}
impl Compressor {
#[allow(dead_code)]
pub fn new(config: CompressorConfig) -> Self {
let attack_coeff = Self::time_to_coeff(config.attack_secs, config.sample_rate);
let release_coeff = Self::time_to_coeff(config.release_secs, config.sample_rate);
Self {
config,
envelope: 0.0,
attack_coeff,
release_coeff,
last_gain_reduction_db: 0.0,
}
}
#[allow(dead_code)]
fn time_to_coeff(time_secs: f32, sample_rate: f32) -> f32 {
if time_secs <= 0.0 || sample_rate <= 0.0 {
return 0.0;
}
(-1.0_f32 / (time_secs * sample_rate)).exp()
}
#[allow(dead_code)]
fn compute_gain_reduction_db(&self, input_db: f32) -> f32 {
let threshold = self.config.threshold_db;
let ratio = self.config.ratio;
match self.config.knee {
KneeMode::Hard => {
if input_db <= threshold {
0.0
} else {
(input_db - threshold) * (1.0 - 1.0 / ratio)
}
}
KneeMode::Soft(knee_width) => {
let half_knee = knee_width / 2.0;
if input_db < threshold - half_knee {
0.0
} else if input_db > threshold + half_knee {
(input_db - threshold) * (1.0 - 1.0 / ratio)
} else {
let x = input_db - (threshold - half_knee);
let gain = x * x / (2.0 * knee_width);
gain * (1.0 - 1.0 / ratio)
}
}
}
}
#[allow(dead_code)]
pub fn process_sample(&mut self, input: f32) -> f32 {
let abs_input = input.abs();
if abs_input > self.envelope {
self.envelope =
self.attack_coeff * self.envelope + (1.0 - self.attack_coeff) * abs_input;
} else {
self.envelope =
self.release_coeff * self.envelope + (1.0 - self.release_coeff) * abs_input;
}
let level_db = if self.envelope > 1e-10 {
20.0 * self.envelope.log10()
} else {
-120.0
};
let gr_db = self.compute_gain_reduction_db(level_db);
self.last_gain_reduction_db = gr_db;
let total_gain_db = -gr_db + self.config.makeup_gain_db;
let gain_linear = 10.0_f32.powf(total_gain_db / 20.0);
input * gain_linear
}
#[allow(dead_code)]
pub fn process_buffer(&mut self, samples: &mut [f32]) {
for s in samples.iter_mut() {
*s = self.process_sample(*s);
}
}
#[allow(dead_code)]
pub fn gain_reduction_db(&self) -> f32 {
self.last_gain_reduction_db
}
#[allow(dead_code)]
pub fn reset(&mut self) {
self.envelope = 0.0;
self.last_gain_reduction_db = 0.0;
}
#[allow(dead_code)]
pub fn set_attack(&mut self, attack_secs: f32) {
self.config.attack_secs = attack_secs;
self.attack_coeff = Self::time_to_coeff(attack_secs, self.config.sample_rate);
}
#[allow(dead_code)]
pub fn set_release(&mut self, release_secs: f32) {
self.config.release_secs = release_secs;
self.release_coeff = Self::time_to_coeff(release_secs, self.config.sample_rate);
}
#[allow(dead_code)]
pub fn set_threshold(&mut self, threshold_db: f32) {
self.config.threshold_db = threshold_db;
}
#[allow(dead_code)]
pub fn set_ratio(&mut self, ratio: f32) {
self.config.ratio = ratio.max(1.0);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_compressor() -> Compressor {
Compressor::new(CompressorConfig::default())
}
#[test]
fn test_compressor_creation() {
let c = make_compressor();
assert_eq!(c.config.threshold_db, -20.0);
assert_eq!(c.config.ratio, 4.0);
}
#[test]
fn test_silence_passes_through() {
let mut c = make_compressor();
let out = c.process_sample(0.0);
assert_eq!(out, 0.0);
}
#[test]
fn test_envelope_rises_on_signal() {
let mut c = make_compressor();
for _ in 0..100 {
c.process_sample(1.0);
}
assert!(c.envelope > 0.0);
}
#[test]
fn test_gain_reduction_hard_knee_below_threshold() {
let c = make_compressor();
let gr = c.compute_gain_reduction_db(-30.0);
assert_eq!(gr, 0.0, "no reduction below threshold");
}
#[test]
fn test_gain_reduction_hard_knee_above_threshold() {
let c = make_compressor();
let gr = c.compute_gain_reduction_db(-10.0);
assert!((gr - 7.5).abs() < 1e-4);
}
#[test]
fn test_soft_knee_within_knee() {
let config = CompressorConfig {
knee: KneeMode::Soft(10.0),
..CompressorConfig::default()
};
let c = Compressor::new(config);
let gr = c.compute_gain_reduction_db(-20.0);
assert!(gr >= 0.0);
}
#[test]
fn test_process_buffer_modifies_samples() {
let mut c = make_compressor();
let mut buf = vec![0.5_f32; 1000];
c.process_buffer(&mut buf);
for s in &buf {
assert!(s.is_finite());
}
}
#[test]
fn test_reset_clears_envelope() {
let mut c = make_compressor();
for _ in 0..500 {
c.process_sample(1.0);
}
assert!(c.envelope > 0.0);
c.reset();
assert_eq!(c.envelope, 0.0);
}
#[test]
fn test_time_to_coeff_zero_time() {
let coeff = Compressor::time_to_coeff(0.0, 48_000.0);
assert_eq!(coeff, 0.0);
}
#[test]
fn test_time_to_coeff_positive() {
let coeff = Compressor::time_to_coeff(0.01, 48_000.0);
assert!(coeff > 0.0 && coeff < 1.0);
}
#[test]
fn test_set_attack_updates_coeff() {
let mut c = make_compressor();
let old_coeff = c.attack_coeff;
c.set_attack(0.001);
assert_ne!(c.attack_coeff, old_coeff);
}
#[test]
fn test_set_ratio_clamps_to_one() {
let mut c = make_compressor();
c.set_ratio(0.5);
assert_eq!(c.config.ratio, 1.0);
}
#[test]
fn test_makeup_gain_increases_output() {
let config = CompressorConfig {
makeup_gain_db: 6.0,
threshold_db: 0.0, ..CompressorConfig::default()
};
let mut c = Compressor::new(config);
for _ in 0..10000 {
c.process_sample(0.1);
}
let out = c.process_sample(0.1);
assert!(out > 0.1);
}
#[test]
fn test_gain_reduction_accessor() {
let mut c = make_compressor();
for _ in 0..1000 {
c.process_sample(1.0);
}
let gr = c.gain_reduction_db();
assert!(gr >= 0.0);
}
}