#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DetectionMode {
Peak,
Rms,
}
#[derive(Debug, Clone)]
pub struct EnvelopeDetector {
attack_coeff: f64,
release_coeff: f64,
mode: DetectionMode,
envelope: f64,
}
impl EnvelopeDetector {
pub fn new(attack_ms: f64, release_ms: f64, sample_rate: f64, mode: DetectionMode) -> Self {
Self {
attack_coeff: time_constant(attack_ms, sample_rate),
release_coeff: time_constant(release_ms, sample_rate),
mode,
envelope: 0.0,
}
}
pub fn set_times(&mut self, attack_ms: f64, release_ms: f64, sample_rate: f64) {
self.attack_coeff = time_constant(attack_ms, sample_rate);
self.release_coeff = time_constant(release_ms, sample_rate);
}
pub fn reset(&mut self) {
self.envelope = 0.0;
}
pub fn level(&self) -> f64 {
self.envelope
}
#[inline]
pub fn process_sample(&mut self, input: f32) -> f32 {
let input_level = match self.mode {
DetectionMode::Peak => (input as f64).abs(),
DetectionMode::Rms => (input as f64) * (input as f64),
};
let coeff = if input_level > self.envelope {
self.attack_coeff
} else {
self.release_coeff
};
self.envelope = coeff * self.envelope + (1.0 - coeff) * input_level;
let output = match self.mode {
DetectionMode::Peak => self.envelope,
DetectionMode::Rms => self.envelope.sqrt(),
};
output as f32
}
pub fn process(&mut self, input: &[f32], output: &mut [f32]) {
debug_assert_eq!(input.len(), output.len());
for (i, o) in input.iter().zip(output.iter_mut()) {
*o = self.process_sample(*i);
}
}
pub fn process_to_vec(&mut self, input: &[f32]) -> Vec<f32> {
let mut output = vec![0.0f32; input.len()];
self.process(input, &mut output);
output
}
}
fn time_constant(time_ms: f64, sample_rate: f64) -> f64 {
if time_ms <= 0.0 {
return 0.0;
}
let samples = time_ms * 0.001 * sample_rate;
(-1.0 / samples).exp()
}
#[derive(Debug, Clone)]
pub struct DynamicsProcessor {
pub detector: EnvelopeDetector,
threshold_linear: f64,
ratio: f64,
knee_db: f64,
makeup_linear: f64,
gate_mode: bool,
}
impl DynamicsProcessor {
pub fn compressor(
threshold_db: f64,
ratio: f64,
attack_ms: f64,
release_ms: f64,
knee_db: f64,
makeup_db: f64,
sample_rate: f64,
) -> Self {
Self {
detector: EnvelopeDetector::new(
attack_ms,
release_ms,
sample_rate,
DetectionMode::Peak,
),
threshold_linear: crate::db::db_to_linear(threshold_db),
ratio,
knee_db,
makeup_linear: crate::db::db_to_linear(makeup_db),
gate_mode: false,
}
}
pub fn gate(
threshold_db: f64,
ratio: f64,
attack_ms: f64,
release_ms: f64,
sample_rate: f64,
) -> Self {
Self {
detector: EnvelopeDetector::new(
attack_ms,
release_ms,
sample_rate,
DetectionMode::Peak,
),
threshold_linear: crate::db::db_to_linear(threshold_db),
ratio,
knee_db: 0.0,
makeup_linear: 1.0,
gate_mode: true,
}
}
pub fn reset(&mut self) {
self.detector.reset();
}
#[inline]
pub fn compute_gain(&mut self, input: f32) -> f32 {
let env = self.detector.process_sample(input) as f64;
let gain = if self.gate_mode {
if env < self.threshold_linear {
let env_db = crate::db::linear_to_db(env);
let thresh_db = crate::db::linear_to_db(self.threshold_linear);
let diff = thresh_db - env_db;
let reduction = diff * (1.0 - 1.0 / self.ratio);
crate::db::db_to_linear(-reduction)
} else {
1.0
}
} else {
if env > self.threshold_linear {
let env_db = crate::db::linear_to_db(env);
let thresh_db = crate::db::linear_to_db(self.threshold_linear);
let over = env_db - thresh_db;
let compressed = if self.knee_db > 0.0 && over < self.knee_db {
let knee_factor = over / self.knee_db;
over * (1.0 - knee_factor * (1.0 - 1.0 / self.ratio))
} else {
thresh_db + over / self.ratio - thresh_db
};
crate::db::db_to_linear(compressed - over)
} else {
1.0
}
};
(gain * self.makeup_linear) as f32
}
pub fn process(&mut self, samples: &mut [f32]) {
for s in samples.iter_mut() {
let gain = self.compute_gain(*s);
*s *= gain;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_envelope_rises_on_signal() {
let mut env = EnvelopeDetector::new(1.0, 100.0, 44100.0, DetectionMode::Peak);
for _ in 0..100 {
env.process_sample(0.0);
}
assert!(env.level() < 0.001);
for _ in 0..1000 {
env.process_sample(1.0);
}
assert!(
env.level() > 0.9,
"Envelope should rise to ~1.0, got {}",
env.level()
);
}
#[test]
fn test_envelope_falls_on_silence() {
let mut env = EnvelopeDetector::new(0.1, 50.0, 44100.0, DetectionMode::Peak);
for _ in 0..2000 {
env.process_sample(1.0);
}
assert!(env.level() > 0.9);
for _ in 0..10000 {
env.process_sample(0.0);
}
assert!(
env.level() < 0.02,
"Envelope should decay, got {}",
env.level()
);
}
#[test]
fn test_rms_mode() {
let mut env = EnvelopeDetector::new(1.0, 100.0, 44100.0, DetectionMode::Rms);
for _ in 0..5000 {
env.process_sample(0.5);
}
let level = env.process_sample(0.5);
assert!(
(level - 0.5).abs() < 0.05,
"RMS of constant 0.5 should be ~0.5, got {}",
level
);
}
#[test]
fn test_compressor_attenuates_loud() {
let mut comp = DynamicsProcessor::compressor(
-20.0, 4.0, 1.0, 50.0, 0.0, 0.0, 44100.0,
);
let mut samples = vec![1.0f32; 4000];
comp.process(&mut samples);
let last = samples[3999].abs();
assert!(
last < 0.5,
"Compressed output should be < 0.5, got {}",
last
);
}
#[test]
fn test_gate_silences_quiet() {
let mut gate = DynamicsProcessor::gate(
-30.0, 100.0, 0.1, 10.0, 44100.0,
);
let mut samples = vec![0.001f32; 4000];
gate.process(&mut samples);
let last = samples[3999].abs();
assert!(
last < 0.0005,
"Gated signal should be near zero, got {}",
last
);
}
}