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