aether_nodes/
compressor.rs1use aether_core::{node::DspNode, param::ParamBlock, BUFFER_SIZE, MAX_INPUTS};
12
13pub struct Compressor {
14 rms_env: f32,
16 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 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 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 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 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 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 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]; 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 let last = output[BUFFER_SIZE - 1].abs();
145 assert!(last < 0.5, "compressor should reduce gain above threshold, got {last}");
146 }
147}