#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GateState {
Closed,
Opening,
Open,
Closing,
Hold,
}
#[derive(Debug, Clone)]
pub struct NoiseGateConfig {
pub open_threshold_db: f64,
pub close_threshold_db: f64,
pub attack_secs: f64,
pub release_secs: f64,
pub hold_secs: f64,
pub range_db: f64,
pub lookahead_secs: f64,
pub sample_rate: f64,
}
impl Default for NoiseGateConfig {
fn default() -> Self {
Self {
open_threshold_db: -40.0,
close_threshold_db: -45.0,
attack_secs: 0.001,
release_secs: 0.05,
hold_secs: 0.01,
range_db: -80.0,
lookahead_secs: 0.0,
sample_rate: 48000.0,
}
}
}
#[derive(Debug, Clone)]
pub struct NoiseGate {
config: NoiseGateConfig,
envelope: f64,
state: GateState,
hold_counter: usize,
hold_samples: usize,
attack_coeff: f64,
release_coeff: f64,
range_linear: f64,
lookahead_buffer: Vec<f64>,
lookahead_pos: usize,
lookahead_samples: usize,
}
impl NoiseGate {
#[allow(clippy::cast_precision_loss)]
pub fn new(config: NoiseGateConfig) -> Self {
let attack_coeff = if config.attack_secs > 0.0 {
(-1.0 / (config.attack_secs * config.sample_rate)).exp()
} else {
0.0
};
let release_coeff = if config.release_secs > 0.0 {
(-1.0 / (config.release_secs * config.sample_rate)).exp()
} else {
0.0
};
let hold_samples = (config.hold_secs * config.sample_rate) as usize;
let range_linear = db_to_linear(config.range_db);
let lookahead_samples = (config.lookahead_secs * config.sample_rate) as usize;
let lookahead_buffer = vec![0.0; lookahead_samples.max(1)];
Self {
config,
envelope: 0.0,
state: GateState::Closed,
hold_counter: 0,
hold_samples,
attack_coeff,
release_coeff,
range_linear,
lookahead_buffer,
lookahead_pos: 0,
lookahead_samples,
}
}
pub fn state(&self) -> GateState {
self.state
}
pub fn envelope(&self) -> f64 {
self.envelope
}
pub fn current_gain(&self) -> f64 {
self.range_linear + (1.0 - self.range_linear) * self.envelope
}
pub fn reset(&mut self) {
self.envelope = 0.0;
self.state = GateState::Closed;
self.hold_counter = 0;
self.lookahead_pos = 0;
for s in &mut self.lookahead_buffer {
*s = 0.0;
}
}
pub fn process_block(&mut self, samples: &mut [f64]) {
for sample in samples.iter_mut() {
let detection_level = sample.abs();
let detection_db = linear_to_db(detection_level);
match self.state {
GateState::Closed | GateState::Closing => {
if detection_db >= self.config.open_threshold_db {
self.state = GateState::Opening;
}
}
GateState::Opening => {
if detection_db < self.config.close_threshold_db {
self.state = GateState::Closing;
} else if self.envelope >= 0.999 {
self.state = GateState::Open;
}
}
GateState::Open => {
if detection_db < self.config.close_threshold_db {
self.state = GateState::Hold;
self.hold_counter = self.hold_samples;
}
}
GateState::Hold => {
if detection_db >= self.config.open_threshold_db {
self.state = GateState::Open;
} else if self.hold_counter == 0 {
self.state = GateState::Closing;
} else {
self.hold_counter -= 1;
}
}
}
let target = match self.state {
GateState::Opening | GateState::Open | GateState::Hold => 1.0,
GateState::Closed | GateState::Closing => 0.0,
};
let coeff = if target > self.envelope {
self.attack_coeff
} else {
self.release_coeff
};
self.envelope = target + coeff * (self.envelope - target);
let gain = self.current_gain();
if self.lookahead_samples > 0 {
let delayed = self.lookahead_buffer[self.lookahead_pos];
self.lookahead_buffer[self.lookahead_pos] = *sample;
self.lookahead_pos = (self.lookahead_pos + 1) % self.lookahead_buffer.len();
*sample = delayed * gain;
} else {
*sample *= gain;
}
}
}
pub fn process_with_sidechain(&mut self, audio: &mut [f64], sidechain: &[f64]) {
let len = audio.len().min(sidechain.len());
for i in 0..len {
let detection_level = sidechain[i].abs();
let detection_db = linear_to_db(detection_level);
match self.state {
GateState::Closed | GateState::Closing => {
if detection_db >= self.config.open_threshold_db {
self.state = GateState::Opening;
}
}
GateState::Opening => {
if detection_db < self.config.close_threshold_db {
self.state = GateState::Closing;
} else if self.envelope >= 0.999 {
self.state = GateState::Open;
}
}
GateState::Open => {
if detection_db < self.config.close_threshold_db {
self.state = GateState::Hold;
self.hold_counter = self.hold_samples;
}
}
GateState::Hold => {
if detection_db >= self.config.open_threshold_db {
self.state = GateState::Open;
} else if self.hold_counter == 0 {
self.state = GateState::Closing;
} else {
self.hold_counter -= 1;
}
}
}
let target = match self.state {
GateState::Opening | GateState::Open | GateState::Hold => 1.0,
GateState::Closed | GateState::Closing => 0.0,
};
let coeff = if target > self.envelope {
self.attack_coeff
} else {
self.release_coeff
};
self.envelope = target + coeff * (self.envelope - target);
audio[i] *= self.current_gain();
}
}
}
pub fn linear_to_db(linear: f64) -> f64 {
if linear <= 0.0 {
-f64::INFINITY
} else {
20.0 * linear.log10()
}
}
pub fn db_to_linear(db: f64) -> f64 {
if db <= -200.0 {
0.0
} else {
10.0_f64.powf(db / 20.0)
}
}
#[allow(clippy::cast_precision_loss)]
pub fn rms_level(samples: &[f64]) -> f64 {
if samples.is_empty() {
return 0.0;
}
let sum_sq: f64 = samples.iter().map(|&s| s * s).sum();
(sum_sq / samples.len() as f64).sqrt()
}
pub fn peak_level(samples: &[f64]) -> f64 {
samples.iter().map(|s| s.abs()).fold(0.0_f64, f64::max)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let cfg = NoiseGateConfig::default();
assert!((cfg.open_threshold_db - (-40.0)).abs() < 1e-10);
assert!((cfg.close_threshold_db - (-45.0)).abs() < 1e-10);
assert!((cfg.sample_rate - 48000.0).abs() < 1e-10);
}
#[test]
fn test_gate_initial_state() {
let gate = NoiseGate::new(NoiseGateConfig::default());
assert_eq!(gate.state(), GateState::Closed);
assert!((gate.envelope() - 0.0).abs() < 1e-10);
}
#[test]
fn test_gate_opens_on_loud_signal() {
let config = NoiseGateConfig {
open_threshold_db: -40.0,
close_threshold_db: -45.0,
attack_secs: 0.0001,
release_secs: 0.001,
hold_secs: 0.0,
range_db: -80.0,
lookahead_secs: 0.0,
sample_rate: 48000.0,
};
let mut gate = NoiseGate::new(config);
let mut samples = vec![0.5; 1000];
gate.process_block(&mut samples);
assert!(gate.envelope() > 0.9);
}
#[test]
fn test_gate_stays_closed_on_quiet_signal() {
let config = NoiseGateConfig {
open_threshold_db: -20.0,
..NoiseGateConfig::default()
};
let mut gate = NoiseGate::new(config);
let mut samples = vec![0.0001; 1000];
gate.process_block(&mut samples);
assert!(gate.envelope() < 0.1);
}
#[test]
fn test_gate_attenuates_quiet_signal() {
let config = NoiseGateConfig {
open_threshold_db: -10.0,
close_threshold_db: -15.0,
range_db: -60.0,
attack_secs: 0.0,
release_secs: 0.0,
hold_secs: 0.0,
lookahead_secs: 0.0,
sample_rate: 48000.0,
};
let mut gate = NoiseGate::new(config);
let mut samples = vec![0.001; 500]; gate.process_block(&mut samples);
let max_out = samples.iter().map(|s| s.abs()).fold(0.0_f64, f64::max);
assert!(max_out < 0.001);
}
#[test]
fn test_gate_reset() {
let mut gate = NoiseGate::new(NoiseGateConfig::default());
let mut samples = vec![1.0; 100];
gate.process_block(&mut samples);
gate.reset();
assert_eq!(gate.state(), GateState::Closed);
assert!((gate.envelope() - 0.0).abs() < 1e-10);
}
#[test]
fn test_linear_to_db() {
assert!((linear_to_db(1.0) - 0.0).abs() < 1e-10);
assert!((linear_to_db(0.1) - (-20.0)).abs() < 0.01);
assert_eq!(linear_to_db(0.0), -f64::INFINITY);
}
#[test]
fn test_db_to_linear() {
assert!((db_to_linear(0.0) - 1.0).abs() < 1e-10);
assert!((db_to_linear(-20.0) - 0.1).abs() < 0.001);
assert!((db_to_linear(-200.0) - 0.0).abs() < 1e-10);
}
#[test]
fn test_roundtrip_db_conversion() {
let original = 0.5;
let db = linear_to_db(original);
let back = db_to_linear(db);
assert!((back - original).abs() < 1e-10);
}
#[test]
fn test_rms_level() {
let samples = vec![1.0, -1.0, 1.0, -1.0];
assert!((rms_level(&samples) - 1.0).abs() < 1e-10);
}
#[test]
fn test_rms_level_empty() {
assert!((rms_level(&[]) - 0.0).abs() < 1e-10);
}
#[test]
fn test_peak_level() {
let samples = vec![0.1, -0.5, 0.3, -0.2];
assert!((peak_level(&samples) - 0.5).abs() < 1e-10);
}
#[test]
fn test_current_gain_when_closed() {
let config = NoiseGateConfig {
range_db: -60.0,
..NoiseGateConfig::default()
};
let gate = NoiseGate::new(config);
let gain = gate.current_gain();
assert!(gain < 0.01);
}
#[test]
fn test_sidechain_gating() {
let config = NoiseGateConfig {
open_threshold_db: -20.0,
close_threshold_db: -25.0,
attack_secs: 0.0001,
release_secs: 0.001,
hold_secs: 0.0,
range_db: -80.0,
lookahead_secs: 0.0,
sample_rate: 48000.0,
};
let mut gate = NoiseGate::new(config);
let mut audio = vec![0.5; 500];
let sidechain = vec![0.0001; 500]; gate.process_with_sidechain(&mut audio, &sidechain);
assert!(gate.envelope() < 0.1);
}
}