use aether_core::{node::DspNode, param::ParamBlock, BUFFER_SIZE, MAX_INPUTS};
pub struct Compressor {
rms_env: f32,
gain_env: f32,
}
impl Compressor {
pub fn new() -> Self {
Self {
rms_env: 0.0,
gain_env: 1.0,
}
}
#[inline(always)]
fn db_to_linear(db: f32) -> f32 {
10.0f32.powf(db / 20.0)
}
#[inline(always)]
fn linear_to_db(linear: f32) -> f32 {
if linear <= 1e-10 { return -200.0; }
20.0 * linear.log10()
}
}
impl Default for Compressor {
fn default() -> Self { Self::new() }
}
impl DspNode for Compressor {
fn process(
&mut self,
inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
output: &mut [f32; BUFFER_SIZE],
params: &mut ParamBlock,
sample_rate: f32,
) {
let silence = [0.0f32; BUFFER_SIZE];
let input = inputs[0].unwrap_or(&silence);
let threshold_db = params.get(0).current.clamp(-60.0, 0.0);
let ratio = params.get(1).current.clamp(1.0, 20.0);
let attack_ms = params.get(2).current.clamp(0.1, 200.0);
let release_ms = params.get(3).current.clamp(10.0, 2000.0);
let makeup_db = params.get(4).current.clamp(0.0, 24.0);
let knee_db = params.get(5).current.clamp(0.0, 12.0);
let attack_coeff = (-1.0 / (attack_ms * 0.001 * sample_rate)).exp();
let release_coeff = (-1.0 / (release_ms * 0.001 * sample_rate)).exp();
let makeup_linear = Self::db_to_linear(makeup_db);
for i in 0..BUFFER_SIZE {
let x = input[i];
let x2 = x * x;
self.rms_env = if x2 > self.rms_env {
attack_coeff * self.rms_env + (1.0 - attack_coeff) * x2
} else {
release_coeff * self.rms_env + (1.0 - release_coeff) * x2
};
let rms_db = Self::linear_to_db(self.rms_env.sqrt());
let gain_reduction_db = if knee_db > 0.0 {
let knee_start = threshold_db - knee_db * 0.5;
let knee_end = threshold_db + knee_db * 0.5;
if rms_db <= knee_start {
0.0
} else if rms_db >= knee_end {
(rms_db - threshold_db) * (1.0 / ratio - 1.0)
} else {
let t = (rms_db - knee_start) / knee_db;
t * t * 0.5 * (1.0 / ratio - 1.0) * knee_db
}
} else {
if rms_db > threshold_db {
(rms_db - threshold_db) * (1.0 / ratio - 1.0)
} else {
0.0
}
};
let target_gain = Self::db_to_linear(gain_reduction_db);
self.gain_env = if target_gain < self.gain_env {
attack_coeff * self.gain_env + (1.0 - attack_coeff) * target_gain
} else {
release_coeff * self.gain_env + (1.0 - release_coeff) * target_gain
};
output[i] = x * self.gain_env * makeup_linear;
params.tick_all();
}
}
fn type_name(&self) -> &'static str { "Compressor" }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compressor_silence_passthrough() {
let mut comp = Compressor::new();
let mut params = ParamBlock::new();
for &v in &[-20.0f32, 2.0, 1.0, 100.0, 0.0, 0.0] { params.add(v); }
let input = [0.0f32; BUFFER_SIZE];
let inputs = [Some(&input); MAX_INPUTS];
let mut output = [0.0f32; BUFFER_SIZE];
comp.process(&inputs, &mut output, &mut params, 48000.0);
for s in &output { assert!(s.abs() < 1e-6, "silence should pass through as silence"); }
}
#[test]
fn test_compressor_reduces_loud_signal() {
let mut comp = Compressor::new();
let mut params = ParamBlock::new();
for &v in &[-20.0f32, 4.0, 1.0, 100.0, 0.0, 0.0] { params.add(v); }
let input = [0.5f32; BUFFER_SIZE]; let inputs = [Some(&input); MAX_INPUTS];
let mut output = [0.0f32; BUFFER_SIZE];
comp.process(&inputs, &mut output, &mut params, 48000.0);
let last = output[BUFFER_SIZE - 1].abs();
assert!(last < 0.5, "compressor should reduce gain above threshold, got {last}");
}
}