use no_denormals::*;
use crate::dsp::{db_to_ratio, ratio_to_db};
pub struct Compression {
pub threshold: f32,
pub ratio: f32,
pub attack: f32,
pub release: f32,
pub makeup: f32,
pub knee: f32,
envelope: f32,
attack_coeff: f32,
release_coeff: f32,
gain_scratch: Vec<f32>,
}
impl Compression {
pub fn new(sample_rate: f32) -> Self {
let mut comp = Self {
threshold: -20.0,
ratio: 4.0,
attack: 10.0,
release: 100.0,
makeup: 0.0,
knee: 0.0,
envelope: 0.0,
attack_coeff: 0.0,
release_coeff: 0.0,
gain_scratch: Vec::new(),
};
comp.update_coefficients(sample_rate);
comp
}
pub fn set_block_size(&mut self, block_size: usize) {
if self.gain_scratch.len() < block_size {
self.gain_scratch.resize(block_size, 0.0);
}
}
pub fn update_coefficients(&mut self, sample_rate: f32) {
self.attack_coeff = (-1.0 / (self.attack * 0.001 * sample_rate)).exp();
self.release_coeff = (-1.0 / (self.release * 0.001 * sample_rate)).exp();
}
#[inline]
fn compute_gain(&self, input_db: f32) -> f32 {
if self.knee > 0.0 {
let half_knee = self.knee * 0.5;
let lower = self.threshold - half_knee;
let upper = self.threshold + half_knee;
if input_db <= lower {
0.0
} else if input_db >= upper {
(self.threshold + (input_db - self.threshold) / self.ratio) - input_db
} else {
let x = input_db - lower;
let slope = 1.0 / self.ratio - 1.0;
slope * x * x / (2.0 * self.knee)
}
} else {
if input_db <= self.threshold {
0.0
} else {
(self.threshold + (input_db - self.threshold) / self.ratio) - input_db
}
}
}
pub fn run(&mut self, input: &[f32], output: &mut [f32]) {
let len = input.len().min(output.len());
let makeup_linear = db_to_ratio(self.makeup);
self.set_block_size(len);
unsafe {
no_denormals(|| {
for (index, &x) in input[..len].iter().enumerate() {
let input_abs = x.abs();
let input_db = if input_abs > 1e-10 {
20.0 * input_abs.log10()
} else {
-200.0
};
let target_gr = self.compute_gain(input_db);
let coeff = if target_gr < self.envelope {
self.attack_coeff
} else {
self.release_coeff
};
self.envelope = target_gr + coeff * (self.envelope - target_gr);
self.gain_scratch[index] = db_to_ratio(self.envelope) * makeup_linear;
}
crate::simd::mul_elementwise(
&mut output[..len],
&input[..len],
&self.gain_scratch[..len],
);
});
}
}
}
pub struct Limit {
pub gain: f32,
pub ceiling: f32,
pub release: f32,
envelope: f32,
release_coeff: f32,
gain_scratch: Vec<f32>,
}
impl Limit {
pub fn new(sample_rate: f32) -> Self {
let mut lim = Self {
gain: 0.0,
ceiling: 0.0,
release: 100.0,
envelope: 1.0,
release_coeff: 0.0,
gain_scratch: Vec::new(),
};
lim.update_coefficients(sample_rate);
lim
}
pub fn update_coefficients(&mut self, sample_rate: f32) {
self.release_coeff = (-1.0 / (self.release * 0.001 * sample_rate)).exp();
}
pub fn set_block_size(&mut self, block_size: usize) {
if self.gain_scratch.len() < block_size {
self.gain_scratch.resize(block_size, 0.0);
}
}
#[inline]
pub fn process(&mut self, input: f32) -> f32 {
let gain_linear = db_to_ratio(self.gain);
let ceiling_linear = db_to_ratio(self.ceiling);
let amplified = input * gain_linear;
let abs_sample = amplified.abs();
let target = if abs_sample > ceiling_linear {
ceiling_linear / abs_sample
} else {
1.0
};
if target < self.envelope {
self.envelope = target;
} else {
self.envelope = target + self.release_coeff * (self.envelope - target);
}
amplified * self.envelope
}
pub fn run(&mut self, input: &[f32], output: &mut [f32]) {
let len = input.len().min(output.len());
let gain_linear = db_to_ratio(self.gain);
let ceiling_linear = db_to_ratio(self.ceiling);
self.set_block_size(len);
unsafe {
no_denormals(|| {
for (index, &x) in input[..len].iter().enumerate() {
let amplified = x * gain_linear;
let abs_sample = amplified.abs();
let target = if abs_sample > ceiling_linear {
ceiling_linear / abs_sample
} else {
1.0
};
if target < self.envelope {
self.envelope = target;
} else {
self.envelope = target + self.release_coeff * (self.envelope - target);
}
self.gain_scratch[index] = gain_linear * self.envelope;
}
crate::simd::mul_elementwise(
&mut output[..len],
&input[..len],
&self.gain_scratch[..len],
);
});
}
}
}
pub struct Gate {
pub threshold: f32,
pub attack: f32,
pub hold: f32,
pub release: f32,
pub range: f32,
envelope: f32,
hold_remaining_samples: usize,
attack_coeff: f32,
release_coeff: f32,
sample_rate: f32,
gain_scratch: Vec<f32>,
}
impl Gate {
pub fn new(sample_rate: f32) -> Self {
let mut gate = Self {
threshold: -40.0,
attack: 1.0,
hold: 50.0,
release: 100.0,
range: -60.0,
envelope: 0.0,
hold_remaining_samples: 0,
attack_coeff: 0.0,
release_coeff: 0.0,
sample_rate,
gain_scratch: Vec::new(),
};
gate.update_coefficients(sample_rate);
gate
}
pub fn update_coefficients(&mut self, sample_rate: f32) {
self.sample_rate = sample_rate;
self.attack_coeff = (-1.0 / (self.attack.max(0.001) * 0.001 * sample_rate)).exp();
self.release_coeff = (-1.0 / (self.release.max(0.001) * 0.001 * sample_rate)).exp();
}
pub fn set_block_size(&mut self, block_size: usize) {
if self.gain_scratch.len() < block_size {
self.gain_scratch.resize(block_size, 0.0);
}
}
#[inline]
pub fn process(&mut self, input: f32) -> f32 {
let input_abs = input.abs();
let input_db = if input_abs > 1e-10 {
ratio_to_db(input_abs)
} else {
-200.0
};
let target_db = if input_db >= self.threshold {
self.hold_remaining_samples = (self.hold * 0.001 * self.sample_rate) as usize;
0.0
} else if self.hold_remaining_samples > 0 {
self.hold_remaining_samples -= 1;
0.0
} else {
self.range
};
let target_linear = db_to_ratio(target_db);
let coeff = if target_linear > self.envelope {
self.attack_coeff
} else {
self.release_coeff
};
self.envelope = target_linear + coeff * (self.envelope - target_linear);
input * self.envelope
}
pub fn run(&mut self, input: &[f32], output: &mut [f32]) {
let len = input.len().min(output.len());
self.set_block_size(len);
unsafe {
no_denormals(|| {
for (index, &x) in input[..len].iter().enumerate() {
let input_abs = x.abs();
let input_db = if input_abs > 1e-10 {
ratio_to_db(input_abs)
} else {
-200.0
};
let target_db = if input_db >= self.threshold {
self.hold_remaining_samples =
(self.hold * 0.001 * self.sample_rate) as usize;
0.0
} else if self.hold_remaining_samples > 0 {
self.hold_remaining_samples -= 1;
0.0
} else {
self.range
};
let target_linear = db_to_ratio(target_db);
let coeff = if target_linear > self.envelope {
self.attack_coeff
} else {
self.release_coeff
};
self.envelope = target_linear + coeff * (self.envelope - target_linear);
self.gain_scratch[index] = self.envelope;
}
crate::simd::mul_elementwise(
&mut output[..len],
&input[..len],
&self.gain_scratch[..len],
);
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gate_opens_above_threshold() {
let mut gate = Gate::new(44100.0);
gate.threshold = -20.0;
gate.attack = 0.1;
let mut last = 0.0;
for _ in 0..4410 {
last = gate.process(0.5);
}
assert!(
(last.abs() / 0.5) > 0.9,
"gate should be mostly open for a loud signal, got gain {}",
last / 0.5
);
}
#[test]
fn test_gate_closes_below_threshold_after_hold() {
let mut gate = Gate::new(44100.0);
gate.threshold = -20.0;
gate.hold = 1.0;
gate.release = 5.0;
gate.range = -60.0;
let mut last = 0.0;
for _ in 0..44100 {
last = gate.process(0.0001);
}
let gain_db = ratio_to_db((last / 0.0001).abs());
assert!(
gain_db < -50.0,
"gate should be mostly closed for a quiet signal, got {gain_db} dB"
);
}
}