#![forbid(unsafe_code)]
#![allow(clippy::cast_lossless)]
use std::collections::VecDeque;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum KneeType {
#[default]
Hard,
Soft,
}
#[derive(Clone, Debug)]
pub struct CompressorConfig {
pub threshold_db: f64,
pub ratio: f64,
pub attack_ms: f64,
pub release_ms: f64,
pub knee_type: KneeType,
pub knee_width_db: f64,
pub makeup_gain_db: f64,
pub auto_makeup: bool,
pub lookahead_ms: f64,
}
impl Default for CompressorConfig {
fn default() -> Self {
Self {
threshold_db: -20.0,
ratio: 4.0,
attack_ms: 10.0,
release_ms: 100.0,
knee_type: KneeType::Hard,
knee_width_db: 6.0,
makeup_gain_db: 0.0,
auto_makeup: false,
lookahead_ms: 0.0,
}
}
}
impl CompressorConfig {
#[must_use]
pub fn new(threshold_db: f64, ratio: f64) -> Self {
Self {
threshold_db,
ratio,
..Default::default()
}
}
#[must_use]
pub fn with_timing(mut self, attack_ms: f64, release_ms: f64) -> Self {
self.attack_ms = attack_ms.max(0.1);
self.release_ms = release_ms.max(0.1);
self
}
#[must_use]
pub fn with_soft_knee(mut self, width_db: f64) -> Self {
self.knee_type = KneeType::Soft;
self.knee_width_db = width_db.max(0.0);
self
}
#[must_use]
pub fn with_hard_knee(mut self) -> Self {
self.knee_type = KneeType::Hard;
self
}
#[must_use]
pub fn with_makeup_gain(mut self, gain_db: f64) -> Self {
self.makeup_gain_db = gain_db;
self.auto_makeup = false;
self
}
#[must_use]
pub fn with_auto_makeup(mut self) -> Self {
self.auto_makeup = true;
self
}
#[must_use]
pub fn with_lookahead(mut self, lookahead_ms: f64) -> Self {
self.lookahead_ms = lookahead_ms.max(0.0);
self
}
#[must_use]
pub fn db_to_linear(db: f64) -> f64 {
10.0_f64.powf(db / 20.0)
}
#[must_use]
pub fn linear_to_db(linear: f64) -> f64 {
if linear <= 0.0 {
f64::NEG_INFINITY
} else {
20.0 * linear.log10()
}
}
#[must_use]
pub fn calculate_auto_makeup(&self) -> f64 {
if self.ratio <= 1.0 {
return 0.0;
}
let gain_at_threshold = self.threshold_db - (self.threshold_db / self.ratio);
-gain_at_threshold * 0.5
}
}
struct EnvelopeFollower {
envelope: f64,
attack_coeff: f64,
release_coeff: f64,
}
impl EnvelopeFollower {
fn new(attack_ms: f64, release_ms: f64, sample_rate: f64) -> Self {
let attack_coeff = if attack_ms > 0.0 {
(-1.0 / (attack_ms * 0.001 * sample_rate)).exp()
} else {
0.0
};
let release_coeff = if release_ms > 0.0 {
(-1.0 / (release_ms * 0.001 * sample_rate)).exp()
} else {
0.0
};
Self {
envelope: 0.0,
attack_coeff,
release_coeff,
}
}
fn update(&mut self, input_level: f64) {
if input_level > self.envelope {
self.envelope =
self.attack_coeff * self.envelope + (1.0 - self.attack_coeff) * input_level;
} else {
self.envelope =
self.release_coeff * self.envelope + (1.0 - self.release_coeff) * input_level;
}
}
fn level(&self) -> f64 {
self.envelope
}
fn reset(&mut self) {
self.envelope = 0.0;
}
}
struct LookaheadBuffer {
buffer: VecDeque<f64>,
delay_samples: usize,
}
impl LookaheadBuffer {
fn new(lookahead_ms: f64, sample_rate: f64) -> Self {
let delay_samples = (lookahead_ms * 0.001 * sample_rate) as usize;
Self {
buffer: VecDeque::with_capacity(delay_samples + 1),
delay_samples,
}
}
fn process(&mut self, input: f64) -> f64 {
if self.delay_samples == 0 {
return input;
}
self.buffer.push_back(input);
if self.buffer.len() > self.delay_samples {
self.buffer.pop_front().unwrap_or(0.0)
} else {
0.0
}
}
fn reset(&mut self) {
self.buffer.clear();
}
}
pub struct Compressor {
config: CompressorConfig,
envelope: EnvelopeFollower,
lookahead_buffers: Vec<LookaheadBuffer>,
makeup_gain: f64,
gain_reduction_db: f64,
sample_rate: f64,
channels: usize,
}
impl Compressor {
#[must_use]
pub fn new(config: CompressorConfig, sample_rate: f64, channels: usize) -> Self {
let envelope = EnvelopeFollower::new(config.attack_ms, config.release_ms, sample_rate);
let lookahead_buffers: Vec<_> = (0..channels)
.map(|_| LookaheadBuffer::new(config.lookahead_ms, sample_rate))
.collect();
let makeup_gain = if config.auto_makeup {
CompressorConfig::db_to_linear(config.calculate_auto_makeup())
} else {
CompressorConfig::db_to_linear(config.makeup_gain_db)
};
Self {
config,
envelope,
lookahead_buffers,
makeup_gain,
gain_reduction_db: 0.0,
sample_rate,
channels,
}
}
pub fn set_config(&mut self, config: CompressorConfig) {
self.envelope =
EnvelopeFollower::new(config.attack_ms, config.release_ms, self.sample_rate);
self.makeup_gain = if config.auto_makeup {
CompressorConfig::db_to_linear(config.calculate_auto_makeup())
} else {
CompressorConfig::db_to_linear(config.makeup_gain_db)
};
self.config = config;
}
#[must_use]
pub fn config(&self) -> &CompressorConfig {
&self.config
}
#[must_use]
pub fn gain_reduction_db(&self) -> f64 {
self.gain_reduction_db
}
fn calculate_gain_reduction(&self, input_db: f64) -> f64 {
if input_db < self.config.threshold_db {
return 0.0;
}
match self.config.knee_type {
KneeType::Hard => {
let excess = input_db - self.config.threshold_db;
let compressed_excess = excess / self.config.ratio;
-(excess - compressed_excess)
}
KneeType::Soft => {
let half_knee = self.config.knee_width_db / 2.0;
let knee_start = self.config.threshold_db - half_knee;
let knee_end = self.config.threshold_db + half_knee;
if input_db < knee_start {
0.0
} else if input_db > knee_end {
let excess = input_db - self.config.threshold_db;
let compressed_excess = excess / self.config.ratio;
-(excess - compressed_excess)
} else {
let x = input_db - knee_start;
let knee_factor = x / self.config.knee_width_db;
let ratio_blend = 1.0 + (self.config.ratio - 1.0) * knee_factor;
let excess = x;
let compressed = excess / ratio_blend;
-(excess - compressed) * knee_factor
}
}
}
}
pub fn process_interleaved(&mut self, samples: &mut [f64], num_samples: usize) {
for i in 0..num_samples {
let mut peak = 0.0_f64;
for ch in 0..self.channels {
let idx = i * self.channels + ch;
if idx < samples.len() {
peak = peak.max(samples[idx].abs());
}
}
self.envelope.update(peak);
let envelope_db = CompressorConfig::linear_to_db(self.envelope.level());
self.gain_reduction_db = self.calculate_gain_reduction(envelope_db);
let gain = CompressorConfig::db_to_linear(self.gain_reduction_db) * self.makeup_gain;
for ch in 0..self.channels {
let idx = i * self.channels + ch;
if idx < samples.len() && ch < self.lookahead_buffers.len() {
let delayed = self.lookahead_buffers[ch].process(samples[idx]);
samples[idx] = delayed * gain;
}
}
}
}
pub fn process_planar(&mut self, channels: &mut [Vec<f64>]) {
if channels.is_empty() {
return;
}
let num_samples = channels[0].len();
for i in 0..num_samples {
let mut peak = 0.0_f64;
for channel in channels.iter() {
if i < channel.len() {
peak = peak.max(channel[i].abs());
}
}
self.envelope.update(peak);
let envelope_db = CompressorConfig::linear_to_db(self.envelope.level());
self.gain_reduction_db = self.calculate_gain_reduction(envelope_db);
let gain = CompressorConfig::db_to_linear(self.gain_reduction_db) * self.makeup_gain;
for (ch, channel) in channels.iter_mut().enumerate() {
if i < channel.len() && ch < self.lookahead_buffers.len() {
let delayed = self.lookahead_buffers[ch].process(channel[i]);
channel[i] = delayed * gain;
}
}
}
}
pub fn process_channel(&mut self, channel: usize, samples: &mut [f64]) {
if channel >= self.channels {
return;
}
for sample in samples.iter_mut() {
let peak = sample.abs();
self.envelope.update(peak);
let envelope_db = CompressorConfig::linear_to_db(self.envelope.level());
self.gain_reduction_db = self.calculate_gain_reduction(envelope_db);
let gain = CompressorConfig::db_to_linear(self.gain_reduction_db) * self.makeup_gain;
if channel < self.lookahead_buffers.len() {
let delayed = self.lookahead_buffers[channel].process(*sample);
*sample = delayed * gain;
}
}
}
pub fn reset(&mut self) {
self.envelope.reset();
for buffer in &mut self.lookahead_buffers {
buffer.reset();
}
self.gain_reduction_db = 0.0;
}
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_RATE: f64 = 48000.0;
#[test]
fn test_compressor_config_default() {
let config = CompressorConfig::default();
assert_eq!(config.threshold_db, -20.0);
assert_eq!(config.ratio, 4.0);
assert_eq!(config.attack_ms, 10.0);
assert_eq!(config.release_ms, 100.0);
assert_eq!(config.knee_type, KneeType::Hard);
}
#[test]
fn test_compressor_config_new() {
let config = CompressorConfig::new(-12.0, 8.0);
assert_eq!(config.threshold_db, -12.0);
assert_eq!(config.ratio, 8.0);
}
#[test]
fn test_compressor_config_with_timing() {
let config = CompressorConfig::new(-20.0, 4.0).with_timing(5.0, 200.0);
assert_eq!(config.attack_ms, 5.0);
assert_eq!(config.release_ms, 200.0);
}
#[test]
fn test_compressor_config_with_soft_knee() {
let config = CompressorConfig::new(-20.0, 4.0).with_soft_knee(6.0);
assert_eq!(config.knee_type, KneeType::Soft);
assert_eq!(config.knee_width_db, 6.0);
}
#[test]
fn test_compressor_config_with_hard_knee() {
let config = CompressorConfig::new(-20.0, 4.0)
.with_soft_knee(6.0)
.with_hard_knee();
assert_eq!(config.knee_type, KneeType::Hard);
}
#[test]
fn test_compressor_config_with_makeup_gain() {
let config = CompressorConfig::new(-20.0, 4.0).with_makeup_gain(6.0);
assert_eq!(config.makeup_gain_db, 6.0);
assert!(!config.auto_makeup);
}
#[test]
fn test_compressor_config_with_auto_makeup() {
let config = CompressorConfig::new(-20.0, 4.0).with_auto_makeup();
assert!(config.auto_makeup);
}
#[test]
fn test_db_to_linear_and_back() {
let db = -6.0;
let linear = CompressorConfig::db_to_linear(db);
let back = CompressorConfig::linear_to_db(linear);
assert!((back - db).abs() < 1e-10);
}
#[test]
fn test_linear_to_db_zero() {
let result = CompressorConfig::linear_to_db(0.0);
assert_eq!(result, f64::NEG_INFINITY);
}
#[test]
fn test_calculate_auto_makeup() {
let config = CompressorConfig::new(-20.0, 4.0);
let makeup = config.calculate_auto_makeup();
assert!(makeup > 0.0);
}
#[test]
fn test_compressor_new() {
let config = CompressorConfig::default();
let comp = Compressor::new(config, SAMPLE_RATE, 2);
assert_eq!(comp.gain_reduction_db(), 0.0);
}
#[test]
fn test_compressor_no_reduction_below_threshold() {
let config = CompressorConfig::new(-20.0, 4.0).with_timing(1.0, 100.0);
let mut comp = Compressor::new(config, SAMPLE_RATE, 1);
let mut samples = vec![0.001_f64; 100];
comp.process_channel(0, &mut samples);
assert!(
comp.gain_reduction_db() > -1.0,
"Should have minimal gain reduction below threshold"
);
}
#[test]
fn test_compressor_reduces_loud_signal() {
let config = CompressorConfig::new(-40.0, 8.0).with_timing(1.0, 100.0);
let mut comp = Compressor::new(config, SAMPLE_RATE, 1);
let mut samples = vec![1.0_f64; 2000];
comp.process_interleaved(&mut samples, 2000);
assert!(
comp.gain_reduction_db() < -3.0,
"Should have gain reduction, got {}",
comp.gain_reduction_db()
);
}
#[test]
fn test_compressor_process_planar() {
let config = CompressorConfig::new(-20.0, 4.0);
let mut comp = Compressor::new(config, SAMPLE_RATE, 2);
let mut channels = vec![vec![0.5_f64; 256]; 2];
comp.process_planar(&mut channels);
for ch in &channels {
for s in ch {
assert!(s.is_finite());
}
}
}
#[test]
fn test_compressor_reset() {
let config = CompressorConfig::new(-20.0, 4.0);
let mut comp = Compressor::new(config, SAMPLE_RATE, 1);
let mut samples = vec![1.0_f64; 1000];
comp.process_channel(0, &mut samples);
comp.reset();
assert_eq!(comp.gain_reduction_db(), 0.0);
}
#[test]
fn test_compressor_set_config() {
let config = CompressorConfig::new(-20.0, 4.0);
let mut comp = Compressor::new(config, SAMPLE_RATE, 1);
let new_config = CompressorConfig::new(-10.0, 2.0);
comp.set_config(new_config);
assert_eq!(comp.config().threshold_db, -10.0);
assert_eq!(comp.config().ratio, 2.0);
}
#[test]
fn test_compressor_soft_knee() {
let config = CompressorConfig::new(-20.0, 4.0)
.with_soft_knee(6.0)
.with_timing(1.0, 50.0);
let mut comp = Compressor::new(config, SAMPLE_RATE, 1);
let mut samples = vec![0.8_f64; 1000];
comp.process_channel(0, &mut samples);
for s in &samples {
assert!(s.is_finite());
}
}
#[test]
fn test_compressor_with_lookahead() {
let config = CompressorConfig::new(-20.0, 4.0).with_lookahead(5.0);
let mut comp = Compressor::new(config, SAMPLE_RATE, 1);
let mut samples = vec![0.5_f64; 500];
comp.process_channel(0, &mut samples);
for s in &samples {
assert!(s.is_finite());
}
}
#[test]
fn test_compressor_with_auto_makeup() {
let config = CompressorConfig::new(-20.0, 4.0).with_auto_makeup();
let mut comp = Compressor::new(config, SAMPLE_RATE, 2);
let mut samples = vec![0.5_f64; 500];
comp.process_channel(0, &mut samples);
for s in &samples {
assert!(s.is_finite());
}
}
}