#![allow(dead_code)]
#![allow(clippy::cast_precision_loss)]
#[derive(Debug, Clone)]
pub struct CompressorConfig {
pub threshold_db: f32,
pub ratio: f32,
pub attack_ms: f32,
pub release_ms: f32,
pub knee_db: f32,
pub makeup_gain_db: f32,
}
impl CompressorConfig {
#[must_use]
pub fn standard() -> Self {
Self {
threshold_db: -18.0,
ratio: 4.0,
attack_ms: 10.0,
release_ms: 100.0,
knee_db: 6.0,
makeup_gain_db: 3.0,
}
}
#[must_use]
pub fn limiting() -> Self {
Self {
threshold_db: -3.0,
ratio: 100.0,
attack_ms: 0.1,
release_ms: 50.0,
knee_db: 0.0,
makeup_gain_db: 0.0,
}
}
#[must_use]
pub fn vocal() -> Self {
Self {
threshold_db: -20.0,
ratio: 3.0,
attack_ms: 5.0,
release_ms: 80.0,
knee_db: 8.0,
makeup_gain_db: 4.0,
}
}
}
impl Default for CompressorConfig {
fn default() -> Self {
Self::standard()
}
}
pub struct LevelDetector {
pub peak_level: f32,
}
impl LevelDetector {
#[must_use]
pub fn new() -> Self {
Self { peak_level: 0.0 }
}
pub fn process(&mut self, x: f32, attack: f32, release: f32) -> f32 {
let input_level = x.abs();
if input_level > self.peak_level {
self.peak_level += attack * (input_level - self.peak_level);
} else {
self.peak_level += release * (input_level - self.peak_level);
}
self.peak_level
}
pub fn reset(&mut self) {
self.peak_level = 0.0;
}
}
impl Default for LevelDetector {
fn default() -> Self {
Self::new()
}
}
pub struct GainComputerState {
pub last_gain_reduction_db: f32,
}
impl GainComputerState {
#[must_use]
pub fn new() -> Self {
Self {
last_gain_reduction_db: 0.0,
}
}
pub fn compute_gain(&mut self, input_db: f32, config: &CompressorConfig) -> f32 {
let threshold = config.threshold_db;
let ratio = config.ratio;
let knee = config.knee_db;
let half_knee = knee / 2.0;
let gain_reduction_db =
if knee > 0.0 && input_db >= threshold - half_knee && input_db <= threshold + half_knee
{
let knee_input = input_db - threshold + half_knee;
let knee_factor = knee_input / knee;
(1.0 / ratio - 1.0) * (knee_factor * knee_input) / 2.0
} else if input_db > threshold + half_knee {
(input_db - threshold) * (1.0 / ratio - 1.0)
} else {
0.0
};
self.last_gain_reduction_db = gain_reduction_db;
gain_reduction_db
}
}
impl Default for GainComputerState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct GainReduction {
pub peak_db: f32,
pub rms_db: f32,
accumulator: f32,
sample_count: u32,
window_size: u32,
}
impl GainReduction {
#[must_use]
pub fn new(window_size: u32) -> Self {
Self {
window_size,
..Default::default()
}
}
pub fn update(&mut self, reduction_db: f32) {
let abs_reduction = reduction_db.abs();
if abs_reduction > self.peak_db {
self.peak_db = abs_reduction;
}
self.accumulator += abs_reduction * abs_reduction;
self.sample_count += 1;
if self.sample_count >= self.window_size {
self.rms_db = (self.accumulator / self.window_size as f32).sqrt();
self.accumulator = 0.0;
self.sample_count = 0;
}
}
pub fn reset_peak(&mut self) {
self.peak_db = 0.0;
}
}
pub struct Compressor {
config: CompressorConfig,
detector: LevelDetector,
gain_computer: GainComputerState,
gain_reduction_linear: f32,
pub gain_reduction: GainReduction,
smoothed_gr_db: f32,
}
impl Compressor {
#[must_use]
pub fn new(config: CompressorConfig, _sample_rate: u32) -> Self {
Self {
config,
detector: LevelDetector::new(),
gain_computer: GainComputerState::new(),
gain_reduction_linear: 1.0,
gain_reduction: GainReduction::new(4800),
smoothed_gr_db: 0.0,
}
}
fn db_to_linear(db: f32) -> f32 {
10.0_f32.powf(db / 20.0)
}
fn linear_to_db(linear: f32) -> f32 {
20.0 * linear.max(1e-10_f32).log10()
}
fn attack_coeff(attack_ms: f32, sample_rate: u32) -> f32 {
let attack_samples = attack_ms * sample_rate as f32 / 1000.0;
if attack_samples > 0.0 {
1.0 - (-2.2_f32 / attack_samples).exp()
} else {
1.0
}
}
fn release_coeff(release_ms: f32, sample_rate: u32) -> f32 {
let release_samples = release_ms * sample_rate as f32 / 1000.0;
if release_samples > 0.0 {
1.0 - (-2.2_f32 / release_samples).exp()
} else {
1.0
}
}
#[must_use]
pub fn process(&mut self, samples: &[f32], sample_rate: u32) -> Vec<f32> {
let attack = Self::attack_coeff(self.config.attack_ms, sample_rate);
let release = Self::release_coeff(self.config.release_ms, sample_rate);
let makeup = Self::db_to_linear(self.config.makeup_gain_db);
let gr_attack = Self::attack_coeff(self.config.attack_ms, sample_rate);
let gr_release = Self::release_coeff(self.config.release_ms, sample_rate);
samples
.iter()
.map(|&x| {
let level = self.detector.process(x, attack, release);
let level_db = Self::linear_to_db(level);
let gr_db = self.gain_computer.compute_gain(level_db, &self.config);
if gr_db < self.smoothed_gr_db {
self.smoothed_gr_db += gr_attack * (gr_db - self.smoothed_gr_db);
} else {
self.smoothed_gr_db += gr_release * (gr_db - self.smoothed_gr_db);
}
self.gain_reduction_linear = Self::db_to_linear(self.smoothed_gr_db);
self.gain_reduction.update(self.smoothed_gr_db);
x * self.gain_reduction_linear * makeup
})
.collect()
}
pub fn reset(&mut self) {
self.detector.reset();
self.gain_reduction_linear = 1.0;
self.smoothed_gr_db = 0.0;
}
#[must_use]
pub fn current_gain_reduction_db(&self) -> f32 {
-self.smoothed_gr_db
}
}
pub struct Expander {
pub threshold_db: f32,
pub ratio: f32,
pub attack_ms: f32,
pub release_ms: f32,
pub knee_db: f32,
detector: LevelDetector,
smoothed_gain_db: f32,
}
impl Expander {
#[must_use]
pub fn new(
threshold_db: f32,
ratio: f32,
attack_ms: f32,
release_ms: f32,
knee_db: f32,
) -> Self {
Self {
threshold_db,
ratio,
attack_ms,
release_ms,
knee_db,
detector: LevelDetector::new(),
smoothed_gain_db: 0.0,
}
}
#[must_use]
pub fn gate() -> Self {
Self::new(-40.0, 10.0, 1.0, 50.0, 4.0)
}
fn db_to_linear(db: f32) -> f32 {
10.0_f32.powf(db / 20.0)
}
fn linear_to_db(linear: f32) -> f32 {
20.0 * linear.max(1e-10_f32).log10()
}
fn attack_coeff(attack_ms: f32, sample_rate: u32) -> f32 {
let s = attack_ms * sample_rate as f32 / 1000.0;
if s > 0.0 {
1.0 - (-2.2_f32 / s).exp()
} else {
1.0
}
}
fn release_coeff(release_ms: f32, sample_rate: u32) -> f32 {
let s = release_ms * sample_rate as f32 / 1000.0;
if s > 0.0 {
1.0 - (-2.2_f32 / s).exp()
} else {
1.0
}
}
fn compute_expansion_gain(&self, input_db: f32) -> f32 {
let threshold = self.threshold_db;
let ratio = self.ratio;
let half_knee = self.knee_db / 2.0;
if input_db < threshold - half_knee {
(threshold - input_db) * (1.0 - ratio)
} else if input_db <= threshold + half_knee && self.knee_db > 0.0 {
let knee_input = input_db - threshold + half_knee;
(1.0 - ratio) * (knee_input - self.knee_db) * (knee_input - self.knee_db)
/ (2.0 * self.knee_db)
} else {
0.0
}
}
#[must_use]
pub fn process(&mut self, samples: &[f32], sample_rate: u32) -> Vec<f32> {
let attack = Self::attack_coeff(self.attack_ms, sample_rate);
let release = Self::release_coeff(self.release_ms, sample_rate);
samples
.iter()
.map(|&x| {
let level = self.detector.process(x, attack, release);
let level_db = Self::linear_to_db(level);
let gain_db = self.compute_expansion_gain(level_db);
if gain_db < self.smoothed_gain_db {
self.smoothed_gain_db += attack * (gain_db - self.smoothed_gain_db);
} else {
self.smoothed_gain_db += release * (gain_db - self.smoothed_gain_db);
}
x * Self::db_to_linear(self.smoothed_gain_db)
})
.collect()
}
pub fn reset(&mut self) {
self.detector.reset();
self.smoothed_gain_db = 0.0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compressor_config_standard() {
let config = CompressorConfig::standard();
assert_eq!(config.ratio, 4.0);
assert!(config.threshold_db < 0.0);
}
#[test]
fn test_compressor_config_limiting() {
let config = CompressorConfig::limiting();
assert_eq!(config.ratio, 100.0);
assert!(config.attack_ms < 1.0);
}
#[test]
fn test_compressor_config_vocal() {
let config = CompressorConfig::vocal();
assert_eq!(config.ratio, 3.0);
}
#[test]
fn test_level_detector_attack() {
let mut det = LevelDetector::new();
for _ in 0..100 {
det.process(1.0, 0.1, 0.01);
}
assert!(det.peak_level > 0.0);
}
#[test]
fn test_level_detector_release() {
let mut det = LevelDetector::new();
det.peak_level = 1.0;
for _ in 0..100 {
det.process(0.0, 0.1, 0.1);
}
assert!(det.peak_level < 0.5);
}
#[test]
fn test_gain_computer_below_threshold() {
let config = CompressorConfig {
threshold_db: -10.0,
knee_db: 0.0,
..CompressorConfig::standard()
};
let mut computer = GainComputerState::new();
let gr = computer.compute_gain(-20.0, &config);
assert!(
gr >= -0.001,
"Expected no reduction below threshold, got {gr}"
);
}
#[test]
fn test_gain_computer_above_threshold() {
let config = CompressorConfig {
threshold_db: -10.0,
ratio: 4.0,
knee_db: 0.0,
..CompressorConfig::standard()
};
let mut computer = GainComputerState::new();
let gr = computer.compute_gain(0.0, &config);
assert!(
gr < 0.0,
"Expected gain reduction above threshold, got {gr}"
);
}
#[test]
fn test_compressor_output_finite() {
let config = CompressorConfig::standard();
let mut comp = Compressor::new(config, 48000);
let input: Vec<f32> = (0..512).map(|i| (i as f32 * 0.01).sin()).collect();
let output = comp.process(&input, 48000);
assert_eq!(output.len(), 512);
assert!(output.iter().all(|&s| s.is_finite()));
}
#[test]
fn test_compressor_reduces_loud_signal() {
let config = CompressorConfig {
threshold_db: -6.0,
ratio: 10.0,
attack_ms: 1.0,
release_ms: 50.0,
knee_db: 0.0,
makeup_gain_db: 0.0,
};
let mut comp = Compressor::new(config, 48000);
let input = vec![0.9f32; 1024];
let output = comp.process(&input, 48000);
let in_rms: f32 = (input.iter().map(|&s| s * s).sum::<f32>() / input.len() as f32).sqrt();
let out_rms: f32 =
(output.iter().map(|&s| s * s).sum::<f32>() / output.len() as f32).sqrt();
assert!(out_rms < in_rms, "Compressor should reduce loud signal");
}
#[test]
fn test_compressor_limiter() {
let config = CompressorConfig::limiting();
let mut comp = Compressor::new(config, 48000);
let input = vec![0.99f32; 2048];
let output = comp.process(&input, 48000);
assert!(output.iter().all(|&s| s.is_finite()));
}
#[test]
fn test_gain_reduction_tracking() {
let mut gr = GainReduction::new(100);
gr.update(3.0);
gr.update(6.0);
assert!(gr.peak_db >= 6.0);
gr.reset_peak();
assert_eq!(gr.peak_db, 0.0);
}
#[test]
fn test_expander_output_finite() {
let mut exp = Expander::gate();
let input: Vec<f32> = (0..512).map(|i| (i as f32 * 0.01).sin() * 0.1).collect();
let output = exp.process(&input, 48000);
assert_eq!(output.len(), 512);
assert!(output.iter().all(|&s| s.is_finite()));
}
#[test]
fn test_expander_attenuates_below_threshold() {
let mut exp = Expander::new(-10.0, 5.0, 1.0, 50.0, 0.0);
let input = vec![0.001f32; 1024];
let output = exp.process(&input, 48000);
let in_rms: f32 = (input.iter().map(|&s| s * s).sum::<f32>() / input.len() as f32).sqrt();
let out_rms: f32 =
(output.iter().map(|&s| s * s).sum::<f32>() / output.len() as f32).sqrt();
assert!(
out_rms <= in_rms + 1e-6,
"Expander should attenuate or not increase quiet signals"
);
}
#[test]
fn test_compressor_reset() {
let config = CompressorConfig::standard();
let mut comp = Compressor::new(config, 48000);
let _ = comp.process(&vec![0.9f32; 512], 48000);
comp.reset();
assert_eq!(comp.smoothed_gr_db, 0.0);
}
}