1use serde::{Deserialize, Serialize};
4
5use super::EffectContext;
6
7const DEFAULT_THRESHOLD_DB: f32 = -18.0;
8const DEFAULT_RATIO: f32 = 4.0;
9const DEFAULT_ATTACK_MS: f32 = 10.0;
10const DEFAULT_RELEASE_MS: f32 = 100.0;
11const DEFAULT_MAKEUP_DB: f32 = 0.0;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(default)]
16pub struct CompressorSettings {
17 #[serde(alias = "threshold", alias = "threshold_db")]
18 pub threshold_db: f32,
19 pub ratio: f32,
20 #[serde(alias = "attack_ms", alias = "attack")]
21 pub attack_ms: f32,
22 #[serde(alias = "release_ms", alias = "release")]
23 pub release_ms: f32,
24 #[serde(alias = "makeup_db", alias = "makeup_gain_db")]
25 pub makeup_gain_db: f32,
26}
27
28impl CompressorSettings {
29 pub fn new(
31 threshold_db: f32,
32 ratio: f32,
33 attack_ms: f32,
34 release_ms: f32,
35 makeup_gain_db: f32,
36 ) -> Self {
37 Self {
38 threshold_db,
39 ratio,
40 attack_ms,
41 release_ms,
42 makeup_gain_db,
43 }
44 }
45}
46
47impl Default for CompressorSettings {
48 fn default() -> Self {
49 Self {
50 threshold_db: DEFAULT_THRESHOLD_DB,
51 ratio: DEFAULT_RATIO,
52 attack_ms: DEFAULT_ATTACK_MS,
53 release_ms: DEFAULT_RELEASE_MS,
54 makeup_gain_db: DEFAULT_MAKEUP_DB,
55 }
56 }
57}
58
59#[derive(Clone, Serialize, Deserialize)]
61#[serde(default)]
62pub struct CompressorEffect {
63 pub enabled: bool,
64 #[serde(flatten)]
65 pub settings: CompressorSettings,
66 #[serde(skip)]
67 state: Option<CompressorState>,
68}
69
70impl std::fmt::Debug for CompressorEffect {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 f.debug_struct("CompressorEffect")
73 .field("enabled", &self.enabled)
74 .field("settings", &self.settings)
75 .finish()
76 }
77}
78
79impl Default for CompressorEffect {
80 fn default() -> Self {
81 Self {
82 enabled: false,
83 settings: CompressorSettings::default(),
84 state: None,
85 }
86 }
87}
88
89impl CompressorEffect {
90 pub fn process(&mut self, samples: &[f32], context: &EffectContext, _drain: bool) -> Vec<f32> {
100 if !self.enabled {
101 return samples.to_vec();
102 }
103
104 self.ensure_state(context);
105 let Some(state) = self.state.as_mut() else {
106 return samples.to_vec();
107 };
108
109 if samples.is_empty() {
110 return Vec::new();
111 }
112
113 let channels = state.channels;
114 let mut output = Vec::with_capacity(samples.len());
115
116 for frame in samples.chunks(channels) {
117 let mut peak = 0.0_f32;
118 for &sample in frame {
119 peak = peak.max(sample.abs());
120 }
121
122 let level_db = linear_to_db(peak);
123 let target_gain_db = compute_gain_db(level_db, state.threshold_db, state.ratio);
124 state.update_gain(target_gain_db);
125 let gain = db_to_linear(state.current_gain_db + state.makeup_gain_db);
126
127 for &sample in frame {
128 output.push(sample * gain);
129 }
130 }
131
132 output
133 }
134
135 pub fn reset_state(&mut self) {
137 if let Some(state) = self.state.as_mut() {
138 state.reset();
139 }
140 self.state = None;
141 }
142
143 fn ensure_state(&mut self, context: &EffectContext) {
144 let threshold_db = sanitize_threshold_db(self.settings.threshold_db);
145 let ratio = sanitize_ratio(self.settings.ratio);
146 let attack_ms = sanitize_time_ms(self.settings.attack_ms, DEFAULT_ATTACK_MS);
147 let release_ms = sanitize_time_ms(self.settings.release_ms, DEFAULT_RELEASE_MS);
148 let makeup_gain_db = sanitize_makeup_db(self.settings.makeup_gain_db);
149 let channels = context.channels.max(1);
150
151 let needs_reset = self
152 .state
153 .as_ref()
154 .map(|state| {
155 !state.matches(
156 context.sample_rate,
157 channels,
158 threshold_db,
159 ratio,
160 attack_ms,
161 release_ms,
162 makeup_gain_db,
163 )
164 })
165 .unwrap_or(true);
166
167 if needs_reset {
168 self.state = Some(CompressorState::new(
169 context.sample_rate,
170 channels,
171 threshold_db,
172 ratio,
173 attack_ms,
174 release_ms,
175 makeup_gain_db,
176 ));
177 }
178 }
179}
180
181#[derive(Clone, Debug)]
182struct CompressorState {
183 sample_rate: u32,
184 channels: usize,
185 threshold_db: f32,
186 ratio: f32,
187 attack_coeff: f32,
188 release_coeff: f32,
189 makeup_gain_db: f32,
190 current_gain_db: f32,
191}
192
193impl CompressorState {
194 fn new(
195 sample_rate: u32,
196 channels: usize,
197 threshold_db: f32,
198 ratio: f32,
199 attack_ms: f32,
200 release_ms: f32,
201 makeup_gain_db: f32,
202 ) -> Self {
203 let attack_coeff = time_to_coeff(attack_ms, sample_rate);
204 let release_coeff = time_to_coeff(release_ms, sample_rate);
205 Self {
206 sample_rate,
207 channels,
208 threshold_db,
209 ratio,
210 attack_coeff,
211 release_coeff,
212 makeup_gain_db,
213 current_gain_db: 0.0,
214 }
215 }
216
217 fn matches(
218 &self,
219 sample_rate: u32,
220 channels: usize,
221 threshold_db: f32,
222 ratio: f32,
223 attack_ms: f32,
224 release_ms: f32,
225 makeup_gain_db: f32,
226 ) -> bool {
227 self.sample_rate == sample_rate
228 && self.channels == channels
229 && (self.threshold_db - threshold_db).abs() < f32::EPSILON
230 && (self.ratio - ratio).abs() < f32::EPSILON
231 && (self.attack_coeff - time_to_coeff(attack_ms, sample_rate)).abs() < f32::EPSILON
232 && (self.release_coeff - time_to_coeff(release_ms, sample_rate)).abs() < f32::EPSILON
233 && (self.makeup_gain_db - makeup_gain_db).abs() < f32::EPSILON
234 }
235
236 fn update_gain(&mut self, target_gain_db: f32) {
237 let coeff = if target_gain_db < self.current_gain_db {
238 self.attack_coeff
239 } else {
240 self.release_coeff
241 };
242 self.current_gain_db = coeff * self.current_gain_db + (1.0 - coeff) * target_gain_db;
243 }
244
245 fn reset(&mut self) {
246 self.current_gain_db = 0.0;
247 }
248}
249
250fn compute_gain_db(level_db: f32, threshold_db: f32, ratio: f32) -> f32 {
251 if level_db <= threshold_db {
252 0.0
253 } else {
254 let compressed = threshold_db + (level_db - threshold_db) / ratio;
255 compressed - level_db
256 }
257}
258
259fn linear_to_db(value: f32) -> f32 {
260 let v = value.max(f32::MIN_POSITIVE);
261 20.0 * v.log10()
262}
263
264fn db_to_linear(db: f32) -> f32 {
265 10.0_f32.powf(db / 20.0)
266}
267
268fn time_to_coeff(time_ms: f32, sample_rate: u32) -> f32 {
269 if time_ms <= 0.0 || !time_ms.is_finite() {
270 return 0.0;
271 }
272 let t = time_ms / 1000.0;
273 (-1.0 / (t * sample_rate as f32)).exp()
274}
275
276fn sanitize_threshold_db(threshold_db: f32) -> f32 {
277 if !threshold_db.is_finite() {
278 return DEFAULT_THRESHOLD_DB;
279 }
280 threshold_db.min(0.0)
281}
282
283fn sanitize_ratio(ratio: f32) -> f32 {
284 if !ratio.is_finite() {
285 return DEFAULT_RATIO;
286 }
287 ratio.max(1.0)
288}
289
290fn sanitize_time_ms(value: f32, fallback: f32) -> f32 {
291 if !value.is_finite() {
292 return fallback;
293 }
294 value.max(0.0)
295}
296
297fn sanitize_makeup_db(value: f32) -> f32 {
298 if !value.is_finite() {
299 return DEFAULT_MAKEUP_DB;
300 }
301 value
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 fn context(channels: usize) -> EffectContext {
309 EffectContext {
310 sample_rate: 48_000,
311 channels,
312 container_path: None,
313 impulse_response_spec: None,
314 impulse_response_tail_db: -60.0,
315 }
316 }
317
318 fn approx_eq(a: f32, b: f32, eps: f32) -> bool {
319 (a - b).abs() <= eps
320 }
321
322 #[test]
323 fn compressor_disabled_passthrough() {
324 let mut effect = CompressorEffect::default();
325 let samples = vec![0.25_f32, -0.25, 0.5, -0.5];
326 let output = effect.process(&samples, &context(2), false);
327 assert_eq!(output, samples);
328 }
329
330 #[test]
331 fn compressor_applies_gain_reduction() {
332 let mut effect = CompressorEffect::default();
333 effect.enabled = true;
334 effect.settings.threshold_db = -6.0;
335 effect.settings.ratio = 2.0;
336 effect.settings.attack_ms = 0.0;
337 effect.settings.release_ms = 0.0;
338 effect.settings.makeup_gain_db = 0.0;
339
340 let samples = vec![1.0_f32, 1.0];
341 let output = effect.process(&samples, &context(2), false);
342 assert_eq!(output.len(), samples.len());
343
344 let level_db = 0.0;
345 let target_gain_db = (-6.0 + (level_db + 6.0) / 2.0) - level_db;
346 let expected = db_to_linear(target_gain_db);
347 assert!(output.iter().all(|value| approx_eq(*value, expected, 1e-3)));
348 }
349}