audio_engine_core/processor/
saturation.rs1#[derive(Debug, Clone, Copy, PartialEq, Default, serde::Serialize, serde::Deserialize)]
23pub enum SaturationType {
24 #[default]
25 Tape, Tube, Transistor, }
29
30pub struct Saturation {
39 sat_type: SaturationType,
41 drive: f64,
43 threshold: f64,
45 mix: f64,
47 input_gain_db: f64,
49 output_gain_db: f64,
51 input_gain_linear: f64,
53 output_gain_linear: f64,
55 enabled: bool,
57
58 highpass_mode: bool,
61 highpass_cutoff: f64,
63
64 sample_rate: f64,
66 hpf_coef: f64,
68
69 hpf_states: Vec<f64>,
72 prev_inputs: Vec<f64>,
74}
75
76impl Saturation {
77 pub fn new() -> Self {
79 let mut instance = Self {
80 sat_type: SaturationType::Tube,
81 drive: 0.25,
82 threshold: 0.88,
83 mix: 0.2,
84 input_gain_db: 0.0,
85 output_gain_db: 0.0,
86 input_gain_linear: 1.0,
87 output_gain_linear: 1.0,
88 enabled: true,
89 highpass_mode: false,
90 highpass_cutoff: 4000.0,
91 sample_rate: 44100.0,
92 hpf_coef: 0.0, hpf_states: vec![0.0; 2],
95 prev_inputs: vec![0.0; 2],
96 };
97 instance.update_hpf_coef();
99 instance
100 }
101
102 pub fn with_type(sat_type: SaturationType) -> Self {
104 Self {
105 sat_type,
106 ..Self::new()
107 }
108 }
109
110 pub fn set_drive(&mut self, drive: f64) {
112 self.drive = drive.clamp(0.0, 2.0);
113 }
114
115 pub fn set_threshold(&mut self, threshold: f64) {
117 self.threshold = threshold.clamp(0.0, 1.0);
118 }
119
120 pub fn set_mix(&mut self, mix: f64) {
122 self.mix = mix.clamp(0.0, 1.0);
123 }
124
125 pub fn set_input_gain(&mut self, gain_db: f64) {
127 self.input_gain_db = gain_db;
128 self.input_gain_linear = db_to_linear(gain_db);
129 }
130
131 pub fn set_output_gain(&mut self, gain_db: f64) {
133 self.output_gain_db = gain_db;
134 self.output_gain_linear = db_to_linear(gain_db);
135 }
136
137 pub fn set_enabled(&mut self, enabled: bool) {
139 self.enabled = enabled;
140 }
141
142 pub fn set_type(&mut self, sat_type: SaturationType) {
144 self.sat_type = sat_type;
145 }
146
147 pub fn set_highpass_mode(&mut self, enabled: bool) {
149 self.highpass_mode = enabled;
150 }
151
152 pub fn set_highpass_cutoff(&mut self, hz: f64) {
154 self.highpass_cutoff = hz.clamp(1000.0, 12000.0);
155 self.update_hpf_coef();
156 }
157
158 pub fn set_sample_rate(&mut self, sr: f64) {
160 self.sample_rate = sr;
161 self.update_hpf_coef();
162 }
163
164 pub fn set_channel_count(&mut self, channels: usize) {
170 let channels = channels.max(1);
171 if self.hpf_states.len() != channels {
172 self.hpf_states.resize(channels, 0.0);
173 self.prev_inputs.resize(channels, 0.0);
174 }
175 }
176
177 fn update_hpf_coef(&mut self) {
179 self.hpf_coef =
183 self.sample_rate / (self.sample_rate + std::f64::consts::TAU * self.highpass_cutoff);
184 }
185
186 pub fn process(&mut self, samples: &mut [f64]) {
188 self.process_with_channels(samples, 2) }
190
191 pub fn process_with_channels(&mut self, samples: &mut [f64], channels: usize) {
193 if !self.enabled {
194 return;
195 }
196
197 if self.highpass_mode {
198 self.process_highpass(samples, channels);
199 } else {
200 self.process_fullband(samples);
201 }
202 }
203
204 pub fn process_with_sr(&mut self, samples: &mut [f64], channels: usize, sample_rate: f64) {
206 if (self.sample_rate - sample_rate).abs() > 1.0 {
207 self.set_sample_rate(sample_rate);
208 }
209 self.process_with_channels(samples, channels);
210 }
211
212 fn process_fullband(&mut self, samples: &mut [f64]) {
214 let input_gain = self.input_gain_linear;
215 let output_gain = self.output_gain_linear;
216 let threshold = self.threshold;
217 let drive_plus1 = 1.0 + self.drive;
218 let mix = self.mix;
219 let one_minus_mix = 1.0 - mix;
220 let sat_type = self.sat_type;
221
222 for sample in samples.iter_mut() {
223 let dry = *sample * input_gain;
224
225 if dry.abs() > threshold {
226 let driven = dry * drive_plus1;
227 let saturated = Self::apply_saturation_type(sat_type, driven);
228 *sample = (dry * one_minus_mix + saturated * mix) * output_gain;
229 } else {
230 *sample = dry;
231 }
232 }
233 }
234
235 fn process_highpass(&mut self, samples: &mut [f64], channels: usize) {
239 let input_gain = self.input_gain_linear;
240 let output_gain = self.output_gain_linear;
241 let alpha = self.hpf_coef;
242 let threshold = self.threshold;
243 let drive_plus1 = 1.0 + self.drive;
244 let mix = self.mix;
245 let sat_type = self.sat_type;
246
247 debug_assert!(
251 self.hpf_states.len() >= channels,
252 "Saturation HPF state undersized for {} channels (have {}); call set_channel_count during setup",
253 channels,
254 self.hpf_states.len()
255 );
256
257 let frames = samples.len() / channels;
258 for frame in 0..frames {
259 for ch in 0..channels {
260 let idx = frame * channels + ch;
261 if idx >= samples.len() {
262 break;
263 }
264
265 let input = samples[idx] * input_gain;
266
267 let high = alpha * self.hpf_states[ch] + alpha * (input - self.prev_inputs[ch]);
269 self.hpf_states[ch] = high;
270 self.prev_inputs[ch] = input;
271 #[cfg(not(any(
272 target_arch = "x86",
273 target_arch = "x86_64",
274 target_arch = "aarch64"
275 )))]
276 {
277 self.hpf_states[ch] =
278 crate::runtime::flush_subnormal_sample(self.hpf_states[ch]);
279 self.prev_inputs[ch] =
280 crate::runtime::flush_subnormal_sample(self.prev_inputs[ch]);
281 }
282
283 let saturated_high = if high.abs() > threshold {
285 let driven = high * drive_plus1;
286 Self::apply_saturation_type(sat_type, driven)
287 } else {
288 high
289 };
290
291 samples[idx] = (input + (saturated_high - high) * mix) * output_gain;
293 }
294 }
295 }
296
297 #[inline(always)]
298 fn apply_saturation_type(sat_type: SaturationType, x: f64) -> f64 {
299 match sat_type {
300 SaturationType::Tape => x.signum() * (1.0 - (-x.abs()).exp()),
301 SaturationType::Tube => x.tanh(),
302 SaturationType::Transistor => {
303 if x.abs() <= 1.5 {
306 x - (x * x * x) / 3.0
307 } else {
308 x.signum() * 0.375
309 }
310 }
311 }
312 }
313
314 pub fn reset(&mut self) {
316 self.hpf_states.fill(0.0);
317 self.prev_inputs.fill(0.0);
318 }
319
320 pub fn get_settings(&self) -> SaturationSettings {
322 SaturationSettings {
323 sat_type: self.sat_type,
324 drive: self.drive,
325 threshold: self.threshold,
326 mix: self.mix,
327 input_gain_db: self.input_gain_db,
328 output_gain_db: self.output_gain_db,
329 enabled: self.enabled,
330 highpass_mode: self.highpass_mode,
331 highpass_cutoff: self.highpass_cutoff,
332 }
333 }
334}
335
336impl Default for Saturation {
337 fn default() -> Self {
338 Self::new()
339 }
340}
341
342#[derive(Debug, Clone, serde::Serialize)]
344pub struct SaturationSettings {
345 pub sat_type: SaturationType,
346 pub drive: f64,
347 pub threshold: f64,
348 pub mix: f64,
349 pub input_gain_db: f64,
350 pub output_gain_db: f64,
351 pub enabled: bool,
352 pub highpass_mode: bool,
353 pub highpass_cutoff: f64,
354}
355
356use super::dsp::db_to_linear;
358
359#[cfg(test)]
364mod tests {
365 use super::*;
366
367 #[test]
368 fn test_tube_saturation() {
369 let mut sat = Saturation::with_type(SaturationType::Tube);
370 sat.set_enabled(true);
371 sat.set_mix(1.0); let mut samples = vec![0.9, -0.9, 0.5, -0.5];
375 sat.process(&mut samples);
376
377 assert!(samples[0].abs() < 0.9);
379 assert!(samples[1].abs() < 0.9);
380
381 assert!((samples[2].abs() - 0.5).abs() < 0.1);
384 }
385
386 #[test]
387 fn test_disabled() {
388 let mut sat = Saturation::new();
389 sat.set_enabled(false);
390
391 let mut samples = vec![0.9, -0.9, 0.5, -0.5];
392 sat.process(&mut samples);
393
394 assert!((samples[0] - 0.9).abs() < 1e-10);
396 assert!((samples[1] - (-0.9)).abs() < 1e-10);
397 }
398
399 #[test]
400 fn test_cached_linear_gains_update_with_db_setters() {
401 let mut sat = Saturation::new();
402
403 sat.set_input_gain(6.0);
404 sat.set_output_gain(-3.0);
405
406 assert!((sat.input_gain_linear - db_to_linear(6.0)).abs() < 1e-12);
407 assert!((sat.output_gain_linear - db_to_linear(-3.0)).abs() < 1e-12);
408 assert_eq!(sat.input_gain_db, 6.0);
409 assert_eq!(sat.output_gain_db, -3.0);
410 }
411
412 #[test]
413 fn test_threshold() {
414 let mut sat = Saturation::with_type(SaturationType::Tube);
415 sat.set_enabled(true);
416 sat.set_threshold(0.8);
417 sat.set_mix(1.0);
418
419 let mut samples = vec![0.5];
421 sat.process(&mut samples);
422 assert!((samples[0] - 0.5).abs() < 1e-10);
423
424 let mut samples = vec![0.9];
426 sat.process(&mut samples);
427 assert!(samples[0].abs() < 0.9);
428 }
429
430 #[test]
431 fn test_mix() {
432 let mut sat = Saturation::with_type(SaturationType::Tube);
433 sat.set_enabled(true);
434 sat.set_drive(0.0); sat.set_mix(0.5);
436
437 let mut samples = vec![1.0];
438 sat.process(&mut samples);
439
440 let expected = (1.0 + 1.0_f64.tanh()) * 0.5;
443 assert!((samples[0] - expected).abs() < 0.01);
444 }
445
446 #[test]
447 fn test_hpf_coefficient() {
448 let mut sat = Saturation::new();
449 sat.set_sample_rate(44100.0);
450 sat.set_highpass_cutoff(4000.0);
451
452 let expected = 44100.0 / (44100.0 + std::f64::consts::TAU * 4000.0);
458 assert!((sat.hpf_coef - expected).abs() < 0.001);
459 }
460
461 #[test]
462 fn test_hpf_dc_rejection() {
463 let mut sat = Saturation::new();
464 sat.set_highpass_mode(true);
465 sat.set_highpass_cutoff(4000.0);
466 sat.set_sample_rate(44100.0);
467 sat.set_mix(0.5); sat.set_threshold(2.0); let mut samples: Vec<f64> = vec![0.0; 200]; for i in 0..100 {
474 samples[i * 2] = 1.0; samples[i * 2 + 1] = 1.0; }
477 sat.process_with_channels(&mut samples, 2);
478
479 let last_l: f64 = samples.iter().skip(180).step_by(2).take(10).sum::<f64>() / 10.0;
483 let last_r: f64 = samples.iter().skip(181).step_by(2).take(10).sum::<f64>() / 10.0;
484
485 assert!(
487 (last_l - 1.0).abs() < 0.1,
488 "L output should be close to 1.0, got {}",
489 last_l
490 );
491 assert!(
492 (last_r - 1.0).abs() < 0.1,
493 "R output should be close to 1.0, got {}",
494 last_r
495 );
496 }
497
498 #[test]
499 fn test_highpass_flushes_denormals_with_audio_thread_init() {
500 crate::runtime::audio_thread_init();
501 if !crate::runtime::audio_thread_float_mode_is_enabled() {
502 return;
503 }
504
505 let mut sat = Saturation::new();
506 sat.set_highpass_mode(true);
507 let subnormal = f64::from_bits(1);
508 sat.hpf_states[0] = subnormal;
509 sat.prev_inputs[0] = -subnormal;
510 let mut samples = vec![0.0, 0.0];
511 sat.process_with_channels(&mut samples, 2);
512 assert_eq!(sat.hpf_states[0], 0.0);
513 assert_eq!(sat.prev_inputs[0], 0.0);
514 }
515
516 #[test]
517 fn test_highpass_multichannel_after_set_channel_count_does_not_panic() {
518 let mut sat = Saturation::new();
519 sat.set_highpass_mode(true);
520 sat.set_channel_count(6);
521
522 let mut samples = vec![0.5; 6 * 8];
526 sat.process_with_channels(&mut samples, 6);
527
528 assert_eq!(sat.hpf_states.len(), 6);
529 assert_eq!(sat.prev_inputs.len(), 6);
530 }
531
532 #[test]
533 fn test_set_channel_count_resizes_state_off_rt() {
534 let mut sat = Saturation::new();
535 assert_eq!(sat.hpf_states.len(), 2);
536 sat.set_channel_count(8);
537 assert_eq!(sat.hpf_states.len(), 8);
538 assert_eq!(sat.prev_inputs.len(), 8);
539 sat.set_channel_count(0);
541 assert_eq!(sat.hpf_states.len(), 1);
542 }
543}