pub struct Waveshaper {
pub drive: f32,
pub mix: f32,
pub asymmetry: f32,
}
impl Waveshaper {
pub fn new() -> Self {
Self {
drive: 1.0,
mix: 0.0,
asymmetry: 0.22,
}
}
pub fn process(&self, x: f32) -> f32 {
if self.mix < 0.001 {
return x;
}
let driven = x * self.drive;
let sym = driven.tanh();
let k = self.asymmetry.clamp(0.0, 0.6);
let asym = (driven + k * driven.abs()).tanh();
let shaped = sym * (1.0 - k) + asym * k;
let comp = 1.0 / self.drive.max(1.0).sqrt();
x * (1.0 - self.mix) + shaped * comp * self.mix
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_waveshaper_bypass_when_mix_zero() {
let ws = Waveshaper { drive: 10.0, mix: 0.0, asymmetry: 0.3 };
let x = 0.5_f32;
let y = ws.process(x);
assert!((y - x).abs() < 1e-6, "mix=0 should be bypass, got {}", y);
}
#[test]
fn test_waveshaper_output_finite() {
let ws = Waveshaper { drive: 5.0, mix: 1.0, asymmetry: 0.22 };
for i in 0..100 {
let x = (i as f32 * 0.1) - 5.0;
let y = ws.process(x);
assert!(y.is_finite(), "Output non-finite for input {}: {}", x, y);
}
}
#[test]
fn test_waveshaper_output_bounded() {
let ws = Waveshaper { drive: 100.0, mix: 1.0, asymmetry: 0.3 };
for i in 0..100 {
let x = (i as f32 * 0.1) - 5.0;
let y = ws.process(x);
assert!(y.abs() < 3.0, "Output too large ({}) for input {}", y, x);
}
}
#[test]
fn test_waveshaper_zero_input_zero_output() {
let ws = Waveshaper { drive: 5.0, mix: 1.0, asymmetry: 0.22 };
let y = ws.process(0.0);
assert!(y.abs() < 1e-6, "Zero input should give zero output, got {}", y);
}
#[test]
fn test_waveshaper_saturation_reduces_gain() {
let ws = Waveshaper { drive: 50.0, mix: 1.0, asymmetry: 0.0 };
let x = 10.0_f32;
let y = ws.process(x).abs();
assert!(y < x, "Waveshaper should saturate large signals: {} -> {}", x, y);
}
#[test]
fn test_waveshaper_higher_drive_more_saturation() {
let ws_low = Waveshaper { drive: 1.0, mix: 1.0, asymmetry: 0.0 };
let ws_high = Waveshaper { drive: 20.0, mix: 1.0, asymmetry: 0.0 };
let x = 2.0_f32;
let y_low = ws_low.process(x).abs();
let y_high = ws_high.process(x).abs();
assert!(y_high < y_low, "Higher drive should produce smaller output (more saturation): low={}, high={}", y_low, y_high);
}
#[test]
fn test_waveshaper_asymmetry_zero_is_more_symmetric() {
let ws = Waveshaper { drive: 3.0, mix: 1.0, asymmetry: 0.0 };
let x = 0.5_f32;
let y_pos = ws.process(x);
let y_neg = ws.process(-x);
assert!(
(y_pos + y_neg).abs() < 0.01,
"asymmetry=0 should be symmetric: pos={}, neg={}", y_pos, y_neg
);
}
}