use serde::{Deserialize, Serialize};
use tracing::debug;
use crate::dsp_util;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LevelDetector {
current: f32,
attack_coeff: f32,
release_coeff: f32,
}
impl LevelDetector {
#[must_use]
pub fn new(attack: f32, release: f32, sample_rate: f32) -> Self {
Self {
current: 0.0,
attack_coeff: Self::time_to_coeff(attack, sample_rate),
release_coeff: Self::time_to_coeff(release, sample_rate),
}
}
fn time_to_coeff(time: f32, sample_rate: f32) -> f32 {
if time <= 0.0 {
1.0
} else {
1.0 - (-1.0 / (time * sample_rate)).exp()
}
}
#[inline]
#[must_use]
pub fn process(&mut self, input: f32) -> f32 {
let level = if input.is_finite() { input.abs() } else { 0.0 };
let coeff = if level > self.current {
self.attack_coeff
} else {
self.release_coeff
};
self.current += coeff * (level - self.current);
self.current = crate::flush_denormal(self.current);
self.current
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Compressor {
pub threshold_db: f32,
ratio: f32,
pub knee_db: f32,
pub makeup_db: f32,
detector: LevelDetector,
}
impl Compressor {
#[must_use]
pub fn new(threshold_db: f32, ratio: f32, attack: f32, release: f32, sample_rate: f32) -> Self {
debug!(threshold_db, ratio, attack, release, "compressor created");
Self {
threshold_db,
ratio: ratio.max(1.0),
knee_db: 0.0,
makeup_db: 0.0,
detector: LevelDetector::new(attack, release, sample_rate),
}
}
#[must_use]
pub fn ratio(&self) -> f32 {
self.ratio
}
pub fn set_ratio(&mut self, ratio: f32) {
self.ratio = ratio.max(1.0);
}
#[inline]
fn compute_gain_db(&self, input_db: f32) -> f32 {
let t = self.threshold_db;
let r = self.ratio;
let k = self.knee_db;
if k <= 0.0 || (input_db - t).abs() > k * 0.5 {
if input_db <= t {
0.0
} else {
(t + (input_db - t) / r) - input_db
}
} else {
let x = input_db - t + k * 0.5;
(1.0 / r - 1.0) * x * x / (2.0 * k)
}
}
#[inline]
#[must_use]
pub fn process_sample(&mut self, input: f32) -> f32 {
let env = self.detector.process(input);
let env_db = dsp_util::amplitude_to_db(env);
let gain_db = self.compute_gain_db(env_db) + self.makeup_db;
input * dsp_util::db_to_amplitude(gain_db)
}
#[inline]
pub fn process_buffer(&mut self, buffer: &mut [f32]) {
for s in buffer.iter_mut() {
*s = self.process_sample(*s);
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Limiter {
ceiling_db: f32,
release: f32,
compressor: Compressor,
}
impl Limiter {
#[must_use]
pub fn new(ceiling_db: f32, release: f32, sample_rate: f32) -> Self {
let mut comp = Compressor::new(ceiling_db, f32::MAX, 0.0001, release, sample_rate);
comp.knee_db = 0.0;
Self {
ceiling_db,
release,
compressor: comp,
}
}
#[must_use]
pub fn ceiling_db(&self) -> f32 {
self.ceiling_db
}
pub fn set_ceiling_db(&mut self, ceiling_db: f32) {
self.ceiling_db = ceiling_db;
self.compressor.threshold_db = ceiling_db;
}
#[must_use]
pub fn release(&self) -> f32 {
self.release
}
#[inline]
#[must_use]
pub fn process_sample(&mut self, input: f32) -> f32 {
self.compressor.process_sample(input)
}
#[inline]
pub fn process_buffer(&mut self, buffer: &mut [f32]) {
for s in buffer.iter_mut() {
*s = self.process_sample(*s);
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NoiseGate {
pub threshold_db: f32,
detector: LevelDetector,
gate_gain: f32,
hold_counter: u32,
hold_samples: u32,
attack_coeff: f32,
release_coeff: f32,
}
impl NoiseGate {
#[must_use]
pub fn new(threshold_db: f32, attack: f32, hold: f32, release: f32, sample_rate: f32) -> Self {
let attack_time = attack.max(0.001); Self {
threshold_db,
detector: LevelDetector::new(attack, release, sample_rate),
gate_gain: 0.0,
hold_counter: 0,
hold_samples: (hold * sample_rate) as u32,
attack_coeff: 1.0 - (-1.0 / (attack_time * sample_rate)).exp(),
release_coeff: if release > 0.0 {
1.0 - (-1.0 / (release * sample_rate)).exp()
} else {
1.0
},
}
}
#[inline]
#[must_use]
pub fn process_sample(&mut self, input: f32) -> f32 {
let env = self.detector.process(input);
let env_db = dsp_util::amplitude_to_db(env);
let target = if env_db >= self.threshold_db {
self.hold_counter = self.hold_samples;
1.0
} else if self.hold_counter > 0 {
self.hold_counter -= 1;
1.0
} else {
0.0
};
let coeff = if target > self.gate_gain {
self.attack_coeff
} else {
self.release_coeff
};
self.gate_gain += coeff * (target - self.gate_gain);
self.gate_gain = crate::flush_denormal(self.gate_gain);
input * self.gate_gain
}
#[inline]
pub fn process_buffer(&mut self, buffer: &mut [f32]) {
for s in buffer.iter_mut() {
*s = self.process_sample(*s);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_envelope_detector() {
let mut det = LevelDetector::new(0.001, 0.01, 44100.0);
for _ in 0..1000 {
let _ = det.process(1.0);
}
assert!(det.current > 0.9, "detector should track input");
for _ in 0..10000 {
let _ = det.process(0.0);
}
assert!(det.current < 0.01, "detector should release");
}
#[test]
fn test_compressor_below_threshold() {
let mut comp = Compressor::new(-10.0, 4.0, 0.001, 0.01, 44100.0);
let out = comp.process_sample(0.01);
assert!(out.is_finite());
}
#[test]
fn test_compressor_reduces_loud() {
let mut comp = Compressor::new(-20.0, 4.0, 0.0, 0.01, 44100.0);
for _ in 0..1000 {
let _ = comp.process_sample(1.0);
}
let out = comp.process_sample(1.0);
assert!(
out < 1.0,
"compressor should reduce loud signals, got {out}"
);
}
#[test]
fn test_compressor_soft_knee() {
let mut comp = Compressor::new(-20.0, 4.0, 0.001, 0.01, 44100.0);
comp.knee_db = 6.0;
let gain = comp.compute_gain_db(-17.0); assert!(gain < 0.0, "soft knee should apply some reduction");
assert!(gain > -3.0, "soft knee reduction should be gentle");
}
#[test]
fn test_limiter() {
let mut lim = Limiter::new(-0.1, 0.01, 44100.0);
for _ in 0..1000 {
let _ = lim.process_sample(2.0);
}
let out = lim.process_sample(2.0);
assert!(out < 2.0, "limiter should reduce signal");
}
#[test]
fn test_noise_gate_silences() {
let mut gate = NoiseGate::new(-40.0, 0.001, 0.01, 0.01, 44100.0);
for _ in 0..10000 {
let _ = gate.process_sample(0.001);
}
let out = gate.process_sample(0.001);
assert!(
out.abs() < 0.002,
"gate should attenuate quiet signal, got {out}"
);
}
#[test]
fn test_noise_gate_passes_loud() {
let mut gate = NoiseGate::new(-40.0, 0.0, 0.01, 0.01, 44100.0);
for _ in 0..2000 {
let _ = gate.process_sample(0.5);
}
let out = gate.process_sample(0.5);
assert!(out > 0.3, "gate should pass loud signal, got {out}");
}
#[test]
fn test_serde_roundtrip_compressor() {
let comp = Compressor::new(-20.0, 4.0, 0.01, 0.1, 44100.0);
let json = serde_json::to_string(&comp).unwrap();
let back: Compressor = serde_json::from_str(&json).unwrap();
assert!((comp.threshold_db - back.threshold_db).abs() < f32::EPSILON);
}
#[test]
fn test_serde_roundtrip_limiter() {
let lim = Limiter::new(-0.1, 0.01, 44100.0);
let json = serde_json::to_string(&lim).unwrap();
let back: Limiter = serde_json::from_str(&json).unwrap();
assert!((lim.ceiling_db - back.ceiling_db).abs() < f32::EPSILON);
}
#[test]
fn test_serde_roundtrip_gate() {
let gate = NoiseGate::new(-40.0, 0.001, 0.01, 0.05, 44100.0);
let json = serde_json::to_string(&gate).unwrap();
let back: NoiseGate = serde_json::from_str(&json).unwrap();
assert!((gate.threshold_db - back.threshold_db).abs() < f32::EPSILON);
}
#[test]
fn test_compressor_ratio_one_is_unity() {
let mut comp = Compressor::new(-20.0, 1.0, 0.0, 0.01, 44100.0);
for _ in 0..2000 {
let _ = comp.process_sample(1.0);
}
let out = comp.process_sample(1.0);
assert!(
(out - 1.0).abs() < 1e-3,
"ratio=1.0 must be unity gain, got {out}"
);
}
#[test]
fn test_noise_gate_hold_timer_keeps_gate_open() {
let sr = 44100.0;
let hold = 0.1; let mut gate = NoiseGate::new(-10.0, 0.0, hold, 0.001, sr);
for _ in 0..1000 {
let _ = gate.process_sample(1.0);
}
for _ in 0..500 {
let _ = gate.process_sample(0.05);
}
let mid_hold = gate.process_sample(0.05);
assert!(
mid_hold > 0.04,
"gate must remain open during hold window, got {mid_hold}"
);
let hold_samples = (hold * sr) as usize;
for _ in 0..hold_samples + 5000 {
let _ = gate.process_sample(0.05);
}
let after_hold = gate.process_sample(0.05);
assert!(
after_hold < 0.005,
"gate must close after hold expires, got {after_hold}"
);
}
#[test]
fn test_limiter_ceiling_exact_match_passes_through() {
let ceiling_db = -3.0;
let amp_at_ceiling = 10f32.powf(ceiling_db / 20.0);
let mut lim = Limiter::new(ceiling_db, 0.01, 44100.0);
for _ in 0..2000 {
let _ = lim.process_sample(amp_at_ceiling);
}
let out_at = lim.process_sample(amp_at_ceiling);
assert!(
(out_at - amp_at_ceiling).abs() < 1e-3,
"input at ceiling must pass through, got {out_at} (expected {amp_at_ceiling})"
);
let above = amp_at_ceiling * 2.0;
for _ in 0..2000 {
let _ = lim.process_sample(above);
}
let out_above = lim.process_sample(above);
assert!(
out_above < above,
"input above ceiling must be reduced, got {out_above} (input {above})"
);
}
}