Skip to main content

proof_engine/audio/
effects.rs

1//! Audio effects and processing: gain, dynamics, EQ, reverb, delay, modulation,
2//! distortion, pitch, and effect chain management.
3
4use std::f32::consts::{PI, TAU};
5use std::collections::VecDeque;
6
7// ---------------------------------------------------------------------------
8// Core trait
9// ---------------------------------------------------------------------------
10
11/// Every audio effect implements this trait.
12pub trait AudioEffect: Send {
13    fn process_block(&mut self, buffer: &mut [f32], sample_rate: f32);
14    /// Human-readable name for debugging/UI.
15    fn name(&self) -> &str;
16    /// Reset all internal state (e.g. delay lines, filters).
17    fn reset(&mut self);
18}
19
20// ---------------------------------------------------------------------------
21// Helpers
22// ---------------------------------------------------------------------------
23
24#[inline]
25fn db_to_linear(db: f32) -> f32 {
26    10.0_f32.powf(db / 20.0)
27}
28
29#[inline]
30fn linear_to_db(lin: f32) -> f32 {
31    if lin <= 1e-10 { -200.0 } else { 20.0 * lin.log10() }
32}
33
34/// One-pole low-pass: y[n] = alpha*x[n] + (1-alpha)*y[n-1]
35struct OnePole {
36    alpha: f32,
37    state: f32,
38}
39impl OnePole {
40    fn new(cutoff_hz: f32, sample_rate: f32) -> Self {
41        let alpha = 1.0 - (-TAU * cutoff_hz / sample_rate).exp();
42        Self { alpha, state: 0.0 }
43    }
44    fn process(&mut self, x: f32) -> f32 {
45        self.state += self.alpha * (x - self.state);
46        self.state
47    }
48    fn reset(&mut self) { self.state = 0.0; }
49}
50
51// ---------------------------------------------------------------------------
52// Gain
53// ---------------------------------------------------------------------------
54
55/// Linear gain applied to every sample.
56pub struct Gain {
57    pub gain: f32,
58}
59impl Gain {
60    pub fn new(gain: f32) -> Self { Self { gain } }
61}
62impl AudioEffect for Gain {
63    fn process_block(&mut self, buffer: &mut [f32], _sample_rate: f32) {
64        for s in buffer.iter_mut() { *s *= self.gain; }
65    }
66    fn name(&self) -> &str { "Gain" }
67    fn reset(&mut self) {}
68}
69
70// ---------------------------------------------------------------------------
71// GainAutomation
72// ---------------------------------------------------------------------------
73
74/// A breakpoint for gain automation.
75#[derive(Clone, Copy, Debug)]
76pub struct GainBreakpoint {
77    /// Sample index.
78    pub sample: u64,
79    /// Target gain in linear scale.
80    pub gain: f32,
81    /// If true, interpolate exponentially; otherwise linearly.
82    pub exponential: bool,
83}
84
85/// Applies per-sample automated gain from a breakpoint list.
86pub struct GainAutomation {
87    pub breakpoints: Vec<GainBreakpoint>,
88    current_sample: u64,
89    current_gain: f32,
90}
91impl GainAutomation {
92    pub fn new(breakpoints: Vec<GainBreakpoint>) -> Self {
93        let initial = breakpoints.first().map(|b| b.gain).unwrap_or(1.0);
94        Self { breakpoints, current_sample: 0, current_gain: initial }
95    }
96
97    fn gain_at_sample(&self, s: u64) -> f32 {
98        if self.breakpoints.is_empty() { return 1.0; }
99        if s <= self.breakpoints.first().unwrap().sample {
100            return self.breakpoints.first().unwrap().gain;
101        }
102        if s >= self.breakpoints.last().unwrap().sample {
103            return self.breakpoints.last().unwrap().gain;
104        }
105        // Binary search for enclosing pair
106        let idx = self.breakpoints.partition_point(|b| b.sample <= s);
107        let a = &self.breakpoints[idx - 1];
108        let b = &self.breakpoints[idx];
109        let t = (s - a.sample) as f32 / (b.sample - a.sample) as f32;
110        if a.exponential && a.gain > 0.0 && b.gain > 0.0 {
111            a.gain * (b.gain / a.gain).powf(t)
112        } else {
113            a.gain + (b.gain - a.gain) * t
114        }
115    }
116}
117impl AudioEffect for GainAutomation {
118    fn process_block(&mut self, buffer: &mut [f32], _sample_rate: f32) {
119        for s in buffer.iter_mut() {
120            self.current_gain = self.gain_at_sample(self.current_sample);
121            *s *= self.current_gain;
122            self.current_sample += 1;
123        }
124    }
125    fn name(&self) -> &str { "GainAutomation" }
126    fn reset(&mut self) { self.current_sample = 0; }
127}
128
129// ---------------------------------------------------------------------------
130// Compressor
131// ---------------------------------------------------------------------------
132
133/// Soft-knee / hard-knee compressor with optional RMS detection and sidechain.
134pub struct Compressor {
135    /// Threshold in dBFS.
136    pub threshold_db: f32,
137    /// Compression ratio (e.g. 4.0 = 4:1).
138    pub ratio: f32,
139    /// Attack time in milliseconds.
140    pub attack_ms: f32,
141    /// Release time in milliseconds.
142    pub release_ms: f32,
143    /// Soft-knee width in dB (0 = hard knee).
144    pub knee_db: f32,
145    /// Make-up gain in dB.
146    pub makeup_db: f32,
147    /// Use RMS detection (true) or peak (false).
148    pub use_rms: bool,
149    /// Lookahead delay in samples.
150    pub lookahead_samples: usize,
151    /// Current gain reduction in dB (read-only metering).
152    pub gain_reduction_db: f32,
153
154    envelope: f32,
155    rms_sum: f32,
156    rms_buf: Vec<f32>,
157    rms_pos: usize,
158    lookahead: VecDeque<f32>,
159}
160impl Compressor {
161    pub fn new(
162        threshold_db: f32,
163        ratio: f32,
164        attack_ms: f32,
165        release_ms: f32,
166        knee_db: f32,
167        makeup_db: f32,
168        use_rms: bool,
169        lookahead_samples: usize,
170    ) -> Self {
171        let rms_window = 256_usize.max(lookahead_samples);
172        Self {
173            threshold_db,
174            ratio,
175            attack_ms,
176            release_ms,
177            knee_db,
178            makeup_db,
179            use_rms,
180            lookahead_samples,
181            gain_reduction_db: 0.0,
182            envelope: 0.0,
183            rms_sum: 0.0,
184            rms_buf: vec![0.0; rms_window],
185            rms_pos: 0,
186            lookahead: VecDeque::from(vec![0.0; lookahead_samples]),
187        }
188    }
189
190    fn compute_gain_db(&self, level_db: f32) -> f32 {
191        let t = self.threshold_db;
192        let r = self.ratio;
193        let w = self.knee_db;
194        if w > 0.0 {
195            let half = w * 0.5;
196            if level_db < t - half {
197                0.0
198            } else if level_db <= t + half {
199                let x = level_db - t + half;
200                (1.0 / r - 1.0) * x * x / (2.0 * w)
201            } else {
202                (1.0 / r - 1.0) * (level_db - t)
203            }
204        } else if level_db > t {
205            (1.0 / r - 1.0) * (level_db - t)
206        } else {
207            0.0
208        }
209    }
210}
211impl AudioEffect for Compressor {
212    fn process_block(&mut self, buffer: &mut [f32], sample_rate: f32) {
213        let attack_coef = (-1.0 / (self.attack_ms * 0.001 * sample_rate)).exp();
214        let release_coef = (-1.0 / (self.release_ms * 0.001 * sample_rate)).exp();
215        let makeup = db_to_linear(self.makeup_db);
216        let rms_len = self.rms_buf.len() as f32;
217
218        for s in buffer.iter_mut() {
219            // Lookahead: push current into queue, pop delayed sample
220            let delayed = if self.lookahead_samples > 0 {
221                self.lookahead.push_back(*s);
222                self.lookahead.pop_front().unwrap_or(0.0)
223            } else {
224                *s
225            };
226
227            // Level detection
228            let level = if self.use_rms {
229                let old = self.rms_buf[self.rms_pos];
230                let new_sq = (*s) * (*s);
231                self.rms_sum = (self.rms_sum - old + new_sq).max(0.0);
232                self.rms_buf[self.rms_pos] = new_sq;
233                self.rms_pos = (self.rms_pos + 1) % self.rms_buf.len();
234                (self.rms_sum / rms_len).sqrt()
235            } else {
236                s.abs()
237            };
238
239            let level_db = linear_to_db(level);
240            let desired_gain_db = self.compute_gain_db(level_db);
241            // Envelope follower
242            let coef = if desired_gain_db < self.envelope {
243                attack_coef
244            } else {
245                release_coef
246            };
247            self.envelope = desired_gain_db + coef * (self.envelope - desired_gain_db);
248            self.gain_reduction_db = -self.envelope;
249            let gr = db_to_linear(self.envelope);
250            *s = delayed * gr * makeup;
251        }
252    }
253    fn name(&self) -> &str { "Compressor" }
254    fn reset(&mut self) {
255        self.envelope = 0.0;
256        self.rms_sum = 0.0;
257        for v in self.rms_buf.iter_mut() { *v = 0.0; }
258        self.rms_pos = 0;
259        let la = self.lookahead_samples;
260        self.lookahead = VecDeque::from(vec![0.0; la]);
261        self.gain_reduction_db = 0.0;
262    }
263}
264
265// ---------------------------------------------------------------------------
266// Limiter
267// ---------------------------------------------------------------------------
268
269/// Brickwall peak limiter with true-peak (4x oversampling) and lookahead.
270pub struct Limiter {
271    pub threshold_db: f32,
272    pub release_ms: f32,
273    pub lookahead_samples: usize,
274    /// Current gain reduction in dB.
275    pub gain_reduction_db: f32,
276
277    envelope: f32,
278    lookahead: VecDeque<f32>,
279}
280impl Limiter {
281    pub fn new(threshold_db: f32, release_ms: f32, lookahead_samples: usize) -> Self {
282        Self {
283            threshold_db,
284            release_ms,
285            lookahead_samples,
286            gain_reduction_db: 0.0,
287            envelope: 0.0,
288            lookahead: VecDeque::from(vec![0.0; lookahead_samples.max(1)]),
289        }
290    }
291
292    /// 4× oversampled true-peak detection for a single sample.
293    fn true_peak(x: f32, prev: f32) -> f32 {
294        // Linear interpolation upsample 4×
295        let mut max = x.abs();
296        for k in 1..4usize {
297            let t = k as f32 / 4.0;
298            let interp = prev + (x - prev) * t;
299            max = max.max(interp.abs());
300        }
301        max
302    }
303}
304impl AudioEffect for Limiter {
305    fn process_block(&mut self, buffer: &mut [f32], sample_rate: f32) {
306        let release_coef = (-1.0 / (self.release_ms * 0.001 * sample_rate)).exp();
307        let threshold = db_to_linear(self.threshold_db);
308        let mut prev = 0.0f32;
309
310        for s in buffer.iter_mut() {
311            let delayed = if self.lookahead_samples > 0 {
312                self.lookahead.push_back(*s);
313                self.lookahead.pop_front().unwrap_or(0.0)
314            } else {
315                *s
316            };
317
318            let tp = Self::true_peak(*s, prev);
319            prev = *s;
320            let gain_needed = if tp > threshold { threshold / tp.max(1e-10) } else { 1.0 };
321            // Attack is instant (brickwall), release follows envelope
322            let target_db = linear_to_db(gain_needed);
323            let current_db = if target_db < self.envelope {
324                target_db
325            } else {
326                target_db + release_coef * (self.envelope - target_db)
327            };
328            self.envelope = current_db;
329            self.gain_reduction_db = -(current_db.min(0.0));
330            let gr = db_to_linear(current_db);
331            *s = delayed * gr;
332        }
333    }
334    fn name(&self) -> &str { "Limiter" }
335    fn reset(&mut self) {
336        self.envelope = 0.0;
337        self.gain_reduction_db = 0.0;
338        let la = self.lookahead_samples.max(1);
339        self.lookahead = VecDeque::from(vec![0.0; la]);
340    }
341}
342
343// ---------------------------------------------------------------------------
344// Gate
345// ---------------------------------------------------------------------------
346
347#[derive(Clone, Copy, PartialEq, Eq, Debug)]
348pub enum GateState { Closed, Attacking, Open, Releasing, Holding }
349
350/// Noise gate with hysteresis (separate open/close thresholds), hold, and flip (duck) mode.
351pub struct Gate {
352    pub open_threshold_db: f32,
353    pub close_threshold_db: f32,
354    /// Range in dB: how much to attenuate when gate is closed (negative value, e.g. -80).
355    pub range_db: f32,
356    pub attack_ms: f32,
357    pub hold_ms: f32,
358    pub release_ms: f32,
359    /// Flip: gate opens when signal is ABOVE threshold (ducking).
360    pub flip: bool,
361
362    state: GateState,
363    gain: f32,
364    hold_samples_remaining: usize,
365}
366impl Gate {
367    pub fn new(
368        open_threshold_db: f32,
369        close_threshold_db: f32,
370        range_db: f32,
371        attack_ms: f32,
372        hold_ms: f32,
373        release_ms: f32,
374        flip: bool,
375    ) -> Self {
376        Self {
377            open_threshold_db,
378            close_threshold_db,
379            range_db,
380            attack_ms,
381            hold_ms,
382            release_ms,
383            flip,
384            state: GateState::Closed,
385            gain: db_to_linear(range_db),
386            hold_samples_remaining: 0,
387        }
388    }
389}
390impl AudioEffect for Gate {
391    fn process_block(&mut self, buffer: &mut [f32], sample_rate: f32) {
392        let open_lin = db_to_linear(self.open_threshold_db);
393        let close_lin = db_to_linear(self.close_threshold_db);
394        let floor = db_to_linear(self.range_db);
395        let attack_step = 1.0 / (self.attack_ms * 0.001 * sample_rate).max(1.0);
396        let release_step = (1.0 - floor) / (self.release_ms * 0.001 * sample_rate).max(1.0);
397        let hold_total = (self.hold_ms * 0.001 * sample_rate) as usize;
398
399        for s in buffer.iter_mut() {
400            let level = s.abs();
401            let above_open = if self.flip { level <= open_lin } else { level >= open_lin };
402            let below_close = if self.flip { level > close_lin } else { level < close_lin };
403
404            self.state = match self.state {
405                GateState::Closed => {
406                    if above_open { GateState::Attacking } else { GateState::Closed }
407                }
408                GateState::Attacking => {
409                    self.gain = (self.gain + attack_step).min(1.0);
410                    if self.gain >= 1.0 { GateState::Open } else { GateState::Attacking }
411                }
412                GateState::Open => {
413                    if below_close {
414                        self.hold_samples_remaining = hold_total;
415                        GateState::Holding
416                    } else {
417                        GateState::Open
418                    }
419                }
420                GateState::Holding => {
421                    if above_open {
422                        GateState::Open
423                    } else if self.hold_samples_remaining == 0 {
424                        GateState::Releasing
425                    } else {
426                        self.hold_samples_remaining -= 1;
427                        GateState::Holding
428                    }
429                }
430                GateState::Releasing => {
431                    self.gain = (self.gain - release_step).max(floor);
432                    if above_open {
433                        GateState::Attacking
434                    } else if self.gain <= floor {
435                        GateState::Closed
436                    } else {
437                        GateState::Releasing
438                    }
439                }
440            };
441            *s *= self.gain;
442        }
443    }
444    fn name(&self) -> &str { "Gate" }
445    fn reset(&mut self) {
446        self.state = GateState::Closed;
447        self.gain = db_to_linear(self.range_db);
448        self.hold_samples_remaining = 0;
449    }
450}
451
452// ---------------------------------------------------------------------------
453// Biquad filter
454// ---------------------------------------------------------------------------
455
456#[derive(Clone, Copy, Debug, PartialEq)]
457pub enum BiquadType {
458    LowPass,
459    HighPass,
460    BandPass,
461    Notch,
462    AllPass,
463    PeakEq,
464    LowShelf,
465    HighShelf,
466}
467
468/// Direct-form II transposed biquad filter.
469#[derive(Clone, Debug)]
470pub struct BiquadBand {
471    pub filter_type: BiquadType,
472    pub frequency: f32,
473    pub q: f32,
474    pub gain_db: f32,
475    pub active: bool,
476    // Coefficients
477    b0: f32, b1: f32, b2: f32,
478    a1: f32, a2: f32,
479    // State
480    s1: f32, s2: f32,
481}
482impl BiquadBand {
483    pub fn new(filter_type: BiquadType, frequency: f32, q: f32, gain_db: f32) -> Self {
484        let mut b = Self {
485            filter_type, frequency, q, gain_db, active: true,
486            b0: 1.0, b1: 0.0, b2: 0.0, a1: 0.0, a2: 0.0,
487            s1: 0.0, s2: 0.0,
488        };
489        b.compute_coefficients(44100.0);
490        b
491    }
492
493    pub fn compute_coefficients(&mut self, sample_rate: f32) {
494        let f = self.frequency;
495        let q = self.q.max(0.001);
496        let a = db_to_linear(self.gain_db / 2.0); // amplitude for peak/shelf
497        let w0 = TAU * f / sample_rate;
498        let cos_w0 = w0.cos();
499        let sin_w0 = w0.sin();
500        let alpha = sin_w0 / (2.0 * q);
501
502        let (b0, b1, b2, a0, a1, a2) = match self.filter_type {
503            BiquadType::LowPass => {
504                let b0 = (1.0 - cos_w0) / 2.0;
505                let b1 = 1.0 - cos_w0;
506                let b2 = (1.0 - cos_w0) / 2.0;
507                let a0 = 1.0 + alpha;
508                let a1 = -2.0 * cos_w0;
509                let a2 = 1.0 - alpha;
510                (b0, b1, b2, a0, a1, a2)
511            }
512            BiquadType::HighPass => {
513                let b0 = (1.0 + cos_w0) / 2.0;
514                let b1 = -(1.0 + cos_w0);
515                let b2 = (1.0 + cos_w0) / 2.0;
516                let a0 = 1.0 + alpha;
517                let a1 = -2.0 * cos_w0;
518                let a2 = 1.0 - alpha;
519                (b0, b1, b2, a0, a1, a2)
520            }
521            BiquadType::BandPass => {
522                let b0 = sin_w0 / 2.0;
523                let b1 = 0.0;
524                let b2 = -sin_w0 / 2.0;
525                let a0 = 1.0 + alpha;
526                let a1 = -2.0 * cos_w0;
527                let a2 = 1.0 - alpha;
528                (b0, b1, b2, a0, a1, a2)
529            }
530            BiquadType::Notch => {
531                let b0 = 1.0;
532                let b1 = -2.0 * cos_w0;
533                let b2 = 1.0;
534                let a0 = 1.0 + alpha;
535                let a1 = -2.0 * cos_w0;
536                let a2 = 1.0 - alpha;
537                (b0, b1, b2, a0, a1, a2)
538            }
539            BiquadType::AllPass => {
540                let b0 = 1.0 - alpha;
541                let b1 = -2.0 * cos_w0;
542                let b2 = 1.0 + alpha;
543                let a0 = 1.0 + alpha;
544                let a1 = -2.0 * cos_w0;
545                let a2 = 1.0 - alpha;
546                (b0, b1, b2, a0, a1, a2)
547            }
548            BiquadType::PeakEq => {
549                let b0 = 1.0 + alpha * a;
550                let b1 = -2.0 * cos_w0;
551                let b2 = 1.0 - alpha * a;
552                let a0 = 1.0 + alpha / a;
553                let a1 = -2.0 * cos_w0;
554                let a2 = 1.0 - alpha / a;
555                (b0, b1, b2, a0, a1, a2)
556            }
557            BiquadType::LowShelf => {
558                let sq = 2.0 * a.sqrt() * alpha;
559                let b0 = a * ((a + 1.0) - (a - 1.0) * cos_w0 + sq);
560                let b1 = 2.0 * a * ((a - 1.0) - (a + 1.0) * cos_w0);
561                let b2 = a * ((a + 1.0) - (a - 1.0) * cos_w0 - sq);
562                let a0 = (a + 1.0) + (a - 1.0) * cos_w0 + sq;
563                let a1 = -2.0 * ((a - 1.0) + (a + 1.0) * cos_w0);
564                let a2 = (a + 1.0) + (a - 1.0) * cos_w0 - sq;
565                (b0, b1, b2, a0, a1, a2)
566            }
567            BiquadType::HighShelf => {
568                let sq = 2.0 * a.sqrt() * alpha;
569                let b0 = a * ((a + 1.0) + (a - 1.0) * cos_w0 + sq);
570                let b1 = -2.0 * a * ((a - 1.0) + (a + 1.0) * cos_w0);
571                let b2 = a * ((a + 1.0) + (a - 1.0) * cos_w0 - sq);
572                let a0 = (a + 1.0) - (a - 1.0) * cos_w0 + sq;
573                let a1 = 2.0 * ((a - 1.0) - (a + 1.0) * cos_w0);
574                let a2 = (a + 1.0) - (a - 1.0) * cos_w0 - sq;
575                (b0, b1, b2, a0, a1, a2)
576            }
577        };
578        let a0_inv = 1.0 / a0;
579        self.b0 = b0 * a0_inv;
580        self.b1 = b1 * a0_inv;
581        self.b2 = b2 * a0_inv;
582        self.a1 = a1 * a0_inv;
583        self.a2 = a2 * a0_inv;
584    }
585
586    #[inline]
587    pub fn process_sample(&mut self, x: f32) -> f32 {
588        let y = self.b0 * x + self.s1;
589        self.s1 = self.b1 * x - self.a1 * y + self.s2;
590        self.s2 = self.b2 * x - self.a2 * y;
591        y
592    }
593
594    pub fn reset(&mut self) { self.s1 = 0.0; self.s2 = 0.0; }
595}
596
597// ---------------------------------------------------------------------------
598// Equalizer
599// ---------------------------------------------------------------------------
600
601/// Parametric EQ with up to 16 bands.
602pub struct Equalizer {
603    pub bands: Vec<BiquadBand>,
604    last_sample_rate: f32,
605}
606impl Equalizer {
607    pub fn new(bands: Vec<BiquadBand>) -> Self {
608        Self { bands, last_sample_rate: 0.0 }
609    }
610
611    pub fn add_band(&mut self, band: BiquadBand) {
612        if self.bands.len() < 16 { self.bands.push(band); }
613    }
614}
615impl AudioEffect for Equalizer {
616    fn process_block(&mut self, buffer: &mut [f32], sample_rate: f32) {
617        if (sample_rate - self.last_sample_rate).abs() > 0.1 {
618            for b in self.bands.iter_mut() { b.compute_coefficients(sample_rate); }
619            self.last_sample_rate = sample_rate;
620        }
621        for s in buffer.iter_mut() {
622            let mut v = *s;
623            for band in self.bands.iter_mut() {
624                if band.active { v = band.process_sample(v); }
625            }
626            *s = v;
627        }
628    }
629    fn name(&self) -> &str { "Equalizer" }
630    fn reset(&mut self) {
631        for b in self.bands.iter_mut() { b.reset(); }
632    }
633}
634
635// ---------------------------------------------------------------------------
636// Reverb (Freeverb-style)
637// ---------------------------------------------------------------------------
638
639const COMB_TUNING: [usize; 8]     = [116, 118, 127, 135, 142, 149, 155, 161];
640const ALLPASS_TUNING: [usize; 4]  = [55, 44, 34, 22];
641
642struct CombFilter {
643    buf: Vec<f32>,
644    pos: usize,
645    feedback: f32,
646    damp1: f32,
647    damp2: f32,
648    filterstore: f32,
649}
650impl CombFilter {
651    fn new(size: usize) -> Self {
652        Self { buf: vec![0.0; size], pos: 0, feedback: 0.5, damp1: 0.5, damp2: 0.5, filterstore: 0.0 }
653    }
654    fn set_damp(&mut self, damp: f32) { self.damp1 = damp; self.damp2 = 1.0 - damp; }
655    fn process(&mut self, input: f32) -> f32 {
656        let output = self.buf[self.pos];
657        self.filterstore = output * self.damp2 + self.filterstore * self.damp1;
658        self.buf[self.pos] = input + self.filterstore * self.feedback;
659        self.pos = (self.pos + 1) % self.buf.len();
660        output
661    }
662    fn reset(&mut self) { for v in self.buf.iter_mut() { *v = 0.0; } self.filterstore = 0.0; self.pos = 0; }
663}
664
665struct AllpassFilter {
666    buf: Vec<f32>,
667    pos: usize,
668    feedback: f32,
669}
670impl AllpassFilter {
671    fn new(size: usize) -> Self {
672        Self { buf: vec![0.0; size], pos: 0, feedback: 0.5 }
673    }
674    fn process(&mut self, input: f32) -> f32 {
675        let bufout = self.buf[self.pos];
676        let output = -input + bufout;
677        self.buf[self.pos] = input + bufout * self.feedback;
678        self.pos = (self.pos + 1) % self.buf.len();
679        output
680    }
681    fn reset(&mut self) { for v in self.buf.iter_mut() { *v = 0.0; } self.pos = 0; }
682}
683
684/// Freeverb-inspired stereo reverb.
685pub struct Reverb {
686    pub room_size: f32,
687    pub damping: f32,
688    pub wet: f32,
689    pub dry: f32,
690    pub pre_delay_ms: f32,
691    pub width: f32,
692
693    combs_l: Vec<CombFilter>,
694    combs_r: Vec<CombFilter>,
695    allpasses_l: Vec<AllpassFilter>,
696    allpasses_r: Vec<AllpassFilter>,
697    pre_delay_buf: VecDeque<f32>,
698    last_sample_rate: f32,
699}
700impl Reverb {
701    pub fn new(room_size: f32, damping: f32, wet: f32, dry: f32, pre_delay_ms: f32, width: f32) -> Self {
702        let stereo_spread = 23;
703        let combs_l: Vec<CombFilter> = COMB_TUNING.iter().map(|&s| CombFilter::new(s)).collect();
704        let combs_r: Vec<CombFilter> = COMB_TUNING.iter().map(|&s| CombFilter::new(s + stereo_spread)).collect();
705        let allpasses_l: Vec<AllpassFilter> = ALLPASS_TUNING.iter().map(|&s| AllpassFilter::new(s)).collect();
706        let allpasses_r: Vec<AllpassFilter> = ALLPASS_TUNING.iter().map(|&s| AllpassFilter::new(s + stereo_spread)).collect();
707        let pre_delay_samples = ((pre_delay_ms * 0.001) * 44100.0) as usize;
708        let pre_delay_buf = VecDeque::from(vec![0.0f32; pre_delay_samples.max(1)]);
709        let mut r = Self {
710            room_size, damping, wet, dry, pre_delay_ms, width,
711            combs_l, combs_r, allpasses_l, allpasses_r,
712            pre_delay_buf,
713            last_sample_rate: 44100.0,
714        };
715        r.update_coefficients();
716        r
717    }
718
719    fn update_coefficients(&mut self) {
720        let feedback = self.room_size * 0.28 + 0.7;
721        let damp = self.damping * 0.4;
722        for c in self.combs_l.iter_mut().chain(self.combs_r.iter_mut()) {
723            c.feedback = feedback;
724            c.set_damp(damp);
725        }
726    }
727}
728impl AudioEffect for Reverb {
729    fn process_block(&mut self, buffer: &mut [f32], sample_rate: f32) {
730        if (sample_rate - self.last_sample_rate).abs() > 0.1 {
731            let pre_samples = ((self.pre_delay_ms * 0.001) * sample_rate) as usize;
732            self.pre_delay_buf = VecDeque::from(vec![0.0f32; pre_samples.max(1)]);
733            self.last_sample_rate = sample_rate;
734        }
735        self.update_coefficients();
736
737        for s in buffer.iter_mut() {
738            // Pre-delay
739            self.pre_delay_buf.push_back(*s);
740            let input = self.pre_delay_buf.pop_front().unwrap_or(0.0);
741
742            // Mono input → both channels
743            let input_mixed = input * 0.015;
744            let mut out_l = 0.0f32;
745            let mut out_r = 0.0f32;
746            for c in self.combs_l.iter_mut() { out_l += c.process(input_mixed); }
747            for c in self.combs_r.iter_mut() { out_r += c.process(input_mixed); }
748            for a in self.allpasses_l.iter_mut() { out_l = a.process(out_l); }
749            for a in self.allpasses_r.iter_mut() { out_r = a.process(out_r); }
750
751            let wet_l = out_l * (0.5 + self.width * 0.5) + out_r * (0.5 - self.width * 0.5);
752            *s = *s * self.dry + wet_l * self.wet;
753        }
754    }
755    fn name(&self) -> &str { "Reverb" }
756    fn reset(&mut self) {
757        for c in self.combs_l.iter_mut().chain(self.combs_r.iter_mut()) { c.reset(); }
758        for a in self.allpasses_l.iter_mut().chain(self.allpasses_r.iter_mut()) { a.reset(); }
759        let n = self.pre_delay_buf.len();
760        self.pre_delay_buf = VecDeque::from(vec![0.0f32; n]);
761    }
762}
763
764// ---------------------------------------------------------------------------
765// Delay
766// ---------------------------------------------------------------------------
767
768/// Stereo-capable delay line with feedback, high-cut in feedback path, and ping-pong.
769pub struct Delay {
770    pub delay_ms: f32,
771    pub feedback: f32,
772    pub highcut_hz: f32,
773    pub ping_pong: bool,
774    pub wet: f32,
775    pub dry: f32,
776    /// Tap tempo BPM (0 = use delay_ms directly).
777    pub tempo_bpm: f32,
778    /// Fraction of a beat (e.g. 0.5 = 8th note).
779    pub beat_division: f32,
780
781    buf_l: Vec<f32>,
782    buf_r: Vec<f32>,
783    pos: usize,
784    hpf_l: OnePole,
785    hpf_r: OnePole,
786    last_sample_rate: f32,
787}
788impl Delay {
789    pub fn new(delay_ms: f32, feedback: f32, highcut_hz: f32, ping_pong: bool, wet: f32, dry: f32) -> Self {
790        let max_samples = (4.0 * 48000.0) as usize; // 4 seconds at 48k
791        Self {
792            delay_ms, feedback, highcut_hz, ping_pong, wet, dry,
793            tempo_bpm: 0.0, beat_division: 1.0,
794            buf_l: vec![0.0; max_samples],
795            buf_r: vec![0.0; max_samples],
796            pos: 0,
797            hpf_l: OnePole::new(highcut_hz, 44100.0),
798            hpf_r: OnePole::new(highcut_hz, 44100.0),
799            last_sample_rate: 44100.0,
800        }
801    }
802
803    fn effective_delay_samples(&self, sample_rate: f32) -> usize {
804        let ms = if self.tempo_bpm > 0.0 {
805            60000.0 / self.tempo_bpm * self.beat_division
806        } else {
807            self.delay_ms
808        };
809        ((ms * 0.001 * sample_rate) as usize).clamp(1, self.buf_l.len() - 1)
810    }
811}
812impl AudioEffect for Delay {
813    fn process_block(&mut self, buffer: &mut [f32], sample_rate: f32) {
814        if (sample_rate - self.last_sample_rate).abs() > 0.1 {
815            self.hpf_l = OnePole::new(self.highcut_hz, sample_rate);
816            self.hpf_r = OnePole::new(self.highcut_hz, sample_rate);
817            self.last_sample_rate = sample_rate;
818        }
819        let delay_samp = self.effective_delay_samples(sample_rate);
820        let len = self.buf_l.len();
821
822        for s in buffer.iter_mut() {
823            let read_pos = (self.pos + len - delay_samp) % len;
824            let del_l = self.buf_l[read_pos];
825            let del_r = self.buf_r[read_pos];
826
827            // High-cut on feedback path
828            let fb_l = self.hpf_l.process(del_l) * self.feedback;
829            let fb_r = self.hpf_r.process(del_r) * self.feedback;
830
831            if self.ping_pong {
832                self.buf_l[self.pos] = *s + fb_r;
833                self.buf_r[self.pos] = fb_l;
834            } else {
835                self.buf_l[self.pos] = *s + fb_l;
836                self.buf_r[self.pos] = *s + fb_r;
837            }
838            self.pos = (self.pos + 1) % len;
839            *s = *s * self.dry + del_l * self.wet;
840        }
841    }
842    fn name(&self) -> &str { "Delay" }
843    fn reset(&mut self) {
844        for v in self.buf_l.iter_mut() { *v = 0.0; }
845        for v in self.buf_r.iter_mut() { *v = 0.0; }
846        self.pos = 0;
847        self.hpf_l.reset();
848        self.hpf_r.reset();
849    }
850}
851
852// ---------------------------------------------------------------------------
853// Chorus
854// ---------------------------------------------------------------------------
855
856/// Multi-voice chorus (up to 8 LFO-modulated delay lines).
857pub struct Chorus {
858    pub voices: usize,
859    pub rate_hz: f32,
860    pub depth_ms: f32,
861    pub spread: f32,
862    pub feedback: f32,
863    pub wet: f32,
864    pub dry: f32,
865
866    buf: Vec<Vec<f32>>,
867    pos: usize,
868    phases: Vec<f32>,
869}
870impl Chorus {
871    pub fn new(voices: usize, rate_hz: f32, depth_ms: f32, spread: f32, feedback: f32, wet: f32, dry: f32) -> Self {
872        let voices = voices.clamp(1, 8);
873        let max_samp = 4096usize;
874        let phases: Vec<f32> = (0..voices).map(|i| i as f32 / voices as f32).collect();
875        Self {
876            voices, rate_hz, depth_ms, spread, feedback, wet, dry,
877            buf: (0..voices).map(|_| vec![0.0f32; max_samp]).collect(),
878            pos: 0,
879            phases,
880        }
881    }
882}
883impl AudioEffect for Chorus {
884    fn process_block(&mut self, buffer: &mut [f32], sample_rate: f32) {
885        let max_samp = self.buf[0].len();
886        let base_delay_samp = (0.5 * 0.001 * sample_rate) as f32; // 0.5ms center
887        let depth_samp = (self.depth_ms * 0.001 * sample_rate).max(1.0);
888        let phase_inc = self.rate_hz / sample_rate;
889
890        for s in buffer.iter_mut() {
891            let mut out = 0.0f32;
892            for v in 0..self.voices {
893                let lfo = (self.phases[v] * TAU).sin();
894                let delay_samp = (base_delay_samp + lfo * depth_samp).max(1.0) as usize;
895                let read_pos = (self.pos + max_samp - delay_samp) % max_samp;
896                let delayed = self.buf[v][read_pos];
897                self.buf[v][self.pos] = *s + delayed * self.feedback;
898                out += delayed;
899                self.phases[v] = (self.phases[v] + phase_inc) % 1.0;
900            }
901            self.pos = (self.pos + 1) % max_samp;
902            out /= self.voices as f32;
903            *s = *s * self.dry + out * self.wet;
904        }
905    }
906    fn name(&self) -> &str { "Chorus" }
907    fn reset(&mut self) {
908        for buf in self.buf.iter_mut() { for v in buf.iter_mut() { *v = 0.0; } }
909        self.pos = 0;
910    }
911}
912
913// ---------------------------------------------------------------------------
914// Flanger
915// ---------------------------------------------------------------------------
916
917/// Short-delay flanger (0–15 ms) with LFO and feedback.
918pub struct Flanger {
919    pub rate_hz: f32,
920    pub depth_ms: f32,
921    pub feedback: f32,
922    pub invert: bool,
923    pub wet: f32,
924    pub dry: f32,
925
926    buf: Vec<f32>,
927    pos: usize,
928    phase: f32,
929}
930impl Flanger {
931    pub fn new(rate_hz: f32, depth_ms: f32, feedback: f32, invert: bool, wet: f32, dry: f32) -> Self {
932        Self {
933            rate_hz, depth_ms, feedback, invert, wet, dry,
934            buf: vec![0.0; 2048],
935            pos: 0,
936            phase: 0.0,
937        }
938    }
939}
940impl AudioEffect for Flanger {
941    fn process_block(&mut self, buffer: &mut [f32], sample_rate: f32) {
942        let max_samp = self.buf.len();
943        let depth_samp = (self.depth_ms * 0.001 * sample_rate).max(1.0);
944        let phase_inc = self.rate_hz / sample_rate;
945        let fb_sign = if self.invert { -1.0 } else { 1.0 };
946
947        for s in buffer.iter_mut() {
948            let lfo = (self.phase * TAU).sin();
949            let delay_samp = (depth_samp * (1.0 + lfo) * 0.5 + 1.0) as usize;
950            let read_pos = (self.pos + max_samp - delay_samp.min(max_samp - 1)) % max_samp;
951            let delayed = self.buf[read_pos];
952            self.buf[self.pos] = *s + delayed * self.feedback * fb_sign;
953            self.pos = (self.pos + 1) % max_samp;
954            self.phase = (self.phase + phase_inc) % 1.0;
955            *s = *s * self.dry + delayed * self.wet;
956        }
957    }
958    fn name(&self) -> &str { "Flanger" }
959    fn reset(&mut self) {
960        for v in self.buf.iter_mut() { *v = 0.0; }
961        self.pos = 0;
962    }
963}
964
965// ---------------------------------------------------------------------------
966// Phaser
967// ---------------------------------------------------------------------------
968
969/// Multi-stage allpass phaser (4/8/12 stages), LFO, feedback, stereo.
970pub struct Phaser {
971    pub stages: usize,
972    pub rate_hz: f32,
973    pub depth: f32,
974    pub feedback: f32,
975    pub base_freq: f32,
976    pub stereo: bool,
977    pub wet: f32,
978    pub dry: f32,
979
980    // Allpass states: [stage][channel] — channel 0 = L, 1 = R
981    ap_state: Vec<[f32; 2]>,
982    phase: f32,
983    fb_l: f32,
984    fb_r: f32,
985}
986impl Phaser {
987    pub fn new(stages: usize, rate_hz: f32, depth: f32, feedback: f32, base_freq: f32, stereo: bool, wet: f32, dry: f32) -> Self {
988        let stages = [4usize, 8, 12].iter().copied().min_by_key(|&s| (s as i32 - stages as i32).abs()).unwrap_or(4);
989        Self {
990            stages, rate_hz, depth, feedback, base_freq, stereo, wet, dry,
991            ap_state: vec![[0.0f32; 2]; stages],
992            phase: 0.0,
993            fb_l: 0.0,
994            fb_r: 0.0,
995        }
996    }
997
998    fn allpass_stage(state: &mut f32, a: f32, x: f32) -> f32 {
999        // First-order allpass: H(z) = (a + z^-1) / (1 + a*z^-1)
1000        // Transposed direct form II
1001        let y = *state + a * x;
1002        *state = x - a * y;
1003        y
1004    }
1005}
1006impl AudioEffect for Phaser {
1007    fn process_block(&mut self, buffer: &mut [f32], sample_rate: f32) {
1008        let phase_inc = self.rate_hz / sample_rate;
1009
1010        for s in buffer.iter_mut() {
1011            let lfo = (self.phase * TAU).sin();
1012            let lfo_r = ((self.phase + 0.25) * TAU).sin(); // 90° for stereo
1013
1014            let freq_l = (self.base_freq * (1.0 + lfo * self.depth)).clamp(20.0, sample_rate * 0.49);
1015            let freq_r = (self.base_freq * (1.0 + lfo_r * self.depth)).clamp(20.0, sample_rate * 0.49);
1016            let a_l = ((PI * freq_l / sample_rate) - 1.0) / ((PI * freq_l / sample_rate) + 1.0);
1017            let a_r = ((PI * freq_r / sample_rate) - 1.0) / ((PI * freq_r / sample_rate) + 1.0);
1018
1019            let mut out_l = *s + self.fb_l * self.feedback;
1020            let mut out_r = *s + self.fb_r * self.feedback;
1021            for i in 0..self.stages {
1022                out_l = Self::allpass_stage(&mut self.ap_state[i][0], a_l, out_l);
1023                if self.stereo {
1024                    out_r = Self::allpass_stage(&mut self.ap_state[i][1], a_r, out_r);
1025                }
1026            }
1027            self.fb_l = out_l;
1028            self.fb_r = out_r;
1029            self.phase = (self.phase + phase_inc) % 1.0;
1030
1031            let wet_out = if self.stereo { (out_l + out_r) * 0.5 } else { out_l };
1032            *s = *s * self.dry + wet_out * self.wet;
1033        }
1034    }
1035    fn name(&self) -> &str { "Phaser" }
1036    fn reset(&mut self) {
1037        for st in self.ap_state.iter_mut() { *st = [0.0, 0.0]; }
1038        self.fb_l = 0.0;
1039        self.fb_r = 0.0;
1040    }
1041}
1042
1043// ---------------------------------------------------------------------------
1044// Distortion
1045// ---------------------------------------------------------------------------
1046
1047#[derive(Clone, Copy, Debug, PartialEq)]
1048pub enum DistortionMode {
1049    SoftClip,
1050    HardClip,
1051    Foldback,
1052    BitCrush { bits: u32, rate_reduction: u32 },
1053    Overdrive,
1054    TubeSaturation,
1055}
1056
1057/// Multi-mode distortion with pre/post EQ bands.
1058pub struct Distortion {
1059    pub mode: DistortionMode,
1060    pub drive: f32,
1061    pub output_gain: f32,
1062    pub pre_filter: Option<BiquadBand>,
1063    pub post_filter: Option<BiquadBand>,
1064    // bit crush state
1065    held_sample: f32,
1066    rate_counter: u32,
1067}
1068impl Distortion {
1069    pub fn new(mode: DistortionMode, drive: f32, output_gain: f32) -> Self {
1070        Self {
1071            mode, drive, output_gain,
1072            pre_filter: None,
1073            post_filter: None,
1074            held_sample: 0.0,
1075            rate_counter: 0,
1076        }
1077    }
1078
1079    fn process_sample(&mut self, x: f32) -> f32 {
1080        let driven = x * self.drive;
1081        match self.mode {
1082            DistortionMode::SoftClip => {
1083                driven.tanh()
1084            }
1085            DistortionMode::HardClip => {
1086                driven.clamp(-1.0, 1.0)
1087            }
1088            DistortionMode::Foldback => {
1089                let mut v = driven;
1090                let threshold = 1.0f32;
1091                while v.abs() > threshold {
1092                    if v > threshold { v = 2.0 * threshold - v; }
1093                    else if v < -threshold { v = -2.0 * threshold - v; }
1094                }
1095                v
1096            }
1097            DistortionMode::BitCrush { bits, rate_reduction } => {
1098                let rate_red = rate_reduction.max(1);
1099                self.rate_counter += 1;
1100                if self.rate_counter >= rate_red {
1101                    self.rate_counter = 0;
1102                    let levels = (2.0f32).powi(bits.clamp(1, 24) as i32);
1103                    self.held_sample = (driven * levels).round() / levels;
1104                }
1105                self.held_sample
1106            }
1107            DistortionMode::Overdrive => {
1108                // Asymmetric waveshaping: positive side harder
1109                if driven >= 0.0 {
1110                    1.0 - (-driven).exp()
1111                } else {
1112                    -1.0 + driven.exp()
1113                }
1114            }
1115            DistortionMode::TubeSaturation => {
1116                // Even-harmonic bias: y = x + 0.2*x^2 - 0.1*x^3, then soft clip
1117                let y = driven + 0.2 * driven * driven - 0.1 * driven.powi(3);
1118                y.tanh()
1119            }
1120        }
1121    }
1122}
1123impl AudioEffect for Distortion {
1124    fn process_block(&mut self, buffer: &mut [f32], sample_rate: f32) {
1125        if let Some(ref mut pf) = self.pre_filter {
1126            pf.compute_coefficients(sample_rate);
1127        }
1128        if let Some(ref mut pf) = self.post_filter {
1129            pf.compute_coefficients(sample_rate);
1130        }
1131        for s in buffer.iter_mut() {
1132            let pre = if let Some(ref mut f) = self.pre_filter { f.process_sample(*s) } else { *s };
1133            let dist = self.process_sample(pre);
1134            let post = if let Some(ref mut f) = self.post_filter { f.process_sample(dist) } else { dist };
1135            *s = post * self.output_gain;
1136        }
1137    }
1138    fn name(&self) -> &str { "Distortion" }
1139    fn reset(&mut self) {
1140        self.held_sample = 0.0;
1141        self.rate_counter = 0;
1142        if let Some(ref mut f) = self.pre_filter { f.reset(); }
1143        if let Some(ref mut f) = self.post_filter { f.reset(); }
1144    }
1145}
1146
1147// ---------------------------------------------------------------------------
1148// Pitch Shifter (granular overlap-add)
1149// ---------------------------------------------------------------------------
1150
1151/// Granular overlap-add pitch shifter.
1152pub struct PitchShifter {
1153    pub pitch_ratio: f32,
1154    pub formant_preserve: bool,
1155
1156    // Input ring buffer
1157    in_buf: Vec<f32>,
1158    in_pos: usize,
1159    // Output accumulation buffer
1160    out_buf: Vec<f32>,
1161    out_pos: usize,
1162    grain_phase: f32,
1163    grain_size: usize,
1164}
1165impl PitchShifter {
1166    pub fn new(pitch_ratio: f32, formant_preserve: bool) -> Self {
1167        let grain_size = 2048usize;
1168        let buf_size = grain_size * 4;
1169        Self {
1170            pitch_ratio,
1171            formant_preserve,
1172            in_buf: vec![0.0; buf_size],
1173            in_pos: 0,
1174            out_buf: vec![0.0; buf_size],
1175            out_pos: 0,
1176            grain_phase: 0.0,
1177            grain_size,
1178        }
1179    }
1180
1181    fn hann_window(i: usize, n: usize) -> f32 {
1182        0.5 * (1.0 - (TAU * i as f32 / (n - 1) as f32).cos())
1183    }
1184}
1185impl AudioEffect for PitchShifter {
1186    fn process_block(&mut self, buffer: &mut [f32], _sample_rate: f32) {
1187        let gs = self.grain_size;
1188        let hop = gs / 4;
1189        let buf_len = self.in_buf.len();
1190        let out_len = self.out_buf.len();
1191
1192        for s in buffer.iter_mut() {
1193            // Write into input ring buffer
1194            self.in_buf[self.in_pos] = *s;
1195            self.in_pos = (self.in_pos + 1) % buf_len;
1196
1197            // Grain processing: every hop samples spawn a new grain
1198            self.grain_phase += 1.0;
1199            if self.grain_phase as usize >= hop {
1200                self.grain_phase = 0.0;
1201                // Read grain from input at pitch-shifted position
1202                let read_offset = (gs as f32 / self.pitch_ratio.max(0.01)) as usize;
1203                for i in 0..gs {
1204                    let in_idx = (self.in_pos + buf_len - read_offset + i) % buf_len;
1205                    let w = Self::hann_window(i, gs);
1206                    let out_idx = (self.out_pos + i) % out_len;
1207                    self.out_buf[out_idx] += self.in_buf[in_idx] * w;
1208                }
1209            }
1210
1211            // Read from output buffer
1212            let out_val = self.out_buf[self.out_pos];
1213            self.out_buf[self.out_pos] = 0.0;
1214            self.out_pos = (self.out_pos + 1) % out_len;
1215            *s = out_val;
1216        }
1217    }
1218    fn name(&self) -> &str { "PitchShifter" }
1219    fn reset(&mut self) {
1220        for v in self.in_buf.iter_mut() { *v = 0.0; }
1221        for v in self.out_buf.iter_mut() { *v = 0.0; }
1222        self.in_pos = 0;
1223        self.out_pos = 0;
1224        self.grain_phase = 0.0;
1225    }
1226}
1227
1228// ---------------------------------------------------------------------------
1229// AutoTune (YIN-based pitch detection + correction)
1230// ---------------------------------------------------------------------------
1231
1232/// YIN pitch detector and chromatic pitch corrector.
1233pub struct AutoTune {
1234    pub speed: f32,  // 0.0 = slow natural, 1.0 = hard snap
1235    pub concert_a: f32, // Hz for A4 (usually 440.0)
1236
1237    yin_buf: Vec<f32>,
1238    yin_pos: usize,
1239    yin_size: usize,
1240    current_shift: f32,
1241    delay_line: Vec<f32>,
1242    delay_pos: usize,
1243}
1244impl AutoTune {
1245    pub fn new(speed: f32) -> Self {
1246        let yin_size = 2048usize;
1247        Self {
1248            speed: speed.clamp(0.0, 1.0),
1249            concert_a: 440.0,
1250            yin_buf: vec![0.0; yin_size],
1251            yin_pos: 0,
1252            yin_size,
1253            current_shift: 1.0,
1254            delay_line: vec![0.0; yin_size],
1255            delay_pos: 0,
1256        }
1257    }
1258
1259    /// YIN algorithm: returns detected period in samples, or 0.0 if unclear.
1260    fn yin_pitch(&self, sample_rate: f32) -> Option<f32> {
1261        let n = self.yin_size / 2;
1262        let mut d = vec![0.0f32; n];
1263        // Difference function
1264        for tau in 1..n {
1265            let mut s = 0.0f32;
1266            for j in 0..n {
1267                let a = self.yin_buf[(self.yin_pos + j) % self.yin_size];
1268                let b = self.yin_buf[(self.yin_pos + j + tau) % self.yin_size];
1269                s += (a - b).powi(2);
1270            }
1271            d[tau] = s;
1272        }
1273        // Cumulative mean normalized difference
1274        let mut cmnd = vec![0.0f32; n];
1275        cmnd[0] = 1.0;
1276        let mut running_sum = 0.0f32;
1277        for tau in 1..n {
1278            running_sum += d[tau];
1279            cmnd[tau] = d[tau] * tau as f32 / running_sum;
1280        }
1281        // Absolute threshold: find first tau where cmnd < 0.1
1282        let threshold = 0.1f32;
1283        for tau in 2..n {
1284            if cmnd[tau] < threshold {
1285                // Parabolic interpolation
1286                let tau_f = if tau > 1 && tau < n - 1 {
1287                    let x0 = cmnd[tau - 1];
1288                    let x1 = cmnd[tau];
1289                    let x2 = cmnd[tau + 1];
1290                    let denom = 2.0 * (2.0 * x1 - x0 - x2);
1291                    if denom.abs() < 1e-10 { tau as f32 }
1292                    else { tau as f32 + (x0 - x2) / denom }
1293                } else { tau as f32 };
1294                return Some(sample_rate / tau_f);
1295            }
1296        }
1297        None
1298    }
1299
1300    fn note_to_freq(midi: i32, concert_a: f32) -> f32 {
1301        concert_a * 2.0f32.powf((midi - 69) as f32 / 12.0)
1302    }
1303
1304    fn freq_to_midi(freq: f32, concert_a: f32) -> f32 {
1305        if freq <= 0.0 { return 0.0; }
1306        69.0 + 12.0 * (freq / concert_a).log2()
1307    }
1308
1309    fn nearest_semitone_freq(freq: f32, concert_a: f32) -> f32 {
1310        let midi_f = Self::freq_to_midi(freq, concert_a);
1311        let midi_rounded = midi_f.round() as i32;
1312        Self::note_to_freq(midi_rounded, concert_a)
1313    }
1314}
1315impl AudioEffect for AutoTune {
1316    fn process_block(&mut self, buffer: &mut [f32], sample_rate: f32) {
1317        let smooth = 1.0 - self.speed;
1318        for s in buffer.iter_mut() {
1319            // Feed into YIN buffer
1320            self.yin_buf[self.yin_pos] = *s;
1321            self.yin_pos = (self.yin_pos + 1) % self.yin_size;
1322
1323            // Write delayed sample to output delay line
1324            self.delay_line[self.delay_pos] = *s;
1325
1326            // Periodic pitch detection (every yin_size/4 samples)
1327            if self.yin_pos % (self.yin_size / 4) == 0 {
1328                if let Some(detected_freq) = self.yin_pitch(sample_rate) {
1329                    if detected_freq > 50.0 && detected_freq < 2000.0 {
1330                        let target_freq = Self::nearest_semitone_freq(detected_freq, self.concert_a);
1331                        let target_shift = target_freq / detected_freq;
1332                        self.current_shift = self.current_shift * smooth + target_shift * (1.0 - smooth);
1333                    }
1334                }
1335            }
1336
1337            // Apply pitch shift via simple resampling from delay line (crude but real)
1338            let read_offset = (self.yin_size as f32 / 2.0 / self.current_shift.max(0.1)) as usize;
1339            let read_idx = (self.delay_pos + self.delay_line.len() - read_offset.min(self.delay_line.len() - 1)) % self.delay_line.len();
1340            *s = self.delay_line[read_idx];
1341
1342            self.delay_pos = (self.delay_pos + 1) % self.delay_line.len();
1343        }
1344    }
1345    fn name(&self) -> &str { "AutoTune" }
1346    fn reset(&mut self) {
1347        for v in self.yin_buf.iter_mut() { *v = 0.0; }
1348        for v in self.delay_line.iter_mut() { *v = 0.0; }
1349        self.yin_pos = 0;
1350        self.delay_pos = 0;
1351        self.current_shift = 1.0;
1352    }
1353}
1354
1355// ---------------------------------------------------------------------------
1356// Tremolo
1357// ---------------------------------------------------------------------------
1358
1359/// Amplitude modulation via LFO.
1360pub struct Tremolo {
1361    pub rate_hz: f32,
1362    pub depth: f32,
1363    phase: f32,
1364}
1365impl Tremolo {
1366    pub fn new(rate_hz: f32, depth: f32) -> Self {
1367        Self { rate_hz, depth, phase: 0.0 }
1368    }
1369}
1370impl AudioEffect for Tremolo {
1371    fn process_block(&mut self, buffer: &mut [f32], sample_rate: f32) {
1372        let phase_inc = self.rate_hz / sample_rate;
1373        for s in buffer.iter_mut() {
1374            let lfo = (self.phase * TAU).sin();
1375            let mod_gain = 1.0 - self.depth * lfo;
1376            *s *= mod_gain;
1377            self.phase = (self.phase + phase_inc) % 1.0;
1378        }
1379    }
1380    fn name(&self) -> &str { "Tremolo" }
1381    fn reset(&mut self) { self.phase = 0.0; }
1382}
1383
1384// ---------------------------------------------------------------------------
1385// Vibrato
1386// ---------------------------------------------------------------------------
1387
1388/// Frequency modulation (pitch wobble) via LFO.
1389pub struct Vibrato {
1390    pub rate_hz: f32,
1391    pub depth_semitones: f32,
1392    phase: f32,
1393    delay_buf: Vec<f32>,
1394    delay_pos: usize,
1395}
1396impl Vibrato {
1397    pub fn new(rate_hz: f32, depth_semitones: f32) -> Self {
1398        Self {
1399            rate_hz,
1400            depth_semitones,
1401            phase: 0.0,
1402            delay_buf: vec![0.0; 2048],
1403            delay_pos: 0,
1404        }
1405    }
1406}
1407impl AudioEffect for Vibrato {
1408    fn process_block(&mut self, buffer: &mut [f32], sample_rate: f32) {
1409        let phase_inc = self.rate_hz / sample_rate;
1410        let max_delay_samp = (self.depth_semitones * 0.01 * sample_rate).max(1.0) as usize;
1411        let max_delay_samp = max_delay_samp.min(self.delay_buf.len() - 1);
1412        let buf_len = self.delay_buf.len();
1413
1414        for s in buffer.iter_mut() {
1415            self.delay_buf[self.delay_pos] = *s;
1416            let lfo = (self.phase * TAU).sin();
1417            let delay_samp = (max_delay_samp as f32 * (lfo * 0.5 + 0.5)).max(1.0) as usize;
1418            let read_pos = (self.delay_pos + buf_len - delay_samp) % buf_len;
1419            *s = self.delay_buf[read_pos];
1420            self.delay_pos = (self.delay_pos + 1) % buf_len;
1421            self.phase = (self.phase + phase_inc) % 1.0;
1422        }
1423    }
1424    fn name(&self) -> &str { "Vibrato" }
1425    fn reset(&mut self) {
1426        for v in self.delay_buf.iter_mut() { *v = 0.0; }
1427        self.delay_pos = 0;
1428        self.phase = 0.0;
1429    }
1430}
1431
1432// ---------------------------------------------------------------------------
1433// Panner
1434// ---------------------------------------------------------------------------
1435
1436#[derive(Clone, Copy, Debug, PartialEq)]
1437pub enum PanLaw {
1438    Linear,
1439    MinusThreeDb,
1440    MinusSixDb,
1441}
1442
1443/// Stereo panner with selectable pan law, Haas effect, and mid-side encode/decode.
1444pub struct Panner {
1445    pub pan: f32, // -1.0 (left) to 1.0 (right)
1446    pub law: PanLaw,
1447    pub haas_delay_ms: f32,
1448    pub ms_encode: bool,
1449
1450    haas_buf: Vec<f32>,
1451    haas_pos: usize,
1452}
1453impl Panner {
1454    pub fn new(pan: f32, law: PanLaw, haas_delay_ms: f32, ms_encode: bool) -> Self {
1455        Self {
1456            pan: pan.clamp(-1.0, 1.0),
1457            law,
1458            haas_delay_ms,
1459            ms_encode,
1460            haas_buf: vec![0.0; 4096],
1461            haas_pos: 0,
1462        }
1463    }
1464
1465    fn gains(&self) -> (f32, f32) {
1466        let p = (self.pan + 1.0) * 0.5; // 0..1
1467        match self.law {
1468            PanLaw::Linear => (1.0 - p, p),
1469            PanLaw::MinusThreeDb => {
1470                let angle = p * PI * 0.5;
1471                (angle.cos(), angle.sin())
1472            }
1473            PanLaw::MinusSixDb => {
1474                // -6dB center: sqrt of linear
1475                ((1.0 - p).sqrt(), p.sqrt())
1476            }
1477        }
1478    }
1479}
1480impl AudioEffect for Panner {
1481    fn process_block(&mut self, buffer: &mut [f32], sample_rate: f32) {
1482        let haas_samp = (self.haas_delay_ms * 0.001 * sample_rate) as usize;
1483        let haas_samp = haas_samp.min(self.haas_buf.len() - 1);
1484        let buf_len = self.haas_buf.len();
1485        let (gl, gr) = self.gains();
1486
1487        for s in buffer.iter_mut() {
1488            self.haas_buf[self.haas_pos] = *s;
1489            let delayed = self.haas_buf[(self.haas_pos + buf_len - haas_samp) % buf_len];
1490            self.haas_pos = (self.haas_pos + 1) % buf_len;
1491
1492            if self.ms_encode {
1493                // Mid-side: mid = (L+R)/2, side = (L-R)/2 — operating on mono, just pass through
1494                let mid = (*s + delayed) * 0.5;
1495                let _side = (*s - delayed) * 0.5;
1496                *s = mid;
1497            } else {
1498                // Apply panning to mono: output is mono × gain (L-channel perspective)
1499                *s = *s * gl + delayed * gr;
1500            }
1501        }
1502    }
1503    fn name(&self) -> &str { "Panner" }
1504    fn reset(&mut self) {
1505        for v in self.haas_buf.iter_mut() { *v = 0.0; }
1506        self.haas_pos = 0;
1507    }
1508}
1509
1510// ---------------------------------------------------------------------------
1511// EffectChain
1512// ---------------------------------------------------------------------------
1513
1514/// A slot in the effect chain with bypass and wet/dry mix.
1515pub struct EffectSlot {
1516    pub effect: Box<dyn AudioEffect>,
1517    pub bypassed: bool,
1518    pub wet: f32,
1519    pub dry: f32,
1520}
1521
1522impl EffectSlot {
1523    pub fn new(effect: Box<dyn AudioEffect>) -> Self {
1524        Self { effect, bypassed: false, wet: 1.0, dry: 0.0 }
1525    }
1526    pub fn with_wet_dry(mut self, wet: f32, dry: f32) -> Self {
1527        self.wet = wet; self.dry = dry; self
1528    }
1529}
1530
1531/// Ordered chain of audio effects. Supports per-slot bypass and wet/dry.
1532pub struct EffectChain {
1533    pub slots: Vec<EffectSlot>,
1534}
1535impl EffectChain {
1536    pub fn new() -> Self { Self { slots: Vec::new() } }
1537
1538    pub fn add(&mut self, slot: EffectSlot) {
1539        self.slots.push(slot);
1540    }
1541
1542    pub fn add_effect(&mut self, effect: Box<dyn AudioEffect>) {
1543        self.slots.push(EffectSlot::new(effect));
1544    }
1545}
1546impl Default for EffectChain {
1547    fn default() -> Self { Self::new() }
1548}
1549impl AudioEffect for EffectChain {
1550    fn process_block(&mut self, buffer: &mut [f32], sample_rate: f32) {
1551        let n = buffer.len();
1552        let mut dry_buf: Vec<f32> = buffer.to_vec();
1553        let mut wet_buf: Vec<f32> = vec![0.0; n];
1554
1555        for slot in self.slots.iter_mut() {
1556            if slot.bypassed { continue; }
1557            // Start from current buffer state
1558            let mut work: Vec<f32> = buffer.to_vec();
1559            slot.effect.process_block(&mut work, sample_rate);
1560
1561            let wet = slot.wet;
1562            let dry = slot.dry;
1563            if (wet - 1.0).abs() < 1e-6 && dry < 1e-6 {
1564                // Pure wet — just copy
1565                buffer.copy_from_slice(&work);
1566            } else {
1567                for i in 0..n {
1568                    buffer[i] = dry_buf[i] * dry + work[i] * wet;
1569                }
1570                // Update dry_buf to current buffer for next stage
1571                dry_buf.copy_from_slice(buffer);
1572            }
1573            let _ = wet_buf.as_mut_slice(); // suppress unused warning
1574        }
1575    }
1576    fn name(&self) -> &str { "EffectChain" }
1577    fn reset(&mut self) {
1578        for slot in self.slots.iter_mut() { slot.effect.reset(); }
1579    }
1580}
1581
1582// ---------------------------------------------------------------------------
1583// Tests
1584// ---------------------------------------------------------------------------
1585
1586#[cfg(test)]
1587mod tests {
1588    use super::*;
1589
1590    #[test]
1591    fn test_gain_unity() {
1592        let mut g = Gain::new(1.0);
1593        let mut buf = vec![0.5f32; 64];
1594        g.process_block(&mut buf, 44100.0);
1595        for s in &buf { assert!((*s - 0.5).abs() < 1e-6); }
1596    }
1597
1598    #[test]
1599    fn test_gain_silence() {
1600        let mut g = Gain::new(0.0);
1601        let mut buf = vec![1.0f32; 64];
1602        g.process_block(&mut buf, 44100.0);
1603        for s in &buf { assert!(s.abs() < 1e-6); }
1604    }
1605
1606    #[test]
1607    fn test_gain_automation_linear() {
1608        let bps = vec![
1609            GainBreakpoint { sample: 0, gain: 0.0, exponential: false },
1610            GainBreakpoint { sample: 100, gain: 1.0, exponential: false },
1611        ];
1612        let mut ga = GainAutomation::new(bps);
1613        let mut buf = vec![1.0f32; 100];
1614        ga.process_block(&mut buf, 44100.0);
1615        // First sample ~0, last sample ~1
1616        assert!(buf[0] < 0.05);
1617        assert!(buf[99] > 0.95);
1618    }
1619
1620    #[test]
1621    fn test_compressor_no_gain_reduction_below_threshold() {
1622        let mut c = Compressor::new(-10.0, 4.0, 1.0, 100.0, 0.0, 0.0, false, 0);
1623        let mut buf = vec![0.001f32; 256]; // well below -10 dBFS
1624        c.process_block(&mut buf, 44100.0);
1625        // Should pass nearly unchanged
1626        assert!(buf[200].abs() > 0.0009);
1627    }
1628
1629    #[test]
1630    fn test_limiter_brickwall() {
1631        let mut lim = Limiter::new(-6.0, 50.0, 0);
1632        let threshold = db_to_linear(-6.0);
1633        let mut buf = vec![1.0f32; 512]; // 0 dBFS, above -6 dBFS threshold
1634        lim.process_block(&mut buf, 44100.0);
1635        for s in &buf[100..] {
1636            assert!(*s <= threshold + 1e-4, "sample {} > threshold {}", s, threshold);
1637        }
1638    }
1639
1640    #[test]
1641    fn test_gate_opens_above_threshold() {
1642        let mut gate = Gate::new(-40.0, -50.0, -80.0, 1.0, 10.0, 50.0, false);
1643        let mut buf = vec![0.01f32; 512]; // above -40 dBFS
1644        gate.process_block(&mut buf, 44100.0);
1645        // After attack, signal should be close to input
1646        let last = buf[400];
1647        assert!(last > 0.005, "gate should be open, got {}", last);
1648    }
1649
1650    #[test]
1651    fn test_biquad_lowpass_dc_passthrough() {
1652        let mut band = BiquadBand::new(BiquadType::LowPass, 1000.0, 0.707, 0.0);
1653        band.compute_coefficients(44100.0);
1654        // DC (constant 1.0) should pass through a low-pass
1655        let mut buf = vec![1.0f32; 512];
1656        for s in buf.iter_mut() { *s = band.process_sample(*s); }
1657        assert!(buf[400].abs() > 0.9, "DC should pass LPF, got {}", buf[400]);
1658    }
1659
1660    #[test]
1661    fn test_equalizer_processes() {
1662        let band = BiquadBand::new(BiquadType::PeakEq, 1000.0, 1.0, 6.0);
1663        let mut eq = Equalizer::new(vec![band]);
1664        let mut buf: Vec<f32> = (0..256).map(|i| (i as f32 * 0.01).sin()).collect();
1665        let orig: Vec<f32> = buf.clone();
1666        eq.process_block(&mut buf, 44100.0);
1667        // EQ should have changed at least some samples
1668        let changed = buf.iter().zip(orig.iter()).any(|(a, b)| (a - b).abs() > 1e-6);
1669        assert!(changed);
1670    }
1671
1672    #[test]
1673    fn test_reverb_adds_tail() {
1674        let mut rev = Reverb::new(0.8, 0.5, 0.5, 0.5, 0.0, 1.0);
1675        let mut buf = vec![0.0f32; 1024];
1676        buf[0] = 1.0; // impulse
1677        rev.process_block(&mut buf, 44100.0);
1678        // After the impulse there should be some reverb tail
1679        let tail_energy: f32 = buf[100..].iter().map(|s| s * s).sum();
1680        assert!(tail_energy > 0.0, "reverb should produce a tail");
1681    }
1682
1683    #[test]
1684    fn test_delay_dry_signal() {
1685        let mut delay = Delay::new(100.0, 0.0, 10000.0, false, 0.0, 1.0);
1686        let mut buf: Vec<f32> = (0..256).map(|i| (i as f32).sin()).collect();
1687        let orig = buf.clone();
1688        delay.process_block(&mut buf, 44100.0);
1689        // Dry=1, wet=0: output should equal input
1690        for i in 0..256 {
1691            assert!((buf[i] - orig[i]).abs() < 1e-5, "dry signal mismatch at {}", i);
1692        }
1693    }
1694
1695    #[test]
1696    fn test_distortion_soft_clip_bounded() {
1697        let mut dist = Distortion::new(DistortionMode::SoftClip, 10.0, 1.0);
1698        let mut buf = vec![100.0f32; 256]; // extreme drive
1699        dist.process_block(&mut buf, 44100.0);
1700        for s in &buf { assert!(s.abs() <= 1.0 + 1e-5, "soft clip exceeded 1.0: {}", s); }
1701    }
1702
1703    #[test]
1704    fn test_distortion_hard_clip_bounded() {
1705        let mut dist = Distortion::new(DistortionMode::HardClip, 10.0, 1.0);
1706        let mut buf = vec![100.0f32; 256];
1707        dist.process_block(&mut buf, 44100.0);
1708        for s in &buf { assert!(s.abs() <= 10.0 + 1e-4); } // drive * 1.0 clamped
1709    }
1710
1711    #[test]
1712    fn test_tremolo_modulates_amplitude() {
1713        let mut trem = Tremolo::new(10.0, 1.0);
1714        let mut buf = vec![1.0f32; 1024];
1715        trem.process_block(&mut buf, 44100.0);
1716        let min = buf.iter().cloned().fold(f32::MAX, f32::min);
1717        let max = buf.iter().cloned().fold(f32::MIN, f32::max);
1718        assert!(max > 0.9, "should have near-full amplitude");
1719        assert!(min < 0.1, "should have near-zero amplitude with depth=1");
1720    }
1721
1722    #[test]
1723    fn test_effect_chain_bypass() {
1724        let mut chain = EffectChain::new();
1725        let mut slot = EffectSlot::new(Box::new(Gain::new(0.0)));
1726        slot.bypassed = true;
1727        chain.add(slot);
1728        let mut buf = vec![1.0f32; 64];
1729        chain.process_block(&mut buf, 44100.0);
1730        // Bypassed gain=0 should leave signal unchanged
1731        for s in &buf { assert!((*s - 1.0).abs() < 1e-6); }
1732    }
1733
1734    #[test]
1735    fn test_chorus_produces_output() {
1736        let mut chorus = Chorus::new(4, 1.5, 2.0, 0.5, 0.3, 0.5, 0.5);
1737        let mut buf: Vec<f32> = (0..512).map(|i| (i as f32 * 0.1).sin()).collect();
1738        chorus.process_block(&mut buf, 44100.0);
1739        let energy: f32 = buf.iter().map(|s| s * s).sum();
1740        assert!(energy > 0.0, "chorus should produce output");
1741    }
1742
1743    #[test]
1744    fn test_phaser_produces_output() {
1745        let mut phaser = Phaser::new(8, 0.5, 0.7, 0.5, 500.0, true, 0.7, 0.3);
1746        let mut buf: Vec<f32> = (0..256).map(|i| (i as f32 * 0.05).sin()).collect();
1747        phaser.process_block(&mut buf, 44100.0);
1748        let energy: f32 = buf.iter().map(|s| s * s).sum();
1749        assert!(energy > 0.0);
1750    }
1751}