Skip to main content

quiver/modules/
timefx.rs

1//! Delay-based and time-domain effect modules.
2
3use super::common::{env_coef, read_interpolated, sanitize_audio};
4use crate::port::{GraphModule, PortDef, PortSpec, PortValues, SignalKind};
5use alloc::vec;
6use alloc::vec::Vec;
7use core::f64::consts::TAU;
8use libm::Libm;
9
10/// Unit Delay (single sample delay)
11///
12/// Delays a signal by one sample. Essential for feedback loops.
13pub struct UnitDelay {
14    buffer: f64,
15    spec: PortSpec,
16}
17
18impl UnitDelay {
19    pub fn new() -> Self {
20        Self {
21            buffer: 0.0,
22            spec: PortSpec {
23                inputs: vec![PortDef::new(0, "in", SignalKind::Audio)],
24                outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
25            },
26        }
27    }
28}
29
30impl Default for UnitDelay {
31    fn default() -> Self {
32        Self::new()
33    }
34}
35
36impl GraphModule for UnitDelay {
37    fn port_spec(&self) -> &PortSpec {
38        &self.spec
39    }
40
41    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
42        let input = inputs.get_or(0, 0.0);
43        outputs.set(10, self.buffer);
44        self.buffer = input;
45    }
46
47    fn reset(&mut self) {
48        self.buffer = 0.0;
49    }
50
51    fn set_sample_rate(&mut self, _: f64) {}
52
53    fn breaks_feedback_cycle(&self) -> bool {
54        true
55    }
56
57    fn type_id(&self) -> &'static str {
58        "unit_delay"
59    }
60}
61
62/// Delay Line
63///
64/// A multi-sample delay line with feedback and wet/dry mix.
65/// Supports CV-controlled delay time for effects like chorus and flanging.
66///
67/// Maximum delay time is 2 seconds at any sample rate.
68pub struct DelayLine {
69    buffer: Vec<f64>,
70    write_pos: usize,
71    sample_rate: f64,
72    /// Slew-smoothed read distance, tracking the delay setpoint gradually to
73    /// avoid zipper/pitch glitches when the `time` CV jumps.
74    smoothed_delay: f64,
75    /// One-pole retain coefficient for `smoothed_delay` (sample-rate aware).
76    delay_smooth_coef: f64,
77    /// Whether `smoothed_delay` has been snapped to its first setpoint yet.
78    delay_primed: bool,
79    spec: PortSpec,
80}
81
82impl DelayLine {
83    /// Maximum delay time in seconds
84    const MAX_DELAY_SECS: f64 = 2.0;
85
86    /// Time constant for delay-time smoothing (a few ms de-zippers modulation
87    /// without audibly lagging deliberate delay-time changes).
88    const DELAY_SMOOTH_SECS: f64 = 0.005;
89
90    pub fn new(sample_rate: f64) -> Self {
91        let buffer_size = (sample_rate * Self::MAX_DELAY_SECS) as usize + 1;
92        Self {
93            buffer: vec![0.0; buffer_size],
94            write_pos: 0,
95            sample_rate,
96            smoothed_delay: 0.0,
97            delay_smooth_coef: env_coef(Self::DELAY_SMOOTH_SECS, sample_rate),
98            delay_primed: false,
99            spec: PortSpec {
100                inputs: vec![
101                    PortDef::new(0, "in", SignalKind::Audio),
102                    PortDef::new(1, "time", SignalKind::CvUnipolar)
103                        .with_default(0.5)
104                        .with_attenuverter(),
105                    PortDef::new(2, "feedback", SignalKind::CvUnipolar)
106                        .with_default(0.0)
107                        .with_attenuverter(),
108                    PortDef::new(3, "mix", SignalKind::CvUnipolar)
109                        .with_default(0.5)
110                        .with_attenuverter(),
111                ],
112                outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
113            },
114        }
115    }
116}
117
118impl Default for DelayLine {
119    fn default() -> Self {
120        Self::new(44100.0)
121    }
122}
123
124impl GraphModule for DelayLine {
125    fn port_spec(&self) -> &PortSpec {
126        &self.spec
127    }
128
129    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
130        // Q160: sanitize so a non-finite input can never enter the feedback
131        // buffer (where it would recirculate forever, latching NaN).
132        let input = sanitize_audio(inputs.get_or(0, 0.0));
133        let time_cv = inputs.get_or(1, 0.5).clamp(0.0, 1.0);
134        let feedback = inputs.get_or(2, 0.0).clamp(0.0, 0.99); // Prevent runaway
135        let mix = inputs.get_or(3, 0.5).clamp(0.0, 1.0);
136
137        // Map time CV (0-1) to delay time (1ms to max delay, exponential)
138        let min_delay_ms = 1.0;
139        let max_delay_ms = Self::MAX_DELAY_SECS * 1000.0;
140        let delay_ms = min_delay_ms * Libm::<f64>::pow(max_delay_ms / min_delay_ms, time_cv);
141        let target_delay =
142            (delay_ms * self.sample_rate / 1000.0).clamp(1.0, (self.buffer.len() - 1) as f64);
143
144        // Slew-limit the read distance toward its setpoint with a one-pole
145        // smoother so a step in `time` glides instead of jumping (no clicks).
146        // Snap on the first tick so startup does not sweep up from zero.
147        if self.delay_primed {
148            self.smoothed_delay =
149                target_delay + (self.smoothed_delay - target_delay) * self.delay_smooth_coef;
150        } else {
151            self.smoothed_delay = target_delay;
152            self.delay_primed = true;
153        }
154        let delay_samples = self.smoothed_delay;
155
156        // Read from delay line
157        let delayed = read_interpolated(&self.buffer, self.write_pos, delay_samples);
158
159        // Write input + feedback to buffer
160        self.buffer[self.write_pos] = input + delayed * feedback;
161
162        // Advance write position
163        self.write_pos = (self.write_pos + 1) % self.buffer.len();
164
165        // Mix dry and wet signals
166        let output = input * (1.0 - mix) + delayed * mix;
167        outputs.set(10, output);
168    }
169
170    fn reset(&mut self) {
171        self.buffer.fill(0.0);
172        self.write_pos = 0;
173        self.smoothed_delay = 0.0;
174        self.delay_primed = false;
175    }
176
177    fn set_sample_rate(&mut self, sample_rate: f64) {
178        self.sample_rate = sample_rate;
179        let buffer_size = (sample_rate * Self::MAX_DELAY_SECS) as usize + 1;
180        self.buffer = vec![0.0; buffer_size];
181        self.write_pos = 0;
182        self.smoothed_delay = 0.0;
183        self.delay_smooth_coef = env_coef(Self::DELAY_SMOOTH_SECS, sample_rate);
184        self.delay_primed = false;
185    }
186
187    fn breaks_feedback_cycle(&self) -> bool {
188        true
189    }
190
191    fn type_id(&self) -> &'static str {
192        "delay_line"
193    }
194}
195
196/// Chorus Effect
197///
198/// Classic chorus effect using multiple modulated delay lines.
199/// Creates a rich, shimmering sound by mixing slightly detuned copies
200/// of the input signal.
201pub struct Chorus {
202    /// Three delay lines for rich chorus
203    delay_buffers: [Vec<f64>; 3],
204    write_pos: usize,
205    /// LFO phases for each voice
206    lfo_phases: [f64; 3],
207    sample_rate: f64,
208    spec: PortSpec,
209}
210
211impl Chorus {
212    /// Maximum modulation delay in milliseconds
213    const MAX_MOD_DELAY_MS: f64 = 25.0;
214    /// Base delay in milliseconds
215    const BASE_DELAY_MS: f64 = 7.0;
216
217    /// Modulated delay (in samples) for one chorus voice.
218    ///
219    /// The LFO term is made **unipolar** (`sin*0.5 + 0.5`, range `0..=1`) so the
220    /// delay stays within `[base, base + mod_depth]` and is always positive.
221    /// A bipolar sweep (`base + sin*mod_depth`) goes negative whenever
222    /// `mod_depth > base` — which is true at the stock `depth_cv = 0.5`
223    /// (`base = 7 ms`, `mod_depth = 12.5 ms`), where it clamps the trough of the
224    /// sweep to 1 sample and one-sidedly distorts the chorus.
225    #[inline]
226    fn voice_delay_samples(base_delay_samples: f64, mod_depth_samples: f64, lfo_val: f64) -> f64 {
227        base_delay_samples + (lfo_val * 0.5 + 0.5) * mod_depth_samples
228    }
229
230    pub fn new(sample_rate: f64) -> Self {
231        let buffer_size =
232            ((Self::MAX_MOD_DELAY_MS + Self::BASE_DELAY_MS) * sample_rate / 1000.0) as usize + 10;
233        Self {
234            delay_buffers: [
235                vec![0.0; buffer_size],
236                vec![0.0; buffer_size],
237                vec![0.0; buffer_size],
238            ],
239            write_pos: 0,
240            // Offset phases for each voice to create movement
241            lfo_phases: [0.0, 0.33, 0.67],
242            sample_rate,
243            spec: PortSpec {
244                inputs: vec![
245                    PortDef::new(0, "in", SignalKind::Audio),
246                    PortDef::new(1, "rate", SignalKind::CvUnipolar)
247                        .with_default(0.3)
248                        .with_attenuverter(),
249                    PortDef::new(2, "depth", SignalKind::CvUnipolar)
250                        .with_default(0.5)
251                        .with_attenuverter(),
252                    PortDef::new(3, "mix", SignalKind::CvUnipolar)
253                        .with_default(0.5)
254                        .with_attenuverter(),
255                ],
256                outputs: vec![
257                    PortDef::new(10, "out", SignalKind::Audio),
258                    PortDef::new(11, "left", SignalKind::Audio),
259                    PortDef::new(12, "right", SignalKind::Audio),
260                ],
261            },
262        }
263    }
264}
265
266impl Default for Chorus {
267    fn default() -> Self {
268        Self::new(44100.0)
269    }
270}
271
272impl GraphModule for Chorus {
273    fn port_spec(&self) -> &PortSpec {
274        &self.spec
275    }
276
277    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
278        // Q160: sanitize so a non-finite input can never enter the modulated
279        // delay buffer.
280        let input = sanitize_audio(inputs.get_or(0, 0.0));
281        let rate_cv = inputs.get_or(1, 0.3).clamp(0.0, 1.0);
282        let depth_cv = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
283        let mix = inputs.get_or(3, 0.5).clamp(0.0, 1.0);
284
285        // Map rate CV to LFO frequency (0.1 Hz to 5 Hz)
286        let lfo_freq = 0.1 * Libm::<f64>::pow(50.0, rate_cv);
287
288        // Map depth CV to modulation depth in ms
289        let mod_depth_ms = depth_cv * Self::MAX_MOD_DELAY_MS;
290
291        let base_delay_samples = Self::BASE_DELAY_MS * self.sample_rate / 1000.0;
292        let mod_depth_samples = mod_depth_ms * self.sample_rate / 1000.0;
293
294        let mut wet_sum = 0.0;
295        let mut left_sum = 0.0;
296        let mut right_sum = 0.0;
297
298        for i in 0..3 {
299            // Calculate modulated delay for this voice
300            let lfo_val = Libm::<f64>::sin(self.lfo_phases[i] * core::f64::consts::TAU);
301            let delay_samples =
302                Self::voice_delay_samples(base_delay_samples, mod_depth_samples, lfo_val)
303                    .clamp(1.0, (self.delay_buffers[i].len() - 1) as f64);
304
305            // Read from this voice's delay line
306            let delayed = read_interpolated(&self.delay_buffers[i], self.write_pos, delay_samples);
307
308            wet_sum += delayed;
309
310            // Stereo spread: voice 0 center, voice 1 left, voice 2 right
311            match i {
312                0 => {
313                    left_sum += delayed * 0.5;
314                    right_sum += delayed * 0.5;
315                }
316                1 => left_sum += delayed,
317                2 => right_sum += delayed,
318                _ => {}
319            }
320
321            // Write input to this voice's delay buffer
322            self.delay_buffers[i][self.write_pos] = input;
323
324            // Advance LFO phase with slight detuning between voices
325            let freq_mult = 1.0 + (i as f64 - 1.0) * 0.1; // Slight frequency offset
326            let phase_inc = lfo_freq * freq_mult / self.sample_rate;
327            self.lfo_phases[i] += phase_inc;
328            if self.lfo_phases[i] >= 1.0 {
329                self.lfo_phases[i] -= 1.0;
330            }
331        }
332
333        // Normalize wet signal (3 voices)
334        wet_sum /= 3.0;
335        left_sum /= 2.0;
336        right_sum /= 2.0;
337
338        // Advance write position
339        self.write_pos = (self.write_pos + 1) % self.delay_buffers[0].len();
340
341        // Mix dry and wet
342        let mono_out = input * (1.0 - mix) + wet_sum * mix;
343        let left_out = input * (1.0 - mix) + left_sum * mix;
344        let right_out = input * (1.0 - mix) + right_sum * mix;
345
346        outputs.set(10, mono_out);
347        outputs.set(11, left_out);
348        outputs.set(12, right_out);
349    }
350
351    fn reset(&mut self) {
352        for buffer in &mut self.delay_buffers {
353            buffer.fill(0.0);
354        }
355        self.write_pos = 0;
356        self.lfo_phases = [0.0, 0.33, 0.67];
357    }
358
359    fn set_sample_rate(&mut self, sample_rate: f64) {
360        self.sample_rate = sample_rate;
361        let buffer_size =
362            ((Self::MAX_MOD_DELAY_MS + Self::BASE_DELAY_MS) * sample_rate / 1000.0) as usize + 10;
363        for buffer in &mut self.delay_buffers {
364            *buffer = vec![0.0; buffer_size];
365        }
366        self.write_pos = 0;
367    }
368
369    fn type_id(&self) -> &'static str {
370        "chorus"
371    }
372}
373
374/// Flanger
375///
376/// Classic flanging effect using a short modulated delay with feedback.
377///
378/// Mono-in, stereo-out: the two delay lines share one LFO but read it at a
379/// per-channel phase offset controlled by the `spread` input, decorrelating the
380/// left and right sweeps. The legacy `out` port reproduces the historical mono
381/// channel exactly and is bit-identical to `left`, so existing patches keep
382/// working; connect `left`/`right` for the stereo image.
383pub struct Flanger {
384    /// Dual delay lines, indexed `[left, right]`.
385    buffers: [Vec<f64>; 2],
386    write_pos: usize,
387    lfo_phase: f64,
388    sample_rate: f64,
389    spec: PortSpec,
390}
391
392impl Flanger {
393    const MAX_DELAY_MS: f64 = 10.0;
394
395    pub fn new(sample_rate: f64) -> Self {
396        let buffer_size = (sample_rate * Self::MAX_DELAY_MS / 1000.0) as usize + 10;
397        Self {
398            buffers: [vec![0.0; buffer_size], vec![0.0; buffer_size]],
399            write_pos: 0,
400            lfo_phase: 0.0,
401            sample_rate,
402            spec: PortSpec {
403                inputs: vec![
404                    PortDef::new(0, "in", SignalKind::Audio),
405                    PortDef::new(1, "rate", SignalKind::CvUnipolar)
406                        .with_default(0.3)
407                        .with_attenuverter(),
408                    PortDef::new(2, "depth", SignalKind::CvUnipolar)
409                        .with_default(0.5)
410                        .with_attenuverter(),
411                    PortDef::new(3, "feedback", SignalKind::CvBipolar)
412                        .with_default(0.0)
413                        .with_attenuverter(),
414                    PortDef::new(4, "mix", SignalKind::CvUnipolar)
415                        .with_default(0.5)
416                        .with_attenuverter(),
417                    // Stereo spread: 0 collapses to mono (L==R==out), 1 offsets
418                    // the right sweep by 180 degrees for maximum decorrelation.
419                    PortDef::new(5, "spread", SignalKind::CvUnipolar)
420                        .with_default(0.5)
421                        .with_attenuverter(),
422                ],
423                outputs: vec![
424                    PortDef::new(10, "out", SignalKind::Audio),
425                    PortDef::new(11, "left", SignalKind::Audio),
426                    PortDef::new(12, "right", SignalKind::Audio),
427                ],
428            },
429        }
430    }
431}
432
433impl Default for Flanger {
434    fn default() -> Self {
435        Self::new(44100.0)
436    }
437}
438
439impl GraphModule for Flanger {
440    fn port_spec(&self) -> &PortSpec {
441        &self.spec
442    }
443
444    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
445        // Q160: sanitize so a non-finite input can never enter the feedback
446        // delay buffer.
447        let input = sanitize_audio(inputs.get_or(0, 0.0));
448        let rate_cv = inputs.get_or(1, 0.3).clamp(0.0, 1.0);
449        let depth_cv = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
450        let feedback = inputs.get_or(3, 0.0).clamp(-0.95, 0.95);
451        let mix = inputs.get_or(4, 0.5).clamp(0.0, 1.0);
452        let spread = inputs.get_or(5, 0.5).clamp(0.0, 1.0);
453
454        let lfo_freq = 0.05 * Libm::<f64>::pow(100.0, rate_cv);
455        let base_delay_ms = 1.0;
456        let mod_depth_ms = depth_cv * (Self::MAX_DELAY_MS - base_delay_ms);
457
458        // Per-channel LFO phase offset: spread 0..1 maps to 0..0.5 cycles
459        // (0..180 degrees). The left channel tracks the base phase (so `out`
460        // stays bit-identical to the historical mono behavior); the right
461        // channel leads by the offset to decorrelate the two sweeps.
462        let phase_offset = spread * 0.5;
463        let max_read = (self.buffers[0].len() - 1) as f64;
464
465        let mut wet = [0.0; 2];
466        for (ch, w) in wet.iter_mut().enumerate() {
467            let phase = self.lfo_phase + if ch == 0 { 0.0 } else { phase_offset };
468            let lfo = (Libm::<f64>::sin(phase * TAU) + 1.0) * 0.5;
469            let delay_ms = base_delay_ms + lfo * mod_depth_ms;
470            let delay_samples = (delay_ms * self.sample_rate / 1000.0).clamp(1.0, max_read);
471            let delayed = read_interpolated(&self.buffers[ch], self.write_pos, delay_samples);
472            // Per-channel feedback tap keeps the two lines independent.
473            self.buffers[ch][self.write_pos] = input + delayed * feedback;
474            *w = delayed;
475        }
476
477        self.lfo_phase += lfo_freq / self.sample_rate;
478        if self.lfo_phase >= 1.0 {
479            self.lfo_phase -= 1.0;
480        }
481        self.write_pos = (self.write_pos + 1) % self.buffers[0].len();
482
483        let left = input * (1.0 - mix) + wet[0] * mix;
484        let right = input * (1.0 - mix) + wet[1] * mix;
485        // `out` mirrors `left` for backward compatibility with mono patches.
486        outputs.set(10, left);
487        outputs.set(11, left);
488        outputs.set(12, right);
489    }
490
491    fn reset(&mut self) {
492        for buffer in &mut self.buffers {
493            buffer.fill(0.0);
494        }
495        self.write_pos = 0;
496        self.lfo_phase = 0.0;
497    }
498
499    fn set_sample_rate(&mut self, sample_rate: f64) {
500        self.sample_rate = sample_rate;
501        let buffer_size = (sample_rate * Self::MAX_DELAY_MS / 1000.0) as usize + 10;
502        for buffer in &mut self.buffers {
503            *buffer = vec![0.0; buffer_size];
504        }
505        self.write_pos = 0;
506    }
507
508    fn type_id(&self) -> &'static str {
509        "flanger"
510    }
511}
512
513/// Phaser
514///
515/// Classic phaser effect using cascaded all-pass filters.
516///
517/// Mono-in, stereo-out: two independent allpass chains share one LFO but read
518/// it at a per-channel phase offset controlled by the `spread` input, giving
519/// decorrelated left/right notch sweeps and per-channel feedback taps. The
520/// legacy `out` port reproduces the historical mono channel exactly and is
521/// bit-identical to `left`, so existing patches keep working.
522pub struct Phaser {
523    /// Previous input per allpass stage (`x[n-1]`), indexed `[channel][stage]`.
524    allpass_x1: [[f64; 6]; 2],
525    /// Previous output per allpass stage (`y[n-1]`), indexed `[channel][stage]`.
526    allpass_y1: [[f64; 6]; 2],
527    lfo_phase: f64,
528    sample_rate: f64,
529    spec: PortSpec,
530}
531
532impl Phaser {
533    pub fn new(sample_rate: f64) -> Self {
534        Self {
535            allpass_x1: [[0.0; 6]; 2],
536            allpass_y1: [[0.0; 6]; 2],
537            lfo_phase: 0.0,
538            sample_rate,
539            spec: PortSpec {
540                inputs: vec![
541                    PortDef::new(0, "in", SignalKind::Audio),
542                    PortDef::new(1, "rate", SignalKind::CvUnipolar)
543                        .with_default(0.3)
544                        .with_attenuverter(),
545                    PortDef::new(2, "depth", SignalKind::CvUnipolar)
546                        .with_default(0.7)
547                        .with_attenuverter(),
548                    PortDef::new(3, "feedback", SignalKind::CvBipolar)
549                        .with_default(0.0)
550                        .with_attenuverter(),
551                    PortDef::new(4, "mix", SignalKind::CvUnipolar)
552                        .with_default(0.5)
553                        .with_attenuverter(),
554                    PortDef::new(5, "stages", SignalKind::CvUnipolar).with_default(1.0),
555                    // Stereo spread: 0 collapses to mono (L==R==out), 1 offsets
556                    // the right sweep by 180 degrees for maximum decorrelation.
557                    PortDef::new(6, "spread", SignalKind::CvUnipolar)
558                        .with_default(0.5)
559                        .with_attenuverter(),
560                ],
561                outputs: vec![
562                    PortDef::new(10, "out", SignalKind::Audio),
563                    PortDef::new(11, "left", SignalKind::Audio),
564                    PortDef::new(12, "right", SignalKind::Audio),
565                ],
566            },
567        }
568    }
569
570    /// First-order allpass section with a truly flat magnitude response.
571    ///
572    /// Implements `H(z) = (coef + z^-1) / (1 + coef z^-1)`, i.e.
573    /// `y[n] = coef*x[n] + x[n-1] - coef*y[n-1]`. Unit magnitude holds at every
574    /// frequency (DC gain `+1`, Nyquist gain `-1`) for all `|coef| < 1`, so
575    /// cascading these produces the phase-only notches a phaser needs rather
576    /// than the moving-lowpass coloration of the previous (non-allpass)
577    /// topology.
578    fn allpass(input: f64, x1: &mut f64, y1: &mut f64, coef: f64) -> f64 {
579        let output = coef * input + *x1 - coef * *y1;
580        *x1 = input;
581        *y1 = output;
582        output
583    }
584}
585
586impl Default for Phaser {
587    fn default() -> Self {
588        Self::new(44100.0)
589    }
590}
591
592impl GraphModule for Phaser {
593    fn port_spec(&self) -> &PortSpec {
594        &self.spec
595    }
596
597    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
598        // Q160: sanitize so a non-finite input can never enter the all-pass
599        // feedback chain.
600        let input = sanitize_audio(inputs.get_or(0, 0.0));
601        let rate_cv = inputs.get_or(1, 0.3).clamp(0.0, 1.0);
602        let depth = inputs.get_or(2, 0.7).clamp(0.0, 1.0);
603        let feedback = inputs.get_or(3, 0.0).clamp(-0.95, 0.95);
604        let mix = inputs.get_or(4, 0.5).clamp(0.0, 1.0);
605        let stages_cv = inputs.get_or(5, 1.0).clamp(0.0, 1.0);
606
607        let num_stages = if stages_cv < 0.33 {
608            2
609        } else if stages_cv < 0.66 {
610            4
611        } else {
612            6
613        };
614
615        let spread = inputs.get_or(6, 0.5).clamp(0.0, 1.0);
616
617        let lfo_freq = 0.05 * Libm::<f64>::pow(100.0, rate_cv);
618
619        let min_freq = 200.0;
620        let max_freq = 4000.0;
621
622        // Per-channel LFO phase offset: spread 0..1 maps to 0..0.5 cycles
623        // (0..180 degrees). The left channel tracks the base phase (so `out`
624        // stays bit-identical to the historical mono behavior); the right
625        // channel leads by the offset to decorrelate the notch sweeps.
626        let phase_offset = spread * 0.5;
627
628        let mut wet = [0.0; 2];
629        for (ch, w) in wet.iter_mut().enumerate() {
630            let phase = self.lfo_phase + if ch == 0 { 0.0 } else { phase_offset };
631            let lfo = Libm::<f64>::sin(phase * TAU);
632            let freq = min_freq + (lfo * 0.5 + 0.5) * depth * (max_freq - min_freq);
633
634            let omega = TAU * freq / self.sample_rate;
635            let tan_w = Libm::<f64>::tan(omega * 0.5);
636            let coef = (1.0 - tan_w) / (1.0 + tan_w);
637
638            // Per-channel feedback tap from this chain's last stage.
639            let mut signal = input + self.allpass_y1[ch][num_stages - 1] * feedback;
640            for i in 0..num_stages {
641                signal = Self::allpass(
642                    signal,
643                    &mut self.allpass_x1[ch][i],
644                    &mut self.allpass_y1[ch][i],
645                    coef,
646                );
647            }
648            *w = signal;
649        }
650
651        self.lfo_phase += lfo_freq / self.sample_rate;
652        if self.lfo_phase >= 1.0 {
653            self.lfo_phase -= 1.0;
654        }
655
656        let left = input * (1.0 - mix) + wet[0] * mix;
657        let right = input * (1.0 - mix) + wet[1] * mix;
658        // `out` mirrors `left` for backward compatibility with mono patches.
659        outputs.set(10, left);
660        outputs.set(11, left);
661        outputs.set(12, right);
662    }
663
664    fn reset(&mut self) {
665        self.allpass_x1 = [[0.0; 6]; 2];
666        self.allpass_y1 = [[0.0; 6]; 2];
667        self.lfo_phase = 0.0;
668    }
669
670    fn set_sample_rate(&mut self, sample_rate: f64) {
671        self.sample_rate = sample_rate;
672    }
673
674    fn type_id(&self) -> &'static str {
675        "phaser"
676    }
677}
678
679// ============================================================================
680// P3 Effects: Tremolo, Vibrato, Distortion
681// ============================================================================
682
683/// Tremolo
684///
685/// Amplitude modulation effect with adjustable rate, depth, and waveform.
686/// Creates classic "wobbly" volume effect.
687pub struct Tremolo {
688    lfo_phase: f64,
689    sample_rate: f64,
690    spec: PortSpec,
691}
692
693impl Tremolo {
694    pub fn new(sample_rate: f64) -> Self {
695        Self {
696            lfo_phase: 0.0,
697            sample_rate,
698            spec: PortSpec {
699                inputs: vec![
700                    PortDef::new(0, "in", SignalKind::Audio),
701                    PortDef::new(1, "rate", SignalKind::CvUnipolar)
702                        .with_default(0.3)
703                        .with_attenuverter(),
704                    PortDef::new(2, "depth", SignalKind::CvUnipolar)
705                        .with_default(0.5)
706                        .with_attenuverter(),
707                    PortDef::new(3, "shape", SignalKind::CvUnipolar)
708                        .with_default(0.0)
709                        .with_attenuverter(),
710                ],
711                outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
712            },
713        }
714    }
715}
716
717impl Default for Tremolo {
718    fn default() -> Self {
719        Self::new(44100.0)
720    }
721}
722
723impl GraphModule for Tremolo {
724    fn port_spec(&self) -> &PortSpec {
725        &self.spec
726    }
727
728    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
729        let input = inputs.get_or(0, 0.0);
730        let rate_cv = inputs.get_or(1, 0.3).clamp(0.0, 1.0);
731        let depth = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
732        let shape = inputs.get_or(3, 0.0).clamp(0.0, 1.0);
733
734        // Rate: 0.1Hz to 20Hz (exponential)
735        let lfo_freq = 0.1 * Libm::<f64>::pow(200.0, rate_cv);
736
737        // Generate LFO: blend between sine and triangle based on shape
738        let phase_rad = self.lfo_phase * TAU;
739        let sine = Libm::<f64>::sin(phase_rad);
740        let triangle = 1.0 - 4.0 * Libm::<f64>::fabs(self.lfo_phase - 0.5);
741        let lfo = sine * (1.0 - shape) + triangle * shape;
742
743        // Advance phase
744        self.lfo_phase += lfo_freq / self.sample_rate;
745        if self.lfo_phase >= 1.0 {
746            self.lfo_phase -= 1.0;
747        }
748
749        // Apply amplitude modulation
750        // LFO ranges -1 to 1, convert to modulation amount
751        let modulation = 1.0 - depth * 0.5 * (1.0 - lfo);
752        outputs.set(10, input * modulation);
753    }
754
755    fn reset(&mut self) {
756        self.lfo_phase = 0.0;
757    }
758
759    fn set_sample_rate(&mut self, sample_rate: f64) {
760        self.sample_rate = sample_rate;
761    }
762
763    fn type_id(&self) -> &'static str {
764        "tremolo"
765    }
766}
767
768/// Vibrato
769///
770/// Pitch modulation effect using a modulated delay line.
771/// Creates classic pitch wobble effect.
772pub struct Vibrato {
773    buffer: Vec<f64>,
774    write_pos: usize,
775    lfo_phase: f64,
776    sample_rate: f64,
777    spec: PortSpec,
778}
779
780impl Vibrato {
781    const MAX_DELAY_MS: f64 = 20.0;
782
783    pub fn new(sample_rate: f64) -> Self {
784        let buffer_size = (sample_rate * Self::MAX_DELAY_MS / 1000.0) as usize + 10;
785        Self {
786            buffer: vec![0.0; buffer_size],
787            write_pos: 0,
788            lfo_phase: 0.0,
789            sample_rate,
790            spec: PortSpec {
791                inputs: vec![
792                    PortDef::new(0, "in", SignalKind::Audio),
793                    PortDef::new(1, "rate", SignalKind::CvUnipolar)
794                        .with_default(0.3)
795                        .with_attenuverter(),
796                    PortDef::new(2, "depth", SignalKind::CvUnipolar)
797                        .with_default(0.5)
798                        .with_attenuverter(),
799                    PortDef::new(3, "mix", SignalKind::CvUnipolar)
800                        .with_default(1.0)
801                        .with_attenuverter(),
802                ],
803                outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
804            },
805        }
806    }
807}
808
809impl Default for Vibrato {
810    fn default() -> Self {
811        Self::new(44100.0)
812    }
813}
814
815impl GraphModule for Vibrato {
816    fn port_spec(&self) -> &PortSpec {
817        &self.spec
818    }
819
820    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
821        let input = inputs.get_or(0, 0.0);
822        let rate_cv = inputs.get_or(1, 0.3).clamp(0.0, 1.0);
823        let depth = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
824        let mix = inputs.get_or(3, 1.0).clamp(0.0, 1.0);
825
826        // Rate: 0.1Hz to 15Hz (exponential)
827        let lfo_freq = 0.1 * Libm::<f64>::pow(150.0, rate_cv);
828
829        // Base delay at center of modulation range
830        let base_delay_ms = Self::MAX_DELAY_MS * 0.5;
831        let mod_depth_ms = depth * base_delay_ms * 0.9;
832
833        // Sinusoidal LFO
834        let lfo = Libm::<f64>::sin(self.lfo_phase * TAU);
835        self.lfo_phase += lfo_freq / self.sample_rate;
836        if self.lfo_phase >= 1.0 {
837            self.lfo_phase -= 1.0;
838        }
839
840        // Calculate modulated delay
841        let delay_ms = base_delay_ms + lfo * mod_depth_ms;
842        let delay_samples =
843            (delay_ms * self.sample_rate / 1000.0).clamp(1.0, (self.buffer.len() - 1) as f64);
844
845        // Read before writing (matching DelayLine/Flanger/Chorus) so the
846        // minimum effective delay is `delay_samples`, not one sample shorter.
847        let delayed = read_interpolated(&self.buffer, self.write_pos, delay_samples);
848
849        // Write to buffer and advance
850        self.buffer[self.write_pos] = input;
851        self.write_pos = (self.write_pos + 1) % self.buffer.len();
852
853        outputs.set(10, input * (1.0 - mix) + delayed * mix);
854    }
855
856    fn reset(&mut self) {
857        self.buffer.fill(0.0);
858        self.write_pos = 0;
859        self.lfo_phase = 0.0;
860    }
861
862    fn set_sample_rate(&mut self, sample_rate: f64) {
863        self.sample_rate = sample_rate;
864        let buffer_size = (sample_rate * Self::MAX_DELAY_MS / 1000.0) as usize + 10;
865        // Reset the buffer and write cursor: `write_pos` is a direct (non-modulo)
866        // index at the write site, so a stale value left over from a larger
867        // buffer would index out of bounds after lowering the sample rate
868        // shrinks the buffer. Matches Chorus/Flanger/DelayLine.
869        self.buffer = vec![0.0; buffer_size];
870        self.write_pos = 0;
871        self.lfo_phase = 0.0;
872    }
873
874    fn type_id(&self) -> &'static str {
875        "vibrato"
876    }
877}
878
879/// Freeverb-style comb filter tunings at 44.1kHz
880const COMB_TUNINGS_44100: [usize; 8] = [1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617];
881
882/// Freeverb-style all-pass filter tunings at 44.1kHz
883const ALLPASS_TUNINGS_44100: [usize; 4] = [556, 441, 341, 225];
884
885/// Stereo spread (samples offset for right channel)
886const STEREO_SPREAD: usize = 23;
887
888/// Maximum buffer size for comb filters (accommodates up to 96kHz)
889const MAX_COMB_SIZE: usize = 4096;
890
891/// Maximum buffer size for all-pass filters
892const MAX_ALLPASS_SIZE: usize = 1500;
893
894/// Maximum pre-delay buffer (100ms at 96kHz)
895const MAX_PREDELAY_SIZE: usize = 9600;
896
897/// Algorithmic reverb using Freeverb architecture
898///
899/// Features 8 parallel comb filters with damping, followed by
900/// 4 series all-pass filters for diffusion. Produces stereo output.
901///
902/// # Ports
903/// - Input 0: Audio input
904/// - Input 1: Room size (0-1, default 0.5)
905/// - Input 2: Damping (0-1, default 0.5)
906/// - Input 3: Wet/dry mix (0-1, default 0.5)
907/// - Input 4: Pre-delay time (0-1, maps to 0-100ms)
908/// - Output 10: Left channel
909/// - Output 11: Right channel
910pub struct Reverb {
911    // Comb filters (8 left, 8 right) - heap allocated due to size
912    comb_buffers_l: Vec<Vec<f64>>,
913    comb_buffers_r: Vec<Vec<f64>>,
914    comb_pos_l: [usize; 8],
915    comb_pos_r: [usize; 8],
916    comb_filter_state_l: [f64; 8], // Lowpass state for damping
917    comb_filter_state_r: [f64; 8],
918
919    // All-pass filters (4 left, 4 right)
920    allpass_buffers_l: Vec<Vec<f64>>,
921    allpass_buffers_r: Vec<Vec<f64>>,
922    allpass_pos_l: [usize; 4],
923    allpass_pos_r: [usize; 4],
924
925    // Pre-delay
926    predelay_buffer: Vec<f64>,
927    predelay_pos: usize,
928
929    // Current tunings (scaled for sample rate)
930    comb_lengths: [usize; 8],
931    allpass_lengths: [usize; 4],
932    /// Right-channel decorrelation offset, scaled with sample rate.
933    stereo_spread: usize,
934
935    sample_rate: f64,
936    spec: PortSpec,
937}
938
939impl Reverb {
940    /// Create a new reverb with the given sample rate
941    pub fn new(sample_rate: f64) -> Self {
942        let mut reverb = Self {
943            comb_buffers_l: (0..8).map(|_| vec![0.0; MAX_COMB_SIZE]).collect(),
944            comb_buffers_r: (0..8).map(|_| vec![0.0; MAX_COMB_SIZE]).collect(),
945            comb_pos_l: [0; 8],
946            comb_pos_r: [0; 8],
947            comb_filter_state_l: [0.0; 8],
948            comb_filter_state_r: [0.0; 8],
949
950            allpass_buffers_l: (0..4).map(|_| vec![0.0; MAX_ALLPASS_SIZE]).collect(),
951            allpass_buffers_r: (0..4).map(|_| vec![0.0; MAX_ALLPASS_SIZE]).collect(),
952            allpass_pos_l: [0; 4],
953            allpass_pos_r: [0; 4],
954
955            predelay_buffer: vec![0.0; MAX_PREDELAY_SIZE],
956            predelay_pos: 0,
957
958            comb_lengths: [0; 8],
959            allpass_lengths: [0; 4],
960            stereo_spread: STEREO_SPREAD,
961
962            sample_rate,
963            spec: PortSpec {
964                inputs: vec![
965                    PortDef::new(0, "in", SignalKind::Audio),
966                    PortDef::new(1, "size", SignalKind::CvUnipolar).with_default(0.5),
967                    PortDef::new(2, "damping", SignalKind::CvUnipolar).with_default(0.5),
968                    PortDef::new(3, "mix", SignalKind::CvUnipolar).with_default(0.5),
969                    PortDef::new(4, "predelay", SignalKind::CvUnipolar).with_default(0.0),
970                ],
971                outputs: vec![
972                    PortDef::new(10, "left", SignalKind::Audio),
973                    PortDef::new(11, "right", SignalKind::Audio),
974                ],
975            },
976        };
977        reverb.update_tunings();
978        reverb
979    }
980
981    /// Update filter tunings based on sample rate
982    fn update_tunings(&mut self) {
983        let ratio = self.sample_rate / 44100.0;
984
985        for (i, &base) in COMB_TUNINGS_44100.iter().enumerate() {
986            self.comb_lengths[i] = ((base as f64 * ratio) as usize).min(MAX_COMB_SIZE - 1);
987        }
988
989        for (i, &base) in ALLPASS_TUNINGS_44100.iter().enumerate() {
990            self.allpass_lengths[i] = ((base as f64 * ratio) as usize).min(MAX_ALLPASS_SIZE - 1);
991        }
992
993        // Scale the stereo decorrelation offset with sample rate too, so the
994        // right channel stays as decorrelated at 96 kHz as it is at 44.1 kHz
995        // (a raw 23-sample offset would shrink relative to the tunings).
996        self.stereo_spread = (Libm::<f64>::round(STEREO_SPREAD as f64 * ratio) as usize).max(1);
997    }
998
999    /// Process a single comb filter with damping
1000    #[inline]
1001    fn process_comb(
1002        buffer: &mut [f64],
1003        pos: &mut usize,
1004        filter_state: &mut f64,
1005        input: f64,
1006        length: usize,
1007        feedback: f64,
1008        damping: f64,
1009    ) -> f64 {
1010        let output = buffer[*pos];
1011
1012        // Damping lowpass filter
1013        *filter_state = output * (1.0 - damping) + *filter_state * damping;
1014
1015        // Write input + filtered feedback
1016        buffer[*pos] = input + *filter_state * feedback;
1017
1018        *pos += 1;
1019        if *pos >= length {
1020            *pos = 0;
1021        }
1022
1023        output
1024    }
1025
1026    /// Process a single all-pass filter
1027    #[inline]
1028    fn process_allpass(buffer: &mut [f64], pos: &mut usize, input: f64, length: usize) -> f64 {
1029        const ALLPASS_FEEDBACK: f64 = 0.5;
1030
1031        let buffered = buffer[*pos];
1032        let output = -input + buffered;
1033
1034        buffer[*pos] = input + buffered * ALLPASS_FEEDBACK;
1035
1036        *pos += 1;
1037        if *pos >= length {
1038            *pos = 0;
1039        }
1040
1041        output
1042    }
1043}
1044
1045impl Default for Reverb {
1046    fn default() -> Self {
1047        Self::new(44100.0)
1048    }
1049}
1050
1051impl GraphModule for Reverb {
1052    fn port_spec(&self) -> &PortSpec {
1053        &self.spec
1054    }
1055
1056    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1057        // Q160: sanitize so a non-finite input can never enter the comb/allpass
1058        // feedback network (where it would latch NaN across the whole tail).
1059        let input = sanitize_audio(inputs.get_or(0, 0.0));
1060        let size = inputs.get_or(1, 0.5).clamp(0.0, 1.0);
1061        let damping = inputs.get_or(2, 0.5).clamp(0.0, 1.0);
1062        let mix = inputs.get_or(3, 0.5).clamp(0.0, 1.0);
1063        let predelay_cv = inputs.get_or(4, 0.0).clamp(0.0, 1.0);
1064
1065        // Freeverb scaling
1066        let room_scale = 0.28 + size * 0.7;
1067        let damp = damping * 0.4;
1068
1069        // Pre-delay (0-100ms)
1070        let predelay_samples =
1071            (predelay_cv * 0.1 * self.sample_rate).min(MAX_PREDELAY_SIZE as f64 - 1.0) as usize;
1072
1073        // Write to pre-delay buffer
1074        self.predelay_buffer[self.predelay_pos] = input;
1075        let predelay_read_pos = if self.predelay_pos >= predelay_samples {
1076            self.predelay_pos - predelay_samples
1077        } else {
1078            MAX_PREDELAY_SIZE - (predelay_samples - self.predelay_pos)
1079        };
1080        let predelayed = if predelay_samples > 0 {
1081            self.predelay_buffer[predelay_read_pos]
1082        } else {
1083            input
1084        };
1085        self.predelay_pos = (self.predelay_pos + 1) % MAX_PREDELAY_SIZE;
1086
1087        // Process 8 parallel comb filters (accumulate for left and right)
1088        let mut comb_out_l = 0.0;
1089        let mut comb_out_r = 0.0;
1090
1091        for i in 0..8 {
1092            // Left channel
1093            let length_l = self.comb_lengths[i];
1094            comb_out_l += Self::process_comb(
1095                &mut self.comb_buffers_l[i],
1096                &mut self.comb_pos_l[i],
1097                &mut self.comb_filter_state_l[i],
1098                predelayed,
1099                length_l,
1100                room_scale,
1101                damp,
1102            );
1103
1104            // Right channel (with stereo spread offset for decorrelation)
1105            let length_r = (self.comb_lengths[i] + self.stereo_spread).min(MAX_COMB_SIZE - 1);
1106            comb_out_r += Self::process_comb(
1107                &mut self.comb_buffers_r[i],
1108                &mut self.comb_pos_r[i],
1109                &mut self.comb_filter_state_r[i],
1110                predelayed,
1111                length_r,
1112                room_scale,
1113                damp,
1114            );
1115        }
1116
1117        // Scale comb output
1118        comb_out_l *= 0.125;
1119        comb_out_r *= 0.125;
1120
1121        // Process 4 series all-pass filters
1122        let mut allpass_out_l = comb_out_l;
1123        let mut allpass_out_r = comb_out_r;
1124
1125        for i in 0..4 {
1126            let length_l = self.allpass_lengths[i];
1127            allpass_out_l = Self::process_allpass(
1128                &mut self.allpass_buffers_l[i],
1129                &mut self.allpass_pos_l[i],
1130                allpass_out_l,
1131                length_l,
1132            );
1133
1134            let length_r = (self.allpass_lengths[i] + self.stereo_spread).min(MAX_ALLPASS_SIZE - 1);
1135            allpass_out_r = Self::process_allpass(
1136                &mut self.allpass_buffers_r[i],
1137                &mut self.allpass_pos_r[i],
1138                allpass_out_r,
1139                length_r,
1140            );
1141        }
1142
1143        // Wet/dry mix
1144        let left = input * (1.0 - mix) + allpass_out_l * mix;
1145        let right = input * (1.0 - mix) + allpass_out_r * mix;
1146
1147        outputs.set(10, left);
1148        outputs.set(11, right);
1149    }
1150
1151    fn reset(&mut self) {
1152        for buf in &mut self.comb_buffers_l {
1153            buf.iter_mut().for_each(|x| *x = 0.0);
1154        }
1155        for buf in &mut self.comb_buffers_r {
1156            buf.iter_mut().for_each(|x| *x = 0.0);
1157        }
1158        self.comb_pos_l = [0; 8];
1159        self.comb_pos_r = [0; 8];
1160        self.comb_filter_state_l = [0.0; 8];
1161        self.comb_filter_state_r = [0.0; 8];
1162
1163        for buf in &mut self.allpass_buffers_l {
1164            buf.iter_mut().for_each(|x| *x = 0.0);
1165        }
1166        for buf in &mut self.allpass_buffers_r {
1167            buf.iter_mut().for_each(|x| *x = 0.0);
1168        }
1169        self.allpass_pos_l = [0; 4];
1170        self.allpass_pos_r = [0; 4];
1171
1172        self.predelay_buffer.iter_mut().for_each(|x| *x = 0.0);
1173        self.predelay_pos = 0;
1174    }
1175
1176    fn set_sample_rate(&mut self, sample_rate: f64) {
1177        self.sample_rate = sample_rate;
1178        self.update_tunings();
1179        self.reset();
1180    }
1181
1182    fn type_id(&self) -> &'static str {
1183        "reverb"
1184    }
1185}
1186
1187// =============================================================================
1188// Vocoder - Spectral Vocoding Effect
1189// =============================================================================
1190
1191#[cfg(test)]
1192mod tests {
1193    use super::*;
1194
1195    #[test]
1196    fn test_unit_delay() {
1197        let mut delay = UnitDelay::new();
1198        let mut inputs = PortValues::new();
1199        let mut outputs = PortValues::new();
1200
1201        // First sample
1202        inputs.set(0, 1.0);
1203        delay.tick(&inputs, &mut outputs);
1204        assert!((outputs.get(10).unwrap() - 0.0).abs() < 0.01); // Should be initial value
1205
1206        // Second sample
1207        inputs.set(0, 2.0);
1208        delay.tick(&inputs, &mut outputs);
1209        assert!((outputs.get(10).unwrap() - 1.0).abs() < 0.01); // Should be previous input
1210    }
1211    #[test]
1212    fn test_delay_line() {
1213        let mut delay = DelayLine::new(44100.0);
1214        let mut inputs = PortValues::new();
1215        let mut outputs = PortValues::new();
1216
1217        // Set delay time to minimum and mix to wet only
1218        inputs.set(1, 0.0); // Minimum time
1219        inputs.set(2, 0.0); // No feedback
1220        inputs.set(3, 1.0); // 100% wet
1221
1222        // Feed an impulse
1223        inputs.set(0, 1.0);
1224        delay.tick(&inputs, &mut outputs);
1225
1226        // First output should be from empty buffer (near zero)
1227        let first_out = outputs.get(10).unwrap();
1228        assert!(first_out.abs() < 0.1);
1229
1230        // Continue processing
1231        inputs.set(0, 0.0);
1232        for _ in 0..100 {
1233            delay.tick(&inputs, &mut outputs);
1234        }
1235
1236        // Eventually should output our impulse
1237        let out = outputs.get(10).unwrap();
1238        assert!(out.is_finite());
1239    }
1240    #[test]
1241    fn test_delay_line_feedback() {
1242        let mut delay = DelayLine::new(44100.0);
1243        let mut inputs = PortValues::new();
1244        let mut outputs = PortValues::new();
1245
1246        // Set high feedback
1247        inputs.set(1, 0.0); // Minimum time
1248        inputs.set(2, 0.5); // 50% feedback
1249        inputs.set(3, 0.5); // 50% wet
1250
1251        // Feed an impulse
1252        inputs.set(0, 1.0);
1253        delay.tick(&inputs, &mut outputs);
1254
1255        // Process more samples with no input
1256        inputs.set(0, 0.0);
1257        for _ in 0..1000 {
1258            delay.tick(&inputs, &mut outputs);
1259        }
1260
1261        // Output should still be finite (feedback doesn't blow up)
1262        let out = outputs.get(10).unwrap();
1263        assert!(out.is_finite());
1264    }
1265    #[test]
1266    fn test_delay_line_reset() {
1267        let mut delay = DelayLine::new(44100.0);
1268        let mut inputs = PortValues::new();
1269        let mut outputs = PortValues::new();
1270
1271        // Feed some signal
1272        inputs.set(0, 1.0);
1273        for _ in 0..100 {
1274            delay.tick(&inputs, &mut outputs);
1275        }
1276
1277        // Reset
1278        delay.reset();
1279
1280        // Buffer should be cleared
1281        inputs.set(0, 0.0);
1282        inputs.set(3, 1.0); // 100% wet
1283        delay.tick(&inputs, &mut outputs);
1284        let out = outputs.get(10).unwrap();
1285        assert!(out.abs() < 0.01);
1286    }
1287    #[test]
1288    fn test_chorus() {
1289        let mut chorus = Chorus::new(44100.0);
1290        let mut inputs = PortValues::new();
1291        let mut outputs = PortValues::new();
1292
1293        // Default settings
1294        inputs.set(0, 0.5); // Input signal
1295
1296        // Process several samples to let LFOs move
1297        for _ in 0..1000 {
1298            chorus.tick(&inputs, &mut outputs);
1299        }
1300
1301        // Should produce output on all three ports
1302        let mono = outputs.get(10).unwrap();
1303        let left = outputs.get(11).unwrap();
1304        let right = outputs.get(12).unwrap();
1305
1306        assert!(mono.is_finite());
1307        assert!(left.is_finite());
1308        assert!(right.is_finite());
1309    }
1310    #[test]
1311    fn test_chorus_stereo_spread() {
1312        let mut chorus = Chorus::new(44100.0);
1313        let mut inputs = PortValues::new();
1314        let mut outputs = PortValues::new();
1315
1316        // Set mix to 100% wet
1317        inputs.set(0, 1.0); // Input signal
1318        inputs.set(1, 0.5); // Rate
1319        inputs.set(2, 0.5); // Depth
1320        inputs.set(3, 1.0); // 100% wet
1321
1322        // Process many samples
1323        let mut left_sum = 0.0;
1324        let mut right_sum = 0.0;
1325        for _ in 0..10000 {
1326            chorus.tick(&inputs, &mut outputs);
1327            left_sum += outputs.get(11).unwrap().abs();
1328            right_sum += outputs.get(12).unwrap().abs();
1329        }
1330
1331        // Both channels should have significant output
1332        assert!(left_sum > 1.0);
1333        assert!(right_sum > 1.0);
1334    }
1335    #[test]
1336    fn test_chorus_reset() {
1337        let mut chorus = Chorus::new(44100.0);
1338        let mut inputs = PortValues::new();
1339        let mut outputs = PortValues::new();
1340
1341        // Feed signal
1342        inputs.set(0, 1.0);
1343        for _ in 0..1000 {
1344            chorus.tick(&inputs, &mut outputs);
1345        }
1346
1347        // Reset
1348        chorus.reset();
1349
1350        // Check LFO phases are reset
1351        inputs.set(0, 0.0);
1352        inputs.set(3, 1.0); // 100% wet
1353        chorus.tick(&inputs, &mut outputs);
1354
1355        // Output should be near zero after reset with zero input
1356        let out = outputs.get(10).unwrap();
1357        assert!(out.abs() < 0.1);
1358    }
1359    #[test]
1360    fn test_delay_line_type_id() {
1361        let delay = DelayLine::new(44100.0);
1362        assert_eq!(delay.type_id(), "delay_line");
1363    }
1364    #[test]
1365    fn test_chorus_type_id() {
1366        let chorus = Chorus::new(44100.0);
1367        assert_eq!(chorus.type_id(), "chorus");
1368    }
1369    #[test]
1370    fn test_delay_line_default() {
1371        let delay = DelayLine::default();
1372        assert_eq!(delay.type_id(), "delay_line");
1373    }
1374    #[test]
1375    fn test_chorus_default() {
1376        let chorus = Chorus::default();
1377        assert_eq!(chorus.type_id(), "chorus");
1378    }
1379    #[test]
1380    fn test_flanger() {
1381        let mut flanger = Flanger::new(44100.0);
1382        let mut inputs = PortValues::new();
1383        let mut outputs = PortValues::new();
1384
1385        inputs.set(0, 1.0);
1386        for _ in 0..1000 {
1387            flanger.tick(&inputs, &mut outputs);
1388        }
1389
1390        let out = outputs.get(10).unwrap();
1391        assert!(out.is_finite());
1392    }
1393    #[test]
1394    fn test_flanger_default() {
1395        let flanger = Flanger::default();
1396        assert_eq!(flanger.type_id(), "flanger");
1397    }
1398    #[test]
1399    fn test_phaser() {
1400        let mut phaser = Phaser::new(44100.0);
1401        let mut inputs = PortValues::new();
1402        let mut outputs = PortValues::new();
1403
1404        inputs.set(0, 1.0);
1405        for _ in 0..1000 {
1406            phaser.tick(&inputs, &mut outputs);
1407        }
1408
1409        let out = outputs.get(10).unwrap();
1410        assert!(out.is_finite());
1411    }
1412    #[test]
1413    fn test_phaser_default() {
1414        let phaser = Phaser::default();
1415        assert_eq!(phaser.type_id(), "phaser");
1416    }
1417    #[test]
1418    fn test_phaser_stages() {
1419        let mut phaser = Phaser::new(44100.0);
1420        let mut inputs = PortValues::new();
1421        let mut outputs = PortValues::new();
1422
1423        inputs.set(0, 1.0);
1424        inputs.set(5, 0.0); // 2 stages
1425
1426        for _ in 0..100 {
1427            phaser.tick(&inputs, &mut outputs);
1428        }
1429        let out_2 = outputs.get(10).unwrap();
1430
1431        phaser.reset();
1432        inputs.set(5, 1.0); // 6 stages
1433
1434        for _ in 0..100 {
1435            phaser.tick(&inputs, &mut outputs);
1436        }
1437        let out_6 = outputs.get(10).unwrap();
1438
1439        // Both should produce valid output
1440        assert!(out_2.is_finite());
1441        assert!(out_6.is_finite());
1442    }
1443
1444    // Q144: Flanger and Phaser are now mono-in / stereo-out. The `out` port
1445    // (id 10) must stay bit-identical to `left` (id 11) for backward compat,
1446    // spread=0 must collapse to a mono image (L==R==out), and spread>0 must
1447    // decorrelate the left and right channels.
1448
1449    #[test]
1450    fn test_flanger_out_mirrors_left_and_mono_at_zero_spread() {
1451        let mut flanger = Flanger::new(44100.0);
1452        let mut inputs = PortValues::new();
1453        let mut outputs = PortValues::new();
1454
1455        inputs.set(2, 0.8); // depth
1456        inputs.set(4, 1.0); // full wet exposes the wet paths
1457        inputs.set(5, 0.0); // spread = 0 -> mono
1458
1459        for k in 0..5000 {
1460            inputs.set(0, Libm::<f64>::sin(k as f64 * 0.03));
1461            flanger.tick(&inputs, &mut outputs);
1462            let out = outputs.get(10).unwrap();
1463            let left = outputs.get(11).unwrap();
1464            let right = outputs.get(12).unwrap();
1465            assert_eq!(out, left, "out must equal left");
1466            assert_eq!(left, right, "spread=0 must give bit-identical L/R");
1467        }
1468    }
1469
1470    #[test]
1471    fn test_flanger_stereo_decorrelation() {
1472        let mut flanger = Flanger::new(44100.0);
1473        let mut inputs = PortValues::new();
1474        let mut outputs = PortValues::new();
1475
1476        inputs.set(1, 0.5); // rate
1477        inputs.set(2, 0.9); // depth
1478        inputs.set(4, 1.0); // full wet
1479        inputs.set(5, 1.0); // spread = 180 degrees
1480
1481        let mut diff = 0.0;
1482        for k in 0..20000 {
1483            inputs.set(0, Libm::<f64>::sin(k as f64 * 0.05));
1484            flanger.tick(&inputs, &mut outputs);
1485            // `out` still tracks the left channel with spread engaged.
1486            assert_eq!(outputs.get(10).unwrap(), outputs.get(11).unwrap());
1487            let left = outputs.get(11).unwrap();
1488            let right = outputs.get(12).unwrap();
1489            diff += (left - right).abs();
1490        }
1491        assert!(
1492            diff > 1.0,
1493            "left/right should decorrelate with spread; diff = {diff}"
1494        );
1495    }
1496
1497    #[test]
1498    fn test_phaser_out_mirrors_left_and_mono_at_zero_spread() {
1499        let mut phaser = Phaser::new(44100.0);
1500        let mut inputs = PortValues::new();
1501        let mut outputs = PortValues::new();
1502
1503        inputs.set(2, 0.8); // depth
1504        inputs.set(4, 1.0); // full wet
1505        inputs.set(6, 0.0); // spread = 0 -> mono
1506
1507        for k in 0..5000 {
1508            inputs.set(0, Libm::<f64>::sin(k as f64 * 0.03));
1509            phaser.tick(&inputs, &mut outputs);
1510            let out = outputs.get(10).unwrap();
1511            let left = outputs.get(11).unwrap();
1512            let right = outputs.get(12).unwrap();
1513            assert_eq!(out, left, "out must equal left");
1514            assert_eq!(left, right, "spread=0 must give bit-identical L/R");
1515        }
1516    }
1517
1518    #[test]
1519    fn test_phaser_stereo_decorrelation() {
1520        let mut phaser = Phaser::new(44100.0);
1521        let mut inputs = PortValues::new();
1522        let mut outputs = PortValues::new();
1523
1524        inputs.set(2, 0.9); // depth
1525        inputs.set(4, 1.0); // full wet
1526        inputs.set(6, 1.0); // spread = 180 degrees
1527
1528        let mut diff = 0.0;
1529        for k in 0..20000 {
1530            inputs.set(0, Libm::<f64>::sin(k as f64 * 0.07));
1531            phaser.tick(&inputs, &mut outputs);
1532            assert_eq!(outputs.get(10).unwrap(), outputs.get(11).unwrap());
1533            let left = outputs.get(11).unwrap();
1534            let right = outputs.get(12).unwrap();
1535            diff += (left - right).abs();
1536        }
1537        assert!(
1538            diff > 1.0,
1539            "phaser left/right should decorrelate with spread; diff = {diff}"
1540        );
1541    }
1542
1543    #[test]
1544    fn test_unit_delay_default_reset_sample_rate() {
1545        let mut delay = UnitDelay::default();
1546        let mut inputs = PortValues::new();
1547        let mut outputs = PortValues::new();
1548        inputs.set(0, 5.0);
1549        delay.tick(&inputs, &mut outputs);
1550
1551        delay.reset();
1552        assert!(delay.buffer == 0.0);
1553
1554        delay.set_sample_rate(48000.0);
1555        assert_eq!(delay.type_id(), "unit_delay");
1556    }
1557    #[test]
1558    fn test_reverb_default_reset_sample_rate() {
1559        let mut reverb = Reverb::default();
1560        assert_eq!(reverb.sample_rate, 44100.0);
1561
1562        // Feed some signal
1563        let mut inputs = PortValues::new();
1564        let mut outputs = PortValues::new();
1565        inputs.set(0, 0.5);
1566        reverb.tick(&inputs, &mut outputs);
1567
1568        // Reset should clear buffers
1569        reverb.reset();
1570        assert_eq!(reverb.predelay_pos, 0);
1571        assert_eq!(reverb.comb_pos_l, [0; 8]);
1572        assert_eq!(reverb.comb_pos_r, [0; 8]);
1573        assert_eq!(reverb.allpass_pos_l, [0; 4]);
1574        assert_eq!(reverb.allpass_pos_r, [0; 4]);
1575
1576        // Sample rate change
1577        reverb.set_sample_rate(48000.0);
1578        assert_eq!(reverb.sample_rate, 48000.0);
1579
1580        assert_eq!(reverb.type_id(), "reverb");
1581        assert_eq!(reverb.port_spec().inputs.len(), 5);
1582        assert_eq!(reverb.port_spec().outputs.len(), 2);
1583    }
1584    #[test]
1585    fn test_reverb_stereo_output() {
1586        let mut reverb = Reverb::new(44100.0);
1587        let mut inputs = PortValues::new();
1588        let mut outputs = PortValues::new();
1589
1590        // Send an impulse
1591        inputs.set(0, 1.0);
1592        inputs.set(3, 1.0); // Full wet
1593        reverb.tick(&inputs, &mut outputs);
1594
1595        // Feed silence and track total energy
1596        inputs.set(0, 0.0);
1597        let mut total_energy = 0.0;
1598        for _ in 0..3000 {
1599            reverb.tick(&inputs, &mut outputs);
1600            total_energy += outputs.get(10).unwrap().abs();
1601            total_energy += outputs.get(11).unwrap().abs();
1602        }
1603
1604        // We should have accumulated some reverb energy
1605        assert!(
1606            total_energy > 0.01,
1607            "Reverb should produce output after impulse, got total_energy={}",
1608            total_energy
1609        );
1610    }
1611    #[test]
1612    fn test_reverb_dry_signal() {
1613        let mut reverb = Reverb::new(44100.0);
1614        let mut inputs = PortValues::new();
1615        let mut outputs = PortValues::new();
1616
1617        // Full dry
1618        inputs.set(0, 0.75);
1619        inputs.set(3, 0.0); // Mix = 0 (full dry)
1620        reverb.tick(&inputs, &mut outputs);
1621
1622        let left = outputs.get(10).unwrap();
1623        let right = outputs.get(11).unwrap();
1624
1625        // With 0% wet, output should equal input
1626        assert!(
1627            (left - 0.75).abs() < 0.001,
1628            "Full dry should pass through: got {}",
1629            left
1630        );
1631        assert!(
1632            (right - 0.75).abs() < 0.001,
1633            "Full dry should pass through: got {}",
1634            right
1635        );
1636    }
1637    #[test]
1638    fn test_reverb_room_size() {
1639        let mut reverb1 = Reverb::new(44100.0);
1640        let mut reverb2 = Reverb::new(44100.0);
1641        let mut inputs1 = PortValues::new();
1642        let mut inputs2 = PortValues::new();
1643        let mut outputs1 = PortValues::new();
1644        let mut outputs2 = PortValues::new();
1645
1646        // Impulse response with different room sizes
1647        inputs1.set(0, 1.0);
1648        inputs1.set(1, 0.1); // Small room
1649        inputs1.set(3, 1.0); // Full wet
1650        reverb1.tick(&inputs1, &mut outputs1);
1651
1652        inputs2.set(0, 1.0);
1653        inputs2.set(1, 0.9); // Large room
1654        inputs2.set(3, 1.0); // Full wet
1655        reverb2.tick(&inputs2, &mut outputs2);
1656
1657        // Process more samples with silence
1658        inputs1.set(0, 0.0);
1659        inputs2.set(0, 0.0);
1660        let mut energy1 = 0.0;
1661        let mut energy2 = 0.0;
1662
1663        for _ in 0..5000 {
1664            reverb1.tick(&inputs1, &mut outputs1);
1665            reverb2.tick(&inputs2, &mut outputs2);
1666            energy1 += outputs1.get(10).unwrap().abs();
1667            energy2 += outputs2.get(10).unwrap().abs();
1668        }
1669
1670        // Larger room should have longer decay (more energy over time)
1671        assert!(
1672            energy2 > energy1,
1673            "Larger room should have longer decay: small={}, large={}",
1674            energy1,
1675            energy2
1676        );
1677    }
1678    #[test]
1679    fn test_reverb_predelay() {
1680        let mut reverb = Reverb::new(44100.0);
1681        let mut inputs = PortValues::new();
1682        let mut outputs = PortValues::new();
1683
1684        // With predelay, the wet signal should be delayed
1685        inputs.set(0, 1.0); // Impulse
1686        inputs.set(3, 1.0); // Full wet
1687        inputs.set(4, 1.0); // Max predelay (100ms = 4410 samples at 44.1kHz)
1688
1689        // First tick
1690        reverb.tick(&inputs, &mut outputs);
1691
1692        // At sample 0, with 100ms predelay, wet signal should still be 0
1693        let first_output = outputs.get(10).unwrap();
1694
1695        // Feed silence and track energy
1696        inputs.set(0, 0.0);
1697        let mut total_energy = 0.0;
1698
1699        // Run enough samples to pass the predelay plus comb filter delay
1700        for _ in 0..6000 {
1701            reverb.tick(&inputs, &mut outputs);
1702            total_energy += outputs.get(10).unwrap().abs();
1703        }
1704
1705        assert!(
1706            total_energy > 0.01,
1707            "Reverb should appear after predelay period, got energy={}",
1708            total_energy
1709        );
1710        assert!(
1711            first_output.abs() < 0.001,
1712            "First sample should be near zero due to predelay, got {}",
1713            first_output
1714        );
1715    }
1716    #[test]
1717    fn test_reverb_damping() {
1718        let mut reverb_low = Reverb::new(44100.0);
1719        let mut reverb_high = Reverb::new(44100.0);
1720        let mut inputs = PortValues::new();
1721        let mut outputs_low = PortValues::new();
1722        let mut outputs_high = PortValues::new();
1723
1724        // Impulse
1725        inputs.set(0, 1.0);
1726        inputs.set(2, 0.1); // Low damping
1727        inputs.set(3, 1.0);
1728        reverb_low.tick(&inputs, &mut outputs_low);
1729
1730        inputs.set(2, 0.9); // High damping
1731        reverb_high.tick(&inputs, &mut outputs_high);
1732
1733        // Process more
1734        inputs.set(0, 0.0);
1735        for _ in 0..3000 {
1736            reverb_low.tick(&inputs, &mut outputs_low);
1737            reverb_high.tick(&inputs, &mut outputs_high);
1738        }
1739
1740        // Both should produce some output (the damping affects character, not overall level dramatically)
1741        // This test verifies both modes work without errors
1742        let out_low = outputs_low.get(10).unwrap();
1743        let out_high = outputs_high.get(10).unwrap();
1744
1745        // Just verify they produce valid output
1746        assert!(out_low.is_finite());
1747        assert!(out_high.is_finite());
1748    }
1749    #[test]
1750    fn test_reverb_tunings_scale_with_sample_rate() {
1751        let reverb_44 = Reverb::new(44100.0);
1752        let reverb_48 = Reverb::new(48000.0);
1753
1754        // Higher sample rate should have proportionally longer comb lengths
1755        let ratio = 48000.0 / 44100.0;
1756
1757        for i in 0..8 {
1758            let expected = (reverb_44.comb_lengths[i] as f64 * ratio) as usize;
1759            assert!(
1760                (reverb_48.comb_lengths[i] as i64 - expected as i64).abs() < 2,
1761                "Comb filter {} should scale with sample rate",
1762                i
1763            );
1764        }
1765    }
1766
1767    /// Drive a sinusoid through a single allpass stage and return the
1768    /// steady-state RMS gain (input RMS / output RMS).
1769    #[cfg(test)]
1770    fn allpass_rms_gain(coef: f64, freq_norm: f64) -> f64 {
1771        let mut x1 = 0.0;
1772        let mut y1 = 0.0;
1773        let n = 40_000;
1774        let warmup = 8_000;
1775        let mut sum_in = 0.0;
1776        let mut sum_out = 0.0;
1777        for i in 0..n {
1778            let x = Libm::<f64>::sin(TAU * freq_norm * i as f64);
1779            let y = Phaser::allpass(x, &mut x1, &mut y1, coef);
1780            if i >= warmup {
1781                sum_in += x * x;
1782                sum_out += y * y;
1783            }
1784        }
1785        Libm::<f64>::sqrt(sum_out / sum_in)
1786    }
1787
1788    #[test]
1789    fn test_phaser_allpass_unit_magnitude() {
1790        // Q020: a genuine first-order allpass must have unit magnitude at every
1791        // frequency. Check near DC and near Nyquist for several coefficients.
1792        // The previous (non-allpass) topology gave ~0.2 gain at Nyquist for
1793        // coef = 0.5 — this test would have failed there.
1794        for &coef in &[-0.6, -0.2, 0.2, 0.5, 0.8] {
1795            let dc_gain = allpass_rms_gain(coef, 0.001);
1796            let nyq_gain = allpass_rms_gain(coef, 0.499);
1797            assert!(
1798                (dc_gain - 1.0).abs() < 0.01,
1799                "DC gain {} not ~1.0 for coef {}",
1800                dc_gain,
1801                coef
1802            );
1803            assert!(
1804                (nyq_gain - 1.0).abs() < 0.01,
1805                "Nyquist gain {} not ~1.0 for coef {}",
1806                nyq_gain,
1807                coef
1808            );
1809        }
1810    }
1811
1812    #[test]
1813    fn test_chorus_delay_stays_positive() {
1814        // Q021: across the full LFO cycle at maximum depth the per-voice delay
1815        // must stay strictly above the 1-sample clamp floor, so the sweep is
1816        // never one-sidedly flattened.
1817        let sample_rate = 44100.0;
1818        let base = Chorus::BASE_DELAY_MS * sample_rate / 1000.0;
1819        let mod_depth = Chorus::MAX_MOD_DELAY_MS * sample_rate / 1000.0;
1820
1821        let steps = 2000;
1822        let mut min_delay = f64::INFINITY;
1823        for k in 0..steps {
1824            let lfo = Libm::<f64>::sin((k as f64 / steps as f64) * TAU);
1825            let delay = Chorus::voice_delay_samples(base, mod_depth, lfo);
1826            min_delay = min_delay.min(delay);
1827        }
1828        assert!(
1829            min_delay > 1.0,
1830            "minimum chorus delay {} hit the clamp floor",
1831            min_delay
1832        );
1833        // The trough of a unipolar sweep sits exactly at the base delay.
1834        let trough = Chorus::voice_delay_samples(base, mod_depth, -1.0);
1835        assert!(
1836            (trough - base).abs() < 1e-9,
1837            "trough {} != base {}",
1838            trough,
1839            base
1840        );
1841    }
1842
1843    #[test]
1844    fn test_delay_line_time_smoothing() {
1845        // Q022: a step in the time CV must not move the read distance
1846        // discontinuously; the one-pole smoother eases it over many samples.
1847        let mut delay = DelayLine::new(44100.0);
1848        let mut inputs = PortValues::new();
1849        let mut outputs = PortValues::new();
1850
1851        inputs.set(0, 0.0);
1852        inputs.set(1, 0.0); // minimum delay time
1853        inputs.set(2, 0.0);
1854        inputs.set(3, 0.5);
1855        delay.tick(&inputs, &mut outputs); // prime (snaps to min)
1856        let start = delay.smoothed_delay;
1857
1858        // Step the time CV to maximum.
1859        inputs.set(1, 1.0);
1860        delay.tick(&inputs, &mut outputs);
1861        let after_one = delay.smoothed_delay;
1862
1863        let full_jump = (delay.buffer.len() - 1) as f64 - start;
1864        let moved = after_one - start;
1865        assert!(moved > 0.0, "smoother did not move toward setpoint");
1866        assert!(
1867            moved < full_jump * 0.05,
1868            "smoother jumped {} of a {}-sample step in one tick",
1869            moved,
1870            full_jump
1871        );
1872
1873        // It should take many samples to traverse most of the step.
1874        let mut ticks = 1;
1875        while delay.smoothed_delay < start + full_jump * 0.9 && ticks < 100_000 {
1876            delay.tick(&inputs, &mut outputs);
1877            ticks += 1;
1878        }
1879        assert!(
1880            ticks > 100,
1881            "smoother converged too fast in {} ticks",
1882            ticks
1883        );
1884    }
1885
1886    #[test]
1887    fn test_vibrato_exact_delay() {
1888        // Q023: with modulation depth 0 the delay is constant, so an impulse
1889        // must emerge after exactly round(delay_ms * fs / 1000) samples. The
1890        // pre-fix write-before-read order produced this one sample early.
1891        let sample_rate = 44100.0;
1892        let mut vib = Vibrato::new(sample_rate);
1893        let mut inputs = PortValues::new();
1894        let mut outputs = PortValues::new();
1895
1896        inputs.set(1, 0.3); // rate (irrelevant at zero depth)
1897        inputs.set(2, 0.0); // depth = 0 -> constant delay
1898        inputs.set(3, 1.0); // 100% wet
1899
1900        let expected = (Vibrato::MAX_DELAY_MS * 0.5 * sample_rate / 1000.0).round() as usize;
1901
1902        inputs.set(0, 1.0); // impulse at tick 0
1903        vib.tick(&inputs, &mut outputs);
1904        let mut peak_idx = if outputs.get(10).unwrap().abs() > 0.5 {
1905            Some(0usize)
1906        } else {
1907            None
1908        };
1909
1910        inputs.set(0, 0.0);
1911        for i in 1..(expected + 50) {
1912            vib.tick(&inputs, &mut outputs);
1913            if peak_idx.is_none() && outputs.get(10).unwrap().abs() > 0.5 {
1914                peak_idx = Some(i);
1915            }
1916        }
1917        assert_eq!(
1918            peak_idx,
1919            Some(expected),
1920            "impulse emerged at {:?}, expected {}",
1921            peak_idx,
1922            expected
1923        );
1924    }
1925
1926    #[test]
1927    fn test_reverb_stereo_spread_scales_with_sample_rate() {
1928        // Q024: the right-channel decorrelation offset must scale with sample
1929        // rate, not stay a fixed 23 samples.
1930        let reverb_44 = Reverb::new(44100.0);
1931        assert_eq!(reverb_44.stereo_spread, STEREO_SPREAD);
1932
1933        let reverb_88 = Reverb::new(88200.0);
1934        // 88.2 kHz is exactly 2x -> spread ~46, not 23.
1935        assert_eq!(reverb_88.stereo_spread, 46);
1936
1937        for i in 0..8 {
1938            let length_l = reverb_88.comb_lengths[i];
1939            let length_r = (length_l + reverb_88.stereo_spread).min(MAX_COMB_SIZE - 1);
1940            assert_eq!(
1941                length_r - length_l,
1942                46,
1943                "right comb {} should lead left by the scaled spread",
1944                i
1945            );
1946        }
1947    }
1948
1949    // ---- Q157: Tremolo unit tests ----
1950
1951    #[test]
1952    fn test_tremolo_am_depth() {
1953        // A DC carrier isolates the amplitude modulation. Full depth must swing
1954        // the output across (nearly) the whole [0, carrier] range; zero depth
1955        // must leave the carrier untouched.
1956        let mut trem = Tremolo::new(44100.0);
1957        let mut inputs = PortValues::new();
1958        let mut outputs = PortValues::new();
1959        inputs.set(0, 1.0); // DC carrier
1960        inputs.set(1, 1.0); // fast rate (~20 Hz) to sweep the LFO quickly
1961        inputs.set(3, 0.0); // sine shape
1962
1963        inputs.set(2, 1.0); // full depth
1964        let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
1965        for _ in 0..44_100 {
1966            trem.tick(&inputs, &mut outputs);
1967            let o = outputs.get(10).unwrap();
1968            assert!(o.is_finite());
1969            lo = lo.min(o);
1970            hi = hi.max(o);
1971        }
1972        assert!(
1973            lo < 0.1 && hi > 0.9,
1974            "full-depth AM must reach near 0 and near the carrier: lo={lo} hi={hi}"
1975        );
1976
1977        trem.reset();
1978        inputs.set(2, 0.0); // zero depth
1979        let (mut lo0, mut hi0) = (f64::INFINITY, f64::NEG_INFINITY);
1980        for _ in 0..4410 {
1981            trem.tick(&inputs, &mut outputs);
1982            let o = outputs.get(10).unwrap();
1983            lo0 = lo0.min(o);
1984            hi0 = hi0.max(o);
1985        }
1986        assert!(
1987            (hi0 - lo0) < 1e-9 && (hi0 - 1.0).abs() < 1e-9,
1988            "zero-depth tremolo must pass the carrier unchanged: span={}",
1989            hi0 - lo0
1990        );
1991    }
1992
1993    #[test]
1994    fn test_tremolo_reset_and_sample_rate() {
1995        let mut trem = Tremolo::default();
1996        assert_eq!(trem.type_id(), "tremolo");
1997        assert_eq!(trem.sample_rate, 44100.0);
1998        let mut inputs = PortValues::new();
1999        let mut outputs = PortValues::new();
2000        inputs.set(0, 1.0);
2001        inputs.set(1, 0.5);
2002        for _ in 0..500 {
2003            trem.tick(&inputs, &mut outputs);
2004        }
2005        assert!(trem.lfo_phase != 0.0);
2006        trem.reset();
2007        assert_eq!(trem.lfo_phase, 0.0);
2008        trem.set_sample_rate(48000.0);
2009        assert_eq!(trem.sample_rate, 48000.0);
2010        trem.tick(&inputs, &mut outputs);
2011        assert!(outputs.get(10).unwrap().is_finite());
2012    }
2013
2014    // ---- Q157: Vibrato pitch-modulation depth ----
2015
2016    /// Collect the spacing (in samples) between successive upward zero crossings
2017    /// of `sig`, then return `max_interval - min_interval`.
2018    fn zero_crossing_interval_spread(sig: &[f64]) -> f64 {
2019        let mut crossings = Vec::new();
2020        for i in 1..sig.len() {
2021            if sig[i - 1] <= 0.0 && sig[i] > 0.0 {
2022                crossings.push(i);
2023            }
2024        }
2025        if crossings.len() < 3 {
2026            return 0.0;
2027        }
2028        let mut min_iv = f64::INFINITY;
2029        let mut max_iv = f64::NEG_INFINITY;
2030        for w in crossings.windows(2) {
2031            let iv = (w[1] - w[0]) as f64;
2032            min_iv = min_iv.min(iv);
2033            max_iv = max_iv.max(iv);
2034        }
2035        max_iv - min_iv
2036    }
2037
2038    #[test]
2039    fn test_vibrato_pitch_modulation_depth() {
2040        // Vibrato modulates a delay line, so the output pitch wobbles: the
2041        // spacing between the output's zero crossings must vary far more with a
2042        // large modulation depth than with zero depth (constant delay).
2043        let run = |depth: f64| -> f64 {
2044            let mut vib = Vibrato::new(44100.0);
2045            let mut inputs = PortValues::new();
2046            let mut outputs = PortValues::new();
2047            inputs.set(1, 0.78); // ~5 Hz LFO
2048            inputs.set(2, depth);
2049            inputs.set(3, 1.0); // 100% wet
2050            let mut out = Vec::with_capacity(20_000);
2051            let dt = 500.0 / 44100.0;
2052            let mut phase = 0.0f64;
2053            for _ in 0..20_000 {
2054                let s = Libm::<f64>::sin(TAU * phase);
2055                phase += dt;
2056                if phase >= 1.0 {
2057                    phase -= 1.0;
2058                }
2059                inputs.set(0, s);
2060                vib.tick(&inputs, &mut outputs);
2061                out.push(outputs.get(10).unwrap());
2062            }
2063            zero_crossing_interval_spread(&out)
2064        };
2065
2066        let spread_off = run(0.0);
2067        let spread_on = run(0.8);
2068        assert!(
2069            spread_off < 3.0,
2070            "zero-depth vibrato should have near-constant pitch: spread={spread_off}"
2071        );
2072        assert!(
2073            spread_on > spread_off + 10.0,
2074            "depth-0.8 vibrato must wobble the pitch: on={spread_on} off={spread_off}"
2075        );
2076    }
2077
2078    #[test]
2079    fn test_vibrato_reset_and_sample_rate() {
2080        let mut vib = Vibrato::default();
2081        assert_eq!(vib.type_id(), "vibrato");
2082        let mut inputs = PortValues::new();
2083        let mut outputs = PortValues::new();
2084        inputs.set(0, 1.0);
2085        inputs.set(2, 0.5);
2086        for _ in 0..500 {
2087            vib.tick(&inputs, &mut outputs);
2088        }
2089        assert!(vib.lfo_phase != 0.0);
2090        vib.reset();
2091        assert_eq!(vib.lfo_phase, 0.0);
2092        assert_eq!(vib.write_pos, 0);
2093        assert!(vib.buffer.iter().all(|&x| x == 0.0));
2094        vib.set_sample_rate(48000.0);
2095        assert_eq!(vib.sample_rate, 48000.0);
2096        vib.tick(&inputs, &mut outputs);
2097        assert!(outputs.get(10).unwrap().is_finite());
2098    }
2099
2100    #[test]
2101    fn test_vibrato_lowering_sample_rate_does_not_panic() {
2102        // Regression: at 96kHz the delay buffer is large; ticking advances
2103        // write_pos to a large value. Lowering the sample rate shrinks the
2104        // buffer, and the write site indexes it directly (non-modulo). If
2105        // set_sample_rate leaves write_pos stale, the next tick panics with
2106        // index-out-of-bounds. It must reset write_pos.
2107        let mut vib = Vibrato::new(96000.0);
2108        let mut inputs = PortValues::new();
2109        let mut outputs = PortValues::new();
2110        inputs.set(0, 0.5);
2111        inputs.set(2, 0.5);
2112        // Advance write_pos well past a shrunken buffer's length.
2113        for _ in 0..1000 {
2114            vib.tick(&inputs, &mut outputs);
2115        }
2116        // Lower the sample rate: buffer shrinks from ~1930 to ~451 samples.
2117        vib.set_sample_rate(22050.0);
2118        assert_eq!(vib.write_pos, 0, "write_pos must be reset after resize");
2119        // Must not panic on the next tick.
2120        vib.tick(&inputs, &mut outputs);
2121        assert!(outputs.get(10).unwrap().is_finite());
2122    }
2123}