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