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 {
36 return -200.0;
37 }
38 20.0 * linear.log10()
39 }
40}
41
42impl Default for Compressor {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48impl DspNode for Compressor {
49 fn process(
50 &mut self,
51 inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
52 output: &mut [f32; BUFFER_SIZE],
53 params: &mut ParamBlock,
54 sample_rate: f32,
55 ) {
56 let silence = [0.0f32; BUFFER_SIZE];
57 let input = inputs[0].unwrap_or(&silence);
58
59 let threshold_db = params.get(0).current.clamp(-60.0, 0.0);
60 let ratio = params.get(1).current.clamp(1.0, 20.0);
61 let attack_ms = params.get(2).current.clamp(0.1, 200.0);
62 let release_ms = params.get(3).current.clamp(10.0, 2000.0);
63 let makeup_db = params.get(4).current.clamp(0.0, 24.0);
64 let knee_db = params.get(5).current.clamp(0.0, 12.0);
65
66 let attack_coeff = (-1.0 / (attack_ms * 0.001 * sample_rate)).exp();
67 let release_coeff = (-1.0 / (release_ms * 0.001 * sample_rate)).exp();
68 let makeup_linear = Self::db_to_linear(makeup_db);
69
70 for i in 0..BUFFER_SIZE {
71 let x = input[i];
72
73 let x2 = x * x;
75 self.rms_env = if x2 > self.rms_env {
76 attack_coeff * self.rms_env + (1.0 - attack_coeff) * x2
77 } else {
78 release_coeff * self.rms_env + (1.0 - release_coeff) * x2
79 };
80 let rms_db = Self::linear_to_db(self.rms_env.sqrt());
81
82 let gain_reduction_db = if knee_db > 0.0 {
84 let knee_start = threshold_db - knee_db * 0.5;
85 let knee_end = threshold_db + knee_db * 0.5;
86 if rms_db <= knee_start {
87 0.0
88 } else if rms_db >= knee_end {
89 (rms_db - threshold_db) * (1.0 / ratio - 1.0)
90 } else {
91 let t = (rms_db - knee_start) / knee_db;
93 t * t * 0.5 * (1.0 / ratio - 1.0) * knee_db
94 }
95 } else {
96 if rms_db > threshold_db {
97 (rms_db - threshold_db) * (1.0 / ratio - 1.0)
98 } else {
99 0.0
100 }
101 };
102
103 let target_gain = Self::db_to_linear(gain_reduction_db);
104
105 self.gain_env = if target_gain < self.gain_env {
107 attack_coeff * self.gain_env + (1.0 - attack_coeff) * target_gain
108 } else {
109 release_coeff * self.gain_env + (1.0 - release_coeff) * target_gain
110 };
111
112 output[i] = x * self.gain_env * makeup_linear;
113 params.tick_all();
114 }
115 }
116
117 fn type_name(&self) -> &'static str {
118 "Compressor"
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn test_compressor_silence_passthrough() {
128 let mut comp = Compressor::new();
129 let mut params = ParamBlock::new();
130 for &v in &[-20.0f32, 2.0, 1.0, 100.0, 0.0, 0.0] {
132 params.add(v);
133 }
134 let input = [0.0f32; BUFFER_SIZE];
135 let inputs = [Some(&input); MAX_INPUTS];
136 let mut output = [0.0f32; BUFFER_SIZE];
137 comp.process(&inputs, &mut output, &mut params, 48000.0);
138 for s in &output {
139 assert!(s.abs() < 1e-6, "silence should pass through as silence");
140 }
141 }
142
143 #[test]
144 fn test_compressor_reduces_loud_signal() {
145 let mut comp = Compressor::new();
146 let mut params = ParamBlock::new();
147 for &v in &[-20.0f32, 4.0, 1.0, 100.0, 0.0, 0.0] {
149 params.add(v);
150 }
151 let input = [0.5f32; BUFFER_SIZE]; let inputs = [Some(&input); MAX_INPUTS];
153 let mut output = [0.0f32; BUFFER_SIZE];
154 comp.process(&inputs, &mut output, &mut params, 48000.0);
155 let last = output[BUFFER_SIZE - 1].abs();
157 assert!(
158 last < 0.5,
159 "compressor should reduce gain above threshold, got {last}"
160 );
161 }
162}