Skip to main content

aether_nodes/
compressor.rs

1//! RMS Compressor — dynamics processor.
2//!
3//! Param layout:
4//!   0 = threshold  (dB, -60 – 0)
5//!   1 = ratio      (1:1 – 20:1)
6//!   2 = attack     (ms, 0.1 – 200)
7//!   3 = release    (ms, 10 – 2000)
8//!   4 = makeup     (dB, 0 – 24)
9//!   5 = knee       (dB, 0 – 12, soft-knee width)
10
11use aether_core::{node::DspNode, param::ParamBlock, BUFFER_SIZE, MAX_INPUTS};
12
13pub struct Compressor {
14    /// RMS envelope follower state.
15    rms_env: f32,
16    /// Gain reduction envelope (smoothed).
17    gain_env: f32,
18}
19
20impl Compressor {
21    pub fn new() -> Self {
22        Self {
23            rms_env: 0.0,
24            gain_env: 1.0,
25        }
26    }
27
28    #[inline(always)]
29    fn db_to_linear(db: f32) -> f32 {
30        10.0f32.powf(db / 20.0)
31    }
32
33    #[inline(always)]
34    fn linear_to_db(linear: f32) -> f32 {
35        if linear <= 1e-10 { return -200.0; }
36        20.0 * linear.log10()
37    }
38}
39
40impl Default for Compressor {
41    fn default() -> Self { Self::new() }
42}
43
44impl DspNode for Compressor {
45    fn process(
46        &mut self,
47        inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
48        output: &mut [f32; BUFFER_SIZE],
49        params: &mut ParamBlock,
50        sample_rate: f32,
51    ) {
52        let silence = [0.0f32; BUFFER_SIZE];
53        let input = inputs[0].unwrap_or(&silence);
54
55        let threshold_db = params.get(0).current.clamp(-60.0, 0.0);
56        let ratio        = params.get(1).current.clamp(1.0, 20.0);
57        let attack_ms    = params.get(2).current.clamp(0.1, 200.0);
58        let release_ms   = params.get(3).current.clamp(10.0, 2000.0);
59        let makeup_db    = params.get(4).current.clamp(0.0, 24.0);
60        let knee_db      = params.get(5).current.clamp(0.0, 12.0);
61
62        let attack_coeff  = (-1.0 / (attack_ms  * 0.001 * sample_rate)).exp();
63        let release_coeff = (-1.0 / (release_ms * 0.001 * sample_rate)).exp();
64        let makeup_linear = Self::db_to_linear(makeup_db);
65
66        for i in 0..BUFFER_SIZE {
67            let x = input[i];
68
69            // RMS envelope follower (squared signal, smoothed)
70            let x2 = x * x;
71            self.rms_env = if x2 > self.rms_env {
72                attack_coeff  * self.rms_env + (1.0 - attack_coeff)  * x2
73            } else {
74                release_coeff * self.rms_env + (1.0 - release_coeff) * x2
75            };
76            let rms_db = Self::linear_to_db(self.rms_env.sqrt());
77
78            // Gain computer with soft knee
79            let gain_reduction_db = if knee_db > 0.0 {
80                let knee_start = threshold_db - knee_db * 0.5;
81                let knee_end   = threshold_db + knee_db * 0.5;
82                if rms_db <= knee_start {
83                    0.0
84                } else if rms_db >= knee_end {
85                    (rms_db - threshold_db) * (1.0 / ratio - 1.0)
86                } else {
87                    // Soft knee interpolation
88                    let t = (rms_db - knee_start) / knee_db;
89                    t * t * 0.5 * (1.0 / ratio - 1.0) * knee_db
90                }
91            } else {
92                if rms_db > threshold_db {
93                    (rms_db - threshold_db) * (1.0 / ratio - 1.0)
94                } else {
95                    0.0
96                }
97            };
98
99            let target_gain = Self::db_to_linear(gain_reduction_db);
100
101            // Smooth gain envelope
102            self.gain_env = if target_gain < self.gain_env {
103                attack_coeff  * self.gain_env + (1.0 - attack_coeff)  * target_gain
104            } else {
105                release_coeff * self.gain_env + (1.0 - release_coeff) * target_gain
106            };
107
108            output[i] = x * self.gain_env * makeup_linear;
109            params.tick_all();
110        }
111    }
112
113    fn type_name(&self) -> &'static str { "Compressor" }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_compressor_silence_passthrough() {
122        let mut comp = Compressor::new();
123        let mut params = ParamBlock::new();
124        // threshold=0dB, ratio=1, attack=1ms, release=100ms, makeup=0dB, knee=0
125        for &v in &[-20.0f32, 2.0, 1.0, 100.0, 0.0, 0.0] { params.add(v); }
126        let input = [0.0f32; BUFFER_SIZE];
127        let inputs = [Some(&input); MAX_INPUTS];
128        let mut output = [0.0f32; BUFFER_SIZE];
129        comp.process(&inputs, &mut output, &mut params, 48000.0);
130        for s in &output { assert!(s.abs() < 1e-6, "silence should pass through as silence"); }
131    }
132
133    #[test]
134    fn test_compressor_reduces_loud_signal() {
135        let mut comp = Compressor::new();
136        let mut params = ParamBlock::new();
137        // threshold=-20dB, ratio=4, attack=1ms, release=100ms, makeup=0dB, knee=0
138        for &v in &[-20.0f32, 4.0, 1.0, 100.0, 0.0, 0.0] { params.add(v); }
139        let input = [0.5f32; BUFFER_SIZE]; // ~-6dB, above threshold
140        let inputs = [Some(&input); MAX_INPUTS];
141        let mut output = [0.0f32; BUFFER_SIZE];
142        comp.process(&inputs, &mut output, &mut params, 48000.0);
143        // After settling, output should be quieter than input
144        let last = output[BUFFER_SIZE - 1].abs();
145        assert!(last < 0.5, "compressor should reduce gain above threshold, got {last}");
146    }
147}