#![allow(dead_code)]
#[derive(Debug, Clone)]
pub struct DuckingConfig {
pub threshold_db: f32,
pub duck_amount_db: f32,
pub attack_ms: f32,
pub release_ms: f32,
pub hold_ms: f32,
pub sample_rate: f32,
}
impl Default for DuckingConfig {
fn default() -> Self {
Self {
threshold_db: -30.0,
duck_amount_db: -12.0,
attack_ms: 10.0,
release_ms: 200.0,
hold_ms: 50.0,
sample_rate: 48000.0,
}
}
}
impl DuckingConfig {
#[must_use]
pub fn new(sample_rate: f32) -> Self {
Self {
sample_rate,
..Default::default()
}
}
#[must_use]
pub fn with_threshold(mut self, db: f32) -> Self {
self.threshold_db = db;
self
}
#[must_use]
pub fn with_duck_amount(mut self, db: f32) -> Self {
self.duck_amount_db = db.min(0.0);
self
}
#[must_use]
pub fn with_attack(mut self, ms: f32) -> Self {
self.attack_ms = ms.max(0.1);
self
}
#[must_use]
pub fn with_release(mut self, ms: f32) -> Self {
self.release_ms = ms.max(1.0);
self
}
#[must_use]
pub fn with_hold(mut self, ms: f32) -> Self {
self.hold_ms = ms.max(0.0);
self
}
}
#[allow(clippy::cast_precision_loss)]
fn db_to_linear(db: f32) -> f32 {
10.0f32.powf(db / 20.0)
}
#[allow(clippy::cast_precision_loss)]
fn linear_to_db(lin: f32) -> f32 {
if lin <= 0.0 {
-120.0
} else {
20.0 * lin.log10()
}
}
#[allow(clippy::cast_precision_loss)]
fn time_constant(ms: f32, sample_rate: f32) -> f32 {
if ms <= 0.0 || sample_rate <= 0.0 {
return 1.0;
}
let samples = ms * 0.001 * sample_rate;
(-1.0f32 / samples).exp()
}
#[derive(Debug)]
pub struct Ducker {
config: DuckingConfig,
envelope: f32,
gain: f32,
attack_coeff: f32,
release_coeff: f32,
threshold_linear: f32,
duck_gain: f32,
hold_counter: u32,
hold_samples: u32,
}
impl Ducker {
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
#[must_use]
pub fn new(config: DuckingConfig) -> Self {
let attack_coeff = time_constant(config.attack_ms, config.sample_rate);
let release_coeff = time_constant(config.release_ms, config.sample_rate);
let threshold_linear = db_to_linear(config.threshold_db);
let duck_gain = db_to_linear(config.duck_amount_db);
let hold_samples = (config.hold_ms * 0.001 * config.sample_rate) as u32;
Self {
config,
envelope: 0.0,
gain: 1.0,
attack_coeff,
release_coeff,
threshold_linear,
duck_gain,
hold_counter: 0,
hold_samples,
}
}
pub fn process_sample(&mut self, sidechain_abs: f32) -> f32 {
if sidechain_abs > self.envelope {
self.envelope =
self.attack_coeff * self.envelope + (1.0 - self.attack_coeff) * sidechain_abs;
} else {
self.envelope =
self.release_coeff * self.envelope + (1.0 - self.release_coeff) * sidechain_abs;
}
let target = if self.envelope > self.threshold_linear {
self.hold_counter = self.hold_samples;
self.duck_gain
} else if self.hold_counter > 0 {
self.hold_counter -= 1;
self.duck_gain
} else {
1.0
};
if target < self.gain {
self.gain = self.attack_coeff * self.gain + (1.0 - self.attack_coeff) * target;
} else {
self.gain = self.release_coeff * self.gain + (1.0 - self.release_coeff) * target;
}
self.gain
}
pub fn process_buffers(&mut self, music: &mut [f32], sidechain: &[f32]) {
let len = music.len().min(sidechain.len());
for i in 0..len {
let sc_abs = sidechain[i].abs();
let gain = self.process_sample(sc_abs);
music[i] *= gain;
}
}
pub fn reset(&mut self) {
self.envelope = 0.0;
self.gain = 1.0;
self.hold_counter = 0;
}
#[must_use]
pub fn envelope(&self) -> f32 {
self.envelope
}
#[must_use]
pub fn current_gain(&self) -> f32 {
self.gain
}
#[must_use]
pub fn current_gain_db(&self) -> f32 {
linear_to_db(self.gain)
}
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
pub fn set_sample_rate(&mut self, sr: f32) {
self.config.sample_rate = sr;
self.attack_coeff = time_constant(self.config.attack_ms, sr);
self.release_coeff = time_constant(self.config.release_ms, sr);
self.hold_samples = (self.config.hold_ms * 0.001 * sr) as u32;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_db_to_linear_zero() {
let v = db_to_linear(0.0);
assert!((v - 1.0).abs() < 1e-5);
}
#[test]
fn test_db_to_linear_minus6() {
let v = db_to_linear(-6.0206);
assert!((v - 0.5).abs() < 0.01);
}
#[test]
fn test_linear_to_db_one() {
let v = linear_to_db(1.0);
assert!(v.abs() < 1e-5);
}
#[test]
fn test_linear_to_db_zero() {
let v = linear_to_db(0.0);
assert_eq!(v, -120.0);
}
#[test]
fn test_time_constant_positive() {
let c = time_constant(10.0, 48000.0);
assert!(c > 0.0 && c < 1.0);
}
#[test]
fn test_time_constant_zero_ms() {
let c = time_constant(0.0, 48000.0);
assert!((c - 1.0).abs() < 1e-5);
}
#[test]
fn test_ducker_initial_state() {
let ducker = Ducker::new(DuckingConfig::default());
assert!((ducker.current_gain() - 1.0).abs() < 1e-5);
assert!((ducker.envelope() - 0.0).abs() < 1e-5);
}
#[test]
fn test_ducker_no_sidechain() {
let mut ducker = Ducker::new(DuckingConfig::default());
for _ in 0..1000 {
let g = ducker.process_sample(0.0);
assert!(g > 0.99);
}
}
#[test]
fn test_ducker_with_loud_sidechain() {
let config = DuckingConfig {
threshold_db: -30.0,
duck_amount_db: -12.0,
attack_ms: 1.0,
release_ms: 50.0,
hold_ms: 0.0,
sample_rate: 48000.0,
};
let mut ducker = Ducker::new(config);
for _ in 0..4800 {
ducker.process_sample(0.9);
}
assert!(ducker.current_gain() < 0.5);
}
#[test]
fn test_ducker_release() {
let config = DuckingConfig {
threshold_db: -30.0,
duck_amount_db: -12.0,
attack_ms: 1.0,
release_ms: 10.0,
hold_ms: 0.0,
sample_rate: 48000.0,
};
let mut ducker = Ducker::new(config);
for _ in 0..4800 {
ducker.process_sample(0.9);
}
let ducked_gain = ducker.current_gain();
for _ in 0..48000 {
ducker.process_sample(0.0);
}
assert!(ducker.current_gain() > ducked_gain);
}
#[test]
fn test_process_buffers() {
let mut ducker = Ducker::new(DuckingConfig::default());
let mut music = vec![1.0f32; 100];
let sidechain = vec![0.0f32; 100];
ducker.process_buffers(&mut music, &sidechain);
for &s in &music {
assert!(s > 0.99);
}
}
#[test]
fn test_reset() {
let mut ducker = Ducker::new(DuckingConfig::default());
for _ in 0..1000 {
ducker.process_sample(0.9);
}
ducker.reset();
assert!((ducker.current_gain() - 1.0).abs() < 1e-5);
assert!((ducker.envelope() - 0.0).abs() < 1e-5);
}
#[test]
fn test_config_builder() {
let cfg = DuckingConfig::new(44100.0)
.with_threshold(-20.0)
.with_duck_amount(-6.0)
.with_attack(5.0)
.with_release(100.0)
.with_hold(30.0);
assert!((cfg.sample_rate - 44100.0).abs() < 1e-5);
assert!((cfg.threshold_db - (-20.0)).abs() < 1e-5);
assert!((cfg.duck_amount_db - (-6.0)).abs() < 1e-5);
assert!((cfg.attack_ms - 5.0).abs() < 1e-5);
assert!((cfg.release_ms - 100.0).abs() < 1e-5);
assert!((cfg.hold_ms - 30.0).abs() < 1e-5);
}
#[test]
fn test_set_sample_rate() {
let mut ducker = Ducker::new(DuckingConfig::new(48000.0));
ducker.set_sample_rate(96000.0);
assert!((ducker.config.sample_rate - 96000.0).abs() < 1e-5);
}
}