#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WaveshaperCurve {
SoftClip,
HardClip,
Foldback,
Tube,
Chebyshev2,
Chebyshev3,
}
impl WaveshaperCurve {
#[must_use]
pub fn apply(&self, x: f32) -> f32 {
match self {
Self::SoftClip => x.tanh(),
Self::HardClip => x.clamp(-1.0, 1.0),
Self::Foldback => {
let mut v = x;
while !(-1.0..=1.0).contains(&v) {
if v > 1.0 {
v = 2.0 - v;
}
if v < -1.0 {
v = -2.0 - v;
}
}
v
}
Self::Tube => {
if x >= 0.0 {
1.0 - (-3.0 * x).exp()
} else {
-(1.0 - (3.0 * x).exp())
}
}
Self::Chebyshev2 => {
2.0 * x * x - 1.0
}
Self::Chebyshev3 => {
4.0 * x * x * x - 3.0 * x
}
}
}
}
#[derive(Debug, Clone)]
pub struct Waveshaper {
pub curve: WaveshaperCurve,
pub drive: f32,
pub output_gain: f32,
pub mix: f32,
dc_x1: f32,
dc_y1: f32,
dc_coeff: f32,
}
impl Waveshaper {
#[must_use]
pub fn new(curve: WaveshaperCurve, drive: f32) -> Self {
Self {
curve,
drive: drive.max(0.0),
output_gain: 1.0,
mix: 1.0,
dc_x1: 0.0,
dc_y1: 0.0,
dc_coeff: 0.995,
}
}
pub fn set_mix(&mut self, mix: f32) {
self.mix = mix.clamp(0.0, 1.0);
}
pub fn set_output_gain(&mut self, gain: f32) {
self.output_gain = gain.clamp(0.0, 2.0);
}
pub fn process_sample(&mut self, input: f32) -> f32 {
let driven = input * (1.0 + self.drive);
let shaped = self.curve.apply(driven);
let dc_blocked = self.dc_block(shaped);
let wet = dc_blocked * self.output_gain;
input * (1.0 - self.mix) + wet * self.mix
}
pub fn process_buffer(&mut self, buffer: &mut [f32]) {
for sample in buffer.iter_mut() {
*sample = self.process_sample(*sample);
}
}
pub fn reset(&mut self) {
self.dc_x1 = 0.0;
self.dc_y1 = 0.0;
}
fn dc_block(&mut self, x: f32) -> f32 {
let y = x - self.dc_x1 + self.dc_coeff * self.dc_y1;
self.dc_x1 = x;
self.dc_y1 = y;
y
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_soft_clip_bounded() {
let curve = WaveshaperCurve::SoftClip;
for i in -100..=100 {
#[allow(clippy::cast_precision_loss)]
let x = i as f32 * 0.1;
let y = curve.apply(x);
assert!(y >= -1.0 && y <= 1.0, "SoftClip out of range at x={x}: {y}");
}
}
#[test]
fn test_hard_clip_exact() {
let curve = WaveshaperCurve::HardClip;
assert!((curve.apply(0.5) - 0.5).abs() < 1e-6);
assert!((curve.apply(2.0) - 1.0).abs() < 1e-6);
assert!((curve.apply(-3.0) - (-1.0)).abs() < 1e-6);
}
#[test]
fn test_foldback_bounded() {
let curve = WaveshaperCurve::Foldback;
let y = curve.apply(1.5);
assert!(y >= -1.0 && y <= 1.0, "Foldback out of range: {y}");
}
#[test]
fn test_tube_asymmetric() {
let curve = WaveshaperCurve::Tube;
let pos = curve.apply(0.5);
let neg = curve.apply(-0.5);
assert!(pos > 0.0);
assert!(neg < 0.0);
}
#[test]
fn test_chebyshev2_at_zero() {
let curve = WaveshaperCurve::Chebyshev2;
assert!((curve.apply(0.0) - (-1.0)).abs() < 1e-6);
}
#[test]
fn test_chebyshev3_at_one() {
let curve = WaveshaperCurve::Chebyshev3;
assert!((curve.apply(1.0) - 1.0).abs() < 1e-6);
}
#[test]
fn test_waveshaper_process_sample() {
let mut ws = Waveshaper::new(WaveshaperCurve::SoftClip, 1.0);
let out = ws.process_sample(0.5);
assert!(out.is_finite());
}
#[test]
fn test_waveshaper_dry_mix() {
let mut ws = Waveshaper::new(WaveshaperCurve::HardClip, 1.0);
ws.set_mix(0.0); ws.reset();
for _ in 0..100 {
ws.process_sample(0.3);
}
let out = ws.process_sample(0.3);
assert!((out - 0.3).abs() < 0.05);
}
#[test]
fn test_waveshaper_process_buffer() {
let mut ws = Waveshaper::new(WaveshaperCurve::SoftClip, 0.5);
let mut buf = vec![0.1, 0.5, -0.3, 0.9, -0.8];
ws.process_buffer(&mut buf);
for v in &buf {
assert!(v.is_finite());
}
}
#[test]
fn test_waveshaper_reset() {
let mut ws = Waveshaper::new(WaveshaperCurve::Tube, 2.0);
ws.process_sample(0.5);
ws.reset();
assert!((ws.dc_x1).abs() < 1e-6);
assert!((ws.dc_y1).abs() < 1e-6);
}
#[test]
fn test_output_gain() {
let mut ws = Waveshaper::new(WaveshaperCurve::HardClip, 0.0);
ws.set_output_gain(0.5);
assert!((ws.output_gain - 0.5).abs() < 1e-6);
}
#[test]
fn test_output_gain_clamp() {
let mut ws = Waveshaper::new(WaveshaperCurve::SoftClip, 0.0);
ws.set_output_gain(-1.0);
assert!((ws.output_gain).abs() < 1e-6);
ws.set_output_gain(5.0);
assert!((ws.output_gain - 2.0).abs() < 1e-6);
}
#[test]
fn test_drive_non_negative() {
let ws = Waveshaper::new(WaveshaperCurve::SoftClip, -5.0);
assert!(ws.drive >= 0.0);
}
#[test]
fn test_all_curves_produce_finite() {
let curves = [
WaveshaperCurve::SoftClip,
WaveshaperCurve::HardClip,
WaveshaperCurve::Foldback,
WaveshaperCurve::Tube,
WaveshaperCurve::Chebyshev2,
WaveshaperCurve::Chebyshev3,
];
for curve in &curves {
let mut ws = Waveshaper::new(*curve, 2.0);
for i in -10..=10 {
#[allow(clippy::cast_precision_loss)]
let x = i as f32 * 0.1;
let y = ws.process_sample(x);
assert!(y.is_finite(), "NaN for curve {:?} at x={x}", curve);
}
}
}
#[test]
fn test_mix_clamp() {
let mut ws = Waveshaper::new(WaveshaperCurve::SoftClip, 1.0);
ws.set_mix(1.5);
assert!((ws.mix - 1.0).abs() < 1e-6);
ws.set_mix(-0.5);
assert!(ws.mix.abs() < 1e-6);
}
}