aether_nodes/
waveshaper.rs1use aether_core::{node::DspNode, param::ParamBlock, BUFFER_SIZE, MAX_INPUTS};
10
11pub struct Waveshaper {
12 tone_state: f32,
14 crush_phase: f32,
16 crush_held: f32,
17}
18
19impl Waveshaper {
20 pub fn new() -> Self {
21 Self {
22 tone_state: 0.0,
23 crush_phase: 0.0,
24 crush_held: 0.0,
25 }
26 }
27
28 #[inline(always)]
30 fn tanh_approx(x: f32) -> f32 {
31 let x2 = x * x;
33 x * (27.0 + x2) / (27.0 + 9.0 * x2)
34 }
35
36 #[inline(always)]
38 fn tube_sat(x: f32) -> f32 {
39 if x >= 0.0 {
40 1.0 - (-x).exp()
41 } else {
42 -1.0 + (x).exp()
43 }
44 }
45}
46
47impl Default for Waveshaper {
48 fn default() -> Self { Self::new() }
49}
50
51impl DspNode for Waveshaper {
52 fn process(
53 &mut self,
54 inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
55 output: &mut [f32; BUFFER_SIZE],
56 params: &mut ParamBlock,
57 sample_rate: f32,
58 ) {
59 let silence = [0.0f32; BUFFER_SIZE];
60 let input = inputs[0].unwrap_or(&silence);
61
62 let drive = params.get(0).current.clamp(0.0, 1.0);
63 let mode = params.get(1).current as u32;
64 let tone = params.get(2).current.clamp(0.0, 1.0);
65 let wet = params.get(3).current.clamp(0.0, 1.0);
66
67 let gain = 1.0 + drive * 19.0;
69
70 let tone_coeff = tone * 0.95;
72
73 for (i, out) in output.iter_mut().enumerate() {
74 let dry = input[i];
75 let driven = dry * gain;
76
77 let shaped = match mode {
78 0 => Self::tanh_approx(driven),
79 1 => driven.clamp(-1.0, 1.0),
80 2 => {
81 let mut v = driven;
83 while v.abs() > 1.0 {
84 if v > 1.0 { v = 2.0 - v; }
85 else if v < -1.0 { v = -2.0 - v; }
86 }
87 v
88 }
89 3 => {
90 let crush_rate = 1.0 - drive * 0.95; self.crush_phase += crush_rate;
93 if self.crush_phase >= 1.0 {
94 self.crush_phase -= 1.0;
95 let bits = 4.0 + (1.0 - drive) * 12.0;
97 let levels = 2.0f32.powf(bits);
98 self.crush_held = (driven * levels).round() / levels;
99 }
100 self.crush_held
101 }
102 _ => Self::tube_sat(driven),
103 };
104
105 let normalized = shaped / gain.sqrt();
107
108 self.tone_state = self.tone_state + tone_coeff * (normalized - self.tone_state);
110 let toned = normalized + tone * (normalized - self.tone_state);
111
112 *out = dry + wet * (toned - dry);
113 params.tick_all();
114 }
115 let _ = sample_rate;
116 }
117
118 fn type_name(&self) -> &'static str { "Waveshaper" }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn test_waveshaper_zero_drive_passthrough() {
127 let mut ws = Waveshaper::new();
128 let mut params = ParamBlock::new();
129 for &v in &[0.0f32, 0.0, 0.5, 1.0] { params.add(v); }
130 let input: [f32; BUFFER_SIZE] = std::array::from_fn(|i| (i as f32 / BUFFER_SIZE as f32) * 0.5);
131 let inputs = [Some(&input); MAX_INPUTS];
132 let mut output = [0.0f32; BUFFER_SIZE];
133 ws.process(&inputs, &mut output, &mut params, 48000.0);
134 for i in 0..BUFFER_SIZE {
136 assert!((output[i] - input[i]).abs() < 0.05, "zero drive should be near-passthrough");
137 }
138 }
139
140 #[test]
141 fn test_waveshaper_hard_clip_bounds() {
142 let mut ws = Waveshaper::new();
143 let mut params = ParamBlock::new();
144 for &v in &[1.0f32, 1.0, 0.0, 1.0] { params.add(v); } let input = [2.0f32; BUFFER_SIZE]; let inputs = [Some(&input); MAX_INPUTS];
147 let mut output = [0.0f32; BUFFER_SIZE];
148 ws.process(&inputs, &mut output, &mut params, 48000.0);
149 for s in &output {
150 assert!(s.abs() <= 1.5, "hard clip output should be bounded, got {s}");
151 }
152 }
153}