1use std::sync::atomic::Ordering;
2use std::sync::Arc;
3
4use crate::context::{AudioContextRegistration, AudioParamId, BaseAudioContext};
5use crate::param::{AudioParam, AudioParamDescriptor};
6use crate::render::{
7 AudioParamValues, AudioProcessor, AudioRenderQuantum, AudioWorkletGlobalScope,
8};
9use crate::{AtomicF32, RENDER_QUANTUM_SIZE};
10
11use super::{AudioNode, AudioNodeOptions, ChannelConfig, ChannelCountMode, ChannelInterpretation};
12
13fn db_to_lin(val: f32) -> f32 {
15 (10.0_f32).powf(val / 20.)
16}
17
18fn lin_to_db(val: f32) -> f32 {
22 if val == 0. {
23 -1000.
24 } else {
25 20. * val.log10() }
27}
28
29#[derive(Clone, Debug)]
39pub struct DynamicsCompressorOptions {
40 pub attack: f32,
41 pub knee: f32,
42 pub ratio: f32,
43 pub release: f32,
44 pub threshold: f32,
45 pub audio_node_options: AudioNodeOptions,
46}
47
48impl Default for DynamicsCompressorOptions {
49 fn default() -> Self {
50 Self {
51 attack: 0.003, knee: 30., ratio: 12., release: 0.25, threshold: -24., audio_node_options: AudioNodeOptions {
57 channel_count: 2,
58 channel_count_mode: ChannelCountMode::ClampedMax,
59 channel_interpretation: ChannelInterpretation::Speakers,
60 },
61 }
62 }
63}
64
65#[track_caller]
73#[inline(always)]
74fn assert_valid_channel_count(count: usize) {
75 assert!(
76 count <= 2,
77 "NotSupportedError - DynamicsCompressorNode channel count cannot be greater than two"
78 );
79}
80
81#[track_caller]
89#[inline(always)]
90fn assert_valid_channel_count_mode(mode: ChannelCountMode) {
91 assert_ne!(
92 mode,
93 ChannelCountMode::Max,
94 "NotSupportedError - DynamicsCompressorNode channel count mode cannot be set to max"
95 );
96}
97
98#[derive(Debug)]
139pub struct DynamicsCompressorNode {
140 registration: AudioContextRegistration,
141 channel_config: ChannelConfig,
142 attack: AudioParam,
143 knee: AudioParam,
144 ratio: AudioParam,
145 release: AudioParam,
146 threshold: AudioParam,
147 reduction: Arc<AtomicF32>,
148}
149
150impl AudioNode for DynamicsCompressorNode {
151 fn registration(&self) -> &AudioContextRegistration {
152 &self.registration
153 }
154
155 fn channel_config(&self) -> &ChannelConfig {
156 &self.channel_config
157 }
158
159 fn number_of_inputs(&self) -> usize {
160 1
161 }
162
163 fn number_of_outputs(&self) -> usize {
164 1
165 }
166
167 fn set_channel_count(&self, count: usize) {
169 assert_valid_channel_count(count);
170 self.channel_config.set_count(count, self.registration());
171 }
172
173 fn set_channel_count_mode(&self, mode: ChannelCountMode) {
175 assert_valid_channel_count_mode(mode);
176 self.channel_config
177 .set_count_mode(mode, self.registration());
178 }
179}
180
181impl DynamicsCompressorNode {
182 pub fn new<C: BaseAudioContext>(context: &C, options: DynamicsCompressorOptions) -> Self {
183 context.base().register(move |registration| {
184 assert_valid_channel_count(options.audio_node_options.channel_count);
185 assert_valid_channel_count_mode(options.audio_node_options.channel_count_mode);
186
187 let attack_param_opts = AudioParamDescriptor {
190 name: String::new(),
191 min_value: 0.,
192 max_value: 1.,
193 default_value: 0.003,
194 automation_rate: crate::param::AutomationRate::K,
195 };
196 let (mut attack_param, attack_proc) =
197 context.create_audio_param(attack_param_opts, ®istration);
198 attack_param.set_automation_rate_constrained(true);
199 attack_param.set_value(options.attack);
200
201 let knee_param_opts = AudioParamDescriptor {
202 name: String::new(),
203 min_value: 0.,
204 max_value: 40.,
205 default_value: 30.,
206 automation_rate: crate::param::AutomationRate::K,
207 };
208 let (mut knee_param, knee_proc) =
209 context.create_audio_param(knee_param_opts, ®istration);
210 knee_param.set_automation_rate_constrained(true);
211 knee_param.set_value(options.knee);
212
213 let ratio_param_opts = AudioParamDescriptor {
214 name: String::new(),
215 min_value: 1.,
216 max_value: 20.,
217 default_value: 12.,
218 automation_rate: crate::param::AutomationRate::K,
219 };
220 let (mut ratio_param, ratio_proc) =
221 context.create_audio_param(ratio_param_opts, ®istration);
222 ratio_param.set_automation_rate_constrained(true);
223 ratio_param.set_value(options.ratio);
224
225 let release_param_opts = AudioParamDescriptor {
226 name: String::new(),
227 min_value: 0.,
228 max_value: 1.,
229 default_value: 0.25,
230 automation_rate: crate::param::AutomationRate::K,
231 };
232 let (mut release_param, release_proc) =
233 context.create_audio_param(release_param_opts, ®istration);
234 release_param.set_automation_rate_constrained(true);
235 release_param.set_value(options.release);
236
237 let threshold_param_opts = AudioParamDescriptor {
238 name: String::new(),
239 min_value: -100.,
240 max_value: 0.,
241 default_value: -24.,
242 automation_rate: crate::param::AutomationRate::K,
243 };
244 let (mut threshold_param, threshold_proc) =
245 context.create_audio_param(threshold_param_opts, ®istration);
246 threshold_param.set_automation_rate_constrained(true);
247 threshold_param.set_value(options.threshold);
248
249 let reduction = Arc::new(AtomicF32::new(0.));
250
251 let ring_buffer_size =
254 (context.sample_rate() * 0.006 / RENDER_QUANTUM_SIZE as f32).ceil() as usize + 1;
255 let ring_buffer = Vec::<AudioRenderQuantum>::with_capacity(ring_buffer_size);
256
257 let render = DynamicsCompressorRenderer {
258 attack: attack_proc,
259 knee: knee_proc,
260 ratio: ratio_proc,
261 release: release_proc,
262 threshold: threshold_proc,
263 reduction: Arc::clone(&reduction),
264 ring_buffer,
265 ring_index: 0,
266 prev_detector_value: 0.,
267 };
268
269 let node = DynamicsCompressorNode {
270 registration,
271 channel_config: options.audio_node_options.into(),
272 attack: attack_param,
273 knee: knee_param,
274 ratio: ratio_param,
275 release: release_param,
276 threshold: threshold_param,
277 reduction,
278 };
279
280 (node, Box::new(render))
281 })
282 }
283
284 pub fn attack(&self) -> &AudioParam {
285 &self.attack
286 }
287
288 pub fn knee(&self) -> &AudioParam {
289 &self.knee
290 }
291
292 pub fn ratio(&self) -> &AudioParam {
293 &self.ratio
294 }
295
296 pub fn release(&self) -> &AudioParam {
297 &self.release
298 }
299
300 pub fn threshold(&self) -> &AudioParam {
301 &self.threshold
302 }
303
304 pub fn reduction(&self) -> f32 {
305 self.reduction.load(Ordering::Relaxed)
306 }
307}
308
309struct DynamicsCompressorRenderer {
310 attack: AudioParamId,
311 knee: AudioParamId,
312 ratio: AudioParamId,
313 release: AudioParamId,
314 threshold: AudioParamId,
315 reduction: Arc<AtomicF32>,
316 ring_buffer: Vec<AudioRenderQuantum>,
317 ring_index: usize,
318 prev_detector_value: f32,
319}
320
321#[allow(clippy::non_send_fields_in_send_ty)]
325unsafe impl Send for DynamicsCompressorRenderer {}
326
327impl AudioProcessor for DynamicsCompressorRenderer {
331 fn process(
332 &mut self,
333 inputs: &[AudioRenderQuantum],
334 outputs: &mut [AudioRenderQuantum],
335 params: AudioParamValues<'_>,
336 scope: &AudioWorkletGlobalScope,
337 ) -> bool {
338 let input = inputs[0].clone();
340 let output = &mut outputs[0];
341 let sample_rate = scope.sample_rate;
342
343 let ring_size = self.ring_buffer.capacity();
344 if self.ring_buffer.len() < ring_size {
346 let mut silence = input.clone();
347 silence.make_silent();
348 self.ring_buffer.resize(ring_size, silence);
349 }
350
351 let threshold = params.get(&self.threshold)[0];
354 let knee = params.get(&self.knee)[0];
355 let ratio = params.get(&self.ratio)[0];
356 let threshold = if knee > 0. {
367 threshold + knee / 2.
368 } else {
369 threshold
370 };
371 let half_knee = knee / 2.;
372 let knee_partial = (1. / ratio - 1.) / (2. * knee);
374
375 let attack = params.get(&self.attack)[0];
377 let release = params.get(&self.release)[0];
378 let attack_tau = (-1. / (attack * sample_rate)).exp();
379 let release_tau = (-1. / (release * sample_rate)).exp();
380
381 let full_range_gain = threshold + (-threshold / ratio);
388 let full_range_makeup = 1. / db_to_lin(full_range_gain);
389 let makeup_gain = lin_to_db(full_range_makeup.powf(0.6));
390
391 let mut prev_detector_value = self.prev_detector_value;
392
393 let mut reduction_gain = 0.; let mut reduction_gains = [0.; 128]; let mut detector_values = [0.; 128]; for i in 0..RENDER_QUANTUM_SIZE {
398 let mut max = f32::MIN;
401
402 for channel in input.channels().iter() {
403 let sample = channel[i].abs();
404 if sample > max {
405 max = sample;
406 }
407 }
408
409 let sample_db = lin_to_db(max);
412
413 let sample_attenuated = if sample_db <= threshold - half_knee {
418 sample_db
419 } else if sample_db <= threshold + half_knee {
420 sample_db + (sample_db - threshold + half_knee).powi(2) * knee_partial
421 } else {
422 threshold + (sample_db - threshold) / ratio
423 };
424 let sample_attenuation = sample_db - sample_attenuated;
426
427 let detector_value = if sample_attenuation > prev_detector_value {
432 attack_tau * prev_detector_value + (1. - attack_tau) * sample_attenuation
433 } else {
435 release_tau * prev_detector_value + (1. - release_tau) * sample_attenuation
436 };
437
438 detector_values[i] = detector_value;
439 reduction_gain = -1. * detector_value + makeup_gain;
441 reduction_gains[i] = db_to_lin(reduction_gain);
443 prev_detector_value = detector_value;
445 }
446
447 self.prev_detector_value = prev_detector_value;
449 self.reduction.store(reduction_gain, Ordering::Relaxed);
451
452 self.ring_buffer[self.ring_index] = input;
454
455 let read_index = (self.ring_index + 1) % ring_size;
457 let delayed = &self.ring_buffer[read_index];
458
459 self.ring_index = read_index;
460
461 *output = delayed.clone();
462
463 if output.is_silent() {
466 output.make_silent(); return false;
468 }
469
470 output.channels_mut().iter_mut().for_each(|channel| {
471 channel
472 .iter_mut()
473 .zip(reduction_gains.iter())
474 .for_each(|(o, g)| *o *= g);
475 });
476
477 true
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use float_eq::assert_float_eq;
484
485 use crate::context::OfflineAudioContext;
486 use crate::node::AudioScheduledSourceNode;
487
488 use super::*;
489
490 #[test]
491 fn test_constructor_default() {
492 let context = OfflineAudioContext::new(1, 1, 44_100.);
493 let compressor = DynamicsCompressorNode::new(&context, Default::default());
494
495 assert_float_eq!(compressor.attack().value(), 0.003, abs <= 0.);
496 assert_float_eq!(compressor.knee().value(), 30., abs <= 0.);
497 assert_float_eq!(compressor.ratio().value(), 12., abs <= 0.);
498 assert_float_eq!(compressor.release().value(), 0.25, abs <= 0.);
499 assert_float_eq!(compressor.threshold().value(), -24., abs <= 0.);
500 }
501
502 #[test]
503 fn test_constructor_non_default() {
504 let context = OfflineAudioContext::new(1, 1, 44_100.);
505 let compressor = DynamicsCompressorNode::new(
506 &context,
507 DynamicsCompressorOptions {
508 attack: 0.5,
509 knee: 12.,
510 ratio: 1.,
511 release: 0.75,
512 threshold: -60.,
513 ..DynamicsCompressorOptions::default()
514 },
515 );
516
517 assert_float_eq!(compressor.attack().value(), 0.5, abs <= 0.);
518 assert_float_eq!(compressor.knee().value(), 12., abs <= 0.);
519 assert_float_eq!(compressor.ratio().value(), 1., abs <= 0.);
520 assert_float_eq!(compressor.release().value(), 0.75, abs <= 0.);
521 assert_float_eq!(compressor.threshold().value(), -60., abs <= 0.);
522 }
523
524 #[test]
525 fn test_inner_delay() {
526 let sample_rate = 44_100.;
527 let compressor_delay = 0.006;
528 let non_zero_index = (compressor_delay * sample_rate / RENDER_QUANTUM_SIZE as f32).ceil()
531 as usize
532 * RENDER_QUANTUM_SIZE;
533
534 let mut context = OfflineAudioContext::new(1, 128 * 8, sample_rate);
535
536 let compressor = DynamicsCompressorNode::new(&context, Default::default());
537 compressor.connect(&context.destination());
538
539 let mut buffer = context.create_buffer(1, 128 * 5, sample_rate);
540 let signal = [1.; 128 * 5];
541 buffer.copy_to_channel(&signal, 0);
542
543 let mut src = context.create_buffer_source();
544 src.set_buffer(buffer);
545 src.connect(&compressor);
546 src.start();
547
548 let res = context.start_rendering_sync();
549 let chan = res.channel_data(0).as_slice();
550
551 assert_float_eq!(
553 chan[0..non_zero_index],
554 vec![0.; non_zero_index][..],
555 abs_all <= 0.
556 );
557
558 for sample in chan.iter().take(128 * 8).skip(non_zero_index) {
560 assert!(*sample != 0.);
561 }
562 }
563
564 #[test]
565 fn test_db_to_lin() {
566 assert_float_eq!(db_to_lin(0.), 1., abs <= 0.);
567 assert_float_eq!(db_to_lin(-20.), 0.1, abs <= 1e-8);
568 assert_float_eq!(db_to_lin(-40.), 0.01, abs <= 1e-8);
569 assert_float_eq!(db_to_lin(-60.), 0.001, abs <= 1e-8);
570 }
571
572 #[test]
573 fn test_lin_to_db() {
574 assert_float_eq!(lin_to_db(1.), 0., abs <= 0.);
575 assert_float_eq!(lin_to_db(0.1), -20., abs <= 0.);
576 assert_float_eq!(lin_to_db(0.01), -40., abs <= 0.);
577 assert_float_eq!(lin_to_db(0.001), -60., abs <= 0.);
578 assert_float_eq!(lin_to_db(0.), -1000., abs <= 0.);
580 }
581
582 }