#![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, Clone, Copy, PartialEq, Eq)]
pub enum SidechainMode {
Peak,
Rms,
StereoMax,
}
#[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,
sidechain_mode: SidechainMode,
rms_accumulator: f32,
rms_count: u32,
rms_window: u32,
rms_level: f32,
}
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;
let rms_window = (config.sample_rate * 0.003).max(1.0) as u32;
Self {
config,
envelope: 0.0,
gain: 1.0,
attack_coeff,
release_coeff,
threshold_linear,
duck_gain,
hold_counter: 0,
hold_samples,
sidechain_mode: SidechainMode::Peak,
rms_accumulator: 0.0,
rms_count: 0,
rms_window,
rms_level: 0.0,
}
}
pub fn set_sidechain_mode(&mut self, mode: SidechainMode) {
self.sidechain_mode = mode;
}
#[must_use]
pub fn sidechain_mode(&self) -> SidechainMode {
self.sidechain_mode
}
fn detect_level(&mut self, sidechain_abs: f32) -> f32 {
match self.sidechain_mode {
SidechainMode::Peak | SidechainMode::StereoMax => sidechain_abs,
SidechainMode::Rms => {
self.rms_accumulator += sidechain_abs * sidechain_abs;
self.rms_count += 1;
if self.rms_count >= self.rms_window {
self.rms_level = (self.rms_accumulator / self.rms_window as f32).sqrt();
self.rms_accumulator = 0.0;
self.rms_count = 0;
}
self.rms_level
}
}
}
pub fn process_sample(&mut self, sidechain_abs: f32) -> f32 {
let level = self.detect_level(sidechain_abs);
if level > self.envelope {
self.envelope = self.attack_coeff * self.envelope + (1.0 - self.attack_coeff) * level;
} else {
self.envelope = self.release_coeff * self.envelope + (1.0 - self.release_coeff) * level;
}
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 process_stereo_sidechain(
&mut self,
music_l: &mut [f32],
music_r: &mut [f32],
sidechain_l: &[f32],
sidechain_r: &[f32],
) {
let len = music_l
.len()
.min(music_r.len())
.min(sidechain_l.len())
.min(sidechain_r.len());
for i in 0..len {
let sc_abs = sidechain_l[i].abs().max(sidechain_r[i].abs());
let gain = self.process_sample(sc_abs);
music_l[i] *= gain;
music_r[i] *= gain;
}
}
pub fn reset(&mut self) {
self.envelope = 0.0;
self.gain = 1.0;
self.hold_counter = 0;
self.rms_accumulator = 0.0;
self.rms_count = 0;
self.rms_level = 0.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;
self.rms_window = (sr * 0.003).max(1.0) 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);
}
#[test]
fn test_sidechain_mode_rms() {
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);
ducker.set_sidechain_mode(SidechainMode::Rms);
assert_eq!(ducker.sidechain_mode(), SidechainMode::Rms);
for _ in 0..4800 {
ducker.process_sample(0.9);
}
assert!(ducker.current_gain() < 0.5);
}
#[test]
fn test_stereo_sidechain_ducking() {
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);
let mut music_l = vec![1.0f32; 4800];
let mut music_r = vec![1.0f32; 4800];
let sc_l = vec![0.9f32; 4800]; let sc_r = vec![0.0f32; 4800];
ducker.process_stereo_sidechain(&mut music_l, &mut music_r, &sc_l, &sc_r);
let last_l = music_l[4799];
let last_r = music_r[4799];
assert!(last_l < 0.5, "Left should be ducked, got {last_l}");
assert!(last_r < 0.5, "Right should be ducked, got {last_r}");
assert!(
(last_l - last_r).abs() < 1e-5,
"Stereo image should be preserved"
);
}
#[test]
fn test_stereo_sidechain_no_trigger() {
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);
let mut music_l = vec![1.0f32; 100];
let mut music_r = vec![1.0f32; 100];
let sc_l = vec![0.0f32; 100];
let sc_r = vec![0.0f32; 100];
ducker.process_stereo_sidechain(&mut music_l, &mut music_r, &sc_l, &sc_r);
for &s in &music_l {
assert!(s > 0.99);
}
for &s in &music_r {
assert!(s > 0.99);
}
}
}