use std::sync::atomic::Ordering;
use std::sync::Arc;
use crate::context::{AudioContextRegistration, AudioParamId, BaseAudioContext};
use crate::param::{AudioParam, AudioParamDescriptor};
use crate::render::{AudioParamValues, AudioProcessor, AudioRenderQuantum, RenderScope};
use crate::{AtomicF32, RENDER_QUANTUM_SIZE};
use super::{AudioNode, ChannelConfig, ChannelConfigOptions};
fn db_to_lin(val: f32) -> f32 {
(10.0_f32).powf(val / 20.)
}
fn lin_to_db(val: f32) -> f32 {
if val == 0. {
-1000.
} else {
20. * val.log10() }
}
#[derive(Clone, Debug)]
pub struct DynamicsCompressorOptions {
pub attack: f32,
pub knee: f32,
pub ratio: f32,
pub release: f32,
pub threshold: f32,
pub channel_config: ChannelConfigOptions,
}
impl Default for DynamicsCompressorOptions {
fn default() -> Self {
Self {
attack: 0.003, knee: 30., ratio: 12., release: 0.25, threshold: -24., channel_config: ChannelConfigOptions::default(),
}
}
}
pub struct DynamicsCompressorNode {
registration: AudioContextRegistration,
channel_config: ChannelConfig,
attack: AudioParam,
knee: AudioParam,
ratio: AudioParam,
release: AudioParam,
threshold: AudioParam,
reduction: Arc<AtomicF32>,
}
impl AudioNode for DynamicsCompressorNode {
fn registration(&self) -> &AudioContextRegistration {
&self.registration
}
fn channel_config(&self) -> &ChannelConfig {
&self.channel_config
}
fn number_of_inputs(&self) -> usize {
1
}
fn number_of_outputs(&self) -> usize {
1
}
}
impl DynamicsCompressorNode {
pub fn new<C: BaseAudioContext>(context: &C, options: DynamicsCompressorOptions) -> Self {
context.register(move |registration| {
let attack_param_opts = AudioParamDescriptor {
min_value: 0.,
max_value: 1.,
default_value: 0.003,
automation_rate: crate::param::AutomationRate::K,
};
let (mut attack_param, attack_proc) =
context.create_audio_param(attack_param_opts, ®istration);
attack_param.set_automation_rate_constrained(true);
attack_param.set_value(options.attack);
let knee_param_opts = AudioParamDescriptor {
min_value: 0.,
max_value: 40.,
default_value: 30.,
automation_rate: crate::param::AutomationRate::K,
};
let (mut knee_param, knee_proc) =
context.create_audio_param(knee_param_opts, ®istration);
knee_param.set_automation_rate_constrained(true);
knee_param.set_value(options.knee);
let ratio_param_opts = AudioParamDescriptor {
min_value: 1.,
max_value: 20.,
default_value: 12.,
automation_rate: crate::param::AutomationRate::K,
};
let (mut ratio_param, ratio_proc) =
context.create_audio_param(ratio_param_opts, ®istration);
ratio_param.set_automation_rate_constrained(true);
ratio_param.set_value(options.ratio);
let release_param_opts = AudioParamDescriptor {
min_value: 0.,
max_value: 1.,
default_value: 0.25,
automation_rate: crate::param::AutomationRate::K,
};
let (mut release_param, release_proc) =
context.create_audio_param(release_param_opts, ®istration);
release_param.set_automation_rate_constrained(true);
release_param.set_value(options.release);
let threshold_param_opts = AudioParamDescriptor {
min_value: -100.,
max_value: 0.,
default_value: -24.,
automation_rate: crate::param::AutomationRate::K,
};
let (mut threshold_param, threshold_proc) =
context.create_audio_param(threshold_param_opts, ®istration);
threshold_param.set_automation_rate_constrained(true);
threshold_param.set_value(options.threshold);
let reduction = Arc::new(AtomicF32::new(0.));
let ring_buffer_size =
(context.sample_rate() * 0.006 / RENDER_QUANTUM_SIZE as f32).ceil() as usize + 1;
let ring_buffer = Vec::<AudioRenderQuantum>::with_capacity(ring_buffer_size);
let render = DynamicsCompressorRenderer {
attack: attack_proc,
knee: knee_proc,
ratio: ratio_proc,
release: release_proc,
threshold: threshold_proc,
reduction: reduction.clone(),
ring_buffer,
ring_index: 0,
prev_detector_value: 0.,
};
let node = DynamicsCompressorNode {
registration,
channel_config: options.channel_config.into(),
attack: attack_param,
knee: knee_param,
ratio: ratio_param,
release: release_param,
threshold: threshold_param,
reduction,
};
(node, Box::new(render))
})
}
pub fn attack(&self) -> &AudioParam {
&self.attack
}
pub fn knee(&self) -> &AudioParam {
&self.knee
}
pub fn ratio(&self) -> &AudioParam {
&self.ratio
}
pub fn release(&self) -> &AudioParam {
&self.release
}
pub fn threshold(&self) -> &AudioParam {
&self.threshold
}
pub fn reduction(&self) -> f32 {
self.reduction.load(Ordering::SeqCst)
}
}
struct DynamicsCompressorRenderer {
attack: AudioParamId,
knee: AudioParamId,
ratio: AudioParamId,
release: AudioParamId,
threshold: AudioParamId,
reduction: Arc<AtomicF32>,
ring_buffer: Vec<AudioRenderQuantum>,
ring_index: usize,
prev_detector_value: f32,
}
#[allow(clippy::non_send_fields_in_send_ty)]
unsafe impl Send for DynamicsCompressorRenderer {}
impl AudioProcessor for DynamicsCompressorRenderer {
fn process(
&mut self,
inputs: &[AudioRenderQuantum],
outputs: &mut [AudioRenderQuantum],
params: AudioParamValues,
scope: &RenderScope,
) -> bool {
let input = inputs[0].clone();
let output = &mut outputs[0];
let sample_rate = scope.sample_rate;
let ring_size = self.ring_buffer.capacity();
if self.ring_buffer.len() < ring_size {
let mut silence = input.clone();
silence.make_silent();
self.ring_buffer.resize(ring_size, silence);
}
let threshold = params.get(&self.threshold)[0];
let knee = params.get(&self.knee)[0];
let ratio = params.get(&self.ratio)[0];
let threshold = if knee > 0. {
threshold + knee / 2.
} else {
threshold
};
let half_knee = knee / 2.;
let knee_partial = (1. / ratio - 1.) / (2. * knee);
let attack = params.get(&self.attack)[0];
let release = params.get(&self.release)[0];
let attack_tau = (-1. / (attack * sample_rate)).exp();
let release_tau = (-1. / (release * sample_rate)).exp();
let full_range_gain = threshold + (-threshold / ratio);
let full_range_makeup = 1. / db_to_lin(full_range_gain);
let makeup_gain = lin_to_db(full_range_makeup.powf(0.6));
let mut prev_detector_value = self.prev_detector_value;
let mut reduction_gain = 0.; let mut reduction_gains = [0.; 128]; let mut detector_values = [0.; 128];
for i in 0..RENDER_QUANTUM_SIZE {
let mut max = f32::MIN;
for channel in input.channels().iter() {
let sample = channel[i].abs();
if sample > max {
max = sample;
}
}
let sample_db = lin_to_db(max);
let sample_attenuated = if sample_db <= threshold - half_knee {
sample_db
} else if sample_db <= threshold + half_knee {
sample_db + (sample_db - threshold + half_knee).powi(2) * knee_partial
} else {
threshold + (sample_db - threshold) / ratio
};
let sample_attenuation = sample_db - sample_attenuated;
let detector_value = if sample_attenuation > prev_detector_value {
attack_tau * prev_detector_value + (1. - attack_tau) * sample_attenuation
} else {
release_tau * prev_detector_value + (1. - release_tau) * sample_attenuation
};
detector_values[i] = detector_value;
reduction_gain = -1. * detector_value + makeup_gain;
reduction_gains[i] = db_to_lin(reduction_gain);
prev_detector_value = detector_value;
}
self.prev_detector_value = prev_detector_value;
self.reduction.store(reduction_gain, Ordering::SeqCst);
self.ring_buffer[self.ring_index] = input;
let read_index = (self.ring_index + 1) % ring_size;
let delayed = &self.ring_buffer[read_index];
self.ring_index = read_index;
*output = delayed.clone();
if output.is_silent() {
output.make_silent(); return false;
}
output.channels_mut().iter_mut().for_each(|channel| {
channel
.iter_mut()
.zip(reduction_gains.iter())
.for_each(|(o, g)| *o *= g);
});
true
}
}
#[cfg(test)]
mod tests {
use float_eq::assert_float_eq;
use crate::context::OfflineAudioContext;
use crate::node::AudioScheduledSourceNode;
use super::*;
#[test]
fn test_constructor() {
{
let context = OfflineAudioContext::new(1, 0, 44_100.);
let compressor = DynamicsCompressorNode::new(&context, Default::default());
assert_float_eq!(compressor.attack().value(), 0.003, abs <= 0.);
assert_float_eq!(compressor.knee().value(), 30., abs <= 0.);
assert_float_eq!(compressor.ratio().value(), 12., abs <= 0.);
assert_float_eq!(compressor.release().value(), 0.25, abs <= 0.);
assert_float_eq!(compressor.threshold().value(), -24., abs <= 0.);
}
{
let context = OfflineAudioContext::new(1, 0, 44_100.);
let compressor = DynamicsCompressorNode::new(
&context,
DynamicsCompressorOptions {
attack: 0.5,
knee: 12.,
ratio: 1.,
release: 0.75,
threshold: -60.,
..DynamicsCompressorOptions::default()
},
);
assert_float_eq!(compressor.attack().value(), 0.5, abs <= 0.);
assert_float_eq!(compressor.knee().value(), 12., abs <= 0.);
assert_float_eq!(compressor.ratio().value(), 1., abs <= 0.);
assert_float_eq!(compressor.release().value(), 0.75, abs <= 0.);
assert_float_eq!(compressor.threshold().value(), -60., abs <= 0.);
}
}
#[test]
fn test_inner_delay() {
let sample_rate = 44_100.;
let compressor_delay = 0.006;
let non_zero_index = (compressor_delay * sample_rate / RENDER_QUANTUM_SIZE as f32).ceil()
as usize
* RENDER_QUANTUM_SIZE;
let context = OfflineAudioContext::new(1, 128 * 8, sample_rate);
let compressor = DynamicsCompressorNode::new(&context, Default::default());
compressor.connect(&context.destination());
let mut buffer = context.create_buffer(1, 128 * 5, sample_rate);
let signal = [1.; 128 * 5];
buffer.copy_to_channel(&signal, 0);
let src = context.create_buffer_source();
src.set_buffer(buffer);
src.connect(&compressor);
src.start();
let res = context.start_rendering_sync();
let chan = res.channel_data(0).as_slice();
assert_float_eq!(
chan[0..non_zero_index],
vec![0.; non_zero_index][..],
abs_all <= 0.
);
for sample in chan.iter().take(128 * 8).skip(non_zero_index) {
assert!(*sample != 0.);
}
}
#[test]
fn test_db_to_lin() {
assert_float_eq!(db_to_lin(0.), 1., abs <= 0.);
assert_float_eq!(db_to_lin(-20.), 0.1, abs <= 1e-8);
assert_float_eq!(db_to_lin(-40.), 0.01, abs <= 1e-8);
assert_float_eq!(db_to_lin(-60.), 0.001, abs <= 1e-8);
}
#[test]
fn test_lin_to_db() {
assert_float_eq!(lin_to_db(1.), 0., abs <= 0.);
assert_float_eq!(lin_to_db(0.1), -20., abs <= 0.);
assert_float_eq!(lin_to_db(0.01), -40., abs <= 0.);
assert_float_eq!(lin_to_db(0.001), -60., abs <= 0.);
assert_float_eq!(lin_to_db(0.), -1000., abs <= 0.);
}
}