use non_empty_slice::NonEmptySlice;
use crate::operations::traits::AudioDynamicRange;
use crate::operations::types::{CompressorConfig, DynamicRangeMethod, KneeType, LimiterConfig};
use crate::repr::AudioData;
use crate::traits::StandardSample;
use crate::utils::audio_math::{
amplitude_to_db as linear_to_db, db_to_amplitude as db_to_linear, ms_to_samples,
};
use crate::{
AudioSampleError, AudioSampleResult, AudioSamples, AudioTypeConversion, ConvertTo,
ParameterError,
};
use std::collections::VecDeque;
use std::num::NonZeroUsize;
#[derive(Debug, Clone)]
pub struct EnvelopeFollower {
envelope: f64,
attack_coeff: f64,
release_coeff: f64,
detector_state: f64,
rms_window: VecDeque<f64>,
rms_window_size: usize,
rms_sum: f64,
}
impl EnvelopeFollower {
#[inline]
#[must_use]
pub fn new(
attack_ms: f64,
release_ms: f64,
sample_rate: f64,
detection_method: DynamicRangeMethod,
) -> Self {
let attack_coeff = if attack_ms > 0.0 {
(-1.0 / (attack_ms * 0.001 * sample_rate)).exp()
} else {
0.0
};
let release_coeff = if release_ms > 0.0 {
(-1.0 / (release_ms * 0.001 * sample_rate)).exp()
} else {
0.0
};
let rms_window_size = match detection_method {
DynamicRangeMethod::Rms | DynamicRangeMethod::Hybrid => {
(0.01 * sample_rate).max(1.0) as usize
}
DynamicRangeMethod::Peak => 1,
};
Self {
envelope: 0.0,
attack_coeff,
release_coeff,
detector_state: 0.0,
rms_window: VecDeque::with_capacity(rms_window_size),
rms_window_size,
rms_sum: 0.0,
}
}
#[inline]
pub fn process(&mut self, input: f64, detection_method: DynamicRangeMethod) -> f64 {
let detector_value = match detection_method {
DynamicRangeMethod::Peak => input.abs(),
DynamicRangeMethod::Rms => {
let sample_squared: f64 = input * input;
self.rms_window.push_back(sample_squared);
self.rms_sum += sample_squared;
if self.rms_window.len() > self.rms_window_size
&& let Some(old_sample) = self.rms_window.pop_front()
{
self.rms_sum -= old_sample;
}
if self.rms_window.is_empty() {
0.0
} else {
(self.rms_sum / self.rms_window.len() as f64).sqrt()
}
}
DynamicRangeMethod::Hybrid => {
let peak = input.abs();
let sample_squared = input * input;
self.rms_window.push_back(sample_squared);
self.rms_sum += sample_squared;
if self.rms_window.len() > self.rms_window_size
&& let Some(old_sample) = self.rms_window.pop_front()
{
self.rms_sum -= old_sample;
}
let rms = if self.rms_window.is_empty() {
0.0
} else {
(self.rms_sum / self.rms_window.len() as f64).sqrt()
};
peak.max(rms)
}
};
let coeff = if detector_value > self.detector_state {
self.attack_coeff
} else {
self.release_coeff
};
self.detector_state = detector_value;
self.envelope = coeff * self.envelope + (1.0 - coeff) * detector_value;
self.envelope
}
#[inline]
pub fn reset(&mut self) {
self.envelope = 0.0;
self.detector_state = 0.0;
self.rms_window.clear();
self.rms_sum = 0.0;
}
}
#[derive(Debug, Clone)]
pub struct LookaheadBuffer {
buffer: Vec<f64>,
write_pos: usize,
read_pos: usize,
size: usize,
is_full: bool,
}
impl LookaheadBuffer {
#[inline]
#[must_use]
pub fn new(lookahead_samples: NonZeroUsize) -> Self {
Self {
buffer: vec![0.0; lookahead_samples.get()],
write_pos: 0,
read_pos: 0,
size: lookahead_samples.get(),
is_full: false,
}
}
#[inline]
pub fn process(&mut self, input: f64) -> f64 {
let output = if self.is_full {
self.buffer[self.read_pos]
} else {
0.0
};
self.buffer[self.write_pos] = input;
if self.is_full {
self.read_pos = (self.read_pos + 1) % self.size;
}
self.write_pos = (self.write_pos + 1) % self.size;
if self.write_pos == self.read_pos && !self.is_full {
self.is_full = true;
}
output
}
#[inline]
#[must_use]
pub fn peek(&self) -> f64 {
self.buffer[self.write_pos]
}
#[inline]
pub fn reset(&mut self) {
self.buffer.fill(0.0);
self.write_pos = 0;
self.read_pos = 0;
self.is_full = false;
}
}
#[inline]
#[must_use]
pub fn calculate_compression_gain(
input_level_db: f64,
threshold_db: f64,
ratio: f64,
knee_type: KneeType,
knee_width_db: f64,
) -> f64 {
let overshoot = input_level_db - threshold_db;
if overshoot <= 0.0 {
return 0.0;
}
match knee_type {
KneeType::Hard => {
overshoot - overshoot / ratio
}
KneeType::Soft => {
let half_knee = knee_width_db / 2.0;
if overshoot <= half_knee {
let knee_ratio = overshoot / half_knee;
let smooth_ratio = ((ratio - 1.0) * knee_ratio).mul_add(knee_ratio, 1.0);
overshoot - overshoot / smooth_ratio
} else {
let knee_gain = half_knee - half_knee / ratio;
knee_gain + (overshoot - half_knee) - (overshoot - half_knee) / ratio
}
}
}
}
#[inline]
#[must_use]
pub fn calculate_limiting_gain(
input_level_db: f64,
ceiling_db: f64,
knee_type: KneeType,
knee_width_db: f64,
) -> f64 {
let overshoot = input_level_db - ceiling_db;
if overshoot <= 0.0 {
return 0.0;
}
match knee_type {
KneeType::Hard => {
overshoot
}
KneeType::Soft => {
let half_knee = knee_width_db / 2.0;
if overshoot <= half_knee {
let knee_ratio = overshoot / half_knee;
overshoot * knee_ratio * knee_ratio
} else {
let knee_gain = half_knee;
knee_gain + (overshoot - half_knee)
}
}
}
}
impl<T> AudioDynamicRange for AudioSamples<'_, T>
where
T: StandardSample,
Self: AudioTypeConversion<Sample = T>,
{
fn apply_compressor(&mut self, config: &CompressorConfig) -> AudioSampleResult<()> {
let sample_rate = self.sample_rate_hz();
config.validate(sample_rate)?;
let lookahead_samples = ms_to_samples(config.lookahead_ms, sample_rate);
let lookahead_samples = lookahead_samples.max(1);
let lookahead_samples = unsafe { NonZeroUsize::new_unchecked(lookahead_samples) };
match &mut self.data {
AudioData::Mono(samples) => {
let mut envelope_follower = EnvelopeFollower::new(
config.attack_ms,
config.release_ms,
sample_rate,
config.detection_method,
);
let mut lookahead_buffer = LookaheadBuffer::new(lookahead_samples);
let mut gain_reductions = Vec::with_capacity(samples.len().get());
for &sample in samples.iter() {
let sample_f: f64 = sample.convert_to();
let envelope = envelope_follower.process(sample_f, config.detection_method);
let envelope_db = linear_to_db(envelope);
let gain_reduction_db = calculate_compression_gain(
envelope_db,
config.threshold_db,
config.ratio,
config.knee_type,
config.knee_width_db,
);
gain_reductions.push(gain_reduction_db);
}
for i in 0..samples.len().get() {
let sample_f: f64 = samples[i].convert_to();
let delayed_sample = lookahead_buffer.process(sample_f);
if i >= lookahead_samples.get() {
let gain_reduction_db = gain_reductions[i - lookahead_samples.get()];
let gain_linear = db_to_linear(-gain_reduction_db);
let makeup_gain = db_to_linear(config.makeup_gain_db);
let output_sample = delayed_sample * gain_linear * makeup_gain;
samples[i - lookahead_samples.get()] = output_sample.convert_to();
}
}
for i in 0..lookahead_samples.get() {
let delayed_sample = lookahead_buffer.process(0.0);
if lookahead_samples <= samples.len() {
let sample_idx = samples.len().get() - lookahead_samples.get() + i;
if sample_idx < samples.len().get() {
let gain_reduction_db = gain_reductions[sample_idx];
let gain_linear = db_to_linear(-gain_reduction_db);
let makeup_gain = db_to_linear(config.makeup_gain_db);
let output_sample = delayed_sample * gain_linear * makeup_gain;
samples[sample_idx] = output_sample.convert_to();
}
}
}
}
AudioData::Multi(samples) => {
let num_channels = samples.nrows().get();
let num_samples = samples.ncols().get();
for channel in 0..num_channels {
let mut envelope_follower = EnvelopeFollower::new(
config.attack_ms,
config.release_ms,
sample_rate,
config.detection_method,
);
let mut lookahead_buffer = LookaheadBuffer::new(lookahead_samples);
let mut gain_reductions = Vec::with_capacity(num_samples);
for sample_idx in 0..num_samples {
let sample_f: f64 = samples[[channel, sample_idx]].convert_to();
let envelope = envelope_follower.process(sample_f, config.detection_method);
let envelope_db = linear_to_db(envelope);
let gain_reduction_db = calculate_compression_gain(
envelope_db,
config.threshold_db,
config.ratio,
config.knee_type,
config.knee_width_db,
);
gain_reductions.push(gain_reduction_db);
}
for sample_idx in 0..num_samples {
let sample_f: f64 = samples[[channel, sample_idx]].convert_to();
let delayed_sample = lookahead_buffer.process(sample_f);
if sample_idx >= lookahead_samples.get() {
let gain_reduction_db =
gain_reductions[sample_idx - lookahead_samples.get()];
let gain_linear = db_to_linear(-gain_reduction_db);
let makeup_gain = db_to_linear(config.makeup_gain_db);
let output_sample = delayed_sample * gain_linear * makeup_gain;
samples[[channel, sample_idx - lookahead_samples.get()]] =
output_sample.convert_to();
}
}
for i in 0..lookahead_samples.get() {
let delayed_sample = lookahead_buffer.process(0.0);
if lookahead_samples.get() <= num_samples {
let sample_idx = num_samples - lookahead_samples.get() + i;
if sample_idx < num_samples {
let gain_reduction_db = gain_reductions[sample_idx];
let gain_linear = db_to_linear(-gain_reduction_db);
let makeup_gain = db_to_linear(config.makeup_gain_db);
let output_sample = delayed_sample * gain_linear * makeup_gain;
samples[[channel, sample_idx]] = output_sample.convert_to();
}
}
}
}
}
}
Ok(())
}
fn apply_limiter(&mut self, config: &LimiterConfig) -> AudioSampleResult<()> {
let sample_rate = self.sample_rate_hz();
let config = config.validate(sample_rate)?;
let lookahead_samples = ms_to_samples(config.lookahead_ms, sample_rate);
let lookahead_samples = lookahead_samples.max(1);
let lookahead_samples = unsafe { NonZeroUsize::new_unchecked(lookahead_samples) };
match &mut self.data {
AudioData::Mono(samples) => {
let mut envelope_follower = EnvelopeFollower::new(
config.attack_ms,
config.release_ms,
sample_rate,
config.detection_method,
);
let mut lookahead_buffer = LookaheadBuffer::new(lookahead_samples);
let mut gain_reductions = Vec::with_capacity(samples.len().get());
for &sample in samples.iter() {
let sample_f: f64 = sample.convert_to();
let envelope = envelope_follower.process(sample_f, config.detection_method);
let envelope_db = linear_to_db(envelope);
let gain_reduction_db = calculate_limiting_gain(
envelope_db,
config.ceiling_db,
config.knee_type,
config.knee_width_db,
);
gain_reductions.push(gain_reduction_db);
}
for i in 0..samples.len().get() {
let sample_f: f64 = samples[i].convert_to();
let delayed_sample = lookahead_buffer.process(sample_f);
if i >= lookahead_samples.get() {
let gain_reduction_db = gain_reductions[i - lookahead_samples.get()];
let gain_linear = db_to_linear(-gain_reduction_db);
let output_sample = delayed_sample * gain_linear;
samples[i - lookahead_samples.get()] = output_sample.convert_to();
}
}
for i in 0..lookahead_samples.get() {
let delayed_sample = lookahead_buffer.process(0.0);
if lookahead_samples <= samples.len() {
let sample_idx = samples.len().get() - lookahead_samples.get() + i;
if sample_idx < samples.len().get() {
let gain_reduction_db = gain_reductions[sample_idx];
let gain_linear = db_to_linear(-gain_reduction_db);
let output_sample = delayed_sample * gain_linear;
samples[sample_idx] = output_sample.convert_to();
}
}
}
}
AudioData::Multi(samples) => {
let num_channels = samples.nrows().get();
let num_samples = samples.ncols().get();
for channel in 0..num_channels {
let mut envelope_follower = EnvelopeFollower::new(
config.attack_ms,
config.release_ms,
sample_rate,
config.detection_method,
);
let mut lookahead_buffer = LookaheadBuffer::new(lookahead_samples);
let mut gain_reductions = Vec::with_capacity(num_samples);
for sample_idx in 0..num_samples {
let sample_f: f64 = samples[[channel, sample_idx]].convert_to();
let envelope = envelope_follower.process(sample_f, config.detection_method);
let envelope_db = linear_to_db(envelope);
let gain_reduction_db = calculate_limiting_gain(
envelope_db,
config.ceiling_db,
config.knee_type,
config.knee_width_db,
);
gain_reductions.push(gain_reduction_db);
}
for sample_idx in 0..num_samples {
let sample_f: f64 = samples[[channel, sample_idx]].convert_to();
let delayed_sample = lookahead_buffer.process(sample_f);
if sample_idx >= lookahead_samples.get() {
let gain_reduction_db =
gain_reductions[sample_idx - lookahead_samples.get()];
let gain_linear = db_to_linear(-gain_reduction_db);
let output_sample = delayed_sample * gain_linear;
samples[[channel, sample_idx - lookahead_samples.get()]] =
output_sample.convert_to();
}
}
for i in 0..lookahead_samples.get() {
let delayed_sample = lookahead_buffer.process(0.0);
if lookahead_samples.get() <= num_samples {
let sample_idx = num_samples - lookahead_samples.get() + i;
if sample_idx < num_samples {
let gain_reduction_db = gain_reductions[sample_idx];
let gain_linear = db_to_linear(-gain_reduction_db);
let output_sample = delayed_sample * gain_linear;
samples[[channel, sample_idx]] = output_sample.convert_to();
}
}
}
}
}
}
Ok(())
}
fn apply_compressor_sidechain(
&mut self,
config: &CompressorConfig,
sidechain_signal: &Self,
) -> AudioSampleResult<()> {
let sample_rate = self.sample_rate_hz();
config.validate(sample_rate)?;
if !config.side_chain.enabled {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"side_chain_config",
"Side-chain is not enabled in configuration",
)));
}
match (&mut self.data, &sidechain_signal.data) {
(AudioData::Mono(main_samples), AudioData::Mono(sc_samples)) => {
if main_samples.len() != sc_samples.len() {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"signal_lengths",
"Main signal and sidechain signal must have the same length",
)));
}
let mut envelope_follower = EnvelopeFollower::new(
config.attack_ms,
config.release_ms,
sample_rate,
config.detection_method,
);
let lookahead_samples = ms_to_samples(config.lookahead_ms, sample_rate);
let lookahead_samples = lookahead_samples.max(1);
let lookahead_samples = unsafe { NonZeroUsize::new_unchecked(lookahead_samples) };
let mut lookahead_buffer = LookaheadBuffer::new(lookahead_samples);
let mut gain_reductions = Vec::with_capacity(main_samples.len().get());
for &sc_sample in sc_samples {
let sc_f: f64 = sc_sample.convert_to();
let envelope = envelope_follower.process(sc_f, config.detection_method);
let envelope_db = linear_to_db(envelope);
let gain_reduction_db = calculate_compression_gain(
envelope_db,
config.threshold_db,
config.ratio,
config.knee_type,
config.knee_width_db,
);
gain_reductions.push(gain_reduction_db);
}
for i in 0..main_samples.len().get() {
let sample_f: f64 = main_samples[i].convert_to();
let delayed_sample = lookahead_buffer.process(sample_f);
if i >= lookahead_samples.get() {
let gain_reduction_db = gain_reductions[i - lookahead_samples.get()];
let gain_linear = db_to_linear(-gain_reduction_db);
let makeup_gain = db_to_linear(config.makeup_gain_db);
let output_sample = delayed_sample * gain_linear * makeup_gain;
main_samples[i - lookahead_samples.get()] = output_sample.convert_to();
}
}
for i in 0..lookahead_samples.get() {
let delayed_sample = lookahead_buffer.process(0.0);
let sample_idx = main_samples.len().get() - lookahead_samples.get() + i;
if sample_idx < main_samples.len().get() {
let gain_reduction_db = gain_reductions[sample_idx];
let gain_linear = db_to_linear(-gain_reduction_db);
let makeup_gain = db_to_linear(config.makeup_gain_db);
let output_sample = delayed_sample * gain_linear * makeup_gain;
main_samples[sample_idx] = output_sample.convert_to();
}
}
}
_ => {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Side-chain processing with multi-channel audio not yet implemented",
)));
}
}
Ok(())
}
fn apply_limiter_sidechain(
&mut self,
config: &LimiterConfig,
sidechain_signal: &Self,
) -> AudioSampleResult<()> {
let sample_rate = self.sample_rate_hz();
config.validate(sample_rate)?;
if !config.side_chain.enabled {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"side_chain_config",
"Side-chain is not enabled in configuration",
)));
}
match (&mut self.data, &sidechain_signal.data) {
(AudioData::Mono(main_samples), AudioData::Mono(sc_samples)) => {
if main_samples.len() != sc_samples.len() {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"signal_lengths",
"Main signal and sidechain signal must have the same length",
)));
}
let mut envelope_follower = EnvelopeFollower::new(
config.attack_ms,
config.release_ms,
sample_rate,
config.detection_method,
);
let lookahead_samples = ms_to_samples(config.lookahead_ms, sample_rate);
let lookahead_samples = lookahead_samples.max(1);
let lookahead_samples = unsafe { NonZeroUsize::new_unchecked(lookahead_samples) };
let mut lookahead_buffer = LookaheadBuffer::new(lookahead_samples);
let mut gain_reductions = Vec::with_capacity(main_samples.len().get());
for &sc_sample in sc_samples {
let sc_f: f64 = sc_sample.convert_to();
let envelope = envelope_follower.process(sc_f, config.detection_method);
let envelope_db = linear_to_db(envelope);
let gain_reduction_db = calculate_limiting_gain(
envelope_db,
config.ceiling_db,
config.knee_type,
config.knee_width_db,
);
gain_reductions.push(gain_reduction_db);
}
for i in 0..main_samples.len().get() {
let sample_f: f64 = main_samples[i].convert_to();
let delayed_sample = lookahead_buffer.process(sample_f);
if i >= lookahead_samples.get() {
let gain_reduction_db = gain_reductions[i - lookahead_samples.get()];
let gain_linear = db_to_linear(-gain_reduction_db);
let output_sample = delayed_sample * gain_linear;
main_samples[i - lookahead_samples.get()] = output_sample.convert_to();
}
}
for i in 0..lookahead_samples.get() {
let delayed_sample = lookahead_buffer.process(0.0);
let sample_idx = main_samples.len().get() - lookahead_samples.get() + i;
if sample_idx < main_samples.len().get() {
let gain_reduction_db = gain_reductions[sample_idx];
let gain_linear = db_to_linear(-gain_reduction_db);
let output_sample = delayed_sample * gain_linear;
main_samples[sample_idx] = output_sample.convert_to();
}
}
}
_ => {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Side-chain processing with multi-channel audio not yet implemented",
)));
}
}
Ok(())
}
fn get_compression_curve(
&self,
config: &CompressorConfig,
input_levels_db: &NonEmptySlice<f64>,
) -> AudioSampleResult<Vec<f64>> {
let output_levels = input_levels_db
.iter()
.map(|&input_db| {
let gain_reduction_db = calculate_compression_gain(
input_db,
config.threshold_db,
config.ratio,
config.knee_type,
config.knee_width_db,
);
input_db - gain_reduction_db + config.makeup_gain_db
})
.collect();
Ok(output_levels)
}
fn get_gain_reduction(&self, config: &CompressorConfig) -> AudioSampleResult<Vec<f64>> {
let sample_rate = self.sample_rate_hz();
match &self.data {
AudioData::Mono(samples) => {
let mut envelope_follower = EnvelopeFollower::new(
config.attack_ms,
config.release_ms,
sample_rate,
config.detection_method,
);
let mut gain_reductions = Vec::with_capacity(samples.len().get());
for &sample in samples {
let sample_f: f64 = sample.convert_to();
let envelope = envelope_follower.process(sample_f, config.detection_method);
let envelope_db = linear_to_db(envelope);
let gain_reduction_db = calculate_compression_gain(
envelope_db,
config.threshold_db,
config.ratio,
config.knee_type,
config.knee_width_db,
);
gain_reductions.push(gain_reduction_db);
}
Ok(gain_reductions)
}
AudioData::Multi(samples) => {
let num_samples = samples.ncols().get();
let mut envelope_follower = EnvelopeFollower::new(
config.attack_ms,
config.release_ms,
sample_rate,
config.detection_method,
);
let mut gain_reductions = Vec::with_capacity(num_samples);
for sample_idx in 0..num_samples {
let sample_f: f64 = samples[[0, sample_idx]].convert_to();
let envelope = envelope_follower.process(sample_f, config.detection_method);
let envelope_db = linear_to_db(envelope);
let gain_reduction_db = calculate_compression_gain(
envelope_db,
config.threshold_db,
config.ratio,
config.knee_type,
config.knee_width_db,
);
gain_reductions.push(gain_reduction_db);
}
Ok(gain_reductions)
}
}
}
fn apply_gate(
&mut self,
threshold_db: f64,
ratio: f64,
attack_ms: f64,
release_ms: f64,
) -> AudioSampleResult<()> {
let sample_rate = self.sample_rate_hz();
match &mut self.data {
AudioData::Mono(samples) => {
let mut envelope_follower = EnvelopeFollower::new(
attack_ms,
release_ms,
sample_rate,
DynamicRangeMethod::Peak, );
for sample in samples.iter_mut() {
let sample_f: f64 = (*sample).convert_to();
let envelope = envelope_follower.process(sample_f, DynamicRangeMethod::Peak);
let envelope_db = linear_to_db(envelope);
let gain_reduction_db = if envelope_db < threshold_db {
let undershoot = threshold_db - envelope_db;
undershoot * (ratio - 1.0) / ratio
} else {
0.0
};
let gain_linear = db_to_linear(-gain_reduction_db);
let output_sample = sample_f * gain_linear;
*sample = output_sample.convert_to();
}
}
AudioData::Multi(samples) => {
let num_channels = samples.nrows().get();
let num_samples = samples.ncols().get();
for channel in 0..num_channels {
let mut envelope_follower = EnvelopeFollower::new(
attack_ms,
release_ms,
sample_rate,
DynamicRangeMethod::Peak,
);
for sample_idx in 0..num_samples {
let sample_f: f64 = samples[[channel, sample_idx]].convert_to();
let envelope =
envelope_follower.process(sample_f, DynamicRangeMethod::Peak);
let envelope_db = linear_to_db(envelope);
let gain_reduction_db = if envelope_db < threshold_db {
let undershoot = threshold_db - envelope_db;
undershoot * (ratio - 1.0) / ratio
} else {
0.0
};
let gain_linear = db_to_linear(-gain_reduction_db);
let output_sample = sample_f * gain_linear;
samples[[channel, sample_idx]] = output_sample.convert_to();
}
}
}
}
Ok(())
}
fn apply_expander(
&mut self,
threshold_db: f64,
ratio: f64,
attack_ms: f64,
release_ms: f64,
) -> AudioSampleResult<()> {
let sample_rate = self.sample_rate_hz();
match &mut self.data {
AudioData::Mono(samples) => {
let mut envelope_follower = EnvelopeFollower::new(
attack_ms,
release_ms,
sample_rate,
DynamicRangeMethod::Rms, );
for sample in samples.iter_mut() {
let sample_f: f64 = (*sample).convert_to();
let envelope = envelope_follower.process(sample_f, DynamicRangeMethod::Rms);
let envelope_db = linear_to_db(envelope);
let gain_change_db = if envelope_db < threshold_db {
let undershoot = threshold_db - envelope_db;
undershoot * (ratio - 1.0) } else {
1.0
};
let gain_linear = db_to_linear(-gain_change_db); let output_sample = sample_f * gain_linear;
*sample = output_sample.convert_to();
}
}
AudioData::Multi(samples) => {
let num_channels = samples.nrows().get();
let num_samples = samples.ncols().get();
for channel in 0..num_channels {
let mut envelope_follower = EnvelopeFollower::new(
attack_ms,
release_ms,
sample_rate,
DynamicRangeMethod::Rms,
);
for sample_idx in 0..num_samples {
let sample_f: f64 = samples[[channel, sample_idx]].convert_to();
let envelope = envelope_follower.process(sample_f, DynamicRangeMethod::Rms);
let envelope_db = linear_to_db(envelope);
let gain_change_db = if envelope_db < threshold_db {
let undershoot = threshold_db - envelope_db;
undershoot * (ratio - 1.0) } else {
0.0
};
let gain_linear = db_to_linear(-gain_change_db); let output_sample = sample_f * gain_linear;
samples[[channel, sample_idx]] = output_sample.convert_to();
}
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AudioSamples;
use crate::sample_rate;
use ndarray::Array1;
use non_empty_slice::non_empty_vec;
#[test]
fn test_compressor_basic() {
let data = Array1::from_vec(vec![0.1f32, 0.8, 0.2, 0.9, 0.1]);
let mut audio = AudioSamples::new_mono(data, sample_rate!(44100)).unwrap();
let config = CompressorConfig::new();
let result = audio.apply_compressor(&config);
assert!(result.is_ok());
}
#[test]
fn test_limiter_basic() {
let data = Array1::from_vec(vec![0.1f32, 0.8, 0.2, 0.9, 0.1]);
let mut audio = AudioSamples::new_mono(data, sample_rate!(44100)).unwrap();
let config = LimiterConfig::default();
let result = audio.apply_limiter(&config);
assert!(result.is_ok());
}
#[test]
fn test_compression_curve() {
let data = Array1::from_vec(vec![0.1f32, 0.2, 0.3, 0.4, 0.5]);
let audio = AudioSamples::new_mono(data, sample_rate!(44100)).unwrap();
let config = CompressorConfig::default();
let input_levels = non_empty_vec![-40.0, -30.0, -20.0, -10.0, 0.0];
let result = audio.get_compression_curve(&config, input_levels.as_non_empty_slice());
assert!(result.is_ok());
let output_levels = result.unwrap();
assert_eq!(output_levels.len(), input_levels.len().get());
for (i, &input_db) in input_levels.iter().enumerate() {
if input_db > config.threshold_db {
assert!(
output_levels[i] < input_db,
"Output level {} should be less than input level {} for compression",
output_levels[i],
input_db
);
}
}
}
#[test]
fn test_gain_reduction() {
let data = Array1::from_vec(vec![0.1f32, 0.8, 0.2, 0.9, 0.1]);
let audio = AudioSamples::new_mono(data, sample_rate!(44100)).unwrap();
let config = CompressorConfig::new();
let result = audio.get_gain_reduction(&config);
assert!(result.is_ok());
let gain_reductions = result.unwrap();
assert_eq!(gain_reductions.len(), 5);
for &gr in &gain_reductions {
assert!(gr >= 0.0, "Gain reduction should be non-negative: {}", gr);
}
}
#[test]
fn test_gate_basic() {
let data = Array1::from_vec(vec![0.001f32, 0.8, 0.002, 0.9, 0.001]);
let mut audio = AudioSamples::new_mono(data, sample_rate!(44100)).unwrap();
let result = audio.apply_gate(-20.0, 10.0, 1.0, 10.0);
assert!(result.is_ok());
}
#[test]
fn test_expander_basic() {
let data = Array1::from_vec(vec![0.1f32, 0.8, 0.2, 0.9, 0.1]);
let mut audio = AudioSamples::new_mono(data, sample_rate!(44100)).unwrap();
let result = audio.apply_expander(-20.0, 2.0, 1.0, 10.0);
assert!(result.is_ok());
}
#[test]
fn test_envelope_follower() {
let mut envelope = EnvelopeFollower::new(1.0, 10.0, 44100.0, DynamicRangeMethod::Peak);
let output1 = envelope.process(0.5, DynamicRangeMethod::Peak);
let output2 = envelope.process(0.5, DynamicRangeMethod::Peak);
assert!(output1 > 0.0);
assert!(output2 > output1); }
#[test]
fn test_lookahead_buffer() {
let mut buffer = LookaheadBuffer::new(crate::nzu!(3));
assert_eq!(buffer.process(1.0), 0.0); assert_eq!(buffer.process(2.0), 0.0); assert_eq!(buffer.process(3.0), 0.0); assert_eq!(buffer.process(4.0), 1.0); assert_eq!(buffer.process(5.0), 2.0); }
#[test]
fn test_db_linear_conversion() {
let linear: f64 = 0.5;
let db = linear_to_db(linear);
let back_to_linear = db_to_linear(db);
assert!((linear - back_to_linear).abs() < 1e-10);
}
#[test]
fn test_compression_gain_calculation() {
let gain_reduction: f64 = calculate_compression_gain(
-6.0, -12.0, 4.0, KneeType::Hard,
2.0, );
assert!((gain_reduction - 4.5).abs() < 1e-10);
}
#[test]
fn test_limiting_gain_calculation() {
let gain_reduction: f64 = calculate_limiting_gain(
-0.5, -1.0, KneeType::Hard,
1.0, );
assert!((gain_reduction - 0.5).abs() < 1e-10);
}
#[test]
fn test_compressor_config_validation() {
let mut config: CompressorConfig = CompressorConfig::new();
assert!(config.validate(44100.0).is_ok());
config.threshold_db = 5.0;
assert!(config.validate(44100.0).is_err());
config.threshold_db = -12.0;
config.ratio = 0.5;
assert!(config.validate(44100.0).is_err());
}
#[test]
fn test_limiter_config_validation() {
let config = LimiterConfig::default();
let config = config.validate(44100.0);
assert!(
config.is_ok(),
"Valid limiter config failed validation: {:?}",
config
);
let mut config = config.expect("safe unwrap");
config.ceiling_db = 5.0;
let config = config.validate(44100.0);
assert!(
config.is_err(),
"Invalid ceiling_db passed validation: {:?}",
config
);
let mut config = LimiterConfig::default();
config.ceiling_db = -1.0;
config.attack_ms = 0.0;
assert!(config.validate(44100.0).is_err());
}
#[test]
fn test_multi_channel_compressor() {
let data = ndarray::Array2::from_shape_vec(
(2, 5),
vec![0.1f32, 0.8, 0.2, 0.9, 0.1, 0.2f32, 0.7, 0.3, 0.8, 0.2],
)
.unwrap();
let mut audio = AudioSamples::new_multi_channel(data.into(), sample_rate!(44100)).unwrap();
let config = CompressorConfig::new();
let result = audio.apply_compressor(&config);
assert!(result.is_ok());
}
#[test]
fn test_multi_channel_limiter() {
let data = ndarray::Array2::from_shape_vec(
(2, 5),
vec![0.1f32, 0.8, 0.2, 0.9, 0.1, 0.2f32, 0.7, 0.3, 0.8, 0.2],
)
.unwrap();
let mut audio = AudioSamples::new_multi_channel(data.into(), sample_rate!(44100)).unwrap();
let config = LimiterConfig::default();
let result = audio.apply_limiter(&config);
assert!(result.is_ok());
}
#[test]
fn test_compressor_presets() {
let data = Array1::from_vec(vec![0.1f32, 0.8, 0.2, 0.9, 0.1]);
let mut audio = AudioSamples::new_mono(data, sample_rate!(44100)).unwrap();
let vocal_config = CompressorConfig::vocal();
assert!(audio.apply_compressor(&vocal_config).is_ok());
let drum_config = CompressorConfig::drum();
assert!(audio.apply_compressor(&drum_config).is_ok());
let bus_config = CompressorConfig::bus();
assert!(audio.apply_compressor(&bus_config).is_ok());
}
#[test]
fn test_limiter_presets() {
let data = Array1::from_vec(vec![0.1f32, 0.8, 0.2, 0.9, 0.1]);
let mut audio = AudioSamples::new_mono(data, sample_rate!(44100)).unwrap();
let transparent_config = LimiterConfig::transparent();
assert!(audio.apply_limiter(&transparent_config).is_ok());
let mastering_config = LimiterConfig::mastering();
assert!(audio.apply_limiter(&mastering_config).is_ok());
let broadcast_config = LimiterConfig::broadcast();
assert!(audio.apply_limiter(&broadcast_config).is_ok());
}
}