use crate::AudioEffect;
#[derive(Debug, Clone)]
pub struct OverdriveConfig {
pub drive: f32,
pub tone: f32,
pub level: f32,
}
impl Default for OverdriveConfig {
fn default() -> Self {
Self {
drive: 5.0,
tone: 0.5,
level: 0.5,
}
}
}
pub struct Overdrive {
config: OverdriveConfig,
tone_filter: f32,
wet_mix: f32,
}
impl Overdrive {
#[must_use]
pub fn new(config: OverdriveConfig) -> Self {
Self {
config,
tone_filter: 0.0,
wet_mix: 1.0,
}
}
fn soft_clip(x: f32) -> f32 {
if x > 1.0 {
2.0 / 3.0
} else if x < -1.0 {
-2.0 / 3.0
} else {
x - (x * x * x) / 3.0
}
}
}
impl AudioEffect for Overdrive {
const EFFECT_ID: &'static str = "overdrive";
fn process_sample(&mut self, input: f32) -> f32 {
let driven = input * self.config.drive;
let clipped = Self::soft_clip(driven);
let tone_coeff = 1.0 - self.config.tone;
self.tone_filter = clipped * (1.0 - tone_coeff) + self.tone_filter * tone_coeff;
let wet_out = self.tone_filter * self.config.level;
wet_out * self.wet_mix + input * (1.0 - self.wet_mix)
}
fn reset(&mut self) {
self.tone_filter = 0.0;
}
fn set_wet_dry(&mut self, wet: f32) {
self.wet_mix = wet.clamp(0.0, 1.0);
}
fn wet_dry(&self) -> f32 {
self.wet_mix
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_overdrive() {
let config = OverdriveConfig::default();
let mut overdrive = Overdrive::new(config);
let output = overdrive.process_sample(0.5);
assert!(output.is_finite());
}
#[test]
fn test_soft_clip() {
assert!(Overdrive::soft_clip(0.0).abs() < 0.01);
assert!(Overdrive::soft_clip(0.5).abs() < 1.0);
assert!(Overdrive::soft_clip(2.0) <= 1.0);
}
#[test]
fn test_overdrive_wet_dry_default_is_one() {
let od = Overdrive::new(OverdriveConfig::default());
assert!((od.wet_dry() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_overdrive_set_wet_dry_stores_value() {
let mut od = Overdrive::new(OverdriveConfig::default());
od.set_wet_dry(0.4);
assert!((od.wet_dry() - 0.4).abs() < 1e-6);
}
#[test]
fn test_overdrive_dry_only_passes_input() {
let mut od = Overdrive::new(OverdriveConfig::default());
od.set_wet_dry(0.0);
let out = od.process_sample(0.3);
assert!((out - 0.3).abs() < 1e-5, "dry-only output={out}, want 0.3");
}
}