Skip to main content

sim_lib_audio_dsp/
dynamics.rs

1use sim_lib_audio_graph_core::{PrepareConfig, ProcessBlock, Processor};
2
3use crate::common::{
4    db_to_gain, gain_to_db, input_sample, output_channels, prepare_channels,
5    prepared_output_channels,
6};
7
8/// A peak envelope follower with separate attack and release time constants.
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct DynamicsEnvelope {
11    value: f32,
12    attack_coeff: f32,
13    release_coeff: f32,
14}
15
16impl DynamicsEnvelope {
17    /// Creates an envelope follower from attack and release times in
18    /// milliseconds at the given sample rate.
19    pub fn new(sample_rate_hz: f32, attack_ms: f32, release_ms: f32) -> Self {
20        Self {
21            value: 0.0,
22            attack_coeff: time_coeff(sample_rate_hz, attack_ms),
23            release_coeff: time_coeff(sample_rate_hz, release_ms),
24        }
25    }
26
27    /// Advances the envelope with one input sample and returns the new level.
28    pub fn next(&mut self, input: f32) -> f32 {
29        let level = input.abs();
30        let coeff = if level > self.value {
31            self.attack_coeff
32        } else {
33            self.release_coeff
34        };
35        self.value = coeff * self.value + (1.0 - coeff) * level;
36        self.value
37    }
38
39    /// Resets the envelope to silence.
40    pub fn reset(&mut self) {
41        self.value = 0.0;
42    }
43}
44
45fn time_coeff(sample_rate_hz: f32, time_ms: f32) -> f32 {
46    let samples = (sample_rate_hz * time_ms.max(0.001)) / 1000.0;
47    (-1.0 / samples).exp()
48}
49
50/// Nonlinear transfer curve used by [`Waveshaper`].
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum Waveshape {
53    /// Hyperbolic-tangent soft saturation.
54    Tanh,
55    /// Cubic soft saturation.
56    Cubic,
57    /// Hard clip to `-1.0..=1.0`.
58    HardClip,
59}
60
61/// A [`Processor`] applying a nonlinear waveshaping curve with drive and output
62/// gain.
63#[derive(Clone, Debug, PartialEq)]
64pub struct Waveshaper {
65    drive: f32,
66    output_gain: f32,
67    shape: Waveshape,
68}
69
70impl Waveshaper {
71    /// Creates a waveshaper with the given curve and drive (clamped to `>= 0`).
72    pub fn new(shape: Waveshape, drive: f32) -> Self {
73        Self {
74            drive: drive.max(0.0),
75            output_gain: 1.0,
76            shape,
77        }
78    }
79
80    /// Returns the waveshaper with its post-curve output gain set.
81    pub fn with_output_gain(mut self, output_gain: f32) -> Self {
82        self.output_gain = output_gain;
83        self
84    }
85
86    /// Applies the drive, curve, and output gain to one sample.
87    pub fn process_sample(&self, input: f32) -> f32 {
88        let x = input * self.drive;
89        let shaped = match self.shape {
90            Waveshape::Tanh => x.tanh(),
91            Waveshape::Cubic => (x - x.powi(3) / 3.0).clamp(-1.0, 1.0),
92            Waveshape::HardClip => x.clamp(-1.0, 1.0),
93        };
94        shaped * self.output_gain
95    }
96}
97
98impl Processor for Waveshaper {
99    fn prepare(&mut self, _cfg: PrepareConfig) {}
100
101    fn reset(&mut self) {}
102
103    fn process(&mut self, block: &mut ProcessBlock<'_>) {
104        let frames = block.frames as usize;
105        for channel in 0..output_channels(block) {
106            for frame in 0..frames {
107                block.out_audio[channel][frame] =
108                    self.process_sample(input_sample(block, channel, frame));
109            }
110        }
111    }
112}
113
114/// A [`Processor`] soft-clipping via a tanh [`Waveshaper`].
115#[derive(Clone, Debug, PartialEq)]
116pub struct SoftClipper {
117    inner: Waveshaper,
118}
119
120impl SoftClipper {
121    /// Creates a soft clipper with the given drive.
122    pub fn new(drive: f32) -> Self {
123        Self {
124            inner: Waveshaper::new(Waveshape::Tanh, drive),
125        }
126    }
127}
128
129impl Processor for SoftClipper {
130    fn prepare(&mut self, cfg: PrepareConfig) {
131        self.inner.prepare(cfg);
132    }
133
134    fn reset(&mut self) {
135        self.inner.reset();
136    }
137
138    fn process(&mut self, block: &mut ProcessBlock<'_>) {
139        self.inner.process(block);
140    }
141}
142
143/// A per-channel feed-forward compressor [`Processor`].
144#[derive(Clone, Debug, PartialEq)]
145pub struct Compressor {
146    threshold_db: f32,
147    ratio: f32,
148    makeup_gain: f32,
149    sample_rate_hz: f32,
150    attack_ms: f32,
151    release_ms: f32,
152    envelopes: Vec<DynamicsEnvelope>,
153}
154
155impl Compressor {
156    /// Creates a compressor with the given threshold (dB) and ratio (clamped to
157    /// `>= 1.0`), with default timing and unity makeup gain.
158    pub fn new(threshold_db: f32, ratio: f32) -> Self {
159        Self {
160            threshold_db,
161            ratio: ratio.max(1.0),
162            makeup_gain: 1.0,
163            sample_rate_hz: 48_000.0,
164            attack_ms: 5.0,
165            release_ms: 80.0,
166            envelopes: Vec::new(),
167        }
168    }
169
170    /// Returns the compressor with attack and release times (ms) set.
171    pub fn with_timing(mut self, attack_ms: f32, release_ms: f32) -> Self {
172        self.attack_ms = attack_ms;
173        self.release_ms = release_ms;
174        self
175    }
176
177    /// Returns the compressor with makeup gain set, in decibels.
178    pub fn with_makeup_gain_db(mut self, makeup_db: f32) -> Self {
179        self.makeup_gain = db_to_gain(makeup_db);
180        self
181    }
182
183    fn envelope(&self) -> DynamicsEnvelope {
184        DynamicsEnvelope::new(self.sample_rate_hz, self.attack_ms, self.release_ms)
185    }
186
187    fn gain_for_level(&self, level: f32) -> f32 {
188        let level_db = gain_to_db(level);
189        if level_db <= self.threshold_db {
190            return self.makeup_gain;
191        }
192        let compressed_db = self.threshold_db + (level_db - self.threshold_db) / self.ratio;
193        db_to_gain(compressed_db - level_db) * self.makeup_gain
194    }
195
196    #[cfg(all(test, not(debug_assertions)))]
197    pub(crate) fn realtime_state_snapshot(&self) -> Vec<usize> {
198        vec![self.envelopes.capacity()]
199    }
200}
201
202impl Processor for Compressor {
203    fn prepare(&mut self, cfg: PrepareConfig) {
204        self.sample_rate_hz = cfg.sample_rate_hz as f32;
205        let envelope = self.envelope();
206        prepare_channels(&mut self.envelopes, cfg.out_channels as usize, envelope);
207    }
208
209    fn reset(&mut self) {
210        for envelope in &mut self.envelopes {
211            envelope.reset();
212        }
213    }
214
215    fn process(&mut self, block: &mut ProcessBlock<'_>) {
216        let channels = prepared_output_channels(block, self.envelopes.len(), "Compressor");
217        let frames = block.frames as usize;
218        for channel in 0..channels {
219            for frame in 0..frames {
220                let input = input_sample(block, channel, frame);
221                let level = self.envelopes[channel].next(input);
222                block.out_audio[channel][frame] = input * self.gain_for_level(level);
223            }
224        }
225    }
226}
227
228/// A brick-wall limiter [`Processor`] built on a fast, high-ratio compressor.
229#[derive(Clone, Debug, PartialEq)]
230pub struct Limiter {
231    inner: Compressor,
232}
233
234impl Limiter {
235    /// Creates a limiter at the given threshold in decibels.
236    pub fn new(threshold_db: f32) -> Self {
237        Self {
238            inner: Compressor::new(threshold_db, 20.0).with_timing(0.5, 30.0),
239        }
240    }
241
242    #[cfg(all(test, not(debug_assertions)))]
243    pub(crate) fn realtime_state_snapshot(&self) -> Vec<usize> {
244        self.inner.realtime_state_snapshot()
245    }
246}
247
248impl Processor for Limiter {
249    fn prepare(&mut self, cfg: PrepareConfig) {
250        self.inner.prepare(cfg);
251    }
252
253    fn reset(&mut self) {
254        self.inner.reset();
255    }
256
257    fn process(&mut self, block: &mut ProcessBlock<'_>) {
258        self.inner.process(block);
259    }
260}
261
262/// A per-channel noise gate [`Processor`].
263#[derive(Clone, Debug, PartialEq)]
264pub struct Gate {
265    threshold_db: f32,
266    closed_gain: f32,
267    sample_rate_hz: f32,
268    envelopes: Vec<DynamicsEnvelope>,
269}
270
271impl Gate {
272    /// Creates a gate with an open threshold and closed gain, both in decibels.
273    pub fn new(threshold_db: f32, closed_gain_db: f32) -> Self {
274        Self {
275            threshold_db,
276            closed_gain: db_to_gain(closed_gain_db),
277            sample_rate_hz: 48_000.0,
278            envelopes: Vec::new(),
279        }
280    }
281
282    fn envelope(&self) -> DynamicsEnvelope {
283        DynamicsEnvelope::new(self.sample_rate_hz, 2.0, 40.0)
284    }
285
286    #[cfg(all(test, not(debug_assertions)))]
287    pub(crate) fn realtime_state_snapshot(&self) -> Vec<usize> {
288        vec![self.envelopes.capacity()]
289    }
290}
291
292impl Processor for Gate {
293    fn prepare(&mut self, cfg: PrepareConfig) {
294        self.sample_rate_hz = cfg.sample_rate_hz as f32;
295        let envelope = self.envelope();
296        prepare_channels(&mut self.envelopes, cfg.out_channels as usize, envelope);
297    }
298
299    fn reset(&mut self) {
300        for envelope in &mut self.envelopes {
301            envelope.reset();
302        }
303    }
304
305    fn process(&mut self, block: &mut ProcessBlock<'_>) {
306        let channels = prepared_output_channels(block, self.envelopes.len(), "Gate");
307        let frames = block.frames as usize;
308        for channel in 0..channels {
309            for frame in 0..frames {
310                let input = input_sample(block, channel, frame);
311                let level_db = gain_to_db(self.envelopes[channel].next(input));
312                let gain = if level_db < self.threshold_db {
313                    self.closed_gain
314                } else {
315                    1.0
316                };
317                block.out_audio[channel][frame] = input * gain;
318            }
319        }
320    }
321}