devalang_wasm/engine/audio/effects/
processors.rs

1/// Effect processors - individual effect implementations
2use std::f32::consts::PI;
3
4/// Trait for all effect processors
5pub trait EffectProcessor: Send + std::fmt::Debug {
6    /// Process audio samples (stereo interleaved)
7    fn process(&mut self, samples: &mut [f32], sample_rate: u32);
8
9    /// Reset effect state
10    fn reset(&mut self);
11
12    /// Get effect name
13    fn name(&self) -> &str;
14}
15
16/// Chorus effect - multiple detuned voices
17#[derive(Debug)]
18pub struct ChorusProcessor {
19    depth: f32,
20    rate: f32,
21    mix: f32,
22    phase: f32,
23    delay_buffer: Vec<f32>,
24    buffer_pos: usize,
25}
26
27impl ChorusProcessor {
28    pub fn new(depth: f32, rate: f32, mix: f32) -> Self {
29        Self {
30            depth: depth.clamp(0.0, 1.0),
31            rate: rate.clamp(0.1, 10.0),
32            mix: mix.clamp(0.0, 1.0),
33            phase: 0.0,
34            delay_buffer: vec![0.0; 8820], // ~200ms at 44.1kHz
35            buffer_pos: 0,
36        }
37    }
38}
39
40impl Default for ChorusProcessor {
41    fn default() -> Self {
42        Self::new(0.7, 0.5, 0.5)
43    }
44}
45
46impl EffectProcessor for ChorusProcessor {
47    fn process(&mut self, samples: &mut [f32], sample_rate: u32) {
48        let max_delay_samples = (0.020 * sample_rate as f32) as usize; // 20ms max delay
49
50        for i in (0..samples.len()).step_by(2) {
51            // Update LFO phase
52            self.phase += self.rate / sample_rate as f32;
53            if self.phase >= 1.0 {
54                self.phase -= 1.0;
55            }
56
57            // Calculate delay offset using sine LFO
58            let lfo = (2.0 * PI * self.phase).sin();
59            let delay_samples =
60                (self.depth * max_delay_samples as f32 * (lfo + 1.0) / 2.0) as usize;
61            let delay_samples = delay_samples.min(self.delay_buffer.len() - 1);
62
63            // Process left and right channels
64            for ch in 0..2 {
65                if i + ch < samples.len() {
66                    let input = samples[i + ch];
67
68                    // Write to delay buffer
69                    self.delay_buffer[self.buffer_pos] = input;
70
71                    // Read from delayed position
72                    let read_pos = (self.buffer_pos + self.delay_buffer.len() - delay_samples)
73                        % self.delay_buffer.len();
74                    let delayed = self.delay_buffer[read_pos];
75
76                    // Mix wet and dry
77                    samples[i + ch] = input * (1.0 - self.mix) + delayed * self.mix;
78                }
79            }
80
81            self.buffer_pos = (self.buffer_pos + 1) % self.delay_buffer.len();
82        }
83    }
84
85    fn reset(&mut self) {
86        self.phase = 0.0;
87        self.delay_buffer.fill(0.0);
88        self.buffer_pos = 0;
89    }
90
91    fn name(&self) -> &str {
92        "Chorus"
93    }
94}
95
96/// Flanger effect - sweeping comb filter
97#[derive(Debug)]
98pub struct FlangerProcessor {
99    depth: f32,
100    rate: f32,
101    feedback: f32,
102    mix: f32,
103    phase: f32,
104    delay_buffer: Vec<f32>,
105    buffer_pos: usize,
106}
107
108impl FlangerProcessor {
109    pub fn new(depth: f32, rate: f32, feedback: f32, mix: f32) -> Self {
110        Self {
111            depth: depth.clamp(0.0, 1.0),
112            rate: rate.clamp(0.1, 10.0),
113            feedback: feedback.clamp(0.0, 0.95),
114            mix: mix.clamp(0.0, 1.0),
115            phase: 0.0,
116            delay_buffer: vec![0.0; 882], // ~20ms at 44.1kHz
117            buffer_pos: 0,
118        }
119    }
120}
121
122impl Default for FlangerProcessor {
123    fn default() -> Self {
124        Self::new(0.7, 0.5, 0.5, 0.5)
125    }
126}
127
128impl EffectProcessor for FlangerProcessor {
129    fn process(&mut self, samples: &mut [f32], sample_rate: u32) {
130        let max_delay_samples = (0.010 * sample_rate as f32) as usize; // 10ms max delay
131
132        for i in (0..samples.len()).step_by(2) {
133            // Update LFO phase
134            self.phase += self.rate / sample_rate as f32;
135            if self.phase >= 1.0 {
136                self.phase -= 1.0;
137            }
138
139            // Calculate delay offset using sine LFO
140            let lfo = (2.0 * PI * self.phase).sin();
141            let delay_samples =
142                (self.depth * max_delay_samples as f32 * (lfo + 1.0) / 2.0) as usize;
143            let delay_samples = delay_samples.min(self.delay_buffer.len() - 1);
144
145            // Process left and right channels
146            for ch in 0..2 {
147                if i + ch < samples.len() {
148                    let input = samples[i + ch];
149
150                    // Read from delayed position
151                    let read_pos = (self.buffer_pos + self.delay_buffer.len() - delay_samples)
152                        % self.delay_buffer.len();
153                    let delayed = self.delay_buffer[read_pos];
154
155                    // Apply feedback
156                    let output = input + delayed * self.feedback;
157
158                    // Write to delay buffer
159                    self.delay_buffer[self.buffer_pos] = output;
160
161                    // Mix wet and dry
162                    samples[i + ch] = input * (1.0 - self.mix) + delayed * self.mix;
163                }
164            }
165
166            self.buffer_pos = (self.buffer_pos + 1) % self.delay_buffer.len();
167        }
168    }
169
170    fn reset(&mut self) {
171        self.phase = 0.0;
172        self.delay_buffer.fill(0.0);
173        self.buffer_pos = 0;
174    }
175
176    fn name(&self) -> &str {
177        "Flanger"
178    }
179}
180
181/// Phaser effect - allpass filter cascade
182#[derive(Debug)]
183pub struct PhaserProcessor {
184    stages: usize,
185    rate: f32,
186    depth: f32,
187    feedback: f32,
188    mix: f32,
189    phase: f32,
190    allpass_states: Vec<[f32; 2]>, // [left, right] per stage
191}
192
193impl PhaserProcessor {
194    pub fn new(stages: usize, rate: f32, depth: f32, feedback: f32, mix: f32) -> Self {
195        let stages = stages.clamp(2, 12);
196        Self {
197            stages,
198            rate: rate.clamp(0.1, 10.0),
199            depth: depth.clamp(0.0, 1.0),
200            feedback: feedback.clamp(0.0, 0.95),
201            mix: mix.clamp(0.0, 1.0),
202            phase: 0.0,
203            allpass_states: vec![[0.0, 0.0]; stages],
204        }
205    }
206}
207
208impl Default for PhaserProcessor {
209    fn default() -> Self {
210        Self::new(4, 0.5, 0.7, 0.5, 0.5)
211    }
212}
213
214impl EffectProcessor for PhaserProcessor {
215    fn process(&mut self, samples: &mut [f32], sample_rate: u32) {
216        for i in (0..samples.len()).step_by(2) {
217            // Update LFO phase
218            self.phase += self.rate / sample_rate as f32;
219            if self.phase >= 1.0 {
220                self.phase -= 1.0;
221            }
222
223            // Calculate allpass coefficient using sine LFO
224            let lfo = (2.0 * PI * self.phase).sin();
225            let coeff = self.depth * lfo * 0.95; // Range: -0.95 to 0.95
226
227            // Process left and right channels
228            for ch in 0..2 {
229                if i + ch < samples.len() {
230                    let mut signal = samples[i + ch];
231
232                    // Cascade allpass filters
233                    for stage in 0..self.stages {
234                        let state = self.allpass_states[stage][ch];
235                        let output = -signal + coeff * (signal - state);
236                        self.allpass_states[stage][ch] = signal;
237                        signal = output + state;
238                    }
239
240                    // Apply feedback
241                    signal = signal * self.feedback;
242
243                    // Mix wet and dry
244                    samples[i + ch] = samples[i + ch] * (1.0 - self.mix) + signal * self.mix;
245                }
246            }
247        }
248    }
249
250    fn reset(&mut self) {
251        self.phase = 0.0;
252        for state in &mut self.allpass_states {
253            state[0] = 0.0;
254            state[1] = 0.0;
255        }
256    }
257
258    fn name(&self) -> &str {
259        "Phaser"
260    }
261}
262
263/// Compressor effect - dynamic range compression
264#[derive(Debug)]
265pub struct CompressorProcessor {
266    threshold: f32,
267    ratio: f32,
268    attack: f32,
269    release: f32,
270    envelope: f32,
271}
272
273impl CompressorProcessor {
274    pub fn new(threshold: f32, ratio: f32, attack: f32, release: f32) -> Self {
275        Self {
276            threshold,
277            ratio: ratio.max(1.0),
278            attack: attack.max(0.001),
279            release: release.max(0.001),
280            envelope: 0.0,
281        }
282    }
283}
284
285impl Default for CompressorProcessor {
286    fn default() -> Self {
287        Self::new(-20.0, 4.0, 0.005, 0.1)
288    }
289}
290
291impl EffectProcessor for CompressorProcessor {
292    fn process(&mut self, samples: &mut [f32], sample_rate: u32) {
293        let attack_coeff = (-1.0 / (self.attack * sample_rate as f32)).exp();
294        let release_coeff = (-1.0 / (self.release * sample_rate as f32)).exp();
295
296        for i in (0..samples.len()).step_by(2) {
297            // Get stereo sample RMS
298            let left = samples[i];
299            let right = if i + 1 < samples.len() {
300                samples[i + 1]
301            } else {
302                left
303            };
304            let rms = ((left * left + right * right) / 2.0).sqrt();
305
306            // Convert to dB
307            let db = if rms > 0.0001 {
308                20.0 * rms.log10()
309            } else {
310                -100.0
311            };
312
313            // Update envelope
314            let target = if db > self.threshold {
315                self.threshold + (db - self.threshold) / self.ratio
316            } else {
317                db
318            };
319
320            let coeff = if target > self.envelope {
321                attack_coeff
322            } else {
323                release_coeff
324            };
325
326            self.envelope = target + coeff * (self.envelope - target);
327
328            // Calculate gain reduction
329            let gain_db = self.envelope - db;
330            let gain = 10.0_f32.powf(gain_db / 20.0);
331
332            // Apply gain
333            samples[i] *= gain;
334            if i + 1 < samples.len() {
335                samples[i + 1] *= gain;
336            }
337        }
338    }
339
340    fn reset(&mut self) {
341        self.envelope = 0.0;
342    }
343
344    fn name(&self) -> &str {
345        "Compressor"
346    }
347}
348
349/// Distortion effect - waveshaping
350#[derive(Debug)]
351pub struct DistortionProcessor {
352    drive: f32,
353    mix: f32,
354}
355
356impl DistortionProcessor {
357    pub fn new(drive: f32, mix: f32) -> Self {
358        Self {
359            drive: drive.max(1.0),
360            mix: mix.clamp(0.0, 1.0),
361        }
362    }
363}
364
365impl Default for DistortionProcessor {
366    fn default() -> Self {
367        Self::new(10.0, 0.5)
368    }
369}
370
371impl EffectProcessor for DistortionProcessor {
372    fn process(&mut self, samples: &mut [f32], _sample_rate: u32) {
373        for sample in samples.iter_mut() {
374            let input = *sample;
375
376            // Apply drive
377            let driven = input * self.drive;
378
379            // Soft clipping using tanh
380            let distorted = driven.tanh();
381
382            // Mix wet and dry
383            *sample = input * (1.0 - self.mix) + distorted * self.mix;
384        }
385    }
386
387    fn reset(&mut self) {
388        // No state to reset
389    }
390
391    fn name(&self) -> &str {
392        "Distortion"
393    }
394}
395
396/// Delay effect - echo with feedback
397#[derive(Debug)]
398pub struct DelayProcessor {
399    time_ms: f32,
400    feedback: f32,
401    mix: f32,
402    delay_buffer_l: Vec<f32>,
403    delay_buffer_r: Vec<f32>,
404    buffer_pos: usize,
405}
406
407impl DelayProcessor {
408    pub fn new(time_ms: f32, feedback: f32, mix: f32) -> Self {
409        // Allocate buffer for up to 2 seconds of delay
410        let max_samples = 88200; // 2 seconds at 44.1kHz
411        Self {
412            time_ms: time_ms.clamp(1.0, 2000.0),
413            feedback: feedback.clamp(0.0, 0.95),
414            mix: mix.clamp(0.0, 1.0),
415            delay_buffer_l: vec![0.0; max_samples],
416            delay_buffer_r: vec![0.0; max_samples],
417            buffer_pos: 0,
418        }
419    }
420}
421
422impl Default for DelayProcessor {
423    fn default() -> Self {
424        Self::new(250.0, 0.4, 0.3)
425    }
426}
427
428impl EffectProcessor for DelayProcessor {
429    fn process(&mut self, samples: &mut [f32], sample_rate: u32) {
430        let delay_samples = ((self.time_ms / 1000.0) * sample_rate as f32) as usize;
431        let delay_samples = delay_samples.min(self.delay_buffer_l.len() - 1);
432
433        for i in (0..samples.len()).step_by(2) {
434            let input_l = samples[i];
435            let input_r = if i + 1 < samples.len() {
436                samples[i + 1]
437            } else {
438                input_l
439            };
440
441            // Read from delayed position
442            let read_pos = (self.buffer_pos + self.delay_buffer_l.len() - delay_samples)
443                % self.delay_buffer_l.len();
444            let delayed_l = self.delay_buffer_l[read_pos];
445            let delayed_r = self.delay_buffer_r[read_pos];
446
447            // Write to delay buffer with feedback
448            self.delay_buffer_l[self.buffer_pos] = input_l + delayed_l * self.feedback;
449            self.delay_buffer_r[self.buffer_pos] = input_r + delayed_r * self.feedback;
450
451            // Mix wet and dry
452            samples[i] = input_l * (1.0 - self.mix) + delayed_l * self.mix;
453            if i + 1 < samples.len() {
454                samples[i + 1] = input_r * (1.0 - self.mix) + delayed_r * self.mix;
455            }
456
457            self.buffer_pos = (self.buffer_pos + 1) % self.delay_buffer_l.len();
458        }
459    }
460
461    fn reset(&mut self) {
462        self.delay_buffer_l.fill(0.0);
463        self.delay_buffer_r.fill(0.0);
464        self.buffer_pos = 0;
465    }
466
467    fn name(&self) -> &str {
468        "Delay"
469    }
470}
471
472/// Reverb effect - algorithmic reverb using multiple comb and allpass filters
473#[derive(Debug)]
474pub struct ReverbProcessor {
475    room_size: f32,
476    damping: f32,
477    mix: f32,
478    // Comb filters (parallel)
479    comb_buffers: Vec<Vec<f32>>,
480    comb_positions: Vec<usize>,
481    comb_feedback: Vec<f32>,
482    // Allpass filters (series)
483    allpass_buffers: Vec<Vec<f32>>,
484    allpass_positions: Vec<usize>,
485}
486
487impl ReverbProcessor {
488    pub fn new(room_size: f32, damping: f32, mix: f32) -> Self {
489        let room_size = room_size.clamp(0.0, 1.0);
490        let damping = damping.clamp(0.0, 1.0);
491
492        // Comb filter delays (in samples at 44.1kHz)
493        let comb_delays = [1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617];
494        let mut comb_buffers = Vec::new();
495        let mut comb_feedback = Vec::new();
496
497        for &delay in &comb_delays {
498            comb_buffers.push(vec![0.0; delay]);
499            comb_feedback.push(0.84 + room_size * 0.12);
500        }
501
502        // Allpass filter delays
503        let allpass_delays = [556, 441, 341, 225];
504        let mut allpass_buffers = Vec::new();
505
506        for &delay in &allpass_delays {
507            allpass_buffers.push(vec![0.0; delay]);
508        }
509
510        Self {
511            room_size,
512            damping,
513            mix: mix.clamp(0.0, 1.0),
514            comb_buffers,
515            comb_positions: vec![0; 8],
516            comb_feedback,
517            allpass_buffers,
518            allpass_positions: vec![0; 4],
519        }
520    }
521}
522
523impl Default for ReverbProcessor {
524    fn default() -> Self {
525        Self::new(0.5, 0.5, 0.3)
526    }
527}
528
529impl EffectProcessor for ReverbProcessor {
530    fn process(&mut self, samples: &mut [f32], _sample_rate: u32) {
531        for i in 0..samples.len() {
532            let input = samples[i];
533            let mut output = 0.0;
534
535            // Process through parallel comb filters
536            for (j, buffer) in self.comb_buffers.iter_mut().enumerate() {
537                let pos = self.comb_positions[j];
538                let delayed = buffer[pos];
539
540                // Apply feedback with damping
541                let filtered = delayed * (1.0 - self.damping) + buffer[pos] * self.damping;
542                buffer[pos] = input + filtered * self.comb_feedback[j];
543
544                output += delayed;
545
546                self.comb_positions[j] = (pos + 1) % buffer.len();
547            }
548
549            output /= self.comb_buffers.len() as f32;
550
551            // Process through series allpass filters
552            for (j, buffer) in self.allpass_buffers.iter_mut().enumerate() {
553                let pos = self.allpass_positions[j];
554                let delayed = buffer[pos];
555
556                buffer[pos] = output + delayed * 0.5;
557                output = delayed - output * 0.5;
558
559                self.allpass_positions[j] = (pos + 1) % buffer.len();
560            }
561
562            // Mix wet and dry
563            samples[i] = input * (1.0 - self.mix) + output * self.mix;
564        }
565    }
566
567    fn reset(&mut self) {
568        for buffer in &mut self.comb_buffers {
569            buffer.fill(0.0);
570        }
571        for buffer in &mut self.allpass_buffers {
572            buffer.fill(0.0);
573        }
574        self.comb_positions.fill(0);
575        self.allpass_positions.fill(0);
576    }
577
578    fn name(&self) -> &str {
579        "Reverb"
580    }
581}
582
583/// Drive effect - tube-style saturation
584#[derive(Debug)]
585pub struct DriveProcessor {
586    amount: f32,
587    tone: f32,
588    mix: f32,
589}
590
591impl DriveProcessor {
592    pub fn new(amount: f32, tone: f32, mix: f32) -> Self {
593        Self {
594            amount: amount.clamp(0.0, 1.0),
595            tone: tone.clamp(0.0, 1.0),
596            mix: mix.clamp(0.0, 1.0),
597        }
598    }
599}
600
601impl Default for DriveProcessor {
602    fn default() -> Self {
603        Self::new(0.5, 0.5, 0.7)
604    }
605}
606
607impl EffectProcessor for DriveProcessor {
608    fn process(&mut self, samples: &mut [f32], _sample_rate: u32) {
609        let gain = 1.0 + self.amount * 9.0; // 1x to 10x gain
610
611        for sample in samples.iter_mut() {
612            let input = *sample;
613
614            // Apply gain
615            let driven = input * gain;
616
617            // Asymmetric soft clipping (tube-like)
618            let distorted = if driven > 0.0 {
619                driven / (1.0 + driven.abs())
620            } else {
621                driven / (1.0 + driven.abs() * 1.5) // More compression on negative
622            };
623
624            // Simple tone control (lowpass)
625            let toned = distorted * self.tone + input * (1.0 - self.tone);
626
627            // Mix wet and dry
628            *sample = input * (1.0 - self.mix) + toned * self.mix;
629        }
630    }
631
632    fn reset(&mut self) {
633        // No state to reset
634    }
635
636    fn name(&self) -> &str {
637        "Drive"
638    }
639}
640
641#[cfg(test)]
642mod tests {
643    use super::*;
644
645    #[test]
646    fn test_chorus_processor() {
647        let mut chorus = ChorusProcessor::default();
648        let mut samples = vec![0.5, 0.5, 0.3, 0.3];
649        chorus.process(&mut samples, 44100);
650
651        // Should modify samples
652        assert!(
653            samples
654                .iter()
655                .any(|&s| (s - 0.5).abs() > 0.001 || (s - 0.3).abs() > 0.001)
656        );
657    }
658
659    #[test]
660    fn test_distortion_processor() {
661        let mut distortion = DistortionProcessor::new(5.0, 1.0);
662        let mut samples = vec![0.5, 0.5];
663        distortion.process(&mut samples, 44100);
664
665        // Should compress/saturate values with tanh
666        // tanh(0.5 * 5.0) = tanh(2.5) ≈ 0.987
667        // With mix=1.0, output should be close to tanh value
668        assert!(samples[0] > 0.5); // Should increase low values
669        assert!(samples[0] < 1.0); // But stay below 1.0
670        assert!(samples[1] > 0.5);
671        assert!(samples[1] < 1.0);
672    }
673
674    #[test]
675    fn test_compressor_processor() {
676        let mut compressor = CompressorProcessor::default();
677        let mut samples = vec![0.9, 0.9, 0.1, 0.1]; // High and low levels
678        compressor.process(&mut samples, 44100);
679
680        // Compressor should reduce dynamic range
681        let range_before = 0.9 - 0.1;
682        let range_after = (samples[0] - samples[2]).abs();
683        assert!(range_after < range_before);
684    }
685}