Skip to main content

alpha2_audio/
ducking.rs

1#![allow(clippy::cast_precision_loss)]
2
3use crate::{ProcessCtx, Processor, StateBlob, StateError};
4
5/// Reusable peak envelope follower.
6#[derive(Copy, Clone, Debug, PartialEq)]
7pub struct EnvelopeFollower {
8    attack_ms: f32,
9    release_ms: f32,
10    attack: f32,
11    release: f32,
12    value: f32,
13}
14
15impl EnvelopeFollower {
16    /// Creates an attack/release envelope follower. Invalid times fall back to 5 ms attack and
17    /// 100 ms release.
18    #[must_use]
19    pub fn new(attack_ms: f32, release_ms: f32) -> Self {
20        Self {
21            attack_ms: positive(attack_ms, 5.0),
22            release_ms: positive(release_ms, 100.0),
23            attack: 0.0,
24            release: 0.0,
25            value: 0.0,
26        }
27    }
28    /// Computes the attack and release coefficients for `sample_rate`. Call before [`Self::next`].
29    pub fn prepare(&mut self, sample_rate: u32) {
30        let rate = sample_rate.max(1) as f32;
31        self.attack = libm::expf(-1.0 / (self.attack_ms * 0.001 * rate));
32        self.release = libm::expf(-1.0 / (self.release_ms * 0.001 * rate));
33    }
34    /// Feeds one sample and returns the updated envelope. Non-finite input counts as silence.
35    pub fn next(&mut self, sample: f32) -> f32 {
36        if !self.value.is_finite() {
37            self.value = 0.0;
38        }
39        let peak = finite_or_zero(sample).abs();
40        let coefficient = if peak > self.value {
41            self.attack
42        } else {
43            self.release
44        };
45        self.value = coefficient * self.value + (1.0 - coefficient) * peak;
46        if self.value.abs() < 1.0e-30 {
47            self.value = 0.0;
48        }
49        self.value
50    }
51    /// Clears the envelope to silence.
52    pub fn reset(&mut self) {
53        self.value = 0.0;
54    }
55}
56
57/// Sidechain ducker. Inputs are program channels followed by matching sidechain channels.
58///
59/// At most two channels are processed; the envelope follows the loudest sidechain channel.
60#[derive(Clone, Debug)]
61pub struct Ducker {
62    follower: EnvelopeFollower,
63    threshold: f32,
64    depth: f32,
65    bypass: bool,
66}
67
68impl Ducker {
69    /// Creates a ducker with linear threshold and maximum attenuation in decibels.
70    ///
71    /// The sign of `depth_db` is ignored and the attenuation is capped at 96 dB. A non-finite or
72    /// non-positive threshold becomes 0.1.
73    #[must_use]
74    pub fn new(threshold: f32, depth_db: f32, attack_ms: f32, release_ms: f32) -> Self {
75        Self {
76            follower: EnvelopeFollower::new(attack_ms, release_ms),
77            threshold: positive(threshold, 0.1),
78            depth: libm::powf(10.0, -depth_db.abs().min(96.0) / 20.0),
79            bypass: false,
80        }
81    }
82    /// Enables transparent bypass while retaining envelope continuity.
83    pub fn set_bypass(&mut self, bypass: bool) {
84        self.bypass = bypass;
85    }
86}
87
88impl Processor for Ducker {
89    /// The program, then the sidechain.
90    fn inputs_per_output(&self) -> usize {
91        2
92    }
93
94    fn history_samples(&self) -> u32 {
95        crate::processor::settling_samples(f64::from(
96            self.follower.attack.max(self.follower.release),
97        ))
98    }
99
100    fn prepare(&mut self, sample_rate: u32, _max_block: usize) {
101        self.follower.prepare(sample_rate);
102    }
103    fn process(&mut self, _ctx: &ProcessCtx, inputs: &[&[f32]], outputs: &mut [&mut [f32]]) {
104        let channels = outputs.len().min(inputs.len() / 2).min(2);
105        let frames = outputs
106            .iter()
107            .take(channels)
108            .map(|v| v.len())
109            .chain(inputs.iter().take(channels * 2).map(|v| v.len()))
110            .min()
111            .unwrap_or(0);
112        for frame in 0..frames {
113            let sidechain = inputs[channels..channels * 2]
114                .iter()
115                .map(|channel| channel[frame].abs())
116                .fold(0.0_f32, f32::max);
117            let envelope = self.follower.next(sidechain);
118            let mix = ((envelope - self.threshold) / self.threshold.max(1.0e-9)).clamp(0.0, 1.0);
119            let gain = if self.bypass {
120                1.0
121            } else {
122                1.0 + (self.depth - 1.0) * mix
123            };
124            for channel in 0..channels {
125                outputs[channel][frame] = finite_or_zero(inputs[channel][frame]) * gain;
126            }
127        }
128    }
129    fn reset(&mut self) {
130        self.follower.reset();
131    }
132    fn save_state(&self) -> StateBlob {
133        StateBlob::new(self.follower.value.to_le_bytes().to_vec())
134    }
135    fn load_state(&mut self, state: &StateBlob) -> Result<(), StateError> {
136        let bytes = <[u8; 4]>::try_from(state.as_bytes()).map_err(|_| StateError::Mismatch)?;
137        let value = f32::from_le_bytes(bytes);
138        if !value.is_finite() || value < 0.0 {
139            return Err(StateError::Invalid);
140        }
141        self.follower.value = value;
142        Ok(())
143    }
144}
145
146fn finite_or_zero(value: f32) -> f32 {
147    if value.is_finite() { value } else { 0.0 }
148}
149
150fn positive(value: f32, fallback: f32) -> f32 {
151    if value.is_finite() && value > 0.0 {
152        value
153    } else {
154        fallback
155    }
156}