Skip to main content

quiver/modules/
utilities.rs

1//! Utility, logic, CV, and sequencing modules.
2
3use super::common::{sanitize_audio, EdgeDetector, GATE_HIGH_V, GATE_THRESHOLD_V};
4use crate::port::{GraphModule, ParamDef, ParamId, PortDef, PortSpec, PortValues, SignalKind};
5use crate::rng;
6use alloc::format;
7use alloc::vec;
8use alloc::vec::Vec;
9use core::f64::consts::TAU;
10use libm::Libm;
11
12/// Multi-channel Mixer
13///
14/// Sums multiple audio inputs into a single output.
15pub struct Mixer {
16    num_channels: usize,
17    spec: PortSpec,
18}
19
20impl Mixer {
21    pub fn new(num_channels: usize) -> Self {
22        let inputs = (0..num_channels)
23            .map(|i| {
24                PortDef::new(i as u32, format!("ch{}", i), SignalKind::Audio).with_attenuverter()
25            })
26            .collect();
27
28        Self {
29            num_channels,
30            spec: PortSpec {
31                inputs,
32                outputs: vec![PortDef::new(100, "out", SignalKind::Audio)],
33            },
34        }
35    }
36}
37
38impl Default for Mixer {
39    fn default() -> Self {
40        Self::new(4)
41    }
42}
43
44impl GraphModule for Mixer {
45    fn port_spec(&self) -> &PortSpec {
46        &self.spec
47    }
48
49    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
50        let sum: f64 = (0..self.num_channels)
51            .map(|i| inputs.get_or(i as u32, 0.0))
52            .sum();
53        outputs.set(100, sum);
54    }
55
56    fn reset(&mut self) {}
57
58    fn set_sample_rate(&mut self, _: f64) {}
59
60    fn type_id(&self) -> &'static str {
61        "mixer"
62    }
63}
64
65/// DC Offset module
66///
67/// Adds a constant offset to a signal.
68pub struct Offset {
69    pub(crate) offset: f64,
70    spec: PortSpec,
71}
72
73impl Offset {
74    pub fn new(offset: f64) -> Self {
75        Self {
76            offset,
77            spec: PortSpec {
78                inputs: vec![PortDef::new(0, "in", SignalKind::CvBipolar)],
79                outputs: vec![PortDef::new(10, "out", SignalKind::CvBipolar)],
80            },
81        }
82    }
83
84    pub fn set_offset(&mut self, offset: f64) {
85        self.offset = offset;
86    }
87}
88
89impl Default for Offset {
90    fn default() -> Self {
91        Self::new(0.0)
92    }
93}
94
95impl GraphModule for Offset {
96    fn port_spec(&self) -> &PortSpec {
97        &self.spec
98    }
99
100    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
101        let input = inputs.get_or(0, 0.0);
102        outputs.set(10, input + self.offset);
103    }
104
105    fn reset(&mut self) {}
106
107    fn set_sample_rate(&mut self, _: f64) {}
108
109    fn type_id(&self) -> &'static str {
110        "offset"
111    }
112
113    fn params(&self) -> &[ParamDef] {
114        static PARAMS: &[ParamDef] = &[];
115        PARAMS
116    }
117
118    fn get_param(&self, id: ParamId) -> Option<f64> {
119        if id == 0 {
120            Some(self.offset)
121        } else {
122            None
123        }
124    }
125
126    fn set_param(&mut self, id: ParamId, value: f64) {
127        if id == 0 {
128            self.offset = value;
129        }
130    }
131
132    // `offset` is genuine internal state (not an input port); bridge it to introspection.
133    crate::impl_introspect!();
134}
135
136/// Hysteresis band (in semitones) applied by the pitch quantizers so a CV
137/// hovering on a note boundary does not chatter between two notes (Q041).
138const NOTE_HYSTERESIS_SEMITONES: f64 = 0.3;
139
140/// Apply per-note hysteresis to a quantizer.
141///
142/// A new candidate note is only committed once the input has moved
143/// `hysteresis_semitones` *past* the midpoint between the last committed note
144/// and the candidate; otherwise the previous note is held. All voltages are
145/// V/Oct (`1.0` == 12 semitones); `last` is the previously committed output,
146/// `None` on the first sample. This removes the boundary chatter described in
147/// Q041 while leaving clean, decisive note changes untouched.
148fn hysteretic_note(
149    last: Option<f64>,
150    input_v: f64,
151    candidate_v: f64,
152    hysteresis_semitones: f64,
153) -> f64 {
154    match last {
155        None => candidate_v,
156        Some(last_v) => {
157            if candidate_v == last_v {
158                return last_v;
159            }
160            let in_s = input_v * 12.0;
161            let last_s = last_v * 12.0;
162            let cand_s = candidate_v * 12.0;
163            let boundary = (last_s + cand_s) * 0.5;
164            let commit = if cand_s > last_s {
165                in_s >= boundary + hysteresis_semitones
166            } else {
167                in_s <= boundary - hysteresis_semitones
168            };
169            if commit {
170                candidate_v
171            } else {
172                last_v
173            }
174        }
175    }
176}
177
178/// Scale Quantizer
179///
180/// Quantizes CV input to musical scale notes.
181/// Supports major, minor, pentatonic, and chromatic scales.
182pub struct ScaleQuantizer {
183    /// Last committed output voltage, for note-change triggers and hysteresis.
184    last_output: Option<f64>,
185    /// Optional microtuning override (Q146): scale degrees in cents within one
186    /// octave `[0, 1200)`, sorted. When non-empty it replaces the built-in 12-TET
187    /// enum tables. Always present (heap-backed `Vec`), but only populated via the
188    /// alloc-gated [`set_custom_scale`](Self::set_custom_scale) /
189    /// [`load_scala`](Self::load_scala) setters.
190    custom_cents: Vec<f64>,
191    spec: PortSpec,
192}
193
194impl ScaleQuantizer {
195    // Scale intervals (semitones from root)
196    const CHROMATIC: [u8; 12] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
197    const MAJOR: [u8; 7] = [0, 2, 4, 5, 7, 9, 11];
198    const MINOR: [u8; 7] = [0, 2, 3, 5, 7, 8, 10];
199    const PENT_MAJOR: [u8; 5] = [0, 2, 4, 7, 9];
200    const PENT_MINOR: [u8; 5] = [0, 3, 5, 7, 10];
201    const DORIAN: [u8; 7] = [0, 2, 3, 5, 7, 9, 10];
202    const BLUES: [u8; 6] = [0, 3, 5, 6, 7, 10];
203
204    pub fn new(_sample_rate: f64) -> Self {
205        Self {
206            last_output: None,
207            custom_cents: Vec::new(),
208            spec: PortSpec {
209                inputs: vec![
210                    PortDef::new(0, "in", SignalKind::VoltPerOctave),
211                    PortDef::new(1, "root", SignalKind::CvUnipolar)
212                        .with_default(0.0)
213                        .with_attenuverter(),
214                    PortDef::new(2, "scale", SignalKind::CvUnipolar)
215                        .with_default(0.0)
216                        .with_attenuverter(),
217                ],
218                outputs: vec![
219                    PortDef::new(10, "out", SignalKind::VoltPerOctave),
220                    PortDef::new(11, "trigger", SignalKind::Trigger),
221                ],
222            },
223        }
224    }
225
226    fn quantize_to_scale(note: i32, scale: &[u8]) -> i32 {
227        let octave = note.div_euclid(12);
228        let semitone = note.rem_euclid(12);
229
230        // Find the closest scale note, also considering the scale root wrapped
231        // into the NEXT octave (`s + 12`). Without carrying that +12 (Q034), a
232        // top-of-octave input whose nearest note is the next root drops ~an
233        // octave instead of snapping up. Mirrors `Quantizer::quantize`.
234        let mut closest = scale[0] as i32;
235        let mut min_dist = i32::MAX;
236
237        for &s in scale {
238            let s = s as i32;
239            let dist = (semitone - s).abs();
240            if dist < min_dist {
241                min_dist = dist;
242                closest = s;
243            }
244            let dist_wrap = (semitone - (s + 12)).abs();
245            if dist_wrap < min_dist {
246                min_dist = dist_wrap;
247                closest = s + 12;
248            }
249        }
250
251        octave * 12 + closest
252    }
253
254    /// Whether a microtuning custom scale is currently active (Q146).
255    pub fn has_custom_scale(&self) -> bool {
256        !self.custom_cents.is_empty()
257    }
258
259    /// Install a custom microtuning scale (Q146): `cents` are scale degrees within
260    /// one octave, in cents (`0.0` is the root). The list is sorted and reduced
261    /// into `[0, 1200)` internally, so callers need not pre-sort. Passing an empty
262    /// slice clears the override and restores the built-in 12-TET scales.
263    ///
264    /// Non-real-time: allocates. Alloc-tier only.
265    #[cfg(feature = "alloc")]
266    pub fn set_custom_scale(&mut self, cents: &[f64]) {
267        let mut degrees: Vec<f64> = cents
268            .iter()
269            .map(|&c| {
270                let mut r = Libm::<f64>::fmod(c, 1200.0);
271                if r < 0.0 {
272                    r += 1200.0;
273                }
274                r
275            })
276            .collect();
277        degrees.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
278        degrees.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
279        self.custom_cents = degrees;
280    }
281
282    /// Clear any custom microtuning scale, restoring the built-in 12-TET scales.
283    #[cfg(feature = "alloc")]
284    pub fn clear_custom_scale(&mut self) {
285        self.custom_cents.clear();
286    }
287
288    /// Load a Scala (`.scl`) file body as the custom microtuning scale (Q146).
289    ///
290    /// On success the parsed scale's octave-reduced degrees become the active
291    /// scale (see [`set_custom_scale`](Self::set_custom_scale)). On a malformed
292    /// file the current scale is left unchanged and the parse error is returned.
293    ///
294    /// Non-real-time: allocates. Alloc-tier only.
295    #[cfg(feature = "alloc")]
296    pub fn load_scala(&mut self, source: &str) -> Result<(), crate::scala::ScalaError> {
297        let scale = crate::scala::ScalaScale::parse(source)?;
298        self.set_custom_scale(&scale.degrees_within_octave());
299        Ok(())
300    }
301
302    /// Quantize a cents value to the nearest degree of a custom scale (Q146).
303    ///
304    /// `degrees` are sorted degrees within `[0, 1200)`; the search also considers
305    /// each degree wrapped into the next octave so a pitch near the top of the
306    /// octave snaps up to the next root rather than dropping an octave (mirrors
307    /// [`quantize_to_scale`](Self::quantize_to_scale)).
308    fn quantize_custom_cents(input_cents: f64, degrees: &[f64]) -> f64 {
309        if degrees.is_empty() {
310            return input_cents;
311        }
312        let octave = Libm::<f64>::floor(input_cents / 1200.0);
313        let within = input_cents - octave * 1200.0;
314
315        let mut closest = degrees[0];
316        let mut min_dist = f64::MAX;
317        for &d in degrees {
318            let dist = Libm::<f64>::fabs(within - d);
319            if dist < min_dist {
320                min_dist = dist;
321                closest = d;
322            }
323            let dist_wrap = Libm::<f64>::fabs(within - (d + 1200.0));
324            if dist_wrap < min_dist {
325                min_dist = dist_wrap;
326                closest = d + 1200.0;
327            }
328        }
329
330        octave * 1200.0 + closest
331    }
332}
333
334impl Default for ScaleQuantizer {
335    fn default() -> Self {
336        Self::new(44100.0)
337    }
338}
339
340impl GraphModule for ScaleQuantizer {
341    fn port_spec(&self) -> &PortSpec {
342        &self.spec
343    }
344
345    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
346        let input = inputs.get_or(0, 0.0);
347        let root_cv = inputs.get_or(1, 0.0).clamp(0.0, 1.0);
348        let scale_cv = inputs.get_or(2, 0.0).clamp(0.0, 1.0);
349
350        // Root note (0-11 semitones)
351        let root = (root_cv * 11.99) as i32;
352
353        // A custom microtuning scale (Q146) overrides the built-in 12-TET enum
354        // tables and quantizes in cents rather than integer semitones.
355        let candidate_voct = if !self.custom_cents.is_empty() {
356            let root_cents = root as f64 * 100.0;
357            let input_cents = input * 1200.0 - root_cents;
358            let q_cents = Self::quantize_custom_cents(input_cents, &self.custom_cents);
359            (q_cents + root_cents) / 1200.0
360        } else {
361            // Convert V/Oct to semitones from C4
362            let semitones_from_c4 = Libm::<f64>::round(input * 12.0) as i32;
363
364            // Adjust for root
365            let relative_note = semitones_from_c4 - root;
366
367            // Select scale
368            let scale_idx = (scale_cv * 6.99) as u8;
369            let quantized = match scale_idx {
370                0 => Self::quantize_to_scale(relative_note, &Self::CHROMATIC),
371                1 => Self::quantize_to_scale(relative_note, &Self::MAJOR),
372                2 => Self::quantize_to_scale(relative_note, &Self::MINOR),
373                3 => Self::quantize_to_scale(relative_note, &Self::PENT_MAJOR),
374                4 => Self::quantize_to_scale(relative_note, &Self::PENT_MINOR),
375                5 => Self::quantize_to_scale(relative_note, &Self::DORIAN),
376                _ => Self::quantize_to_scale(relative_note, &Self::BLUES),
377            };
378
379            // Convert back to V/Oct with root offset
380            (quantized + root) as f64 / 12.0
381        };
382
383        // Commit the note through hysteresis so a CV parked on a boundary does
384        // not chatter (Q041), and fire the trigger only on an actual committed
385        // note change rather than continuously while quantization is active.
386        let prev = self.last_output;
387        let output_voct = hysteretic_note(prev, input, candidate_voct, NOTE_HYSTERESIS_SEMITONES);
388        let trigger = match prev {
389            Some(p) if (p - output_voct).abs() > 1e-9 => GATE_HIGH_V,
390            _ => 0.0,
391        };
392        self.last_output = Some(output_voct);
393
394        outputs.set(10, output_voct);
395        outputs.set(11, trigger);
396    }
397
398    fn reset(&mut self) {
399        self.last_output = None;
400    }
401
402    fn set_sample_rate(&mut self, _: f64) {}
403
404    fn type_id(&self) -> &'static str {
405        "scale_quantizer"
406    }
407
408    /// Persist the microtuning table (Q146) so a loaded Scala/custom scale survives
409    /// `to_def` -> `from_def`. It is not a scalar `ModuleIntrospection` parameter, so it
410    /// travels through the reserved `ModuleDef.state` channel instead of `parameters`. A
411    /// plain (12-TET) quantizer has no custom cents and returns `None`, keeping state null.
412    #[cfg(feature = "alloc")]
413    fn serialize_state(&self) -> Option<serde_json::Value> {
414        if self.custom_cents.is_empty() {
415            return None;
416        }
417        let cents = serde_json::to_value(&self.custom_cents).ok()?;
418        let mut map = serde_json::Map::new();
419        map.insert(alloc::string::String::from("custom_cents"), cents);
420        Some(serde_json::Value::Object(map))
421    }
422
423    /// Restore the microtuning table saved by [`serialize_state`](Self::serialize_state).
424    /// An absent/empty table is a no-op (stays 12-TET); malformed cents surface a
425    /// descriptive error to the loader.
426    #[cfg(feature = "alloc")]
427    fn deserialize_state(
428        &mut self,
429        state: &serde_json::Value,
430    ) -> Result<(), alloc::string::String> {
431        let Some(cents_val) = state.get("custom_cents") else {
432            return Ok(());
433        };
434        let cents: Vec<f64> = serde_json::from_value(cents_val.clone())
435            .map_err(|e| format!("ScaleQuantizer custom_cents: {e}"))?;
436        self.set_custom_scale(&cents);
437        Ok(())
438    }
439}
440
441/// Euclidean Rhythm Generator
442///
443/// Generates euclidean rhythms - evenly distributed pulses.
444/// Classic algorithm used in many world music traditions.
445pub struct Euclidean {
446    step: usize,
447    pattern: Vec<bool>,
448    /// Pulse count baked into the current `pattern`, so the pulses control is
449    /// no longer inert when the step count is unchanged (Q037).
450    last_pulses: usize,
451    /// Rising-edge detector for the clock input (canonical 2.5V, Q129).
452    clock_edge: EdgeDetector,
453    /// Rising-edge detector for the reset input (canonical 2.5V, Q129).
454    reset_edge: EdgeDetector,
455    /// Whether the current pattern cycle has already fired its accent (Q042).
456    cycle_accented: bool,
457    spec: PortSpec,
458}
459
460impl Euclidean {
461    pub fn new(_sample_rate: f64) -> Self {
462        Self {
463            step: 0,
464            pattern: vec![true; 16],
465            last_pulses: 16,
466            clock_edge: EdgeDetector::new(),
467            reset_edge: EdgeDetector::new(),
468            cycle_accented: false,
469            spec: PortSpec {
470                inputs: vec![
471                    PortDef::new(0, "clock", SignalKind::Trigger),
472                    PortDef::new(1, "steps", SignalKind::CvUnipolar)
473                        .with_default(0.5)
474                        .with_attenuverter(),
475                    PortDef::new(2, "pulses", SignalKind::CvUnipolar)
476                        .with_default(0.25)
477                        .with_attenuverter(),
478                    PortDef::new(3, "rotation", SignalKind::CvUnipolar)
479                        .with_default(0.0)
480                        .with_attenuverter(),
481                    PortDef::new(4, "reset", SignalKind::Trigger),
482                ],
483                outputs: vec![
484                    PortDef::new(10, "out", SignalKind::Trigger),
485                    PortDef::new(11, "accent", SignalKind::Trigger),
486                ],
487            },
488        }
489    }
490
491    fn generate_pattern(steps: usize, pulses: usize) -> Vec<bool> {
492        if steps == 0 || pulses == 0 {
493            return vec![false; steps.max(1)];
494        }
495
496        let pulses = pulses.min(steps);
497        let mut pattern = vec![false; steps];
498
499        // Bresenham-style euclidean distribution
500        let mut bucket = 0;
501        for slot in pattern.iter_mut().take(steps) {
502            bucket += pulses;
503            if bucket >= steps {
504                bucket -= steps;
505                *slot = true;
506            }
507        }
508
509        pattern
510    }
511}
512
513impl Default for Euclidean {
514    fn default() -> Self {
515        Self::new(44100.0)
516    }
517}
518
519impl GraphModule for Euclidean {
520    fn port_spec(&self) -> &PortSpec {
521        &self.spec
522    }
523
524    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
525        let clock = inputs.get_or(0, 0.0);
526        let steps_cv = inputs.get_or(1, 0.5).clamp(0.0, 1.0);
527        let pulses_cv = inputs.get_or(2, 0.25).clamp(0.0, 1.0);
528        let rotation_cv = inputs.get_or(3, 0.0).clamp(0.0, 1.0);
529        let reset = inputs.get_or(4, 0.0);
530
531        // Calculate steps (2-16) and pulses
532        let steps = 2 + (steps_cv * 14.99) as usize;
533        let pulses = (pulses_cv * steps as f64) as usize;
534
535        // Regenerate the pattern whenever the step count OR the pulse count
536        // changes, so the pulses (density) control is live (Q037).
537        if self.pattern.len() != steps || self.last_pulses != pulses {
538            self.pattern = Self::generate_pattern(steps, pulses);
539            self.last_pulses = pulses;
540        }
541
542        // Reset on a rising edge at the canonical gate threshold (Q129).
543        if self.reset_edge.rising(reset) {
544            self.step = 0;
545            self.cycle_accented = false;
546        }
547
548        // Detect a clock rising edge at the canonical gate threshold (Q129).
549        let trigger = self.clock_edge.rising(clock);
550
551        let mut out = 0.0;
552        let mut accent = 0.0;
553
554        if trigger {
555            // Rotation now spans the full 0..steps range (Q042); it shifts which
556            // pattern slot is read at this sequence position.
557            let rotation = ((rotation_cv * steps as f64) as usize).min(steps - 1);
558
559            // A new pattern cycle begins at step 0: re-arm the accent.
560            if self.step == 0 {
561                self.cycle_accented = false;
562            }
563
564            let rotated_step = (self.step + rotation) % steps;
565
566            if self.pattern[rotated_step] {
567                out = GATE_HIGH_V;
568                // Accent the active downbeat of the (rotated) pattern: the first
569                // real pulse of the cycle, so the accent always coincides with a
570                // pulse instead of firing on a pre-rotation counter that may land
571                // on a rest (Q042).
572                if !self.cycle_accented {
573                    accent = GATE_HIGH_V;
574                    self.cycle_accented = true;
575                }
576            }
577
578            self.step = (self.step + 1) % steps;
579        }
580
581        outputs.set(10, out);
582        outputs.set(11, accent);
583    }
584
585    fn reset(&mut self) {
586        self.step = 0;
587        self.cycle_accented = false;
588        self.clock_edge.reset();
589        self.reset_edge.reset();
590    }
591
592    fn set_sample_rate(&mut self, _: f64) {}
593
594    fn type_id(&self) -> &'static str {
595        "euclidean"
596    }
597}
598
599/// Crosstalk Simulator
600///
601/// Simulates signal crosstalk between adjacent channels, a common
602/// phenomenon in analog audio equipment where signals "leak" between
603/// channels due to capacitive coupling or poor isolation.
604///
605/// This is a Phase 3 addition.
606pub struct Crosstalk {
607    sample_rate: f64,
608    /// High-frequency emphasis filter states
609    hf_state: [f64; 2],
610    spec: PortSpec,
611}
612
613impl Crosstalk {
614    pub fn new(sample_rate: f64) -> Self {
615        Self {
616            sample_rate,
617            hf_state: [0.0; 2],
618            spec: PortSpec {
619                inputs: vec![
620                    PortDef::new(0, "in_a", SignalKind::Audio),
621                    PortDef::new(1, "in_b", SignalKind::Audio),
622                    // Crosstalk amount (0-1, typically very low in real gear)
623                    PortDef::new(2, "amount", SignalKind::CvUnipolar).with_default(0.01),
624                    // Frequency-dependent crosstalk (higher = more HF crosstalk)
625                    PortDef::new(3, "hf_emphasis", SignalKind::CvUnipolar).with_default(0.5),
626                ],
627                outputs: vec![
628                    PortDef::new(10, "out_a", SignalKind::Audio),
629                    PortDef::new(11, "out_b", SignalKind::Audio),
630                ],
631            },
632        }
633    }
634}
635
636impl Default for Crosstalk {
637    fn default() -> Self {
638        Self::new(44100.0)
639    }
640}
641
642impl GraphModule for Crosstalk {
643    fn port_spec(&self) -> &PortSpec {
644        &self.spec
645    }
646
647    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
648        let in_a = sanitize_audio(inputs.get_or(0, 0.0));
649        let in_b = sanitize_audio(inputs.get_or(1, 0.0));
650        let amount = inputs.get_or(2, 0.01).clamp(0.0, 0.5);
651        let hf_emphasis = inputs.get_or(3, 0.5).clamp(0.0, 1.0);
652
653        // High-pass filter coefficient for HF emphasis (crosstalk is typically worse at HF)
654        let hf_coef = 0.1 + hf_emphasis * 0.4;
655
656        // Extract high-frequency component for emphasized crosstalk
657        let hf_a = in_a - self.hf_state[0];
658        let hf_b = in_b - self.hf_state[1];
659        self.hf_state[0] += hf_coef * (in_a - self.hf_state[0]);
660        self.hf_state[1] += hf_coef * (in_b - self.hf_state[1]);
661
662        // Mix original signal with emphasized HF crosstalk from other channel
663        let crosstalk_to_a = (in_b * (1.0 - hf_emphasis) + hf_b * hf_emphasis) * amount;
664        let crosstalk_to_b = (in_a * (1.0 - hf_emphasis) + hf_a * hf_emphasis) * amount;
665
666        outputs.set(10, in_a + crosstalk_to_a);
667        outputs.set(11, in_b + crosstalk_to_b);
668    }
669
670    fn reset(&mut self) {
671        self.hf_state = [0.0; 2];
672    }
673
674    fn set_sample_rate(&mut self, sample_rate: f64) {
675        self.sample_rate = sample_rate;
676    }
677
678    fn type_id(&self) -> &'static str {
679        "crosstalk"
680    }
681}
682
683/// Ground Loop Simulator
684///
685/// Simulates ground loop hum and related power supply interference,
686/// common in analog audio equipment. Adds realistic 50/60 Hz hum
687/// with harmonics and modulation from signal activity.
688///
689/// This is a Phase 3 addition.
690pub struct GroundLoop {
691    sample_rate: f64,
692    /// Hum oscillator phase
693    phase: f64,
694    /// Hum frequency (50 or 60 Hz)
695    pub(crate) frequency: f64,
696    /// Thermal modulation state
697    thermal_state: f64,
698    spec: PortSpec,
699}
700
701impl GroundLoop {
702    pub fn new(sample_rate: f64) -> Self {
703        Self {
704            sample_rate,
705            phase: 0.0,
706            frequency: 60.0, // Default to 60 Hz (North America)
707            thermal_state: 0.0,
708            spec: PortSpec {
709                inputs: vec![
710                    PortDef::new(0, "in", SignalKind::Audio),
711                    // Hum level (typically very low)
712                    PortDef::new(1, "level", SignalKind::CvUnipolar).with_default(0.005),
713                    // Signal-dependent modulation (thermal effects)
714                    PortDef::new(2, "modulation", SignalKind::CvUnipolar).with_default(0.1),
715                    // Frequency select (0 = 50 Hz, 1 = 60 Hz)
716                    PortDef::new(3, "freq_select", SignalKind::CvUnipolar).with_default(1.0),
717                ],
718                outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
719            },
720        }
721    }
722
723    /// Create a 50 Hz ground loop (Europe, etc.)
724    pub fn hz_50(sample_rate: f64) -> Self {
725        let mut gl = Self::new(sample_rate);
726        gl.frequency = 50.0;
727        gl
728    }
729
730    /// Create a 60 Hz ground loop (North America)
731    pub fn hz_60(sample_rate: f64) -> Self {
732        let mut gl = Self::new(sample_rate);
733        gl.frequency = 60.0;
734        gl
735    }
736}
737
738impl Default for GroundLoop {
739    fn default() -> Self {
740        Self::new(44100.0)
741    }
742}
743
744impl GraphModule for GroundLoop {
745    fn port_spec(&self) -> &PortSpec {
746        &self.spec
747    }
748
749    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
750        let input = sanitize_audio(inputs.get_or(0, 0.0));
751        let level = inputs.get_or(1, 0.005).clamp(0.0, 0.1);
752        let modulation = inputs.get_or(2, 0.1).clamp(0.0, 1.0);
753        let freq_select = inputs.get_or(3, 1.0);
754
755        // Select frequency based on input
756        let freq = if freq_select > 0.5 { 60.0 } else { 50.0 };
757
758        // Update thermal state based on signal energy (slow integration)
759        let signal_energy = Libm::<f64>::pow(input / 5.0, 2.0);
760        self.thermal_state += (signal_energy - self.thermal_state) * 0.0001;
761
762        // Modulated hum level based on signal activity
763        let modulated_level = level * (1.0 + self.thermal_state * modulation * 10.0);
764
765        // Generate hum with harmonics (fundamental + 2nd + 3rd harmonic)
766        let fundamental = Libm::<f64>::sin(self.phase * TAU);
767        let second_harmonic = Libm::<f64>::sin(self.phase * 2.0 * TAU) * 0.5;
768        let third_harmonic = Libm::<f64>::sin(self.phase * 3.0 * TAU) * 0.25;
769        let hum = (fundamental + second_harmonic + third_harmonic) * modulated_level * 5.0;
770
771        // Advance phase
772        let new_phase = self.phase + freq / self.sample_rate;
773        self.phase = new_phase - Libm::<f64>::floor(new_phase);
774
775        outputs.set(10, input + hum);
776    }
777
778    fn reset(&mut self) {
779        self.phase = 0.0;
780        self.thermal_state = 0.0;
781    }
782
783    fn set_sample_rate(&mut self, sample_rate: f64) {
784        self.sample_rate = sample_rate;
785    }
786
787    fn type_id(&self) -> &'static str {
788        "ground_loop"
789    }
790}
791
792/// Step Sequencer
793///
794/// An 8-step sequencer with clock and reset inputs.
795pub struct StepSequencer {
796    steps: [f64; 8],
797    gates: [bool; 8],
798    current: usize,
799    prev_clock: f64,
800    prev_reset: f64,
801    spec: PortSpec,
802}
803
804impl StepSequencer {
805    pub fn new() -> Self {
806        Self {
807            steps: [0.0; 8],
808            gates: [true; 8],
809            current: 0,
810            prev_clock: 0.0,
811            prev_reset: 0.0,
812            spec: PortSpec {
813                inputs: vec![
814                    PortDef::new(0, "clock", SignalKind::Clock),
815                    PortDef::new(1, "reset", SignalKind::Trigger),
816                ],
817                outputs: vec![
818                    PortDef::new(10, "cv", SignalKind::VoltPerOctave),
819                    PortDef::new(11, "gate", SignalKind::Gate),
820                    PortDef::new(12, "trig", SignalKind::Trigger),
821                ],
822            },
823        }
824    }
825
826    pub fn set_step(&mut self, index: usize, voltage: f64, gate: bool) {
827        if index < 8 {
828            self.steps[index] = voltage;
829            self.gates[index] = gate;
830        }
831    }
832
833    pub fn get_step(&self, index: usize) -> Option<(f64, bool)> {
834        if index < 8 {
835            Some((self.steps[index], self.gates[index]))
836        } else {
837            None
838        }
839    }
840}
841
842impl Default for StepSequencer {
843    fn default() -> Self {
844        Self::new()
845    }
846}
847
848impl GraphModule for StepSequencer {
849    fn port_spec(&self) -> &PortSpec {
850        &self.spec
851    }
852
853    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
854        let clock = inputs.get_or(0, 0.0);
855        let reset = inputs.get_or(1, 0.0);
856
857        let clock_rising = clock > GATE_THRESHOLD_V && self.prev_clock <= GATE_THRESHOLD_V;
858        let reset_rising = reset > GATE_THRESHOLD_V && self.prev_reset <= GATE_THRESHOLD_V;
859
860        let mut trigger = 0.0;
861
862        if reset_rising {
863            self.current = 0;
864            trigger = GATE_HIGH_V;
865        } else if clock_rising {
866            self.current = (self.current + 1) % 8;
867            trigger = GATE_HIGH_V;
868        }
869
870        self.prev_clock = clock;
871        self.prev_reset = reset;
872
873        let cv = self.steps[self.current];
874        let gate = if self.gates[self.current] && clock > GATE_THRESHOLD_V {
875            5.0
876        } else {
877            0.0
878        };
879
880        outputs.set(10, cv);
881        outputs.set(11, gate);
882        outputs.set(12, trigger);
883    }
884
885    fn reset(&mut self) {
886        self.current = 0;
887        self.prev_clock = 0.0;
888        self.prev_reset = 0.0;
889    }
890
891    fn set_sample_rate(&mut self, _: f64) {}
892
893    fn type_id(&self) -> &'static str {
894        "step_sequencer"
895    }
896
897    // Step CV/gate values are internal state (no ports); bridge to introspection.
898    crate::impl_introspect!();
899}
900
901/// Stereo Output
902///
903/// The final output module that provides left and right audio outputs.
904/// Right input is normalled to left for mono compatibility.
905pub struct StereoOutput {
906    spec: PortSpec,
907}
908
909impl StereoOutput {
910    pub fn new() -> Self {
911        Self {
912            spec: PortSpec {
913                inputs: vec![
914                    PortDef::new(0, "left", SignalKind::Audio),
915                    PortDef::new(1, "right", SignalKind::Audio).normalled_to(0),
916                ],
917                outputs: vec![
918                    PortDef::new(0, "left", SignalKind::Audio),
919                    PortDef::new(1, "right", SignalKind::Audio),
920                ],
921            },
922        }
923    }
924}
925
926impl Default for StereoOutput {
927    fn default() -> Self {
928        Self::new()
929    }
930}
931
932impl GraphModule for StereoOutput {
933    fn port_spec(&self) -> &PortSpec {
934        &self.spec
935    }
936
937    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
938        let left = inputs.get_or(0, 0.0);
939        let right = inputs.get_or(1, left); // Mono fallback
940
941        outputs.set(0, left);
942        outputs.set(1, right);
943    }
944
945    fn reset(&mut self) {}
946
947    fn set_sample_rate(&mut self, _: f64) {}
948
949    fn type_id(&self) -> &'static str {
950        "stereo_output"
951    }
952}
953
954/// Sample and Hold
955///
956/// Samples the input signal when triggered and holds the value until the next trigger.
957pub struct SampleAndHold {
958    held_value: f64,
959    trigger_edge: EdgeDetector,
960    spec: PortSpec,
961}
962
963impl SampleAndHold {
964    pub fn new() -> Self {
965        Self {
966            held_value: 0.0,
967            trigger_edge: EdgeDetector::new(),
968            spec: PortSpec {
969                inputs: vec![
970                    PortDef::new(0, "in", SignalKind::CvBipolar),
971                    PortDef::new(1, "trig", SignalKind::Trigger),
972                ],
973                outputs: vec![PortDef::new(10, "out", SignalKind::CvBipolar)],
974            },
975        }
976    }
977}
978
979impl Default for SampleAndHold {
980    fn default() -> Self {
981        Self::new()
982    }
983}
984
985impl GraphModule for SampleAndHold {
986    fn port_spec(&self) -> &PortSpec {
987        &self.spec
988    }
989
990    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
991        let input = inputs.get_or(0, 0.0);
992        let trigger = inputs.get_or(1, 0.0);
993
994        // Sample on rising edge
995        if self.trigger_edge.rising(trigger) {
996            self.held_value = input;
997        }
998
999        outputs.set(10, self.held_value);
1000    }
1001
1002    fn reset(&mut self) {
1003        self.held_value = 0.0;
1004        self.trigger_edge.reset();
1005    }
1006
1007    fn set_sample_rate(&mut self, _: f64) {}
1008
1009    fn type_id(&self) -> &'static str {
1010        "sample_hold"
1011    }
1012}
1013
1014/// Slew Limiter
1015///
1016/// Limits the rate of change of a signal, creating portamento/glide effects.
1017/// Separate rise and fall times allow asymmetric behavior.
1018pub struct SlewLimiter {
1019    current: f64,
1020    sample_rate: f64,
1021    spec: PortSpec,
1022}
1023
1024impl SlewLimiter {
1025    pub fn new(sample_rate: f64) -> Self {
1026        Self {
1027            current: 0.0,
1028            sample_rate,
1029            spec: PortSpec {
1030                inputs: vec![
1031                    PortDef::new(0, "in", SignalKind::CvBipolar),
1032                    PortDef::new(1, "rise", SignalKind::CvUnipolar)
1033                        .with_default(0.5)
1034                        .with_attenuverter(),
1035                    PortDef::new(2, "fall", SignalKind::CvUnipolar)
1036                        .with_default(0.5)
1037                        .with_attenuverter(),
1038                ],
1039                outputs: vec![PortDef::new(10, "out", SignalKind::CvBipolar)],
1040            },
1041        }
1042    }
1043
1044    fn cv_to_rate(&self, cv: f64) -> f64 {
1045        // Map 0-1 CV to rate: 0 = instant, 1 = very slow (~10 seconds)
1046        // Rate is in units per sample
1047        let time = 0.001 + Libm::<f64>::pow(cv.clamp(0.0, 1.0), 2.0) * 10.0; // 1ms to 10s
1048        1.0 / (time * self.sample_rate)
1049    }
1050}
1051
1052impl Default for SlewLimiter {
1053    fn default() -> Self {
1054        Self::new(44100.0)
1055    }
1056}
1057
1058impl GraphModule for SlewLimiter {
1059    fn port_spec(&self) -> &PortSpec {
1060        &self.spec
1061    }
1062
1063    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1064        let target = inputs.get_or(0, 0.0);
1065        let rise_cv = inputs.get_or(1, 0.5);
1066        let fall_cv = inputs.get_or(2, 0.5);
1067
1068        let diff = target - self.current;
1069
1070        if diff > 0.0 {
1071            // Rising
1072            let rate = self.cv_to_rate(rise_cv);
1073            self.current += Libm::<f64>::fmin(diff, rate * 10.0); // Scale for voltage range
1074        } else if diff < 0.0 {
1075            // Falling
1076            let rate = self.cv_to_rate(fall_cv);
1077            self.current += Libm::<f64>::fmax(diff, -rate * 10.0);
1078        }
1079
1080        outputs.set(10, self.current);
1081    }
1082
1083    fn reset(&mut self) {
1084        self.current = 0.0;
1085    }
1086
1087    fn set_sample_rate(&mut self, sample_rate: f64) {
1088        self.sample_rate = sample_rate;
1089    }
1090
1091    fn type_id(&self) -> &'static str {
1092        "slew_limiter"
1093    }
1094}
1095
1096/// Quantizer
1097///
1098/// Quantizes input CV to musical scale degrees.
1099/// Supports chromatic, major, minor, and pentatonic scales.
1100pub struct Quantizer {
1101    pub(crate) scale: Scale,
1102    /// Last committed output voltage, for boundary hysteresis (Q041).
1103    last_output: Option<f64>,
1104    spec: PortSpec,
1105}
1106
1107/// Musical scales for quantization
1108#[derive(Debug, Clone, Copy, PartialEq)]
1109pub enum Scale {
1110    Chromatic,
1111    Major,
1112    Minor,
1113    PentatonicMajor,
1114    PentatonicMinor,
1115    Dorian,
1116    Mixolydian,
1117    Blues,
1118}
1119
1120impl Scale {
1121    /// Returns the semitone offsets for this scale (relative to root)
1122    fn semitones(&self) -> &'static [i32] {
1123        match self {
1124            Scale::Chromatic => &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
1125            Scale::Major => &[0, 2, 4, 5, 7, 9, 11],
1126            Scale::Minor => &[0, 2, 3, 5, 7, 8, 10],
1127            Scale::PentatonicMajor => &[0, 2, 4, 7, 9],
1128            Scale::PentatonicMinor => &[0, 3, 5, 7, 10],
1129            Scale::Dorian => &[0, 2, 3, 5, 7, 9, 10],
1130            Scale::Mixolydian => &[0, 2, 4, 5, 7, 9, 10],
1131            Scale::Blues => &[0, 3, 5, 6, 7, 10],
1132        }
1133    }
1134}
1135
1136impl Quantizer {
1137    pub fn new(scale: Scale) -> Self {
1138        Self {
1139            scale,
1140            last_output: None,
1141            spec: PortSpec {
1142                inputs: vec![PortDef::new(0, "in", SignalKind::VoltPerOctave)],
1143                outputs: vec![PortDef::new(10, "out", SignalKind::VoltPerOctave)],
1144            },
1145        }
1146    }
1147
1148    pub fn chromatic() -> Self {
1149        Self::new(Scale::Chromatic)
1150    }
1151
1152    pub fn major() -> Self {
1153        Self::new(Scale::Major)
1154    }
1155
1156    pub fn minor() -> Self {
1157        Self::new(Scale::Minor)
1158    }
1159
1160    pub fn set_scale(&mut self, scale: Scale) {
1161        self.scale = scale;
1162    }
1163
1164    fn quantize(&self, voltage: f64) -> f64 {
1165        let semitones = self.scale.semitones();
1166
1167        // Convert voltage to semitones (1V = 12 semitones)
1168        let total_semitones = voltage * 12.0;
1169
1170        // Find octave and position within octave
1171        let octave = Libm::<f64>::floor(total_semitones / 12.0);
1172        let within_octave = total_semitones - octave * 12.0;
1173
1174        // Find nearest scale degree
1175        let mut nearest = semitones[0];
1176        let mut min_dist = f64::MAX;
1177
1178        for &semi in semitones {
1179            let dist = (within_octave - semi as f64).abs();
1180            if dist < min_dist {
1181                min_dist = dist;
1182                nearest = semi;
1183            }
1184            // Also check wrapping to next octave
1185            let dist_wrap = (within_octave - (semi + 12) as f64).abs();
1186            if dist_wrap < min_dist {
1187                min_dist = dist_wrap;
1188                nearest = semi + 12;
1189            }
1190        }
1191
1192        // Convert back to voltage
1193        (octave * 12.0 + nearest as f64) / 12.0
1194    }
1195}
1196
1197impl Default for Quantizer {
1198    fn default() -> Self {
1199        Self::chromatic()
1200    }
1201}
1202
1203impl GraphModule for Quantizer {
1204    fn port_spec(&self) -> &PortSpec {
1205        &self.spec
1206    }
1207
1208    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1209        let input = inputs.get_or(0, 0.0);
1210        let candidate = self.quantize(input);
1211        // Hold the note through hysteresis so a CV parked on a boundary does not
1212        // chatter between adjacent scale degrees (Q041).
1213        let committed = hysteretic_note(
1214            self.last_output,
1215            input,
1216            candidate,
1217            NOTE_HYSTERESIS_SEMITONES,
1218        );
1219        self.last_output = Some(committed);
1220        outputs.set(10, committed);
1221    }
1222
1223    fn reset(&mut self) {
1224        self.last_output = None;
1225    }
1226
1227    fn set_sample_rate(&mut self, _: f64) {}
1228
1229    fn type_id(&self) -> &'static str {
1230        "quantizer"
1231    }
1232
1233    // `scale` is internal state (no scale port); bridge to introspection.
1234    crate::impl_introspect!();
1235}
1236
1237/// Clock Generator
1238///
1239/// Generates clock pulses at a specified tempo (BPM).
1240pub struct Clock {
1241    phase: f64,
1242    /// Integer count of completed main cycles, used to derive the divided
1243    /// outputs so they actually divide the tempo (Q035).
1244    cycle: u64,
1245    sample_rate: f64,
1246    spec: PortSpec,
1247}
1248
1249impl Clock {
1250    /// Bpm-control CV that yields exactly 120 BPM through [`Clock::cv_to_bpm`].
1251    ///
1252    /// Since `cv_to_bpm(cv) = 20 * 15^(cv/10)`, solving `20 * 15^(cv/10) = 120`
1253    /// gives `cv = 10 * ln(6) / ln(15) ≈ 6.6164` (Q038 — the old `1.2` default
1254    /// produced only ~27.5 BPM).
1255    const DEFAULT_BPM_CV: f64 = 6.616_418_958_920_283;
1256
1257    pub fn new(sample_rate: f64) -> Self {
1258        Self {
1259            phase: 0.0,
1260            cycle: 0,
1261            sample_rate,
1262            spec: PortSpec {
1263                inputs: vec![
1264                    PortDef::new(0, "bpm", SignalKind::CvUnipolar)
1265                        .with_default(Self::DEFAULT_BPM_CV) // 120 BPM when scaled
1266                        .with_attenuverter(),
1267                    PortDef::new(1, "reset", SignalKind::Trigger),
1268                ],
1269                outputs: vec![
1270                    PortDef::new(10, "out", SignalKind::Clock),
1271                    PortDef::new(11, "div2", SignalKind::Clock),
1272                    PortDef::new(12, "div4", SignalKind::Clock),
1273                ],
1274            },
1275        }
1276    }
1277
1278    fn cv_to_bpm(cv: f64) -> f64 {
1279        // Map 0-10V to 20-300 BPM (exponential)
1280        20.0 * Libm::<f64>::pow(15.0, cv / 10.0)
1281    }
1282}
1283
1284impl Default for Clock {
1285    fn default() -> Self {
1286        Self::new(44100.0)
1287    }
1288}
1289
1290impl GraphModule for Clock {
1291    fn port_spec(&self) -> &PortSpec {
1292        &self.spec
1293    }
1294
1295    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1296        let bpm_cv = inputs.get_or(0, Self::DEFAULT_BPM_CV); // Default 120 BPM
1297        let reset = inputs.get_or(1, 0.0);
1298
1299        let bpm = Self::cv_to_bpm(bpm_cv);
1300        let freq = bpm / 60.0; // Hz
1301
1302        // Reset on trigger
1303        if reset > GATE_THRESHOLD_V {
1304            self.phase = 0.0;
1305            self.cycle = 0;
1306        }
1307
1308        // Main clock output (short pulse at start of each cycle)
1309        let pulse_width = 0.1; // 10% duty cycle
1310        let in_pulse = self.phase < pulse_width;
1311        let main_out = if in_pulse { GATE_HIGH_V } else { 0.0 };
1312
1313        // Divided outputs derived from the integer cycle counter so they
1314        // genuinely divide the tempo (Q035): div2 fires on even cycles, div4 on
1315        // every fourth cycle, both with the same pulse-width window as main.
1316        // Bitwise masks (both divisors are powers of two) keep this MSRV-1.78
1317        // safe, avoiding the newer `u64::is_multiple_of`.
1318        let div2_out = if in_pulse && (self.cycle & 1) == 0 {
1319            GATE_HIGH_V
1320        } else {
1321            0.0
1322        };
1323        let div4_out = if in_pulse && (self.cycle & 3) == 0 {
1324            GATE_HIGH_V
1325        } else {
1326            0.0
1327        };
1328
1329        outputs.set(10, main_out);
1330        outputs.set(11, div2_out);
1331        outputs.set(12, div4_out);
1332
1333        // Advance phase, incrementing the cycle counter on each wrap.
1334        let new_phase = self.phase + freq / self.sample_rate;
1335        let wraps = Libm::<f64>::floor(new_phase);
1336        if wraps > 0.0 {
1337            self.cycle = self.cycle.wrapping_add(wraps as u64);
1338        }
1339        self.phase = new_phase - wraps;
1340    }
1341
1342    fn reset(&mut self) {
1343        self.phase = 0.0;
1344        self.cycle = 0;
1345    }
1346
1347    fn set_sample_rate(&mut self, sample_rate: f64) {
1348        self.sample_rate = sample_rate;
1349    }
1350
1351    fn type_id(&self) -> &'static str {
1352        "clock"
1353    }
1354}
1355
1356/// Attenuverter
1357///
1358/// Attenuates and/or inverts a signal. The level control goes from
1359/// -1 (inverted full scale) through 0 (silence) to +1 (full scale).
1360pub struct Attenuverter {
1361    spec: PortSpec,
1362}
1363
1364impl Attenuverter {
1365    pub fn new() -> Self {
1366        Self {
1367            spec: PortSpec {
1368                inputs: vec![
1369                    PortDef::new(0, "in", SignalKind::CvBipolar),
1370                    PortDef::new(1, "level", SignalKind::CvBipolar).with_default(5.0), // Default to unity gain
1371                ],
1372                outputs: vec![PortDef::new(10, "out", SignalKind::CvBipolar)],
1373            },
1374        }
1375    }
1376}
1377
1378impl Default for Attenuverter {
1379    fn default() -> Self {
1380        Self::new()
1381    }
1382}
1383
1384impl GraphModule for Attenuverter {
1385    fn port_spec(&self) -> &PortSpec {
1386        &self.spec
1387    }
1388
1389    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1390        let input = inputs.get_or(0, 0.0);
1391        let level = inputs.get_or(1, 5.0) / 5.0; // Normalize to -1..+1
1392
1393        outputs.set(10, input * level);
1394    }
1395
1396    fn reset(&mut self) {}
1397
1398    fn set_sample_rate(&mut self, _: f64) {}
1399
1400    fn type_id(&self) -> &'static str {
1401        "attenuverter"
1402    }
1403}
1404
1405/// Multiple (Signal Splitter)
1406///
1407/// Takes one input and copies it to multiple outputs.
1408/// Useful for sending one signal to multiple destinations.
1409pub struct Multiple {
1410    spec: PortSpec,
1411}
1412
1413impl Multiple {
1414    pub fn new() -> Self {
1415        Self {
1416            spec: PortSpec {
1417                inputs: vec![PortDef::new(0, "in", SignalKind::CvBipolar)],
1418                outputs: vec![
1419                    PortDef::new(10, "out1", SignalKind::CvBipolar),
1420                    PortDef::new(11, "out2", SignalKind::CvBipolar),
1421                    PortDef::new(12, "out3", SignalKind::CvBipolar),
1422                    PortDef::new(13, "out4", SignalKind::CvBipolar),
1423                ],
1424            },
1425        }
1426    }
1427}
1428
1429impl Default for Multiple {
1430    fn default() -> Self {
1431        Self::new()
1432    }
1433}
1434
1435impl GraphModule for Multiple {
1436    fn port_spec(&self) -> &PortSpec {
1437        &self.spec
1438    }
1439
1440    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1441        let input = inputs.get_or(0, 0.0);
1442
1443        outputs.set(10, input);
1444        outputs.set(11, input);
1445        outputs.set(12, input);
1446        outputs.set(13, input);
1447    }
1448
1449    fn reset(&mut self) {}
1450
1451    fn set_sample_rate(&mut self, _: f64) {}
1452
1453    fn type_id(&self) -> &'static str {
1454        "multiple"
1455    }
1456}
1457
1458// ============================================================================
1459// Phase 2 Modules: Hardware Fidelity
1460// ============================================================================
1461
1462/// Crossfader / Panner
1463///
1464/// Crossfades between two audio inputs or pans a mono input across stereo outputs.
1465/// The position control goes from -5V (full A/left) to +5V (full B/right).
1466pub struct Crossfader {
1467    spec: PortSpec,
1468}
1469
1470impl Crossfader {
1471    pub fn new() -> Self {
1472        Self {
1473            spec: PortSpec {
1474                inputs: vec![
1475                    PortDef::new(0, "a", SignalKind::Audio),
1476                    PortDef::new(1, "b", SignalKind::Audio),
1477                    PortDef::new(2, "pos", SignalKind::CvBipolar).with_default(0.0),
1478                ],
1479                outputs: vec![
1480                    PortDef::new(10, "out", SignalKind::Audio),
1481                    PortDef::new(11, "left", SignalKind::Audio),
1482                    PortDef::new(12, "right", SignalKind::Audio),
1483                ],
1484            },
1485        }
1486    }
1487}
1488
1489impl Default for Crossfader {
1490    fn default() -> Self {
1491        Self::new()
1492    }
1493}
1494
1495impl GraphModule for Crossfader {
1496    fn port_spec(&self) -> &PortSpec {
1497        &self.spec
1498    }
1499
1500    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1501        let a = inputs.get_or(0, 0.0);
1502        let b = inputs.get_or(1, 0.0);
1503        let pos = inputs.get_or(2, 0.0);
1504
1505        // Map position from -5V to +5V to 0.0 to 1.0
1506        let mix = ((pos / 5.0) + 1.0) / 2.0;
1507        let mix = mix.clamp(0.0, 1.0);
1508
1509        // Equal-power crossfade for smoother transitions
1510        let a_gain = Libm::<f64>::sqrt(1.0 - mix);
1511        let b_gain = Libm::<f64>::sqrt(mix);
1512
1513        // Main output: crossfade between A and B
1514        let out = a * a_gain + b * b_gain;
1515        outputs.set(10, out);
1516
1517        // Stereo outputs: pan the main output
1518        // At pos=-5V: full left, at pos=+5V: full right
1519        outputs.set(11, out * a_gain); // Left
1520        outputs.set(12, out * b_gain); // Right
1521    }
1522
1523    fn reset(&mut self) {}
1524
1525    fn set_sample_rate(&mut self, _: f64) {}
1526
1527    fn type_id(&self) -> &'static str {
1528        "crossfader"
1529    }
1530}
1531
1532/// Logic AND Gate
1533///
1534/// Outputs high (+5V) only when both inputs are high (>2.5V).
1535pub struct LogicAnd {
1536    spec: PortSpec,
1537}
1538
1539impl LogicAnd {
1540    pub fn new() -> Self {
1541        Self {
1542            spec: PortSpec {
1543                inputs: vec![
1544                    PortDef::new(0, "a", SignalKind::Gate),
1545                    PortDef::new(1, "b", SignalKind::Gate),
1546                ],
1547                outputs: vec![PortDef::new(10, "out", SignalKind::Gate)],
1548            },
1549        }
1550    }
1551}
1552
1553impl Default for LogicAnd {
1554    fn default() -> Self {
1555        Self::new()
1556    }
1557}
1558
1559impl GraphModule for LogicAnd {
1560    fn port_spec(&self) -> &PortSpec {
1561        &self.spec
1562    }
1563
1564    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1565        let a = inputs.get_or(0, 0.0) > GATE_THRESHOLD_V;
1566        let b = inputs.get_or(1, 0.0) > GATE_THRESHOLD_V;
1567
1568        outputs.set(10, if a && b { GATE_HIGH_V } else { 0.0 });
1569    }
1570
1571    fn reset(&mut self) {}
1572
1573    fn set_sample_rate(&mut self, _: f64) {}
1574
1575    fn type_id(&self) -> &'static str {
1576        "logic_and"
1577    }
1578}
1579
1580/// Logic OR Gate
1581///
1582/// Outputs high (+5V) when either or both inputs are high (>2.5V).
1583pub struct LogicOr {
1584    spec: PortSpec,
1585}
1586
1587impl LogicOr {
1588    pub fn new() -> Self {
1589        Self {
1590            spec: PortSpec {
1591                inputs: vec![
1592                    PortDef::new(0, "a", SignalKind::Gate),
1593                    PortDef::new(1, "b", SignalKind::Gate),
1594                ],
1595                outputs: vec![PortDef::new(10, "out", SignalKind::Gate)],
1596            },
1597        }
1598    }
1599}
1600
1601impl Default for LogicOr {
1602    fn default() -> Self {
1603        Self::new()
1604    }
1605}
1606
1607impl GraphModule for LogicOr {
1608    fn port_spec(&self) -> &PortSpec {
1609        &self.spec
1610    }
1611
1612    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1613        let a = inputs.get_or(0, 0.0) > GATE_THRESHOLD_V;
1614        let b = inputs.get_or(1, 0.0) > GATE_THRESHOLD_V;
1615
1616        outputs.set(10, if a || b { GATE_HIGH_V } else { 0.0 });
1617    }
1618
1619    fn reset(&mut self) {}
1620
1621    fn set_sample_rate(&mut self, _: f64) {}
1622
1623    fn type_id(&self) -> &'static str {
1624        "logic_or"
1625    }
1626}
1627
1628/// Logic XOR Gate
1629///
1630/// Outputs high (+5V) when exactly one input is high (>2.5V).
1631pub struct LogicXor {
1632    spec: PortSpec,
1633}
1634
1635impl LogicXor {
1636    pub fn new() -> Self {
1637        Self {
1638            spec: PortSpec {
1639                inputs: vec![
1640                    PortDef::new(0, "a", SignalKind::Gate),
1641                    PortDef::new(1, "b", SignalKind::Gate),
1642                ],
1643                outputs: vec![PortDef::new(10, "out", SignalKind::Gate)],
1644            },
1645        }
1646    }
1647}
1648
1649impl Default for LogicXor {
1650    fn default() -> Self {
1651        Self::new()
1652    }
1653}
1654
1655impl GraphModule for LogicXor {
1656    fn port_spec(&self) -> &PortSpec {
1657        &self.spec
1658    }
1659
1660    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1661        let a = inputs.get_or(0, 0.0) > GATE_THRESHOLD_V;
1662        let b = inputs.get_or(1, 0.0) > GATE_THRESHOLD_V;
1663
1664        outputs.set(10, if a ^ b { GATE_HIGH_V } else { 0.0 });
1665    }
1666
1667    fn reset(&mut self) {}
1668
1669    fn set_sample_rate(&mut self, _: f64) {}
1670
1671    fn type_id(&self) -> &'static str {
1672        "logic_xor"
1673    }
1674}
1675
1676/// Logic NOT Gate (Inverter)
1677///
1678/// Inverts the input: outputs high (+5V) when input is low, and vice versa.
1679pub struct LogicNot {
1680    spec: PortSpec,
1681}
1682
1683impl LogicNot {
1684    pub fn new() -> Self {
1685        Self {
1686            spec: PortSpec {
1687                inputs: vec![PortDef::new(0, "in", SignalKind::Gate)],
1688                outputs: vec![PortDef::new(10, "out", SignalKind::Gate)],
1689            },
1690        }
1691    }
1692}
1693
1694impl Default for LogicNot {
1695    fn default() -> Self {
1696        Self::new()
1697    }
1698}
1699
1700impl GraphModule for LogicNot {
1701    fn port_spec(&self) -> &PortSpec {
1702        &self.spec
1703    }
1704
1705    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1706        let input = inputs.get_or(0, 0.0) > GATE_THRESHOLD_V;
1707        outputs.set(10, if input { 0.0 } else { GATE_HIGH_V });
1708    }
1709
1710    fn reset(&mut self) {}
1711
1712    fn set_sample_rate(&mut self, _: f64) {}
1713
1714    fn type_id(&self) -> &'static str {
1715        "logic_not"
1716    }
1717}
1718
1719/// Comparator
1720///
1721/// Compares two CV inputs and outputs a gate based on the comparison.
1722/// Outputs high (+5V) when A > B, otherwise low (0V).
1723/// Also provides inverted output (A <= B).
1724pub struct Comparator {
1725    /// Last committed comparison state: `1` = gt, `-1` = lt, `0` = eq. Used for
1726    /// true stateful hysteresis so a signal dithering around B does not toggle
1727    /// every sample (Q041).
1728    state: i8,
1729    spec: PortSpec,
1730}
1731
1732impl Comparator {
1733    /// Deadband half-width defining the equality region (volts).
1734    const DEADBAND_V: f64 = 0.01;
1735    /// Extra margin, beyond the deadband edge, the input must cross to flip
1736    /// state. A dither smaller than this can no longer cause chatter.
1737    const HYSTERESIS_V: f64 = 0.02;
1738
1739    pub fn new() -> Self {
1740        Self {
1741            state: 0,
1742            spec: PortSpec {
1743                inputs: vec![
1744                    PortDef::new(0, "a", SignalKind::CvBipolar),
1745                    PortDef::new(1, "b", SignalKind::CvBipolar),
1746                ],
1747                outputs: vec![
1748                    PortDef::new(10, "gt", SignalKind::Gate), // A > B
1749                    PortDef::new(11, "lt", SignalKind::Gate), // A < B
1750                    PortDef::new(12, "eq", SignalKind::Gate), // A ≈ B (within threshold)
1751                ],
1752            },
1753        }
1754    }
1755}
1756
1757impl Default for Comparator {
1758    fn default() -> Self {
1759        Self::new()
1760    }
1761}
1762
1763impl GraphModule for Comparator {
1764    fn port_spec(&self) -> &PortSpec {
1765        &self.spec
1766    }
1767
1768    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1769        let a = inputs.get_or(0, 0.0);
1770        let b = inputs.get_or(1, 0.0);
1771        let d = a - b;
1772
1773        let t = Self::DEADBAND_V;
1774        let hy = Self::HYSTERESIS_V;
1775
1776        // Stateful hysteresis: turning an output ON needs the input to cross the
1777        // deadband edge plus the hysteresis margin; turning it OFF happens back
1778        // at the deadband edge. Once committed, a dither smaller than `hy`
1779        // cannot flip the state, eliminating boundary chatter (Q041).
1780        let mut gt = self.state == 1;
1781        let mut lt = self.state == -1;
1782
1783        if gt {
1784            if d < t {
1785                gt = false;
1786            }
1787        } else if d >= t + hy {
1788            gt = true;
1789        }
1790
1791        if lt {
1792            if d > -t {
1793                lt = false;
1794            }
1795        } else if d <= -t - hy {
1796            lt = true;
1797        }
1798
1799        // `gt` and `lt` are mutually exclusive: their ON conditions require
1800        // |d| >= t + hy of opposite sign.
1801        self.state = if gt {
1802            1
1803        } else if lt {
1804            -1
1805        } else {
1806            0
1807        };
1808
1809        outputs.set(10, if gt { GATE_HIGH_V } else { 0.0 });
1810        outputs.set(11, if lt { GATE_HIGH_V } else { 0.0 });
1811        outputs.set(12, if self.state == 0 { GATE_HIGH_V } else { 0.0 });
1812    }
1813
1814    fn reset(&mut self) {
1815        self.state = 0;
1816    }
1817
1818    fn set_sample_rate(&mut self, _: f64) {}
1819
1820    fn type_id(&self) -> &'static str {
1821        "comparator"
1822    }
1823}
1824
1825/// Rectifier
1826///
1827/// Performs full-wave and half-wave rectification of audio/CV signals.
1828/// Also provides absolute value output.
1829pub struct Rectifier {
1830    spec: PortSpec,
1831}
1832
1833impl Rectifier {
1834    pub fn new() -> Self {
1835        Self {
1836            spec: PortSpec {
1837                inputs: vec![PortDef::new(0, "in", SignalKind::Audio)],
1838                outputs: vec![
1839                    PortDef::new(10, "full", SignalKind::Audio), // Full-wave rectified
1840                    PortDef::new(11, "half_pos", SignalKind::Audio), // Half-wave (positive)
1841                    PortDef::new(12, "half_neg", SignalKind::Audio), // Half-wave (negative, inverted)
1842                    PortDef::new(13, "abs", SignalKind::CvUnipolar), // Absolute value (0-10V)
1843                ],
1844            },
1845        }
1846    }
1847}
1848
1849impl Default for Rectifier {
1850    fn default() -> Self {
1851        Self::new()
1852    }
1853}
1854
1855impl GraphModule for Rectifier {
1856    fn port_spec(&self) -> &PortSpec {
1857        &self.spec
1858    }
1859
1860    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1861        let input = inputs.get_or(0, 0.0);
1862
1863        // Full-wave rectification: absolute value, keeps ±5V range as 0-5V
1864        outputs.set(10, Libm::<f64>::fabs(input));
1865
1866        // Half-wave positive: pass positive, block negative
1867        outputs.set(11, Libm::<f64>::fmax(input, 0.0));
1868
1869        // Half-wave negative: pass negative inverted, block positive
1870        outputs.set(12, Libm::<f64>::fmax(-input, 0.0));
1871
1872        // Absolute value scaled to 0-10V unipolar (input ±5V -> output 0-10V)
1873        outputs.set(13, Libm::<f64>::fabs(input) * 2.0);
1874    }
1875
1876    fn reset(&mut self) {}
1877
1878    fn set_sample_rate(&mut self, _: f64) {}
1879
1880    fn type_id(&self) -> &'static str {
1881        "rectifier"
1882    }
1883}
1884
1885/// Precision Adder
1886///
1887/// A high-precision CV adder/mixer with multiple inputs.
1888/// Useful for combining V/Oct signals for transposition.
1889/// Includes a precision 1V/octave offset output for tuning.
1890pub struct PrecisionAdder {
1891    spec: PortSpec,
1892}
1893
1894impl PrecisionAdder {
1895    pub fn new() -> Self {
1896        Self {
1897            spec: PortSpec {
1898                inputs: vec![
1899                    PortDef::new(0, "in1", SignalKind::VoltPerOctave),
1900                    PortDef::new(1, "in2", SignalKind::VoltPerOctave),
1901                    PortDef::new(2, "in3", SignalKind::CvBipolar),
1902                    PortDef::new(3, "in4", SignalKind::CvBipolar),
1903                ],
1904                outputs: vec![
1905                    PortDef::new(10, "sum", SignalKind::VoltPerOctave),
1906                    PortDef::new(11, "inv", SignalKind::VoltPerOctave), // Inverted sum
1907                ],
1908            },
1909        }
1910    }
1911}
1912
1913impl Default for PrecisionAdder {
1914    fn default() -> Self {
1915        Self::new()
1916    }
1917}
1918
1919impl GraphModule for PrecisionAdder {
1920    fn port_spec(&self) -> &PortSpec {
1921        &self.spec
1922    }
1923
1924    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1925        let sum = inputs.get_or(0, 0.0)
1926            + inputs.get_or(1, 0.0)
1927            + inputs.get_or(2, 0.0)
1928            + inputs.get_or(3, 0.0);
1929
1930        outputs.set(10, sum);
1931        outputs.set(11, -sum);
1932    }
1933
1934    fn reset(&mut self) {}
1935
1936    fn set_sample_rate(&mut self, _: f64) {}
1937
1938    fn type_id(&self) -> &'static str {
1939        "precision_adder"
1940    }
1941}
1942
1943/// Voltage-Controlled Switch
1944///
1945/// Routes one of two inputs to the output based on a control signal.
1946/// When CV > 2.5V, output = B; otherwise output = A.
1947/// Also provides complementary outputs.
1948pub struct VcSwitch {
1949    spec: PortSpec,
1950}
1951
1952impl VcSwitch {
1953    pub fn new() -> Self {
1954        Self {
1955            spec: PortSpec {
1956                inputs: vec![
1957                    PortDef::new(0, "a", SignalKind::Audio),
1958                    PortDef::new(1, "b", SignalKind::Audio),
1959                    PortDef::new(2, "cv", SignalKind::Gate).with_default(0.0),
1960                ],
1961                outputs: vec![
1962                    PortDef::new(10, "out", SignalKind::Audio), // Selected input
1963                    PortDef::new(11, "a_out", SignalKind::Audio), // A when selected, else 0
1964                    PortDef::new(12, "b_out", SignalKind::Audio), // B when selected, else 0
1965                ],
1966            },
1967        }
1968    }
1969}
1970
1971impl Default for VcSwitch {
1972    fn default() -> Self {
1973        Self::new()
1974    }
1975}
1976
1977impl GraphModule for VcSwitch {
1978    fn port_spec(&self) -> &PortSpec {
1979        &self.spec
1980    }
1981
1982    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1983        let a = inputs.get_or(0, 0.0);
1984        let b = inputs.get_or(1, 0.0);
1985        let cv = inputs.get_or(2, 0.0);
1986
1987        let select_b = cv > GATE_THRESHOLD_V;
1988
1989        if select_b {
1990            outputs.set(10, b);
1991            outputs.set(11, 0.0);
1992            outputs.set(12, b);
1993        } else {
1994            outputs.set(10, a);
1995            outputs.set(11, a);
1996            outputs.set(12, 0.0);
1997        }
1998    }
1999
2000    fn reset(&mut self) {}
2001
2002    fn set_sample_rate(&mut self, _: f64) {}
2003
2004    fn type_id(&self) -> &'static str {
2005        "vc_switch"
2006    }
2007}
2008
2009/// Bernoulli Gate
2010///
2011/// A probabilistic gate router. On each trigger, randomly routes the signal
2012/// to one of two outputs based on a probability parameter.
2013/// Inspired by Mutable Instruments Branches.
2014pub struct BernoulliGate {
2015    prev_trigger: f64,
2016    /// Latched gate A state, persisted in the struct because the engine hands
2017    /// `tick` a fresh output buffer each sample (Q036).
2018    gate_a: f64,
2019    /// Latched gate B state (see `gate_a`).
2020    gate_b: f64,
2021    spec: PortSpec,
2022}
2023
2024impl BernoulliGate {
2025    pub fn new() -> Self {
2026        Self {
2027            prev_trigger: 0.0,
2028            gate_a: 0.0,
2029            gate_b: 0.0,
2030            spec: PortSpec {
2031                inputs: vec![
2032                    PortDef::new(0, "trig", SignalKind::Trigger),
2033                    PortDef::new(1, "prob", SignalKind::CvUnipolar).with_default(5.0), // 50% default
2034                ],
2035                outputs: vec![
2036                    PortDef::new(10, "a", SignalKind::Trigger),   // Output A
2037                    PortDef::new(11, "b", SignalKind::Trigger),   // Output B
2038                    PortDef::new(12, "gate_a", SignalKind::Gate), // Latched gate A
2039                    PortDef::new(13, "gate_b", SignalKind::Gate), // Latched gate B
2040                ],
2041            },
2042        }
2043    }
2044}
2045
2046impl Default for BernoulliGate {
2047    fn default() -> Self {
2048        Self::new()
2049    }
2050}
2051
2052impl GraphModule for BernoulliGate {
2053    fn port_spec(&self) -> &PortSpec {
2054        &self.spec
2055    }
2056
2057    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
2058        let trigger = inputs.get_or(0, 0.0);
2059        let prob = (inputs.get_or(1, 5.0) / 10.0).clamp(0.0, 1.0); // Normalize to 0-1
2060
2061        let rising_edge = trigger > GATE_THRESHOLD_V && self.prev_trigger <= GATE_THRESHOLD_V;
2062        self.prev_trigger = trigger;
2063
2064        // Default: no trigger output
2065        let mut trig_a = 0.0;
2066        let mut trig_b = 0.0;
2067
2068        if rising_edge {
2069            // Random decision based on probability
2070            let rand_val: f64 = rng::random();
2071            if rand_val < prob {
2072                trig_a = GATE_HIGH_V;
2073            } else {
2074                trig_b = GATE_HIGH_V;
2075            }
2076        }
2077
2078        // Trigger outputs (momentary)
2079        outputs.set(10, trig_a);
2080        outputs.set(11, trig_b);
2081
2082        // Gate outputs track which side was last triggered and latch until the
2083        // other side is triggered. State lives in struct fields (Q036) because
2084        // the output buffer is not persisted across ticks by the engine.
2085        if trig_a > 0.0 {
2086            self.gate_a = GATE_HIGH_V;
2087            self.gate_b = 0.0;
2088        } else if trig_b > 0.0 {
2089            self.gate_a = 0.0;
2090            self.gate_b = GATE_HIGH_V;
2091        }
2092
2093        outputs.set(12, self.gate_a);
2094        outputs.set(13, self.gate_b);
2095    }
2096
2097    fn reset(&mut self) {
2098        self.prev_trigger = 0.0;
2099        self.gate_a = 0.0;
2100        self.gate_b = 0.0;
2101    }
2102
2103    fn set_sample_rate(&mut self, _: f64) {}
2104
2105    fn type_id(&self) -> &'static str {
2106        "bernoulli_gate"
2107    }
2108}
2109
2110/// Min module
2111///
2112/// Outputs the minimum of two input signals.
2113pub struct Min {
2114    spec: PortSpec,
2115}
2116
2117impl Min {
2118    pub fn new() -> Self {
2119        Self {
2120            spec: PortSpec {
2121                inputs: vec![
2122                    PortDef::new(0, "a", SignalKind::CvBipolar),
2123                    PortDef::new(1, "b", SignalKind::CvBipolar),
2124                ],
2125                outputs: vec![PortDef::new(10, "out", SignalKind::CvBipolar)],
2126            },
2127        }
2128    }
2129}
2130
2131impl Default for Min {
2132    fn default() -> Self {
2133        Self::new()
2134    }
2135}
2136
2137impl GraphModule for Min {
2138    fn port_spec(&self) -> &PortSpec {
2139        &self.spec
2140    }
2141
2142    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
2143        let a = inputs.get_or(0, 0.0);
2144        let b = inputs.get_or(1, 0.0);
2145        outputs.set(10, Libm::<f64>::fmin(a, b));
2146    }
2147
2148    fn reset(&mut self) {}
2149
2150    fn set_sample_rate(&mut self, _: f64) {}
2151
2152    fn type_id(&self) -> &'static str {
2153        "min"
2154    }
2155}
2156
2157/// Max module
2158///
2159/// Outputs the maximum of two input signals.
2160pub struct Max {
2161    spec: PortSpec,
2162}
2163
2164impl Max {
2165    pub fn new() -> Self {
2166        Self {
2167            spec: PortSpec {
2168                inputs: vec![
2169                    PortDef::new(0, "a", SignalKind::CvBipolar),
2170                    PortDef::new(1, "b", SignalKind::CvBipolar),
2171                ],
2172                outputs: vec![PortDef::new(10, "out", SignalKind::CvBipolar)],
2173            },
2174        }
2175    }
2176}
2177
2178impl Default for Max {
2179    fn default() -> Self {
2180        Self::new()
2181    }
2182}
2183
2184impl GraphModule for Max {
2185    fn port_spec(&self) -> &PortSpec {
2186        &self.spec
2187    }
2188
2189    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
2190        let a = inputs.get_or(0, 0.0);
2191        let b = inputs.get_or(1, 0.0);
2192        outputs.set(10, Libm::<f64>::fmax(a, b));
2193    }
2194
2195    fn reset(&mut self) {}
2196
2197    fn set_sample_rate(&mut self, _: f64) {}
2198
2199    fn type_id(&self) -> &'static str {
2200        "max"
2201    }
2202}
2203
2204// ============================================================================
2205// Planned Modules: ChordMemory
2206// ============================================================================
2207
2208/// Chord type for the ChordMemory module
2209#[derive(Debug, Clone, Copy, PartialEq)]
2210pub enum ChordType {
2211    Major,
2212    Minor,
2213    Seventh,
2214    MajorSeventh,
2215    MinorSeventh,
2216    Diminished,
2217    Augmented,
2218    Sus2,
2219    Sus4,
2220}
2221
2222impl ChordType {
2223    /// Returns the semitone intervals for this chord type (relative to root)
2224    fn intervals(&self) -> &'static [i32] {
2225        match self {
2226            ChordType::Major => &[0, 4, 7],
2227            ChordType::Minor => &[0, 3, 7],
2228            ChordType::Seventh => &[0, 4, 7, 10],
2229            ChordType::MajorSeventh => &[0, 4, 7, 11],
2230            ChordType::MinorSeventh => &[0, 3, 7, 10],
2231            ChordType::Diminished => &[0, 3, 6],
2232            ChordType::Augmented => &[0, 4, 8],
2233            ChordType::Sus2 => &[0, 2, 7],
2234            ChordType::Sus4 => &[0, 5, 7],
2235        }
2236    }
2237
2238    /// Select chord type from CV value (0.0-1.0)
2239    fn from_cv(cv: f64) -> Self {
2240        match (cv * 8.99) as u8 {
2241            0 => ChordType::Major,
2242            1 => ChordType::Minor,
2243            2 => ChordType::Seventh,
2244            3 => ChordType::MajorSeventh,
2245            4 => ChordType::MinorSeventh,
2246            5 => ChordType::Diminished,
2247            6 => ChordType::Augmented,
2248            7 => ChordType::Sus2,
2249            _ => ChordType::Sus4,
2250        }
2251    }
2252}
2253
2254/// Chord Memory
2255///
2256/// Generates chord voicings from a root note. Outputs 4 V/Oct signals
2257/// representing chord voices. Supports 9 chord types with inversions
2258/// and voice spreading.
2259///
2260/// **Chord types** (selected via CV 0-1):
2261/// - Major, Minor, 7th, Maj7, Min7, Dim, Aug, Sus2, Sus4
2262///
2263/// **Inversion**: Rotates which note is the bass
2264/// **Spread**: Distributes voices across octaves
2265pub struct ChordMemory {
2266    spec: PortSpec,
2267}
2268
2269impl ChordMemory {
2270    pub fn new() -> Self {
2271        Self {
2272            spec: PortSpec {
2273                inputs: vec![
2274                    PortDef::new(0, "root", SignalKind::VoltPerOctave),
2275                    PortDef::new(1, "chord", SignalKind::CvUnipolar)
2276                        .with_default(0.0)
2277                        .with_attenuverter(),
2278                    PortDef::new(2, "inversion", SignalKind::CvUnipolar)
2279                        .with_default(0.0)
2280                        .with_attenuverter(),
2281                    PortDef::new(3, "spread", SignalKind::CvUnipolar)
2282                        .with_default(0.0)
2283                        .with_attenuverter(),
2284                ],
2285                outputs: vec![
2286                    PortDef::new(10, "voice1", SignalKind::VoltPerOctave),
2287                    PortDef::new(11, "voice2", SignalKind::VoltPerOctave),
2288                    PortDef::new(12, "voice3", SignalKind::VoltPerOctave),
2289                    PortDef::new(13, "voice4", SignalKind::VoltPerOctave),
2290                ],
2291            },
2292        }
2293    }
2294}
2295
2296impl Default for ChordMemory {
2297    fn default() -> Self {
2298        Self::new()
2299    }
2300}
2301
2302impl GraphModule for ChordMemory {
2303    fn port_spec(&self) -> &PortSpec {
2304        &self.spec
2305    }
2306
2307    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
2308        let root = inputs.get_or(0, 0.0);
2309        let chord_cv = inputs.get_or(1, 0.0).clamp(0.0, 1.0);
2310        let inversion_cv = inputs.get_or(2, 0.0).clamp(0.0, 1.0);
2311        let spread = inputs.get_or(3, 0.0).clamp(0.0, 1.0);
2312
2313        let chord_type = ChordType::from_cv(chord_cv);
2314        let intervals = chord_type.intervals();
2315        let num_notes = intervals.len();
2316
2317        // Calculate inversion (0, 1, 2, or 3)
2318        let inversion = ((inversion_cv * num_notes as f64) as usize) % num_notes;
2319
2320        // Build chord voices
2321        let mut voices = [0.0f64; 4];
2322        for (i, voice) in voices.iter_mut().enumerate() {
2323            if i < num_notes {
2324                let interval_idx = (i + inversion) % num_notes;
2325                let semitones = intervals[interval_idx];
2326
2327                // Add octave if the interval wrapped around due to inversion
2328                let octave_offset = if i + inversion >= num_notes { 1.0 } else { 0.0 };
2329
2330                // Apply spread (voices spread across octaves)
2331                let spread_offset = spread * (i as f64 / 3.0);
2332
2333                // Convert semitones to V/Oct (1V = 1 octave, so 1 semitone = 1/12 V)
2334                *voice = root + semitones as f64 / 12.0 + octave_offset + spread_offset;
2335            } else {
2336                // For 3-note chords, duplicate the root an octave up for voice 4
2337                // Apply spread to the duplicated voice as well
2338                let spread_offset = spread * (i as f64 / 3.0);
2339                *voice = root + 1.0 + spread_offset;
2340            }
2341        }
2342
2343        outputs.set(10, voices[0]);
2344        outputs.set(11, voices[1]);
2345        outputs.set(12, voices[2]);
2346        outputs.set(13, voices[3]);
2347    }
2348
2349    fn reset(&mut self) {}
2350
2351    fn set_sample_rate(&mut self, _: f64) {}
2352
2353    fn type_id(&self) -> &'static str {
2354        "chord_memory"
2355    }
2356}
2357
2358// ============================================================================
2359// Planned Modules: ParametricEq
2360// ============================================================================
2361
2362/// Arpeggiator pattern types
2363#[derive(Debug, Clone, Copy, PartialEq)]
2364pub enum ArpPattern {
2365    /// Play notes ascending
2366    Up,
2367    /// Play notes descending
2368    Down,
2369    /// Play notes up then down
2370    UpDown,
2371    /// Play notes in random order
2372    Random,
2373}
2374
2375impl ArpPattern {
2376    /// Get pattern from CV (0-1 maps to 4 patterns)
2377    fn from_cv(cv: f64) -> Self {
2378        let cv = cv.clamp(0.0, 1.0);
2379        if cv < 0.25 {
2380            ArpPattern::Up
2381        } else if cv < 0.5 {
2382            ArpPattern::Down
2383        } else if cv < 0.75 {
2384            ArpPattern::UpDown
2385        } else {
2386            ArpPattern::Random
2387        }
2388    }
2389}
2390
2391/// Pattern-based arpeggiator
2392///
2393/// Captures held notes and plays them back in sequence on each clock pulse.
2394/// Supports multiple octave ranges and different playback patterns.
2395///
2396/// # Ports
2397/// - Input 0: V/Oct input note
2398/// - Input 1: Gate input (captures notes on rising edge)
2399/// - Input 2: Clock input (advances sequence)
2400/// - Input 3: Pattern select (0-1 CV maps to Up/Down/UpDown/Random)
2401/// - Input 4: Octave range (0-1 CV maps to 1-4 octaves)
2402/// - Input 5: Reset input (gate)
2403/// - Output 10: V/Oct output
2404/// - Output 11: Gate output
2405/// - Output 12: Trigger output (pulse on each step)
2406pub struct Arpeggiator {
2407    /// Held notes buffer (V/Oct values)
2408    held_notes: [f64; 8],
2409    /// Number of held notes
2410    num_notes: usize,
2411    /// Current step in sequence
2412    current_step: usize,
2413    /// Direction for up-down pattern (true = up)
2414    direction_up: bool,
2415    /// Previous gate state for edge detection
2416    prev_gate: f64,
2417    /// Note captured on the current gate's rising edge, removed on its falling
2418    /// edge so held notes are actually released (Q040).
2419    captured_note: Option<f64>,
2420    /// Previous clock state for edge detection
2421    prev_clock: f64,
2422    /// Previous reset state for edge detection
2423    prev_reset: f64,
2424    /// Random number generator
2425    rng: crate::rng::Rng,
2426    /// Output gate state
2427    gate_out: f64,
2428    /// Trigger countdown (samples remaining)
2429    trigger_countdown: usize,
2430    sample_rate: f64,
2431    spec: PortSpec,
2432}
2433
2434impl Arpeggiator {
2435    /// Trigger pulse length in ms
2436    const TRIGGER_MS: f64 = 1.0;
2437
2438    pub fn new(sample_rate: f64) -> Self {
2439        let spec = PortSpec {
2440            inputs: vec![
2441                PortDef::new(0, "v_oct", SignalKind::VoltPerOctave).with_default(0.0),
2442                PortDef::new(1, "gate", SignalKind::Gate).with_default(0.0),
2443                PortDef::new(2, "clock", SignalKind::Clock).with_default(0.0),
2444                PortDef::new(3, "pattern", SignalKind::CvUnipolar).with_default(0.0),
2445                PortDef::new(4, "octaves", SignalKind::CvUnipolar).with_default(0.0),
2446                PortDef::new(5, "reset", SignalKind::Gate).with_default(0.0),
2447            ],
2448            outputs: vec![
2449                PortDef::new(10, "v_oct_out", SignalKind::VoltPerOctave),
2450                PortDef::new(11, "gate_out", SignalKind::Gate),
2451                PortDef::new(12, "trigger", SignalKind::Trigger),
2452            ],
2453        };
2454
2455        Self {
2456            held_notes: [0.0; 8],
2457            num_notes: 0,
2458            current_step: 0,
2459            direction_up: true,
2460            prev_gate: 0.0,
2461            captured_note: None,
2462            prev_clock: 0.0,
2463            prev_reset: 0.0,
2464            rng: crate::rng::Rng::from_seed(42),
2465            gate_out: 0.0,
2466            trigger_countdown: 0,
2467            sample_rate,
2468            spec,
2469        }
2470    }
2471
2472    /// Add a note to the held notes buffer (keeps sorted)
2473    fn add_note(&mut self, note: f64) {
2474        if self.num_notes >= 8 {
2475            return;
2476        }
2477
2478        // Insert in sorted order
2479        let mut insert_pos = self.num_notes;
2480        for i in 0..self.num_notes {
2481            if note < self.held_notes[i] {
2482                insert_pos = i;
2483                break;
2484            }
2485        }
2486
2487        // Shift notes up
2488        for i in (insert_pos..self.num_notes).rev() {
2489            self.held_notes[i + 1] = self.held_notes[i];
2490        }
2491
2492        self.held_notes[insert_pos] = note;
2493        self.num_notes += 1;
2494    }
2495
2496    /// Remove a note from the held notes buffer
2497    pub fn remove_note(&mut self, note: f64) {
2498        // Find the note (with small tolerance for floating point)
2499        let mut found_idx = None;
2500        for i in 0..self.num_notes {
2501            if (self.held_notes[i] - note).abs() < 0.001 {
2502                found_idx = Some(i);
2503                break;
2504            }
2505        }
2506
2507        if let Some(idx) = found_idx {
2508            // Shift notes down
2509            for i in idx..self.num_notes - 1 {
2510                self.held_notes[i] = self.held_notes[i + 1];
2511            }
2512            self.num_notes -= 1;
2513        }
2514    }
2515
2516    /// Get the current note based on step and pattern
2517    fn get_current_note(&mut self, pattern: ArpPattern, octaves: usize) -> f64 {
2518        if self.num_notes == 0 {
2519            return 0.0;
2520        }
2521
2522        let total_steps = self.num_notes * octaves;
2523        let step = self.current_step % total_steps;
2524
2525        let note_idx = match pattern {
2526            ArpPattern::Up => step % self.num_notes,
2527            ArpPattern::Down => (self.num_notes - 1) - (step % self.num_notes),
2528            ArpPattern::UpDown => {
2529                // Calculate position in up-down cycle
2530                let cycle_len = if self.num_notes > 1 {
2531                    (self.num_notes - 1) * 2
2532                } else {
2533                    1
2534                };
2535                let pos = step % cycle_len;
2536                if pos < self.num_notes {
2537                    pos
2538                } else {
2539                    (self.num_notes - 1) * 2 - pos
2540                }
2541            }
2542            ArpPattern::Random => (self.rng.next_u64() as usize) % self.num_notes,
2543        };
2544
2545        let octave = step / self.num_notes;
2546        let base_note = self.held_notes[note_idx % self.num_notes];
2547
2548        base_note + octave as f64 // Add octave offset (1V per octave)
2549    }
2550}
2551
2552impl Default for Arpeggiator {
2553    fn default() -> Self {
2554        Self::new(44100.0)
2555    }
2556}
2557
2558impl GraphModule for Arpeggiator {
2559    fn port_spec(&self) -> &PortSpec {
2560        &self.spec
2561    }
2562
2563    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
2564        let v_oct = inputs.get_or(0, 0.0);
2565        let gate = inputs.get_or(1, 0.0);
2566        let clock = inputs.get_or(2, 0.0);
2567        let pattern_cv = inputs.get_or(3, 0.0);
2568        let octaves_cv = inputs.get_or(4, 0.0);
2569        let reset = inputs.get_or(5, 0.0);
2570
2571        let pattern = ArpPattern::from_cv(pattern_cv);
2572        let octaves = (1.0 + octaves_cv.clamp(0.0, 1.0) * 3.0) as usize; // 1-4 octaves
2573
2574        // Handle gate input (note capture/release).
2575        // A note is added on the gate's rising edge and released on its falling
2576        // edge (Q040), so the held set reflects the currently-held note rather
2577        // than growing monotonically to the 8-note cap.
2578        if gate > GATE_THRESHOLD_V && self.prev_gate <= GATE_THRESHOLD_V {
2579            // Rising edge - add note and remember it for the matching release.
2580            self.add_note(v_oct);
2581            self.captured_note = Some(v_oct);
2582        } else if gate <= GATE_THRESHOLD_V && self.prev_gate > GATE_THRESHOLD_V {
2583            // Falling edge - remove the note captured on the rising edge.
2584            if let Some(note) = self.captured_note.take() {
2585                self.remove_note(note);
2586            }
2587        }
2588        self.prev_gate = gate;
2589
2590        // Handle reset - also clears the held-note buffer (Q040).
2591        if reset > GATE_THRESHOLD_V && self.prev_reset <= GATE_THRESHOLD_V {
2592            self.current_step = 0;
2593            self.direction_up = true;
2594            self.held_notes = [0.0; 8];
2595            self.num_notes = 0;
2596            self.captured_note = None;
2597        }
2598        self.prev_reset = reset;
2599
2600        // Handle clock (advance sequence)
2601        let mut trigger_out = 0.0;
2602        let clock_rising =
2603            clock > GATE_THRESHOLD_V && self.prev_clock <= GATE_THRESHOLD_V && self.num_notes > 0;
2604
2605        if clock_rising {
2606            self.gate_out = GATE_HIGH_V;
2607            // Start trigger pulse
2608            self.trigger_countdown = (Self::TRIGGER_MS * self.sample_rate / 1000.0) as usize;
2609            trigger_out = GATE_HIGH_V;
2610        }
2611        self.prev_clock = clock;
2612
2613        // Update trigger
2614        if self.trigger_countdown > 0 {
2615            self.trigger_countdown -= 1;
2616            trigger_out = GATE_HIGH_V;
2617        }
2618
2619        // Gate follows clock (simplified - stays high while clock is high)
2620        if clock <= GATE_THRESHOLD_V {
2621            self.gate_out = 0.0;
2622        }
2623
2624        // Get current note
2625        let v_oct_out = if self.num_notes > 0 {
2626            self.get_current_note(pattern, octaves)
2627        } else {
2628            0.0
2629        };
2630
2631        // Advance step AFTER outputting current note
2632        if clock_rising {
2633            self.current_step += 1;
2634        }
2635
2636        outputs.set(10, v_oct_out);
2637        outputs.set(
2638            11,
2639            if self.num_notes > 0 {
2640                self.gate_out
2641            } else {
2642                0.0
2643            },
2644        );
2645        outputs.set(12, trigger_out);
2646    }
2647
2648    fn reset(&mut self) {
2649        self.held_notes = [0.0; 8];
2650        self.num_notes = 0;
2651        self.captured_note = None;
2652        self.current_step = 0;
2653        self.direction_up = true;
2654        self.prev_gate = 0.0;
2655        self.prev_clock = 0.0;
2656        self.prev_reset = 0.0;
2657        self.gate_out = 0.0;
2658        self.trigger_countdown = 0;
2659    }
2660
2661    fn set_sample_rate(&mut self, sample_rate: f64) {
2662        self.sample_rate = sample_rate;
2663    }
2664
2665    fn type_id(&self) -> &'static str {
2666        "arpeggiator"
2667    }
2668}
2669
2670// =============================================================================
2671// Reverb - Algorithmic Reverb (Freeverb Style)
2672// =============================================================================
2673
2674#[cfg(test)]
2675mod tests {
2676    use super::*;
2677    use crate::modules::common::SAFE_AUDIO_LIMIT;
2678
2679    #[test]
2680    fn test_mixer() {
2681        let mut mixer = Mixer::new(4);
2682        let mut inputs = PortValues::new();
2683        let mut outputs = PortValues::new();
2684
2685        inputs.set(0, 1.0);
2686        inputs.set(1, 2.0);
2687        inputs.set(2, 3.0);
2688        inputs.set(3, 4.0);
2689
2690        mixer.tick(&inputs, &mut outputs);
2691
2692        let out = outputs.get(100).unwrap();
2693        assert!((out - 10.0).abs() < 0.01);
2694    }
2695    #[test]
2696    fn test_step_sequencer() {
2697        let mut seq = StepSequencer::new();
2698        seq.set_step(0, 0.0, true);
2699        seq.set_step(1, 0.5, true);
2700        seq.set_step(2, 1.0, true);
2701
2702        let mut inputs = PortValues::new();
2703        let mut outputs = PortValues::new();
2704
2705        // Initial state
2706        seq.tick(&inputs, &mut outputs);
2707        assert!((outputs.get(10).unwrap() - 0.0).abs() < 0.01);
2708
2709        // Clock rising edge
2710        inputs.set(0, 5.0);
2711        seq.tick(&inputs, &mut outputs);
2712        assert!((outputs.get(10).unwrap() - 0.5).abs() < 0.01);
2713
2714        // Clock falling edge, then rising again
2715        inputs.set(0, 0.0);
2716        seq.tick(&inputs, &mut outputs);
2717        inputs.set(0, 5.0);
2718        seq.tick(&inputs, &mut outputs);
2719        assert!((outputs.get(10).unwrap() - 1.0).abs() < 0.01);
2720    }
2721    #[test]
2722    fn test_sample_and_hold() {
2723        let mut sh = SampleAndHold::new();
2724        let mut inputs = PortValues::new();
2725        let mut outputs = PortValues::new();
2726
2727        // Set input value, no trigger
2728        inputs.set(0, 3.0);
2729        inputs.set(1, 0.0);
2730        sh.tick(&inputs, &mut outputs);
2731        // Initial held value should be 0
2732        assert!((outputs.get(10).unwrap() - 0.0).abs() < 0.01);
2733
2734        // Trigger rising edge - should sample input
2735        inputs.set(1, 5.0);
2736        sh.tick(&inputs, &mut outputs);
2737        assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01);
2738
2739        // Change input, but no new trigger - should hold previous value
2740        inputs.set(0, 7.0);
2741        sh.tick(&inputs, &mut outputs);
2742        assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01);
2743
2744        // New trigger - should sample new value
2745        inputs.set(1, 0.0);
2746        sh.tick(&inputs, &mut outputs);
2747        inputs.set(1, 5.0);
2748        sh.tick(&inputs, &mut outputs);
2749        assert!((outputs.get(10).unwrap() - 7.0).abs() < 0.01);
2750    }
2751    #[test]
2752    fn test_slew_limiter() {
2753        let mut slew = SlewLimiter::new(1000.0); // 1kHz sample rate
2754        let mut inputs = PortValues::new();
2755        let mut outputs = PortValues::new();
2756
2757        // Set rise/fall rates (normalized 0-1)
2758        inputs.set(1, 0.5); // Rise rate
2759        inputs.set(2, 0.5); // Fall rate
2760
2761        // Step input from 0 to 5V
2762        inputs.set(0, 5.0);
2763        slew.tick(&inputs, &mut outputs);
2764        let first = outputs.get(10).unwrap();
2765
2766        // Should start rising but not instantly reach target
2767        assert!(first > 0.0);
2768        assert!(first < 5.0);
2769
2770        // Continue rising
2771        for _ in 0..100 {
2772            slew.tick(&inputs, &mut outputs);
2773        }
2774        // Should be close to target now
2775        let after_100 = outputs.get(10).unwrap();
2776        assert!(after_100 > first);
2777    }
2778    #[test]
2779    fn test_quantizer_chromatic() {
2780        let mut quant = Quantizer::new(Scale::Chromatic);
2781        let mut inputs = PortValues::new();
2782        let mut outputs = PortValues::new();
2783
2784        // Exactly on a note
2785        inputs.set(0, 0.0); // C
2786        quant.tick(&inputs, &mut outputs);
2787        assert!((outputs.get(10).unwrap() - 0.0).abs() < 0.01);
2788
2789        // Between C and C#
2790        inputs.set(0, 0.04); // 1/25 of a semitone above C
2791        quant.tick(&inputs, &mut outputs);
2792        // Should quantize to C (0.0)
2793        assert!((outputs.get(10).unwrap() - 0.0).abs() < 0.01);
2794
2795        // Closer to C#
2796        inputs.set(0, 0.07);
2797        quant.tick(&inputs, &mut outputs);
2798        // Should quantize to C# (1/12 = 0.0833...)
2799        let expected_csharp = 1.0 / 12.0;
2800        assert!((outputs.get(10).unwrap() - expected_csharp).abs() < 0.01);
2801    }
2802    #[test]
2803    fn test_quantizer_major_scale() {
2804        let mut quant = Quantizer::new(Scale::Major);
2805        let mut inputs = PortValues::new();
2806        let mut outputs = PortValues::new();
2807
2808        // C# (1 semitone) should snap to C or D
2809        inputs.set(0, 1.0 / 12.0); // C#
2810        quant.tick(&inputs, &mut outputs);
2811        let out = outputs.get(10).unwrap();
2812        // Should be C (0) or D (2/12)
2813        assert!(out.abs() < 0.01 || (out - 2.0 / 12.0).abs() < 0.01);
2814    }
2815    #[test]
2816    fn test_clock() {
2817        let mut clock = Clock::new(1000.0); // 1kHz sample rate
2818        let mut inputs = PortValues::new();
2819        let mut outputs = PortValues::new();
2820
2821        // Set tempo CV: 10V maps to 300 BPM (5 Hz), so 200 samples per beat
2822        inputs.set(0, 10.0); // Maximum tempo
2823
2824        let mut trigger_count = 0;
2825        let mut last_trigger = 0.0;
2826
2827        for _ in 0..1000 {
2828            clock.tick(&inputs, &mut outputs);
2829            let trigger = outputs.get(10).unwrap(); // Main clock output
2830            if trigger > 2.5 && last_trigger <= 2.5 {
2831                trigger_count += 1;
2832            }
2833            last_trigger = trigger;
2834        }
2835
2836        // At 300 BPM (5 Hz), should get ~5 triggers per second
2837        // In 1000 samples at 1kHz, that's 5 triggers
2838        assert!(trigger_count >= 3);
2839    }
2840    #[test]
2841    fn test_attenuverter() {
2842        let mut att = Attenuverter::new();
2843        let mut inputs = PortValues::new();
2844        let mut outputs = PortValues::new();
2845
2846        // Test unity gain (5V = unity in 0-10V range)
2847        inputs.set(0, 5.0); // Input
2848        inputs.set(1, 5.0); // 5V = unity (1.0 multiplier)
2849        att.tick(&inputs, &mut outputs);
2850        assert!((outputs.get(10).unwrap() - 5.0).abs() < 0.1);
2851
2852        // Test half attenuation (2.5V = 0.5 multiplier)
2853        inputs.set(1, 2.5);
2854        att.tick(&inputs, &mut outputs);
2855        assert!((outputs.get(10).unwrap() - 2.5).abs() < 0.1);
2856
2857        // Test zero (0V = 0 multiplier)
2858        inputs.set(1, 0.0);
2859        att.tick(&inputs, &mut outputs);
2860        assert!((outputs.get(10).unwrap() - 0.0).abs() < 0.1);
2861    }
2862    #[test]
2863    fn test_multiple() {
2864        let mut mult = Multiple::new();
2865        let mut inputs = PortValues::new();
2866        let mut outputs = PortValues::new();
2867
2868        inputs.set(0, 3.5);
2869        mult.tick(&inputs, &mut outputs);
2870
2871        // All 4 outputs should have the same value
2872        assert!((outputs.get(10).unwrap() - 3.5).abs() < 0.0001);
2873        assert!((outputs.get(11).unwrap() - 3.5).abs() < 0.0001);
2874        assert!((outputs.get(12).unwrap() - 3.5).abs() < 0.0001);
2875        assert!((outputs.get(13).unwrap() - 3.5).abs() < 0.0001);
2876    }
2877    #[test]
2878    fn test_crossfader() {
2879        let mut xf = Crossfader::new();
2880        let mut inputs = PortValues::new();
2881        let mut outputs = PortValues::new();
2882
2883        inputs.set(0, 5.0); // A
2884        inputs.set(1, -5.0); // B
2885
2886        // Full A (pos = -5V)
2887        inputs.set(2, -5.0);
2888        xf.tick(&inputs, &mut outputs);
2889        assert!((outputs.get(10).unwrap() - 5.0).abs() < 0.1);
2890
2891        // Full B (pos = +5V)
2892        inputs.set(2, 5.0);
2893        xf.tick(&inputs, &mut outputs);
2894        assert!((outputs.get(10).unwrap() - (-5.0)).abs() < 0.1);
2895
2896        // Center (pos = 0V): equal mix
2897        inputs.set(2, 0.0);
2898        xf.tick(&inputs, &mut outputs);
2899        // Equal power mix at center
2900        let out = outputs.get(10).unwrap();
2901        assert!(out.abs() < 1.0); // Should be near zero (equal mix of +5 and -5)
2902    }
2903    #[test]
2904    fn test_logic_and() {
2905        let mut gate = LogicAnd::new();
2906        let mut inputs = PortValues::new();
2907        let mut outputs = PortValues::new();
2908
2909        // Both low
2910        inputs.set(0, 0.0);
2911        inputs.set(1, 0.0);
2912        gate.tick(&inputs, &mut outputs);
2913        assert!(outputs.get(10).unwrap() < 2.5);
2914
2915        // One high
2916        inputs.set(0, 5.0);
2917        inputs.set(1, 0.0);
2918        gate.tick(&inputs, &mut outputs);
2919        assert!(outputs.get(10).unwrap() < 2.5);
2920
2921        // Both high
2922        inputs.set(0, 5.0);
2923        inputs.set(1, 5.0);
2924        gate.tick(&inputs, &mut outputs);
2925        assert!(outputs.get(10).unwrap() > 2.5);
2926    }
2927    #[test]
2928    fn test_logic_or() {
2929        let mut gate = LogicOr::new();
2930        let mut inputs = PortValues::new();
2931        let mut outputs = PortValues::new();
2932
2933        // Both low
2934        inputs.set(0, 0.0);
2935        inputs.set(1, 0.0);
2936        gate.tick(&inputs, &mut outputs);
2937        assert!(outputs.get(10).unwrap() < 2.5);
2938
2939        // One high
2940        inputs.set(0, 5.0);
2941        inputs.set(1, 0.0);
2942        gate.tick(&inputs, &mut outputs);
2943        assert!(outputs.get(10).unwrap() > 2.5);
2944
2945        // Both high
2946        inputs.set(0, 5.0);
2947        inputs.set(1, 5.0);
2948        gate.tick(&inputs, &mut outputs);
2949        assert!(outputs.get(10).unwrap() > 2.5);
2950    }
2951    #[test]
2952    fn test_logic_xor() {
2953        let mut gate = LogicXor::new();
2954        let mut inputs = PortValues::new();
2955        let mut outputs = PortValues::new();
2956
2957        // Both low
2958        inputs.set(0, 0.0);
2959        inputs.set(1, 0.0);
2960        gate.tick(&inputs, &mut outputs);
2961        assert!(outputs.get(10).unwrap() < 2.5);
2962
2963        // One high
2964        inputs.set(0, 5.0);
2965        inputs.set(1, 0.0);
2966        gate.tick(&inputs, &mut outputs);
2967        assert!(outputs.get(10).unwrap() > 2.5);
2968
2969        // Both high
2970        inputs.set(0, 5.0);
2971        inputs.set(1, 5.0);
2972        gate.tick(&inputs, &mut outputs);
2973        assert!(outputs.get(10).unwrap() < 2.5);
2974    }
2975    #[test]
2976    fn test_logic_not() {
2977        let mut gate = LogicNot::new();
2978        let mut inputs = PortValues::new();
2979        let mut outputs = PortValues::new();
2980
2981        // Low input
2982        inputs.set(0, 0.0);
2983        gate.tick(&inputs, &mut outputs);
2984        assert!(outputs.get(10).unwrap() > 2.5);
2985
2986        // High input
2987        inputs.set(0, 5.0);
2988        gate.tick(&inputs, &mut outputs);
2989        assert!(outputs.get(10).unwrap() < 2.5);
2990    }
2991    #[test]
2992    fn test_comparator() {
2993        let mut cmp = Comparator::new();
2994        let mut inputs = PortValues::new();
2995        let mut outputs = PortValues::new();
2996
2997        // A > B
2998        inputs.set(0, 3.0);
2999        inputs.set(1, 1.0);
3000        cmp.tick(&inputs, &mut outputs);
3001        assert!(outputs.get(10).unwrap() > 2.5); // gt
3002        assert!(outputs.get(11).unwrap() < 2.5); // lt
3003        assert!(outputs.get(12).unwrap() < 2.5); // eq
3004
3005        // A < B
3006        inputs.set(0, 1.0);
3007        inputs.set(1, 3.0);
3008        cmp.tick(&inputs, &mut outputs);
3009        assert!(outputs.get(10).unwrap() < 2.5); // gt
3010        assert!(outputs.get(11).unwrap() > 2.5); // lt
3011        assert!(outputs.get(12).unwrap() < 2.5); // eq
3012
3013        // A ≈ B
3014        inputs.set(0, 2.0);
3015        inputs.set(1, 2.0);
3016        cmp.tick(&inputs, &mut outputs);
3017        assert!(outputs.get(10).unwrap() < 2.5); // gt
3018        assert!(outputs.get(11).unwrap() < 2.5); // lt
3019        assert!(outputs.get(12).unwrap() > 2.5); // eq
3020    }
3021    #[test]
3022    fn test_rectifier() {
3023        let mut rect = Rectifier::new();
3024        let mut inputs = PortValues::new();
3025        let mut outputs = PortValues::new();
3026
3027        // Positive input
3028        inputs.set(0, 3.0);
3029        rect.tick(&inputs, &mut outputs);
3030        assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01); // full
3031        assert!((outputs.get(11).unwrap() - 3.0).abs() < 0.01); // half_pos
3032        assert!((outputs.get(12).unwrap()).abs() < 0.01); // half_neg
3033
3034        // Negative input
3035        inputs.set(0, -3.0);
3036        rect.tick(&inputs, &mut outputs);
3037        assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01); // full (abs)
3038        assert!((outputs.get(11).unwrap()).abs() < 0.01); // half_pos
3039        assert!((outputs.get(12).unwrap() - 3.0).abs() < 0.01); // half_neg (inverted)
3040    }
3041    #[test]
3042    fn test_precision_adder() {
3043        let mut adder = PrecisionAdder::new();
3044        let mut inputs = PortValues::new();
3045        let mut outputs = PortValues::new();
3046
3047        inputs.set(0, 1.0);
3048        inputs.set(1, 2.0);
3049        inputs.set(2, 0.5);
3050        inputs.set(3, -0.5);
3051        adder.tick(&inputs, &mut outputs);
3052
3053        assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01); // sum
3054        assert!((outputs.get(11).unwrap() - (-3.0)).abs() < 0.01); // inverted
3055    }
3056    #[test]
3057    fn test_vc_switch() {
3058        let mut sw = VcSwitch::new();
3059        let mut inputs = PortValues::new();
3060        let mut outputs = PortValues::new();
3061
3062        inputs.set(0, 3.0); // A
3063        inputs.set(1, 7.0); // B
3064
3065        // CV low: select A
3066        inputs.set(2, 0.0);
3067        sw.tick(&inputs, &mut outputs);
3068        assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01);
3069        assert!((outputs.get(11).unwrap() - 3.0).abs() < 0.01);
3070        assert!((outputs.get(12).unwrap()).abs() < 0.01);
3071
3072        // CV high: select B
3073        inputs.set(2, 5.0);
3074        sw.tick(&inputs, &mut outputs);
3075        assert!((outputs.get(10).unwrap() - 7.0).abs() < 0.01);
3076        assert!((outputs.get(11).unwrap()).abs() < 0.01);
3077        assert!((outputs.get(12).unwrap() - 7.0).abs() < 0.01);
3078    }
3079    #[test]
3080    fn test_bernoulli_gate() {
3081        let mut bg = BernoulliGate::new();
3082        let mut inputs = PortValues::new();
3083        let mut outputs = PortValues::new();
3084
3085        // Set probability to 100%
3086        inputs.set(1, 10.0);
3087
3088        // Trigger rising edge
3089        inputs.set(0, 0.0);
3090        bg.tick(&inputs, &mut outputs);
3091        inputs.set(0, 5.0);
3092        bg.tick(&inputs, &mut outputs);
3093
3094        // At 100% prob, should always go to A
3095        assert!(outputs.get(10).unwrap() > 2.5); // trig_a
3096        assert!(outputs.get(11).unwrap() < 2.5); // trig_b
3097
3098        // Reset and test 0% probability
3099        bg.reset();
3100        inputs.set(1, 0.0);
3101        inputs.set(0, 0.0);
3102        bg.tick(&inputs, &mut outputs);
3103        inputs.set(0, 5.0);
3104        bg.tick(&inputs, &mut outputs);
3105
3106        // At 0% prob, should always go to B
3107        assert!(outputs.get(10).unwrap() < 2.5); // trig_a
3108        assert!(outputs.get(11).unwrap() > 2.5); // trig_b
3109    }
3110    #[test]
3111    fn test_min() {
3112        let mut m = Min::new();
3113        let mut inputs = PortValues::new();
3114        let mut outputs = PortValues::new();
3115
3116        inputs.set(0, 3.0);
3117        inputs.set(1, 5.0);
3118        m.tick(&inputs, &mut outputs);
3119        assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01);
3120
3121        inputs.set(0, 7.0);
3122        inputs.set(1, 2.0);
3123        m.tick(&inputs, &mut outputs);
3124        assert!((outputs.get(10).unwrap() - 2.0).abs() < 0.01);
3125    }
3126    #[test]
3127    fn test_max() {
3128        let mut m = Max::new();
3129        let mut inputs = PortValues::new();
3130        let mut outputs = PortValues::new();
3131
3132        inputs.set(0, 3.0);
3133        inputs.set(1, 5.0);
3134        m.tick(&inputs, &mut outputs);
3135        assert!((outputs.get(10).unwrap() - 5.0).abs() < 0.01);
3136
3137        inputs.set(0, 7.0);
3138        inputs.set(1, 2.0);
3139        m.tick(&inputs, &mut outputs);
3140        assert!((outputs.get(10).unwrap() - 7.0).abs() < 0.01);
3141    }
3142    #[test]
3143    fn test_mixer_default_reset_sample_rate() {
3144        let mut mixer = Mixer::default();
3145        mixer.reset();
3146        mixer.set_sample_rate(48000.0);
3147        assert_eq!(mixer.type_id(), "mixer");
3148    }
3149    #[test]
3150    fn test_stereo_output_default_reset_sample_rate() {
3151        let mut stereo = StereoOutput::default();
3152        stereo.reset();
3153        stereo.set_sample_rate(48000.0);
3154        assert_eq!(stereo.type_id(), "stereo_output");
3155    }
3156    #[test]
3157    fn test_offset_default_reset_sample_rate() {
3158        let mut offset = Offset::default();
3159        offset.reset();
3160        offset.set_sample_rate(48000.0);
3161        assert_eq!(offset.type_id(), "offset");
3162    }
3163    #[test]
3164    fn test_scale_enum_semitones() {
3165        let scale = Scale::Chromatic;
3166        assert!(scale.semitones().len() == 12);
3167
3168        let scale = Scale::Major;
3169        assert!(scale.semitones().len() == 7);
3170
3171        let scale = Scale::PentatonicMajor;
3172        assert!(scale.semitones().len() == 5);
3173    }
3174    #[test]
3175    fn test_step_sequencer_default_reset_sample_rate() {
3176        let mut seq = StepSequencer::default();
3177        seq.set_step(0, 1.0, true);
3178        let mut inputs = PortValues::new();
3179        let mut outputs = PortValues::new();
3180        inputs.set(0, 5.0);
3181        seq.tick(&inputs, &mut outputs);
3182
3183        seq.reset();
3184        assert!(seq.current == 0);
3185        assert!(seq.prev_clock == 0.0);
3186
3187        seq.set_sample_rate(48000.0);
3188        assert_eq!(seq.type_id(), "step_sequencer");
3189    }
3190    #[test]
3191    fn test_sample_and_hold_default_reset_sample_rate() {
3192        let mut sh = SampleAndHold::default();
3193        let mut inputs = PortValues::new();
3194        let mut outputs = PortValues::new();
3195        inputs.set(0, 5.0);
3196        inputs.set(1, 5.0);
3197        sh.tick(&inputs, &mut outputs);
3198
3199        sh.reset();
3200        assert!(sh.held_value == 0.0);
3201
3202        sh.set_sample_rate(48000.0);
3203        assert_eq!(sh.type_id(), "sample_hold");
3204    }
3205    #[test]
3206    fn test_slew_limiter_default_reset_sample_rate() {
3207        let mut slew = SlewLimiter::default();
3208        assert!(slew.sample_rate == 44100.0);
3209
3210        let mut inputs = PortValues::new();
3211        let mut outputs = PortValues::new();
3212        inputs.set(0, 5.0);
3213        slew.tick(&inputs, &mut outputs);
3214
3215        slew.reset();
3216        assert!(slew.current == 0.0);
3217
3218        slew.set_sample_rate(48000.0);
3219        assert!(slew.sample_rate == 48000.0);
3220
3221        assert_eq!(slew.type_id(), "slew_limiter");
3222    }
3223    #[test]
3224    fn test_quantizer_default_reset_sample_rate() {
3225        let mut quant = Quantizer::default();
3226        quant.reset();
3227        quant.set_sample_rate(48000.0);
3228        assert_eq!(quant.type_id(), "quantizer");
3229    }
3230    #[test]
3231    fn test_clock_default_reset_sample_rate() {
3232        let mut clock = Clock::default();
3233        assert!(clock.sample_rate == 44100.0);
3234
3235        let inputs = PortValues::new();
3236        let mut outputs = PortValues::new();
3237        for _ in 0..100 {
3238            clock.tick(&inputs, &mut outputs);
3239        }
3240
3241        clock.reset();
3242        assert!(clock.phase == 0.0);
3243
3244        clock.set_sample_rate(48000.0);
3245        assert!(clock.sample_rate == 48000.0);
3246
3247        assert_eq!(clock.type_id(), "clock");
3248    }
3249    #[test]
3250    fn test_attenuverter_default_reset_sample_rate() {
3251        let mut att = Attenuverter::default();
3252        att.reset();
3253        att.set_sample_rate(48000.0);
3254        assert_eq!(att.type_id(), "attenuverter");
3255    }
3256    #[test]
3257    fn test_multiple_default_reset_sample_rate() {
3258        let mut mult = Multiple::default();
3259        mult.reset();
3260        mult.set_sample_rate(48000.0);
3261        assert_eq!(mult.type_id(), "multiple");
3262    }
3263    #[test]
3264    fn test_crossfader_default_reset_sample_rate() {
3265        let mut xf = Crossfader::default();
3266        xf.reset();
3267        xf.set_sample_rate(48000.0);
3268        assert_eq!(xf.type_id(), "crossfader");
3269    }
3270    #[test]
3271    fn test_logic_and_default_reset_sample_rate() {
3272        let mut gate = LogicAnd::default();
3273        gate.reset();
3274        gate.set_sample_rate(48000.0);
3275        assert_eq!(gate.type_id(), "logic_and");
3276    }
3277    #[test]
3278    fn test_logic_or_default_reset_sample_rate() {
3279        let mut gate = LogicOr::default();
3280        gate.reset();
3281        gate.set_sample_rate(48000.0);
3282        assert_eq!(gate.type_id(), "logic_or");
3283    }
3284    #[test]
3285    fn test_logic_xor_default_reset_sample_rate() {
3286        let mut gate = LogicXor::default();
3287        gate.reset();
3288        gate.set_sample_rate(48000.0);
3289        assert_eq!(gate.type_id(), "logic_xor");
3290    }
3291    #[test]
3292    fn test_logic_not_default_reset_sample_rate() {
3293        let mut gate = LogicNot::default();
3294        gate.reset();
3295        gate.set_sample_rate(48000.0);
3296        assert_eq!(gate.type_id(), "logic_not");
3297    }
3298    #[test]
3299    fn test_comparator_default_reset_sample_rate() {
3300        let mut cmp = Comparator::default();
3301        cmp.reset();
3302        cmp.set_sample_rate(48000.0);
3303        assert_eq!(cmp.type_id(), "comparator");
3304    }
3305    #[test]
3306    fn test_rectifier_default_reset_sample_rate() {
3307        let mut rect = Rectifier::default();
3308        rect.reset();
3309        rect.set_sample_rate(48000.0);
3310        assert_eq!(rect.type_id(), "rectifier");
3311    }
3312    #[test]
3313    fn test_precision_adder_default_reset_sample_rate() {
3314        let mut adder = PrecisionAdder::default();
3315        adder.reset();
3316        adder.set_sample_rate(48000.0);
3317        assert_eq!(adder.type_id(), "precision_adder");
3318    }
3319    #[test]
3320    fn test_vc_switch_default_reset_sample_rate() {
3321        let mut sw = VcSwitch::default();
3322        sw.reset();
3323        sw.set_sample_rate(48000.0);
3324        assert_eq!(sw.type_id(), "vc_switch");
3325    }
3326    #[test]
3327    fn test_bernoulli_gate_default_reset_sample_rate() {
3328        let mut bg = BernoulliGate::default();
3329        let mut inputs = PortValues::new();
3330        let mut outputs = PortValues::new();
3331        inputs.set(0, 5.0);
3332        bg.tick(&inputs, &mut outputs);
3333
3334        bg.reset();
3335        assert!(bg.prev_trigger == 0.0);
3336
3337        bg.set_sample_rate(48000.0);
3338        assert_eq!(bg.type_id(), "bernoulli_gate");
3339    }
3340    #[test]
3341    fn test_min_default_reset_sample_rate() {
3342        let mut m = Min::default();
3343        m.reset();
3344        m.set_sample_rate(48000.0);
3345        assert_eq!(m.type_id(), "min");
3346    }
3347    #[test]
3348    fn test_max_default_reset_sample_rate() {
3349        let mut m = Max::default();
3350        m.reset();
3351        m.set_sample_rate(48000.0);
3352        assert_eq!(m.type_id(), "max");
3353    }
3354    #[test]
3355    fn test_step_sequencer_skip_disabled() {
3356        let mut seq = StepSequencer::new();
3357        seq.set_step(0, 1.0, true);
3358        seq.set_step(1, 2.0, false); // Disabled step
3359        seq.set_step(2, 3.0, true);
3360
3361        let mut inputs = PortValues::new();
3362        let mut outputs = PortValues::new();
3363
3364        // Initial step
3365        seq.tick(&inputs, &mut outputs);
3366        let _out = outputs.get(10).unwrap_or(0.0);
3367
3368        // Clock to next step
3369        inputs.set(0, 5.0);
3370        seq.tick(&inputs, &mut outputs);
3371    }
3372    #[test]
3373    fn test_quantizer_pentatonic_scale() {
3374        let mut quant = Quantizer::new(Scale::PentatonicMajor);
3375        let mut inputs = PortValues::new();
3376        let mut outputs = PortValues::new();
3377
3378        // Pentatonic scale has notes: 0, 2, 4, 7, 9 semitones
3379        inputs.set(0, 0.0);
3380        quant.tick(&inputs, &mut outputs);
3381        assert!(outputs.get(10).unwrap().abs() < 0.01);
3382    }
3383    #[test]
3384    fn test_quantizer_blues_scale() {
3385        let mut quant = Quantizer::new(Scale::Blues);
3386        let mut inputs = PortValues::new();
3387        let mut outputs = PortValues::new();
3388
3389        inputs.set(0, 0.0);
3390        quant.tick(&inputs, &mut outputs);
3391        assert!(outputs.get(10).is_some());
3392    }
3393    #[test]
3394    fn test_slew_limiter_falling() {
3395        let mut slew = SlewLimiter::new(1000.0);
3396        let mut inputs = PortValues::new();
3397        let mut outputs = PortValues::new();
3398
3399        // First, set to high value
3400        inputs.set(0, 5.0);
3401        inputs.set(1, 10.0); // Fast rise
3402        inputs.set(2, 0.5); // Slower fall
3403        for _ in 0..1000 {
3404            slew.tick(&inputs, &mut outputs);
3405        }
3406
3407        // Now set to low value and observe falling behavior
3408        inputs.set(0, 0.0);
3409        slew.tick(&inputs, &mut outputs);
3410        let falling = outputs.get(10).unwrap();
3411        assert!(falling < 5.0);
3412        assert!(falling > 0.0);
3413    }
3414    #[test]
3415    fn test_scale_dorian_and_mixolydian() {
3416        let scale = Scale::Dorian;
3417        assert!(scale.semitones().len() == 7);
3418
3419        let scale = Scale::Mixolydian;
3420        assert!(scale.semitones().len() == 7);
3421    }
3422    #[test]
3423    fn test_clock_subdivisions() {
3424        let mut clock = Clock::new(1000.0);
3425        let mut inputs = PortValues::new();
3426        let mut outputs = PortValues::new();
3427
3428        inputs.set(0, 5.0); // Medium tempo
3429
3430        // Run and check all outputs exist
3431        for _ in 0..1000 {
3432            clock.tick(&inputs, &mut outputs);
3433        }
3434
3435        // Should have all clock subdivision outputs
3436        assert!(outputs.get(10).is_some()); // Main
3437        assert!(outputs.get(11).is_some()); // /2
3438        assert!(outputs.get(12).is_some()); // /4
3439    }
3440    #[test]
3441    fn test_chord_memory_major() {
3442        let mut cm = ChordMemory::new();
3443        let mut inputs = PortValues::new();
3444        let mut outputs = PortValues::new();
3445
3446        // Root at C4 (0V), major chord (cv=0)
3447        inputs.set(0, 0.0);
3448        inputs.set(1, 0.0); // Major
3449        inputs.set(2, 0.0); // No inversion
3450        inputs.set(3, 0.0); // No spread
3451
3452        cm.tick(&inputs, &mut outputs);
3453
3454        // Major chord: root, major 3rd (+4 semitones), perfect 5th (+7 semitones)
3455        let voice1 = outputs.get(10).unwrap();
3456        let voice2 = outputs.get(11).unwrap();
3457        let voice3 = outputs.get(12).unwrap();
3458        let voice4 = outputs.get(13).unwrap();
3459
3460        assert!((voice1 - 0.0).abs() < 0.01); // Root (C)
3461        assert!((voice2 - 4.0 / 12.0).abs() < 0.01); // Major 3rd (E)
3462        assert!((voice3 - 7.0 / 12.0).abs() < 0.01); // Perfect 5th (G)
3463        assert!((voice4 - 1.0).abs() < 0.01); // Octave (for 3-note chord, voice4 = root+1)
3464    }
3465    #[test]
3466    fn test_chord_memory_minor() {
3467        let mut cm = ChordMemory::new();
3468        let mut inputs = PortValues::new();
3469        let mut outputs = PortValues::new();
3470
3471        inputs.set(0, 0.0);
3472        inputs.set(1, 0.15); // Minor (second chord type, cv ~0.111-0.222)
3473
3474        cm.tick(&inputs, &mut outputs);
3475
3476        // Minor chord: root, minor 3rd (+3 semitones), perfect 5th (+7 semitones)
3477        let voice2 = outputs.get(11).unwrap();
3478        assert!((voice2 - 3.0 / 12.0).abs() < 0.01); // Minor 3rd (Eb)
3479    }
3480    #[test]
3481    fn test_chord_memory_seventh() {
3482        let mut cm = ChordMemory::new();
3483        let mut inputs = PortValues::new();
3484        let mut outputs = PortValues::new();
3485
3486        inputs.set(0, 0.0);
3487        inputs.set(1, 0.26); // Dominant 7th (cv ~0.222-0.333)
3488
3489        cm.tick(&inputs, &mut outputs);
3490
3491        // Dom7 chord: root, major 3rd, perfect 5th, minor 7th (+10 semitones)
3492        let voice4 = outputs.get(13).unwrap();
3493        assert!((voice4 - 10.0 / 12.0).abs() < 0.01); // Minor 7th (Bb)
3494    }
3495    #[test]
3496    fn test_chord_memory_inversion() {
3497        let mut cm = ChordMemory::new();
3498        let mut inputs = PortValues::new();
3499        let mut outputs = PortValues::new();
3500
3501        inputs.set(0, 0.0);
3502        inputs.set(1, 0.0); // Major
3503        inputs.set(2, 0.4); // First inversion (for 3-note chord: ~1/3)
3504
3505        cm.tick(&inputs, &mut outputs);
3506
3507        // First inversion: E in bass, G, C (octave up)
3508        let voice1 = outputs.get(10).unwrap();
3509        let voice2 = outputs.get(11).unwrap();
3510        let voice3 = outputs.get(12).unwrap();
3511
3512        // Voice 1 should be the 3rd (4 semitones = major 3rd)
3513        assert!((voice1 - 4.0 / 12.0).abs() < 0.01);
3514        // Voice 2 should be the 5th (7 semitones)
3515        assert!((voice2 - 7.0 / 12.0).abs() < 0.01);
3516        // Voice 3 should be root + octave (wrapped)
3517        assert!((voice3 - 1.0).abs() < 0.01);
3518    }
3519    #[test]
3520    fn test_chord_memory_spread() {
3521        let mut cm = ChordMemory::new();
3522        let mut inputs = PortValues::new();
3523        let mut outputs = PortValues::new();
3524
3525        inputs.set(0, 0.0);
3526        inputs.set(1, 0.0); // Major
3527        inputs.set(2, 0.0); // No inversion
3528        inputs.set(3, 1.0); // Full spread
3529
3530        cm.tick(&inputs, &mut outputs);
3531
3532        let voice1 = outputs.get(10).unwrap();
3533        let voice2 = outputs.get(11).unwrap();
3534        let voice3 = outputs.get(12).unwrap();
3535        let voice4 = outputs.get(13).unwrap();
3536
3537        // With spread=1.0, voice4 should be ~1 octave higher than without spread
3538        // voice1: 0 + 0/3 = 0
3539        // voice2: 4/12 + 1/3 ≈ 0.666
3540        // voice3: 7/12 + 2/3 ≈ 1.25
3541        // voice4: 1.0 + 1.0 = 2.0 (for 3-note chord)
3542        assert!(voice1 < voice2);
3543        assert!(voice2 < voice3);
3544        assert!(voice3 < voice4);
3545    }
3546    #[test]
3547    fn test_chord_memory_all_chord_types() {
3548        let mut cm = ChordMemory::new();
3549        let mut inputs = PortValues::new();
3550        let mut outputs = PortValues::new();
3551
3552        // Test all 9 chord types produce valid output
3553        for i in 0..9 {
3554            let chord_cv = i as f64 / 9.0;
3555            inputs.set(0, 0.0);
3556            inputs.set(1, chord_cv);
3557
3558            cm.tick(&inputs, &mut outputs);
3559
3560            // All voices should have valid output
3561            assert!(outputs.get(10).is_some());
3562            assert!(outputs.get(11).is_some());
3563            assert!(outputs.get(12).is_some());
3564            assert!(outputs.get(13).is_some());
3565        }
3566    }
3567    #[test]
3568    fn test_chord_memory_default_reset_sample_rate() {
3569        let mut cm = ChordMemory::default();
3570        cm.reset();
3571        cm.set_sample_rate(48000.0);
3572        assert_eq!(cm.type_id(), "chord_memory");
3573
3574        // Verify port spec
3575        assert_eq!(cm.port_spec().inputs.len(), 4);
3576        assert_eq!(cm.port_spec().outputs.len(), 4);
3577    }
3578    #[test]
3579    fn test_chord_type_intervals() {
3580        // Test that all chord types return valid intervals
3581        assert_eq!(ChordType::Major.intervals(), &[0, 4, 7]);
3582        assert_eq!(ChordType::Minor.intervals(), &[0, 3, 7]);
3583        assert_eq!(ChordType::Seventh.intervals(), &[0, 4, 7, 10]);
3584        assert_eq!(ChordType::MajorSeventh.intervals(), &[0, 4, 7, 11]);
3585        assert_eq!(ChordType::MinorSeventh.intervals(), &[0, 3, 7, 10]);
3586        assert_eq!(ChordType::Diminished.intervals(), &[0, 3, 6]);
3587        assert_eq!(ChordType::Augmented.intervals(), &[0, 4, 8]);
3588        assert_eq!(ChordType::Sus2.intervals(), &[0, 2, 7]);
3589        assert_eq!(ChordType::Sus4.intervals(), &[0, 5, 7]);
3590    }
3591    #[test]
3592    fn test_chord_type_from_cv() {
3593        assert_eq!(ChordType::from_cv(0.0), ChordType::Major);
3594        assert_eq!(ChordType::from_cv(0.12), ChordType::Minor);
3595        assert_eq!(ChordType::from_cv(0.23), ChordType::Seventh);
3596        assert_eq!(ChordType::from_cv(1.0), ChordType::Sus4);
3597    }
3598    #[test]
3599    fn test_arp_pattern_from_cv() {
3600        assert_eq!(ArpPattern::from_cv(0.0), ArpPattern::Up);
3601        assert_eq!(ArpPattern::from_cv(0.1), ArpPattern::Up);
3602        assert_eq!(ArpPattern::from_cv(0.3), ArpPattern::Down);
3603        assert_eq!(ArpPattern::from_cv(0.6), ArpPattern::UpDown);
3604        assert_eq!(ArpPattern::from_cv(0.9), ArpPattern::Random);
3605        assert_eq!(ArpPattern::from_cv(1.0), ArpPattern::Random);
3606    }
3607    #[test]
3608    fn test_arpeggiator_default_reset_sample_rate() {
3609        let mut arp = Arpeggiator::default();
3610        assert_eq!(arp.sample_rate, 44100.0);
3611
3612        // Add a note
3613        arp.add_note(0.0);
3614        assert_eq!(arp.num_notes, 1);
3615
3616        // Reset should clear notes
3617        arp.reset();
3618        assert_eq!(arp.num_notes, 0);
3619        assert_eq!(arp.current_step, 0);
3620
3621        // Set sample rate
3622        arp.set_sample_rate(48000.0);
3623        assert_eq!(arp.sample_rate, 48000.0);
3624
3625        assert_eq!(arp.type_id(), "arpeggiator");
3626        assert_eq!(arp.port_spec().inputs.len(), 6);
3627        assert_eq!(arp.port_spec().outputs.len(), 3);
3628    }
3629    #[test]
3630    fn test_arpeggiator_add_remove_notes() {
3631        let mut arp = Arpeggiator::new(44100.0);
3632
3633        // Add notes
3634        arp.add_note(0.0); // C4
3635        arp.add_note(0.5); // F#4
3636        arp.add_note(0.25); // D#4
3637
3638        assert_eq!(arp.num_notes, 3);
3639        // Notes should be sorted
3640        assert_eq!(arp.held_notes[0], 0.0);
3641        assert_eq!(arp.held_notes[1], 0.25);
3642        assert_eq!(arp.held_notes[2], 0.5);
3643
3644        // Remove middle note
3645        arp.remove_note(0.25);
3646        assert_eq!(arp.num_notes, 2);
3647        assert_eq!(arp.held_notes[0], 0.0);
3648        assert_eq!(arp.held_notes[1], 0.5);
3649    }
3650    #[test]
3651    fn test_arpeggiator_up_pattern() {
3652        let mut arp = Arpeggiator::new(44100.0);
3653        let mut inputs = PortValues::new();
3654        let mut outputs = PortValues::new();
3655
3656        // Populate the held chord directly. (A single gate+pitch input can only
3657        // hold one note at a time now that releases remove notes — Q040 — so a
3658        // three-note chord is set up via add_note.)
3659        arp.add_note(0.0); // C4
3660        arp.add_note(0.333); // E4
3661        arp.add_note(0.583); // G4
3662        inputs.set(1, 0.0); // Gate low throughout
3663
3664        assert_eq!(arp.num_notes, 3);
3665
3666        // Send clock pulses and check output
3667        inputs.set(3, 0.0); // Up pattern
3668        let mut notes_out = Vec::new();
3669
3670        for _ in 0..6 {
3671            inputs.set(2, 5.0); // Clock high
3672            arp.tick(&inputs, &mut outputs);
3673            notes_out.push(outputs.get(10).unwrap());
3674
3675            inputs.set(2, 0.0); // Clock low
3676            arp.tick(&inputs, &mut outputs);
3677        }
3678
3679        // Should cycle through notes in ascending order
3680        assert!(notes_out[0] < notes_out[1]);
3681        assert!(notes_out[1] < notes_out[2]);
3682        // Then repeat
3683        assert!((notes_out[3] - notes_out[0]).abs() < 0.01);
3684    }
3685    #[test]
3686    fn test_arpeggiator_trigger_output() {
3687        let mut arp = Arpeggiator::new(44100.0);
3688        let mut inputs = PortValues::new();
3689        let mut outputs = PortValues::new();
3690
3691        // Add a note
3692        inputs.set(0, 0.0);
3693        inputs.set(1, 5.0);
3694        arp.tick(&inputs, &mut outputs);
3695
3696        // Clock pulse should produce trigger
3697        inputs.set(2, 5.0);
3698        arp.tick(&inputs, &mut outputs);
3699        let trigger = outputs.get(12).unwrap();
3700        assert!(trigger > 0.0, "Should output trigger on clock");
3701
3702        // Trigger should continue for a short time
3703        inputs.set(2, 0.0);
3704        arp.tick(&inputs, &mut outputs);
3705        let trigger2 = outputs.get(12).unwrap();
3706        assert!(trigger2 > 0.0, "Trigger should persist briefly");
3707    }
3708    #[test]
3709    fn test_arpeggiator_reset_input() {
3710        let mut arp = Arpeggiator::new(44100.0);
3711        let mut inputs = PortValues::new();
3712        let mut outputs = PortValues::new();
3713
3714        // Add notes and advance steps
3715        inputs.set(0, 0.0);
3716        inputs.set(1, 5.0);
3717        arp.tick(&inputs, &mut outputs);
3718
3719        for _ in 0..5 {
3720            inputs.set(2, 5.0);
3721            arp.tick(&inputs, &mut outputs);
3722            inputs.set(2, 0.0);
3723            arp.tick(&inputs, &mut outputs);
3724        }
3725
3726        let step_before = arp.current_step;
3727        assert!(step_before > 0);
3728
3729        // Send reset
3730        inputs.set(5, 5.0);
3731        arp.tick(&inputs, &mut outputs);
3732
3733        assert_eq!(arp.current_step, 0, "Reset should clear step");
3734    }
3735    #[test]
3736    fn test_arpeggiator_octaves() {
3737        let mut arp = Arpeggiator::new(44100.0);
3738
3739        // Add one note
3740        arp.add_note(0.0); // C4
3741
3742        // With 2 octaves, step 0 should give 0.0, step 1 should give 1.0 (octave higher)
3743        let note1 = arp.get_current_note(ArpPattern::Up, 2);
3744        arp.current_step = 1;
3745        let note2 = arp.get_current_note(ArpPattern::Up, 2);
3746
3747        assert!(
3748            (note2 - note1 - 1.0).abs() < 0.01,
3749            "Second note should be 1 octave higher"
3750        );
3751    }
3752    #[test]
3753    fn test_mixer_summation_bounded() {
3754        // Mixer should not produce unbounded output when summing multiple channels
3755        let mut mixer = Mixer::new(4);
3756        let mut inputs = PortValues::new();
3757        let mut outputs = PortValues::new();
3758
3759        // 4 channels at full scale
3760        for i in 0..4 {
3761            inputs.set(i as u32, 5.0);
3762        }
3763
3764        mixer.tick(&inputs, &mut outputs);
3765        let out = outputs.get(100).unwrap_or(0.0);
3766
3767        // Note: This test documents current behavior (20V output)
3768        // If mixer adds limiting, update this test
3769        assert!(
3770            out.abs() <= SAFE_AUDIO_LIMIT * 2.0,
3771            "Mixer output {} is very high - consider adding limiting",
3772            out
3773        );
3774    }
3775
3776    // ---- Q034: ScaleQuantizer octave-wrap ----
3777
3778    #[test]
3779    fn test_scale_quantizer_octave_wrap_minor_11() {
3780        // Semitone 11 (B above the root) must snap UP to 12, not drop to 0.
3781        assert_eq!(
3782            ScaleQuantizer::quantize_to_scale(11, &ScaleQuantizer::MINOR),
3783            12
3784        );
3785    }
3786
3787    #[test]
3788    fn test_scale_quantizer_monotonic_sweep() {
3789        // A chromatic sweep across two octaves must map to a monotonically
3790        // nondecreasing sequence for every scale — the old code dropped
3791        // top-of-octave notes ~an octave (non-monotonic).
3792        for scale in [
3793            &ScaleQuantizer::MINOR[..],
3794            &ScaleQuantizer::PENT_MAJOR[..],
3795            &ScaleQuantizer::BLUES[..],
3796        ] {
3797            let mut prev = i32::MIN;
3798            for note in 0..=24 {
3799                let q = ScaleQuantizer::quantize_to_scale(note, scale);
3800                assert!(
3801                    q >= prev,
3802                    "non-monotonic: note {} -> {} after {}",
3803                    note,
3804                    q,
3805                    prev
3806                );
3807                prev = q;
3808            }
3809        }
3810    }
3811
3812    // ---- Q041: quantizer / comparator hysteresis ----
3813
3814    #[test]
3815    fn test_quantizer_hysteresis_no_chatter() {
3816        // A slow ramp across two semitone boundaries with a tiny dither must
3817        // cross each boundary exactly once (two committed note changes), not
3818        // chatter every sample.
3819        let mut quant = Quantizer::new(Scale::Chromatic);
3820        let mut inputs = PortValues::new();
3821        let mut outputs = PortValues::new();
3822
3823        let n = 4000;
3824        let mut changes = 0;
3825        let mut prev: Option<f64> = None;
3826        for i in 0..=n {
3827            let base = 2.0 * i as f64 / n as f64; // semitones, 0..2
3828            let dither = if i % 2 == 0 { 0.1 } else { -0.1 };
3829            inputs.set(0, (base + dither) / 12.0);
3830            quant.tick(&inputs, &mut outputs);
3831            let out = outputs.get(10).unwrap();
3832            if let Some(p) = prev {
3833                if (out - p).abs() > 1e-9 {
3834                    changes += 1;
3835                }
3836            }
3837            prev = Some(out);
3838        }
3839        assert_eq!(changes, 2, "expected exactly two boundary crossings");
3840    }
3841
3842    #[test]
3843    fn test_scale_quantizer_trigger_once_per_boundary() {
3844        // The change-trigger must fire once per committed note change, not
3845        // continuously while quantization is active.
3846        let mut sq = ScaleQuantizer::new(44100.0);
3847        let mut inputs = PortValues::new();
3848        let mut outputs = PortValues::new();
3849        inputs.set(2, 0.0); // chromatic scale
3850
3851        let n = 4000;
3852        let mut triggers = 0;
3853        for i in 0..=n {
3854            let base = 2.0 * i as f64 / n as f64;
3855            let dither = if i % 2 == 0 { 0.1 } else { -0.1 };
3856            inputs.set(0, (base + dither) / 12.0);
3857            sq.tick(&inputs, &mut outputs);
3858            if outputs.get(11).unwrap() > 2.5 {
3859                triggers += 1;
3860            }
3861        }
3862        assert_eq!(triggers, 2, "trigger should fire once per boundary");
3863    }
3864
3865    #[test]
3866    fn test_comparator_hysteresis_no_chatter() {
3867        // A signal dithering around B (amplitude between the deadband and the
3868        // hysteresis margin) must not toggle the outputs.
3869        let mut cmp = Comparator::new();
3870        let mut inputs = PortValues::new();
3871        let mut outputs = PortValues::new();
3872        inputs.set(1, 0.0); // B = 0
3873
3874        // Establish equality first.
3875        inputs.set(0, 0.0);
3876        cmp.tick(&inputs, &mut outputs);
3877
3878        let mut gt_high = 0;
3879        let mut lt_high = 0;
3880        for i in 0..200 {
3881            let a = if i % 2 == 0 { 0.02 } else { -0.02 };
3882            inputs.set(0, a);
3883            cmp.tick(&inputs, &mut outputs);
3884            if outputs.get(10).unwrap() > 2.5 {
3885                gt_high += 1;
3886            }
3887            if outputs.get(11).unwrap() > 2.5 {
3888                lt_high += 1;
3889            }
3890            assert!(outputs.get(12).unwrap() > 2.5, "should stay equal");
3891        }
3892        assert_eq!(gt_high, 0, "gt should never fire on sub-band dither");
3893        assert_eq!(lt_high, 0, "lt should never fire on sub-band dither");
3894    }
3895
3896    #[test]
3897    fn test_comparator_still_compares() {
3898        // Decisive inputs still resolve correctly through the hysteresis.
3899        let mut cmp = Comparator::new();
3900        let mut inputs = PortValues::new();
3901        let mut outputs = PortValues::new();
3902
3903        inputs.set(0, 3.0);
3904        inputs.set(1, 1.0);
3905        cmp.tick(&inputs, &mut outputs);
3906        assert!(outputs.get(10).unwrap() > 2.5);
3907
3908        inputs.set(0, 1.0);
3909        inputs.set(1, 3.0);
3910        cmp.tick(&inputs, &mut outputs);
3911        assert!(outputs.get(11).unwrap() > 2.5);
3912
3913        inputs.set(0, 2.0);
3914        inputs.set(1, 2.0);
3915        cmp.tick(&inputs, &mut outputs);
3916        assert!(outputs.get(12).unwrap() > 2.5);
3917    }
3918
3919    // ---- Q035 / Q038: Clock divided outputs and default tempo ----
3920
3921    #[test]
3922    fn test_clock_divided_outputs() {
3923        // Over N main cycles, div2 pulses N/2 times and div4 pulses N/4 times.
3924        let mut clock = Clock::new(1000.0);
3925        let mut inputs = PortValues::new();
3926        let mut outputs = PortValues::new();
3927        inputs.set(0, 5.0); // medium tempo
3928
3929        let (mut main_c, mut div2_c, mut div4_c) = (0, 0, 0);
3930        let (mut pm, mut p2, mut p4) = (0.0, 0.0, 0.0);
3931        for _ in 0..100_000 {
3932            clock.tick(&inputs, &mut outputs);
3933            let m = outputs.get(10).unwrap();
3934            let d2 = outputs.get(11).unwrap();
3935            let d4 = outputs.get(12).unwrap();
3936            let m_rise = m > 2.5 && pm <= 2.5;
3937            if m_rise {
3938                main_c += 1;
3939            }
3940            if d2 > 2.5 && p2 <= 2.5 {
3941                div2_c += 1;
3942            }
3943            if d4 > 2.5 && p4 <= 2.5 {
3944                div4_c += 1;
3945            }
3946            pm = m;
3947            p2 = d2;
3948            p4 = d4;
3949            if m_rise && main_c == 8 {
3950                break;
3951            }
3952        }
3953        assert_eq!(main_c, 8, "main should pulse every cycle");
3954        assert_eq!(div2_c, 4, "div2 should pulse at half rate");
3955        assert_eq!(div4_c, 2, "div4 should pulse at quarter rate");
3956    }
3957
3958    #[test]
3959    fn test_clock_default_tempo_120_bpm() {
3960        // With no bpm input, the port default must yield ~120 BPM.
3961        let mut clock = Clock::default();
3962        let inputs = PortValues::new(); // empty -> use port default
3963        let mut outputs = PortValues::new();
3964
3965        let mut edges = Vec::new();
3966        let mut prev = 0.0;
3967        for i in 0..100_000 {
3968            clock.tick(&inputs, &mut outputs);
3969            let m = outputs.get(10).unwrap();
3970            if m > 2.5 && prev <= 2.5 {
3971                edges.push(i);
3972            }
3973            prev = m;
3974            if edges.len() >= 2 {
3975                break;
3976            }
3977        }
3978        assert!(edges.len() >= 2, "expected at least two clock pulses");
3979        let period = (edges[1] - edges[0]) as f64;
3980        let bpm = 60.0 * 44100.0 / period;
3981        assert!(
3982            (bpm - 120.0).abs() < 1.0,
3983            "default tempo {} BPM should be ~120",
3984            bpm
3985        );
3986    }
3987
3988    // ---- Q036: BernoulliGate latched gates ----
3989
3990    #[test]
3991    fn test_bernoulli_gate_latches() {
3992        let mut bg = BernoulliGate::new();
3993        let mut inputs = PortValues::new();
3994
3995        // Deterministic route to A: 100% probability.
3996        inputs.set(1, 10.0);
3997
3998        // A fresh output buffer every tick (as the engine provides) — the latch
3999        // must survive without reading back the output buffer.
4000        let tick = |bg: &mut BernoulliGate, inputs: &PortValues| {
4001            let mut o = PortValues::new();
4002            bg.tick(inputs, &mut o);
4003            o
4004        };
4005
4006        inputs.set(0, 0.0);
4007        tick(&mut bg, &inputs);
4008        inputs.set(0, 5.0);
4009        let o = tick(&mut bg, &inputs); // rising edge -> A
4010        assert!(o.get(12).unwrap() > 2.5, "gate_a should latch high");
4011        assert!(o.get(13).unwrap() < 2.5);
4012
4013        // Hold across many non-trigger ticks (gate low, no rising edge).
4014        inputs.set(0, 0.0);
4015        for _ in 0..20 {
4016            let o = tick(&mut bg, &inputs);
4017            assert!(o.get(12).unwrap() > 2.5, "gate_a must stay latched");
4018            assert!(o.get(13).unwrap() < 2.5);
4019        }
4020
4021        // Now route to B: 0% probability, new rising edge.
4022        inputs.set(1, 0.0);
4023        inputs.set(0, 0.0);
4024        tick(&mut bg, &inputs);
4025        inputs.set(0, 5.0);
4026        let o = tick(&mut bg, &inputs); // rising edge -> B
4027        assert!(o.get(12).unwrap() < 2.5, "gate_a should release");
4028        assert!(o.get(13).unwrap() > 2.5, "gate_b should latch high");
4029    }
4030
4031    // ---- Q037 / Q042 / Q129: Euclidean ----
4032
4033    #[test]
4034    fn test_euclidean_pulses_control_live() {
4035        // Changing pulses at a constant step count must change the pattern.
4036        let mut euc = Euclidean::new(44100.0);
4037        let mut inputs = PortValues::new();
4038        let mut outputs = PortValues::new();
4039        inputs.set(1, 0.5); // steps -> 9
4040
4041        inputs.set(2, 0.25); // pulses -> 2
4042        euc.tick(&inputs, &mut outputs);
4043        let active_low = euc.pattern.iter().filter(|&&x| x).count();
4044        assert_eq!(active_low, 2);
4045
4046        inputs.set(2, 0.75); // pulses -> 6
4047        euc.tick(&inputs, &mut outputs);
4048        let active_high = euc.pattern.iter().filter(|&&x| x).count();
4049        assert_eq!(active_high, 6);
4050        assert_ne!(active_low, active_high, "pulses control must be live");
4051    }
4052
4053    #[test]
4054    fn test_euclidean_accent_on_rotated_pulse() {
4055        // With rotation, the accent must fire exactly once per cycle and always
4056        // coincide with an actual pulse.
4057        let mut euc = Euclidean::new(44100.0);
4058        let mut inputs = PortValues::new();
4059        let mut outputs = PortValues::new();
4060        inputs.set(1, 0.4003); // steps -> 8
4061        inputs.set(2, 0.5); // pulses -> 4
4062        inputs.set(3, 0.3); // rotation -> 2
4063
4064        let mut accents = 0;
4065        for _ in 0..8 {
4066            inputs.set(0, 5.0); // clock high (rising)
4067            euc.tick(&inputs, &mut outputs);
4068            let out = outputs.get(10).unwrap();
4069            let accent = outputs.get(11).unwrap();
4070            if accent > 2.5 {
4071                accents += 1;
4072                assert!(out > 2.5, "accent must coincide with a pulse");
4073            }
4074            inputs.set(0, 0.0); // clock low
4075            euc.tick(&inputs, &mut outputs);
4076        }
4077        assert_eq!(accents, 1, "exactly one accent per cycle");
4078    }
4079
4080    #[test]
4081    fn test_euclidean_gate_threshold() {
4082        // Clock pulses below the canonical 2.5V threshold must be ignored;
4083        // 5V pulses must produce output (Q129).
4084        let mut euc = Euclidean::new(44100.0);
4085        let mut inputs = PortValues::new();
4086        let mut outputs = PortValues::new();
4087        inputs.set(1, 0.4003); // steps -> 8
4088        inputs.set(2, 1.0); // all pulses active
4089
4090        // 1.0V clock: never crosses threshold -> no pulses.
4091        let mut low_pulses = 0;
4092        for _ in 0..8 {
4093            inputs.set(0, 1.0);
4094            euc.tick(&inputs, &mut outputs);
4095            if outputs.get(10).unwrap() > 2.5 {
4096                low_pulses += 1;
4097            }
4098            inputs.set(0, 0.0);
4099            euc.tick(&inputs, &mut outputs);
4100        }
4101        assert_eq!(low_pulses, 0, "1.0V clock must not trigger");
4102
4103        // 5V clock: produces pulses.
4104        let mut high_pulses = 0;
4105        for _ in 0..8 {
4106            inputs.set(0, 5.0);
4107            euc.tick(&inputs, &mut outputs);
4108            if outputs.get(10).unwrap() > 2.5 {
4109                high_pulses += 1;
4110            }
4111            inputs.set(0, 0.0);
4112            euc.tick(&inputs, &mut outputs);
4113        }
4114        assert!(high_pulses > 0, "5V clock must trigger");
4115    }
4116
4117    // ---- Q040: Arpeggiator note release ----
4118
4119    #[test]
4120    fn test_arpeggiator_releases_notes() {
4121        let mut arp = Arpeggiator::new(44100.0);
4122        let mut inputs = PortValues::new();
4123        let mut outputs = PortValues::new();
4124
4125        // Press a note (rising edge).
4126        inputs.set(0, 0.25);
4127        inputs.set(1, 5.0);
4128        arp.tick(&inputs, &mut outputs);
4129        assert_eq!(arp.num_notes, 1);
4130
4131        // Hold: still one note.
4132        for _ in 0..5 {
4133            arp.tick(&inputs, &mut outputs);
4134        }
4135        assert_eq!(arp.num_notes, 1);
4136
4137        // Release (falling edge) removes the note.
4138        inputs.set(1, 0.0);
4139        arp.tick(&inputs, &mut outputs);
4140        assert_eq!(arp.num_notes, 0, "release must remove the held note");
4141
4142        // Another press/release cycle keeps the count correct.
4143        inputs.set(0, 0.5);
4144        inputs.set(1, 5.0);
4145        arp.tick(&inputs, &mut outputs);
4146        assert_eq!(arp.num_notes, 1);
4147        inputs.set(1, 0.0);
4148        arp.tick(&inputs, &mut outputs);
4149        assert_eq!(arp.num_notes, 0);
4150    }
4151
4152    #[test]
4153    fn test_arpeggiator_reset_clears_held_notes() {
4154        let mut arp = Arpeggiator::new(44100.0);
4155        let mut inputs = PortValues::new();
4156        let mut outputs = PortValues::new();
4157
4158        // Hold a note (gate stays high).
4159        inputs.set(0, 0.0);
4160        inputs.set(1, 5.0);
4161        arp.tick(&inputs, &mut outputs);
4162        assert_eq!(arp.num_notes, 1);
4163
4164        // Reset input (rising edge) empties the held set.
4165        inputs.set(5, 5.0);
4166        arp.tick(&inputs, &mut outputs);
4167        assert_eq!(arp.num_notes, 0, "reset must clear held notes");
4168    }
4169
4170    // ================================================================
4171    // Q146: microtuning / custom scales on ScaleQuantizer
4172    // ================================================================
4173
4174    #[cfg(feature = "alloc")]
4175    #[test]
4176    fn test_custom_scale_snaps_to_degrees() {
4177        // Whole-tone scale: degrees every 200 cents.
4178        let mut sq = ScaleQuantizer::new(44100.0);
4179        assert!(!sq.has_custom_scale());
4180        sq.set_custom_scale(&[0.0, 200.0, 400.0, 600.0, 800.0, 1000.0]);
4181        assert!(sq.has_custom_scale());
4182
4183        let mut inputs = PortValues::new();
4184        let mut outputs = PortValues::new();
4185
4186        // Input just above the 200-cent degree (200 cents = 1/6 V) should snap to
4187        // it, not to a chromatic semitone.
4188        // 210 cents = 0.175 V.
4189        inputs.set(0, 0.175);
4190        // Run a few ticks so hysteresis commits.
4191        for _ in 0..4 {
4192            sq.tick(&inputs, &mut outputs);
4193        }
4194        let out_v = outputs.get(10).unwrap();
4195        // Expect ~200 cents = 0.16667 V.
4196        assert!(
4197            (out_v - 200.0 / 1200.0).abs() < 1e-6,
4198            "custom scale should snap to 200 cents, got {} cents",
4199            out_v * 1200.0
4200        );
4201    }
4202
4203    #[cfg(feature = "alloc")]
4204    #[test]
4205    fn test_load_scala_and_quantize() {
4206        let scl = "\
4207whole tone
42086
4209200.0
4210400.0
4211600.0
4212800.0
42131000.0
42141200.0
4215";
4216        let mut sq = ScaleQuantizer::new(44100.0);
4217        sq.load_scala(scl).unwrap();
4218        assert!(sq.has_custom_scale());
4219
4220        let mut inputs = PortValues::new();
4221        let mut outputs = PortValues::new();
4222        // 390 cents -> nearest whole-tone degree is 400 cents.
4223        inputs.set(0, 390.0 / 1200.0);
4224        for _ in 0..4 {
4225            sq.tick(&inputs, &mut outputs);
4226        }
4227        let out_v = outputs.get(10).unwrap();
4228        assert!(
4229            (out_v - 400.0 / 1200.0).abs() < 1e-6,
4230            "expected 400 cents, got {} cents",
4231            out_v * 1200.0
4232        );
4233    }
4234
4235    #[cfg(feature = "alloc")]
4236    #[test]
4237    fn test_clear_custom_scale_restores_enum() {
4238        let mut sq = ScaleQuantizer::new(44100.0);
4239        sq.set_custom_scale(&[0.0, 200.0, 400.0]);
4240        assert!(sq.has_custom_scale());
4241        sq.clear_custom_scale();
4242        assert!(!sq.has_custom_scale());
4243    }
4244
4245    #[cfg(feature = "alloc")]
4246    #[test]
4247    fn test_load_scala_malformed_leaves_scale_unchanged() {
4248        let mut sq = ScaleQuantizer::new(44100.0);
4249        sq.set_custom_scale(&[0.0, 500.0]);
4250        // Malformed: declares 3 pitches but supplies one.
4251        let err = sq.load_scala("bad\n3\n100.0\n");
4252        assert!(err.is_err());
4253        // Previous custom scale is retained.
4254        assert!(sq.has_custom_scale());
4255    }
4256
4257    // ---- Q161: Quantizer with negative V/Oct (notes below C4) ----
4258
4259    #[test]
4260    fn test_quantizer_negative_voct_chromatic() {
4261        // A fresh quantizer per input avoids boundary hysteresis carrying over.
4262        let cases = [
4263            (-0.5, -0.5),                 // exactly F#3 -> stays
4264            (-1.0, -1.0),                 // C3 -> octave floor, stays
4265            (-13.0 / 12.0, -13.0 / 12.0), // B2 -> negative octave, chromatic passthrough
4266            (-1.0 / 24.0, 0.0),           // half a semitone below C4 -> wraps UP to C4
4267        ];
4268        for (input, expected) in cases {
4269            let mut q = Quantizer::new(Scale::Chromatic);
4270            let mut inputs = PortValues::new();
4271            let mut outputs = PortValues::new();
4272            inputs.set(0, input);
4273            q.tick(&inputs, &mut outputs);
4274            let out = outputs.get(10).unwrap();
4275            assert!(
4276                (out - expected).abs() < 1e-9,
4277                "chromatic {input}V -> {out}V, expected {expected}V"
4278            );
4279        }
4280    }
4281
4282    #[test]
4283    fn test_quantizer_negative_voct_major_scale() {
4284        // -0.5V is F#3; in a major scale it snaps to the nearest degree, F3
4285        // (-7/12 V), proving the negative-octave scale-wrap path works.
4286        let mut q = Quantizer::new(Scale::Major);
4287        let mut inputs = PortValues::new();
4288        let mut outputs = PortValues::new();
4289        inputs.set(0, -0.5);
4290        q.tick(&inputs, &mut outputs);
4291        let out = outputs.get(10).unwrap();
4292        assert!(
4293            (out - (-7.0 / 12.0)).abs() < 1e-9,
4294            "major -0.5V should snap to F3 (-7/12 V), got {out}V"
4295        );
4296
4297        // -1.0V is exactly C3, a scale degree, so it is preserved.
4298        let mut q2 = Quantizer::new(Scale::Major);
4299        inputs.set(0, -1.0);
4300        q2.tick(&inputs, &mut outputs);
4301        assert!((outputs.get(10).unwrap() - (-1.0)).abs() < 1e-9);
4302    }
4303
4304    #[test]
4305    fn test_scale_quantizer_negative_voct() {
4306        // Chromatic passthrough below C4 with the div_euclid/rem_euclid path.
4307        let cases = [(-1.0, -1.0), (-13.0 / 12.0, -13.0 / 12.0)];
4308        for (input, expected) in cases {
4309            let mut sq = ScaleQuantizer::new(44100.0);
4310            let mut inputs = PortValues::new();
4311            let mut outputs = PortValues::new();
4312            inputs.set(0, input);
4313            inputs.set(2, 0.0); // chromatic
4314            sq.tick(&inputs, &mut outputs);
4315            let out = outputs.get(10).unwrap();
4316            assert!(
4317                (out - expected).abs() < 1e-9,
4318                "scale-quantizer chromatic {input}V -> {out}V, expected {expected}V"
4319            );
4320        }
4321
4322        // Minor scale, -0.5V (F#3) snaps to F3 (-7/12 V) below C4.
4323        let mut sq = ScaleQuantizer::new(44100.0);
4324        let mut inputs = PortValues::new();
4325        let mut outputs = PortValues::new();
4326        inputs.set(0, -0.5);
4327        inputs.set(2, 0.3); // scale index 2 == minor
4328        sq.tick(&inputs, &mut outputs);
4329        let out = outputs.get(10).unwrap();
4330        assert!(
4331            (out - (-7.0 / 12.0)).abs() < 1e-9,
4332            "minor -0.5V should snap to F3 (-7/12 V), got {out}V"
4333        );
4334    }
4335
4336    // ---- Q157: Euclidean + ScaleQuantizer reset / sample-rate ----
4337
4338    #[test]
4339    fn test_euclidean_reset_and_sample_rate() {
4340        let mut euc = Euclidean::new(44100.0);
4341        assert_eq!(euc.type_id(), "euclidean");
4342        let mut inputs = PortValues::new();
4343        let mut outputs = PortValues::new();
4344        // Advance the sequencer a few clock pulses so `step` moves off zero.
4345        for _ in 0..3 {
4346            inputs.set(0, 5.0);
4347            euc.tick(&inputs, &mut outputs);
4348            inputs.set(0, 0.0);
4349            euc.tick(&inputs, &mut outputs);
4350        }
4351        assert!(euc.step != 0, "clock pulses should advance the step");
4352        euc.reset();
4353        assert_eq!(euc.step, 0);
4354        assert!(!euc.cycle_accented);
4355        // set_sample_rate is a no-op but must not panic and keep it usable.
4356        euc.set_sample_rate(48000.0);
4357        inputs.set(0, 5.0);
4358        euc.tick(&inputs, &mut outputs);
4359        assert!(outputs.get(10).unwrap().is_finite());
4360    }
4361
4362    #[test]
4363    fn test_scale_quantizer_reset_and_sample_rate() {
4364        let mut sq = ScaleQuantizer::new(44100.0);
4365        assert_eq!(sq.type_id(), "scale_quantizer");
4366        let mut inputs = PortValues::new();
4367        let mut outputs = PortValues::new();
4368        inputs.set(0, 0.25); // some note
4369        sq.tick(&inputs, &mut outputs);
4370        assert!(sq.last_output.is_some());
4371        sq.reset();
4372        assert!(sq.last_output.is_none());
4373        sq.set_sample_rate(48000.0);
4374        sq.tick(&inputs, &mut outputs);
4375        assert!(outputs.get(10).unwrap().is_finite());
4376    }
4377}