use crate::graph::{
ParamDescriptor, ParamFlags, ParamUnit, ProcessContext, Processor, ProcessorInfo, Sig,
};
#[derive(Clone, Debug)]
pub struct Distortion {
pub mode: u32,
pub drive: f32,
pub mix: f32,
pub output: f32,
}
impl Distortion {
pub fn new() -> Self {
Self {
mode: 0,
drive: 2.0,
mix: 0.5,
output: 1.0,
}
}
pub fn snare_default() -> Self {
Self {
mode: 2,
drive: 4.2,
mix: 0.48,
output: 0.95,
}
}
fn shape(&self, x: f32) -> f32 {
let y = self.drive * x;
match self.mode {
0 => y.tanh(),
1 => y.clamp(-1.0, 1.0),
2 => foldback(y),
3 => soft_rational(y),
4 => diode_asym(y),
_ => y.tanh(),
}
}
}
#[inline]
fn foldback(mut y: f32) -> f32 {
for _ in 0..12 {
if y > 1.0 {
y = 2.0 - y;
} else if y < -1.0 {
y = -2.0 - y;
} else {
break;
}
}
y.clamp(-1.0, 1.0)
}
#[inline]
fn soft_rational(y: f32) -> f32 {
y / (1.0 + y * y).sqrt()
}
#[inline]
fn diode_asym(y: f32) -> f32 {
let pos = y.max(0.0);
let neg = y.min(0.0);
(pos * 1.35 + neg * 0.65).tanh()
}
impl Default for Distortion {
fn default() -> Self {
Self::new()
}
}
impl Processor for Distortion {
fn info(&self) -> ProcessorInfo {
ProcessorInfo {
name: "distort",
sig: Sig::MONO,
description: "Mono waveshaper: tanh / hard / fold / soft / diode + mix",
}
}
fn process(&mut self, ctx: &mut ProcessContext) {
let in_ = ctx.inputs[0];
let out = &mut ctx.outputs[0];
for i in 0..ctx.frames {
let x = in_[i];
let w = self.wet_sample(x);
out[i] = (x * (1.0 - self.mix) + w * self.mix) * self.output;
}
}
fn reset(&mut self) {}
fn params(&self) -> Vec<ParamDescriptor> {
vec![
ParamDescriptor {
id: 0,
name: "Mode",
min: 0.0,
max: 4.0,
default: 0.0,
unit: ParamUnit::Linear,
flags: ParamFlags::NONE,
step: 1.0,
group: None,
help: "0=Tanh 1=Hard 2=Fold 3=Soft 4=Diode",
},
ParamDescriptor {
id: 1,
name: "Drive",
min: 0.25,
max: 24.0,
default: 2.0,
unit: ParamUnit::Linear,
flags: ParamFlags::LOG_SCALE,
step: 0.25,
group: None,
help: "Gain into the waveshaper",
},
ParamDescriptor {
id: 2,
name: "Mix",
min: 0.0,
max: 1.0,
default: 0.5,
unit: ParamUnit::Linear,
flags: ParamFlags::NONE,
step: 0.02,
group: None,
help: "Dry/wet blend",
},
ParamDescriptor {
id: 3,
name: "Out",
min: 0.1,
max: 3.0,
default: 1.0,
unit: ParamUnit::Linear,
flags: ParamFlags::NONE,
step: 0.05,
group: None,
help: "Makeup gain after shaping",
},
]
}
fn get_param(&self, id: u32) -> f64 {
match id {
0 => self.mode as f64,
1 => self.drive as f64,
2 => self.mix as f64,
3 => self.output as f64,
_ => 0.0,
}
}
fn set_param(&mut self, id: u32, value: f64) {
match id {
0 => self.mode = value.round().clamp(0.0, 4.0) as u32,
1 => self.drive = value.clamp(0.25, 24.0) as f32,
2 => self.mix = value.clamp(0.0, 1.0) as f32,
3 => self.output = value.clamp(0.1, 3.0) as f32,
_ => {}
}
}
}
impl Distortion {
#[inline]
fn wet_sample(&self, x: f32) -> f32 {
self.shape(x)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::Graph;
#[test]
fn distortion_output_bounded() {
let mut d = Distortion {
mode: 1,
drive: 50.0,
mix: 1.0,
output: 1.0,
};
let input = vec![0.9f32; 8];
let mut outputs_vec = vec![vec![0.0f32; 8]];
let inputs: Vec<&[f32]> = vec![&input];
let mut ctx = ProcessContext {
inputs: &inputs,
outputs: &mut outputs_vec,
frames: 8,
sample_rate: 44100.0,
events: &[],
};
d.process(&mut ctx);
for &s in &outputs_vec[0] {
assert!(s.abs() <= 1.01, "hard+full wet should clip near unit: {s}");
}
}
#[test]
fn snare_default_runs_in_graph() {
let mut g = Graph::new(64);
let n = g.add_node(Box::new(Distortion::snare_default()));
g.run(64, 44100.0, &[]);
let buf = g.output_buffer(n, 0);
assert_eq!(buf.len(), 64);
}
}