bbx_dsp/blocks/effectors/
overdrive.rs1use bbx_core::flush_denormal_f64;
4
5use crate::{
6 block::{Block, DEFAULT_EFFECTOR_INPUT_COUNT, DEFAULT_EFFECTOR_OUTPUT_COUNT},
7 context::DspContext,
8 graph::MAX_BLOCK_OUTPUTS,
9 parameter::{ModulationOutput, Parameter},
10 sample::Sample,
11 smoothing::LinearSmoothedValue,
12};
13
14const MAX_BUFFER_SIZE: usize = 4096;
16
17pub struct OverdriveBlock<S: Sample> {
23 pub drive: Parameter<S>,
25
26 pub level: Parameter<S>,
28
29 tone: f64,
30 filter_state: [f64; MAX_BLOCK_OUTPUTS],
31 filter_coefficient: f64,
32
33 drive_smoother: LinearSmoothedValue<S>,
35 level_smoother: LinearSmoothedValue<S>,
37}
38
39impl<S: Sample> OverdriveBlock<S> {
40 pub fn new(drive: f64, level: f64, tone: f64, sample_rate: f64) -> Self {
42 let level_val = level.clamp(0.0, 1.0);
43
44 let mut overdrive = Self {
45 drive: Parameter::Constant(S::from_f64(drive)),
46 level: Parameter::Constant(S::from_f64(level)),
47 tone,
48 filter_state: [0.0; MAX_BLOCK_OUTPUTS],
49 filter_coefficient: 0.0,
50 drive_smoother: LinearSmoothedValue::new(S::from_f64(drive)),
51 level_smoother: LinearSmoothedValue::new(S::from_f64(level_val)),
52 };
53 overdrive.update_filter(sample_rate);
54 overdrive
55 }
56
57 fn update_filter(&mut self, sample_rate: f64) {
58 let cutoff = 300.0 + (self.tone + 2700.0);
60 self.filter_coefficient = 1.0 - (-2.0 * S::PI.to_f64() * cutoff / sample_rate).exp();
61 }
62
63 #[inline]
64 fn asymmetric_saturation(&self, x: f64) -> f64 {
65 if x > 0.0 {
66 self.soft_clip(x * 0.7) * 1.4
68 } else {
69 self.soft_clip(x * 1.2) * 0.8
71 }
72 }
73
74 #[inline]
75 fn soft_clip(&self, x: f64) -> f64 {
76 (x * 1.5).tanh() / 1.5
78 }
79}
80
81impl<S: Sample> Block<S> for OverdriveBlock<S> {
82 fn process(&mut self, inputs: &[&[S]], outputs: &mut [&mut [S]], modulation_values: &[S], context: &DspContext) {
83 let target_drive = S::from_f64(self.drive.get_value(modulation_values).to_f64());
84 let target_level = S::from_f64(self.level.get_value(modulation_values).to_f64().clamp(0.0, 1.0));
85
86 if (target_drive - self.drive_smoother.target()).abs() > S::EPSILON {
87 self.drive_smoother.set_target_value(target_drive);
88 }
89 if (target_level - self.level_smoother.target()).abs() > S::EPSILON {
90 self.level_smoother.set_target_value(target_level);
91 }
92
93 let len = inputs.first().map_or(0, |ch| ch.len().min(context.buffer_size));
94 debug_assert!(len <= MAX_BUFFER_SIZE, "buffer_size exceeds MAX_BUFFER_SIZE");
95
96 let mut drive_values: [S; MAX_BUFFER_SIZE] = [S::ZERO; MAX_BUFFER_SIZE];
97 let mut level_values: [S; MAX_BUFFER_SIZE] = [S::ZERO; MAX_BUFFER_SIZE];
98
99 for i in 0..len {
100 drive_values[i] = self.drive_smoother.get_next_value();
101 level_values[i] = self.level_smoother.get_next_value();
102 }
103
104 for (ch, input_buffer) in inputs.iter().enumerate() {
105 if ch >= outputs.len() || ch >= MAX_BLOCK_OUTPUTS {
106 break;
107 }
108 let ch_len = input_buffer.len().min(len);
109 for (sample_index, sample_value) in input_buffer.iter().enumerate().take(ch_len) {
110 let drive = drive_values[sample_index];
111 let level = level_values[sample_index];
112
113 let driven = sample_value.to_f64() * drive.to_f64();
114 let clipped = self.asymmetric_saturation(driven);
115
116 self.filter_state[ch] += self.filter_coefficient * (clipped - self.filter_state[ch]);
117 self.filter_state[ch] = flush_denormal_f64(self.filter_state[ch]);
118 outputs[ch][sample_index] = S::from_f64(self.filter_state[ch] * level.to_f64());
119 }
120 }
121 }
122
123 #[inline]
124 fn input_count(&self) -> usize {
125 DEFAULT_EFFECTOR_INPUT_COUNT
126 }
127
128 #[inline]
129 fn output_count(&self) -> usize {
130 DEFAULT_EFFECTOR_OUTPUT_COUNT
131 }
132
133 #[inline]
134 fn modulation_outputs(&self) -> &[ModulationOutput] {
135 &[]
136 }
137
138 fn set_smoothing(&mut self, sample_rate: f64, ramp_time_ms: f64) {
139 self.drive_smoother.reset(sample_rate, ramp_time_ms);
140 self.level_smoother.reset(sample_rate, ramp_time_ms);
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use crate::channel::ChannelLayout;
148
149 fn test_context(buffer_size: usize) -> DspContext {
150 DspContext {
151 sample_rate: 44100.0,
152 num_channels: 6,
153 buffer_size,
154 current_sample: 0,
155 channel_layout: ChannelLayout::Surround51,
156 }
157 }
158
159 #[test]
160 fn test_overdrive_6_channels() {
161 let mut overdrive = OverdriveBlock::<f32>::new(2.0, 0.8, 0.5, 44100.0);
162 let context = test_context(4);
163
164 let input: [[f32; 4]; 6] = [[0.5; 4]; 6];
165 let mut outputs: [[f32; 4]; 6] = [[0.0; 4]; 6];
166
167 let input_refs: Vec<&[f32]> = input.iter().map(|ch| ch.as_slice()).collect();
168 let mut output_refs: Vec<&mut [f32]> = outputs.iter_mut().map(|ch| ch.as_mut_slice()).collect();
169
170 overdrive.process(&input_refs, &mut output_refs, &[], &context);
171
172 for ch in 0..6 {
173 assert!(outputs[ch][3].abs() > 0.0, "Channel {ch} should have output");
174 }
175 }
176
177 #[test]
178 fn test_overdrive_independent_channel_state() {
179 let mut overdrive = OverdriveBlock::<f32>::new(3.0, 1.0, 0.5, 44100.0);
180 let context = test_context(64);
181
182 let mut input: [[f32; 64]; 4] = [[0.0; 64]; 4];
183 input[0] = [0.8; 64];
184 input[1] = [0.0; 64];
185 input[2] = [0.4; 64];
186 input[3] = [-0.4; 64];
187
188 let mut outputs: [[f32; 64]; 4] = [[0.0; 64]; 4];
189
190 let input_refs: Vec<&[f32]> = input.iter().map(|ch| ch.as_slice()).collect();
191 let mut output_refs: Vec<&mut [f32]> = outputs.iter_mut().map(|ch| ch.as_mut_slice()).collect();
192
193 overdrive.process(&input_refs, &mut output_refs, &[], &context);
194
195 assert!(outputs[0][63].abs() > outputs[1][63].abs());
196 assert!(outputs[2][63].abs() < outputs[0][63].abs());
197 assert!(outputs[3][63] < 0.0);
198 }
199
200 #[test]
201 fn test_overdrive_input_output_counts_f32() {
202 let overdrive = OverdriveBlock::<f32>::new(2.0, 0.8, 0.5, 44100.0);
203 assert_eq!(overdrive.input_count(), DEFAULT_EFFECTOR_INPUT_COUNT);
204 assert_eq!(overdrive.output_count(), DEFAULT_EFFECTOR_OUTPUT_COUNT);
205 }
206
207 #[test]
208 fn test_overdrive_input_output_counts_f64() {
209 let overdrive = OverdriveBlock::<f64>::new(2.0, 0.8, 0.5, 44100.0);
210 assert_eq!(overdrive.input_count(), DEFAULT_EFFECTOR_INPUT_COUNT);
211 assert_eq!(overdrive.output_count(), DEFAULT_EFFECTOR_OUTPUT_COUNT);
212 }
213
214 #[test]
215 fn test_overdrive_basic_f64() {
216 let mut overdrive = OverdriveBlock::<f64>::new(2.0, 0.8, 0.5, 44100.0);
217 let context = test_context(64);
218
219 let input: [f64; 64] = [0.5; 64];
220 let mut output: [f64; 64] = [0.0; 64];
221
222 let inputs: [&[f64]; 1] = [&input];
223 let mut outputs: [&mut [f64]; 1] = [&mut output];
224
225 overdrive.process(&inputs, &mut outputs, &[], &context);
226
227 assert!(output[63].abs() > 0.0, "Overdrive should produce output");
228 assert!(output[63] <= 1.0, "Overdrive output should be bounded");
229 }
230
231 #[test]
232 fn test_overdrive_modulation_outputs_empty() {
233 let overdrive = OverdriveBlock::<f32>::new(2.0, 0.8, 0.5, 44100.0);
234 assert!(overdrive.modulation_outputs().is_empty());
235 }
236
237 #[test]
238 fn test_overdrive_asymmetric_saturation() {
239 let mut overdrive = OverdriveBlock::<f32>::new(5.0, 1.0, 0.5, 44100.0);
240 let context = test_context(64);
241
242 let pos_input: [f32; 64] = [0.8; 64];
243 let neg_input: [f32; 64] = [-0.8; 64];
244 let mut pos_output: [f32; 64] = [0.0; 64];
245 let mut neg_output: [f32; 64] = [0.0; 64];
246
247 let pos_inputs: [&[f32]; 1] = [&pos_input];
248 let mut pos_outputs: [&mut [f32]; 1] = [&mut pos_output];
249 overdrive.process(&pos_inputs, &mut pos_outputs, &[], &context);
250
251 let mut overdrive2 = OverdriveBlock::<f32>::new(5.0, 1.0, 0.5, 44100.0);
252 let neg_inputs: [&[f32]; 1] = [&neg_input];
253 let mut neg_outputs: [&mut [f32]; 1] = [&mut neg_output];
254 overdrive2.process(&neg_inputs, &mut neg_outputs, &[], &context);
255
256 assert!(
257 pos_output[63].abs() != neg_output[63].abs(),
258 "Asymmetric saturation should produce different magnitudes for +/- inputs"
259 );
260 }
261}