Skip to main content

quiver/modules/
utilities.rs

1//! Utility, logic, CV, and sequencing modules.
2
3use super::common::{sanitize_audio, EdgeDetector, Memo, 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    /// Memoized rise rate (one `pow` per sample while the rise CV is static).
1022    rise_memo: Memo<2, f64>,
1023    /// Memoized fall rate (one `pow` per sample while the fall CV is static).
1024    fall_memo: Memo<2, f64>,
1025    spec: PortSpec,
1026}
1027
1028impl SlewLimiter {
1029    pub fn new(sample_rate: f64) -> Self {
1030        Self {
1031            current: 0.0,
1032            sample_rate,
1033            rise_memo: Memo::new(0.0),
1034            fall_memo: Memo::new(0.0),
1035            spec: PortSpec {
1036                inputs: vec![
1037                    PortDef::new(0, "in", SignalKind::CvBipolar),
1038                    PortDef::new(1, "rise", SignalKind::CvUnipolar)
1039                        .with_default(0.5)
1040                        .with_attenuverter(),
1041                    PortDef::new(2, "fall", SignalKind::CvUnipolar)
1042                        .with_default(0.5)
1043                        .with_attenuverter(),
1044                ],
1045                outputs: vec![PortDef::new(10, "out", SignalKind::CvBipolar)],
1046            },
1047        }
1048    }
1049
1050    fn cv_to_rate(cv: f64, sample_rate: f64) -> f64 {
1051        // Map 0-1 CV to rate: 0 = instant, 1 = very slow (~10 seconds)
1052        // Rate is in units per sample
1053        let time = 0.001 + Libm::<f64>::pow(cv.clamp(0.0, 1.0), 2.0) * 10.0; // 1ms to 10s
1054        1.0 / (time * sample_rate)
1055    }
1056}
1057
1058impl Default for SlewLimiter {
1059    fn default() -> Self {
1060        Self::new(44100.0)
1061    }
1062}
1063
1064impl GraphModule for SlewLimiter {
1065    fn port_spec(&self) -> &PortSpec {
1066        &self.spec
1067    }
1068
1069    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1070        let target = inputs.get_or(0, 0.0);
1071        let rise_cv = inputs.get_or(1, 0.5);
1072        let fall_cv = inputs.get_or(2, 0.5);
1073
1074        let diff = target - self.current;
1075
1076        // Rates memoized on their CVs (bit-exact miss path). Each branch keeps
1077        // its own memo so the two CVs do not evict each other.
1078        let sample_rate = self.sample_rate;
1079        if diff > 0.0 {
1080            // Rising
1081            let rate = self.rise_memo.get_or_compute([rise_cv, sample_rate], || {
1082                Self::cv_to_rate(rise_cv, sample_rate)
1083            });
1084            self.current += Libm::<f64>::fmin(diff, rate * 10.0); // Scale for voltage range
1085        } else if diff < 0.0 {
1086            // Falling
1087            let rate = self.fall_memo.get_or_compute([fall_cv, sample_rate], || {
1088                Self::cv_to_rate(fall_cv, sample_rate)
1089            });
1090            self.current += Libm::<f64>::fmax(diff, -rate * 10.0);
1091        }
1092
1093        outputs.set(10, self.current);
1094    }
1095
1096    fn reset(&mut self) {
1097        self.current = 0.0;
1098    }
1099
1100    fn set_sample_rate(&mut self, sample_rate: f64) {
1101        self.sample_rate = sample_rate;
1102    }
1103
1104    fn type_id(&self) -> &'static str {
1105        "slew_limiter"
1106    }
1107}
1108
1109/// Quantizer
1110///
1111/// Quantizes input CV to musical scale degrees.
1112/// Supports chromatic, major, minor, and pentatonic scales.
1113pub struct Quantizer {
1114    pub(crate) scale: Scale,
1115    /// Last committed output voltage, for boundary hysteresis (Q041).
1116    last_output: Option<f64>,
1117    spec: PortSpec,
1118}
1119
1120/// Musical scales for quantization
1121#[derive(Debug, Clone, Copy, PartialEq)]
1122pub enum Scale {
1123    Chromatic,
1124    Major,
1125    Minor,
1126    PentatonicMajor,
1127    PentatonicMinor,
1128    Dorian,
1129    Mixolydian,
1130    Blues,
1131}
1132
1133impl Scale {
1134    /// Returns the semitone offsets for this scale (relative to root)
1135    fn semitones(&self) -> &'static [i32] {
1136        match self {
1137            Scale::Chromatic => &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
1138            Scale::Major => &[0, 2, 4, 5, 7, 9, 11],
1139            Scale::Minor => &[0, 2, 3, 5, 7, 8, 10],
1140            Scale::PentatonicMajor => &[0, 2, 4, 7, 9],
1141            Scale::PentatonicMinor => &[0, 3, 5, 7, 10],
1142            Scale::Dorian => &[0, 2, 3, 5, 7, 9, 10],
1143            Scale::Mixolydian => &[0, 2, 4, 5, 7, 9, 10],
1144            Scale::Blues => &[0, 3, 5, 6, 7, 10],
1145        }
1146    }
1147}
1148
1149impl Quantizer {
1150    pub fn new(scale: Scale) -> Self {
1151        Self {
1152            scale,
1153            last_output: None,
1154            spec: PortSpec {
1155                inputs: vec![PortDef::new(0, "in", SignalKind::VoltPerOctave)],
1156                outputs: vec![PortDef::new(10, "out", SignalKind::VoltPerOctave)],
1157            },
1158        }
1159    }
1160
1161    pub fn chromatic() -> Self {
1162        Self::new(Scale::Chromatic)
1163    }
1164
1165    pub fn major() -> Self {
1166        Self::new(Scale::Major)
1167    }
1168
1169    pub fn minor() -> Self {
1170        Self::new(Scale::Minor)
1171    }
1172
1173    pub fn set_scale(&mut self, scale: Scale) {
1174        self.scale = scale;
1175    }
1176
1177    fn quantize(&self, voltage: f64) -> f64 {
1178        let semitones = self.scale.semitones();
1179
1180        // Convert voltage to semitones (1V = 12 semitones)
1181        let total_semitones = voltage * 12.0;
1182
1183        // Find octave and position within octave
1184        let octave = Libm::<f64>::floor(total_semitones / 12.0);
1185        let within_octave = total_semitones - octave * 12.0;
1186
1187        // Find nearest scale degree
1188        let mut nearest = semitones[0];
1189        let mut min_dist = f64::MAX;
1190
1191        for &semi in semitones {
1192            let dist = (within_octave - semi as f64).abs();
1193            if dist < min_dist {
1194                min_dist = dist;
1195                nearest = semi;
1196            }
1197            // Also check wrapping to next octave
1198            let dist_wrap = (within_octave - (semi + 12) as f64).abs();
1199            if dist_wrap < min_dist {
1200                min_dist = dist_wrap;
1201                nearest = semi + 12;
1202            }
1203        }
1204
1205        // Convert back to voltage
1206        (octave * 12.0 + nearest as f64) / 12.0
1207    }
1208}
1209
1210impl Default for Quantizer {
1211    fn default() -> Self {
1212        Self::chromatic()
1213    }
1214}
1215
1216impl GraphModule for Quantizer {
1217    fn port_spec(&self) -> &PortSpec {
1218        &self.spec
1219    }
1220
1221    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1222        let input = inputs.get_or(0, 0.0);
1223        let candidate = self.quantize(input);
1224        // Hold the note through hysteresis so a CV parked on a boundary does not
1225        // chatter between adjacent scale degrees (Q041).
1226        let committed = hysteretic_note(
1227            self.last_output,
1228            input,
1229            candidate,
1230            NOTE_HYSTERESIS_SEMITONES,
1231        );
1232        self.last_output = Some(committed);
1233        outputs.set(10, committed);
1234    }
1235
1236    fn reset(&mut self) {
1237        self.last_output = None;
1238    }
1239
1240    fn set_sample_rate(&mut self, _: f64) {}
1241
1242    fn type_id(&self) -> &'static str {
1243        "quantizer"
1244    }
1245
1246    // `scale` is internal state (no scale port); bridge to introspection.
1247    crate::impl_introspect!();
1248}
1249
1250/// Clock Generator
1251///
1252/// Generates clock pulses at a specified tempo (BPM).
1253pub struct Clock {
1254    phase: f64,
1255    /// Integer count of completed main cycles, used to derive the divided
1256    /// outputs so they actually divide the tempo (Q035).
1257    cycle: u64,
1258    sample_rate: f64,
1259    /// Memoized tempo map `20 · 15^(cv/10)` (one `pow` per sample while the
1260    /// bpm CV is static).
1261    bpm_memo: Memo<1, f64>,
1262    spec: PortSpec,
1263}
1264
1265impl Clock {
1266    /// Bpm-control CV that yields exactly 120 BPM through [`Clock::cv_to_bpm`].
1267    ///
1268    /// Since `cv_to_bpm(cv) = 20 * 15^(cv/10)`, solving `20 * 15^(cv/10) = 120`
1269    /// gives `cv = 10 * ln(6) / ln(15) ≈ 6.6164` (Q038 — the old `1.2` default
1270    /// produced only ~27.5 BPM).
1271    const DEFAULT_BPM_CV: f64 = 6.616_418_958_920_283;
1272
1273    pub fn new(sample_rate: f64) -> Self {
1274        Self {
1275            phase: 0.0,
1276            cycle: 0,
1277            sample_rate,
1278            bpm_memo: Memo::new(0.0),
1279            spec: PortSpec {
1280                inputs: vec![
1281                    PortDef::new(0, "bpm", SignalKind::CvUnipolar)
1282                        .with_default(Self::DEFAULT_BPM_CV) // 120 BPM when scaled
1283                        .with_attenuverter(),
1284                    PortDef::new(1, "reset", SignalKind::Trigger),
1285                ],
1286                outputs: vec![
1287                    PortDef::new(10, "out", SignalKind::Clock),
1288                    PortDef::new(11, "div2", SignalKind::Clock),
1289                    PortDef::new(12, "div4", SignalKind::Clock),
1290                ],
1291            },
1292        }
1293    }
1294
1295    fn cv_to_bpm(cv: f64) -> f64 {
1296        // Map 0-10V to 20-300 BPM (exponential)
1297        20.0 * Libm::<f64>::pow(15.0, cv / 10.0)
1298    }
1299}
1300
1301impl Default for Clock {
1302    fn default() -> Self {
1303        Self::new(44100.0)
1304    }
1305}
1306
1307impl GraphModule for Clock {
1308    fn port_spec(&self) -> &PortSpec {
1309        &self.spec
1310    }
1311
1312    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1313        let bpm_cv = inputs.get_or(0, Self::DEFAULT_BPM_CV); // Default 120 BPM
1314        let reset = inputs.get_or(1, 0.0);
1315
1316        // Tempo map memoized on the bpm CV (bit-exact miss path).
1317        let bpm = self
1318            .bpm_memo
1319            .get_or_compute([bpm_cv], || Self::cv_to_bpm(bpm_cv));
1320        let freq = bpm / 60.0; // Hz
1321
1322        // Reset on trigger
1323        if reset > GATE_THRESHOLD_V {
1324            self.phase = 0.0;
1325            self.cycle = 0;
1326        }
1327
1328        // Main clock output (short pulse at start of each cycle)
1329        let pulse_width = 0.1; // 10% duty cycle
1330        let in_pulse = self.phase < pulse_width;
1331        let main_out = if in_pulse { GATE_HIGH_V } else { 0.0 };
1332
1333        // Divided outputs derived from the integer cycle counter so they
1334        // genuinely divide the tempo (Q035): div2 fires on even cycles, div4 on
1335        // every fourth cycle, both with the same pulse-width window as main.
1336        // Bitwise masks (both divisors are powers of two) keep this MSRV-1.78
1337        // safe, avoiding the newer `u64::is_multiple_of`.
1338        let div2_out = if in_pulse && (self.cycle & 1) == 0 {
1339            GATE_HIGH_V
1340        } else {
1341            0.0
1342        };
1343        let div4_out = if in_pulse && (self.cycle & 3) == 0 {
1344            GATE_HIGH_V
1345        } else {
1346            0.0
1347        };
1348
1349        outputs.set(10, main_out);
1350        outputs.set(11, div2_out);
1351        outputs.set(12, div4_out);
1352
1353        // Advance phase, incrementing the cycle counter on each wrap.
1354        let new_phase = self.phase + freq / self.sample_rate;
1355        let wraps = Libm::<f64>::floor(new_phase);
1356        if wraps > 0.0 {
1357            self.cycle = self.cycle.wrapping_add(wraps as u64);
1358        }
1359        self.phase = new_phase - wraps;
1360    }
1361
1362    fn reset(&mut self) {
1363        self.phase = 0.0;
1364        self.cycle = 0;
1365    }
1366
1367    fn set_sample_rate(&mut self, sample_rate: f64) {
1368        self.sample_rate = sample_rate;
1369    }
1370
1371    fn type_id(&self) -> &'static str {
1372        "clock"
1373    }
1374}
1375
1376/// Attenuverter
1377///
1378/// Attenuates and/or inverts a signal. The level control goes from
1379/// -1 (inverted full scale) through 0 (silence) to +1 (full scale).
1380pub struct Attenuverter {
1381    spec: PortSpec,
1382}
1383
1384impl Attenuverter {
1385    pub fn new() -> Self {
1386        Self {
1387            spec: PortSpec {
1388                inputs: vec![
1389                    PortDef::new(0, "in", SignalKind::CvBipolar),
1390                    PortDef::new(1, "level", SignalKind::CvBipolar).with_default(5.0), // Default to unity gain
1391                ],
1392                outputs: vec![PortDef::new(10, "out", SignalKind::CvBipolar)],
1393            },
1394        }
1395    }
1396}
1397
1398impl Default for Attenuverter {
1399    fn default() -> Self {
1400        Self::new()
1401    }
1402}
1403
1404impl GraphModule for Attenuverter {
1405    fn port_spec(&self) -> &PortSpec {
1406        &self.spec
1407    }
1408
1409    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1410        let input = inputs.get_or(0, 0.0);
1411        let level = inputs.get_or(1, 5.0) / 5.0; // Normalize to -1..+1
1412
1413        outputs.set(10, input * level);
1414    }
1415
1416    fn reset(&mut self) {}
1417
1418    fn set_sample_rate(&mut self, _: f64) {}
1419
1420    fn type_id(&self) -> &'static str {
1421        "attenuverter"
1422    }
1423}
1424
1425/// Multiple (Signal Splitter)
1426///
1427/// Takes one input and copies it to multiple outputs.
1428/// Useful for sending one signal to multiple destinations.
1429pub struct Multiple {
1430    spec: PortSpec,
1431}
1432
1433impl Multiple {
1434    pub fn new() -> Self {
1435        Self {
1436            spec: PortSpec {
1437                inputs: vec![PortDef::new(0, "in", SignalKind::CvBipolar)],
1438                outputs: vec![
1439                    PortDef::new(10, "out1", SignalKind::CvBipolar),
1440                    PortDef::new(11, "out2", SignalKind::CvBipolar),
1441                    PortDef::new(12, "out3", SignalKind::CvBipolar),
1442                    PortDef::new(13, "out4", SignalKind::CvBipolar),
1443                ],
1444            },
1445        }
1446    }
1447}
1448
1449impl Default for Multiple {
1450    fn default() -> Self {
1451        Self::new()
1452    }
1453}
1454
1455impl GraphModule for Multiple {
1456    fn port_spec(&self) -> &PortSpec {
1457        &self.spec
1458    }
1459
1460    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1461        let input = inputs.get_or(0, 0.0);
1462
1463        outputs.set(10, input);
1464        outputs.set(11, input);
1465        outputs.set(12, input);
1466        outputs.set(13, input);
1467    }
1468
1469    fn reset(&mut self) {}
1470
1471    fn set_sample_rate(&mut self, _: f64) {}
1472
1473    fn type_id(&self) -> &'static str {
1474        "multiple"
1475    }
1476}
1477
1478// ============================================================================
1479// Phase 2 Modules: Hardware Fidelity
1480// ============================================================================
1481
1482/// Crossfader / Panner
1483///
1484/// Crossfades between two audio inputs or pans a mono input across stereo outputs.
1485/// The position control goes from -5V (full A/left) to +5V (full B/right).
1486pub struct Crossfader {
1487    spec: PortSpec,
1488}
1489
1490impl Crossfader {
1491    pub fn new() -> Self {
1492        Self {
1493            spec: PortSpec {
1494                inputs: vec![
1495                    PortDef::new(0, "a", SignalKind::Audio),
1496                    PortDef::new(1, "b", SignalKind::Audio),
1497                    PortDef::new(2, "pos", SignalKind::CvBipolar).with_default(0.0),
1498                ],
1499                outputs: vec![
1500                    PortDef::new(10, "out", SignalKind::Audio),
1501                    PortDef::new(11, "left", SignalKind::Audio),
1502                    PortDef::new(12, "right", SignalKind::Audio),
1503                ],
1504            },
1505        }
1506    }
1507}
1508
1509impl Default for Crossfader {
1510    fn default() -> Self {
1511        Self::new()
1512    }
1513}
1514
1515impl GraphModule for Crossfader {
1516    fn port_spec(&self) -> &PortSpec {
1517        &self.spec
1518    }
1519
1520    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1521        let a = inputs.get_or(0, 0.0);
1522        let b = inputs.get_or(1, 0.0);
1523        let pos = inputs.get_or(2, 0.0);
1524
1525        // Map position from -5V to +5V to 0.0 to 1.0
1526        let mix = ((pos / 5.0) + 1.0) / 2.0;
1527        let mix = mix.clamp(0.0, 1.0);
1528
1529        // Equal-power crossfade for smoother transitions
1530        let a_gain = Libm::<f64>::sqrt(1.0 - mix);
1531        let b_gain = Libm::<f64>::sqrt(mix);
1532
1533        // Main output: crossfade between A and B
1534        let out = a * a_gain + b * b_gain;
1535        outputs.set(10, out);
1536
1537        // Stereo outputs: pan the main output
1538        // At pos=-5V: full left, at pos=+5V: full right
1539        outputs.set(11, out * a_gain); // Left
1540        outputs.set(12, out * b_gain); // Right
1541    }
1542
1543    fn reset(&mut self) {}
1544
1545    fn set_sample_rate(&mut self, _: f64) {}
1546
1547    fn type_id(&self) -> &'static str {
1548        "crossfader"
1549    }
1550}
1551
1552/// Logic AND Gate
1553///
1554/// Outputs high (+5V) only when both inputs are high (>2.5V).
1555pub struct LogicAnd {
1556    spec: PortSpec,
1557}
1558
1559impl LogicAnd {
1560    pub fn new() -> Self {
1561        Self {
1562            spec: PortSpec {
1563                inputs: vec![
1564                    PortDef::new(0, "a", SignalKind::Gate),
1565                    PortDef::new(1, "b", SignalKind::Gate),
1566                ],
1567                outputs: vec![PortDef::new(10, "out", SignalKind::Gate)],
1568            },
1569        }
1570    }
1571}
1572
1573impl Default for LogicAnd {
1574    fn default() -> Self {
1575        Self::new()
1576    }
1577}
1578
1579impl GraphModule for LogicAnd {
1580    fn port_spec(&self) -> &PortSpec {
1581        &self.spec
1582    }
1583
1584    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1585        let a = inputs.get_or(0, 0.0) > GATE_THRESHOLD_V;
1586        let b = inputs.get_or(1, 0.0) > GATE_THRESHOLD_V;
1587
1588        outputs.set(10, if a && b { GATE_HIGH_V } else { 0.0 });
1589    }
1590
1591    fn reset(&mut self) {}
1592
1593    fn set_sample_rate(&mut self, _: f64) {}
1594
1595    fn type_id(&self) -> &'static str {
1596        "logic_and"
1597    }
1598}
1599
1600/// Logic OR Gate
1601///
1602/// Outputs high (+5V) when either or both inputs are high (>2.5V).
1603pub struct LogicOr {
1604    spec: PortSpec,
1605}
1606
1607impl LogicOr {
1608    pub fn new() -> Self {
1609        Self {
1610            spec: PortSpec {
1611                inputs: vec![
1612                    PortDef::new(0, "a", SignalKind::Gate),
1613                    PortDef::new(1, "b", SignalKind::Gate),
1614                ],
1615                outputs: vec![PortDef::new(10, "out", SignalKind::Gate)],
1616            },
1617        }
1618    }
1619}
1620
1621impl Default for LogicOr {
1622    fn default() -> Self {
1623        Self::new()
1624    }
1625}
1626
1627impl GraphModule for LogicOr {
1628    fn port_spec(&self) -> &PortSpec {
1629        &self.spec
1630    }
1631
1632    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1633        let a = inputs.get_or(0, 0.0) > GATE_THRESHOLD_V;
1634        let b = inputs.get_or(1, 0.0) > GATE_THRESHOLD_V;
1635
1636        outputs.set(10, if a || b { GATE_HIGH_V } else { 0.0 });
1637    }
1638
1639    fn reset(&mut self) {}
1640
1641    fn set_sample_rate(&mut self, _: f64) {}
1642
1643    fn type_id(&self) -> &'static str {
1644        "logic_or"
1645    }
1646}
1647
1648/// Logic XOR Gate
1649///
1650/// Outputs high (+5V) when exactly one input is high (>2.5V).
1651pub struct LogicXor {
1652    spec: PortSpec,
1653}
1654
1655impl LogicXor {
1656    pub fn new() -> Self {
1657        Self {
1658            spec: PortSpec {
1659                inputs: vec![
1660                    PortDef::new(0, "a", SignalKind::Gate),
1661                    PortDef::new(1, "b", SignalKind::Gate),
1662                ],
1663                outputs: vec![PortDef::new(10, "out", SignalKind::Gate)],
1664            },
1665        }
1666    }
1667}
1668
1669impl Default for LogicXor {
1670    fn default() -> Self {
1671        Self::new()
1672    }
1673}
1674
1675impl GraphModule for LogicXor {
1676    fn port_spec(&self) -> &PortSpec {
1677        &self.spec
1678    }
1679
1680    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1681        let a = inputs.get_or(0, 0.0) > GATE_THRESHOLD_V;
1682        let b = inputs.get_or(1, 0.0) > GATE_THRESHOLD_V;
1683
1684        outputs.set(10, if a ^ b { GATE_HIGH_V } else { 0.0 });
1685    }
1686
1687    fn reset(&mut self) {}
1688
1689    fn set_sample_rate(&mut self, _: f64) {}
1690
1691    fn type_id(&self) -> &'static str {
1692        "logic_xor"
1693    }
1694}
1695
1696/// Logic NOT Gate (Inverter)
1697///
1698/// Inverts the input: outputs high (+5V) when input is low, and vice versa.
1699pub struct LogicNot {
1700    spec: PortSpec,
1701}
1702
1703impl LogicNot {
1704    pub fn new() -> Self {
1705        Self {
1706            spec: PortSpec {
1707                inputs: vec![PortDef::new(0, "in", SignalKind::Gate)],
1708                outputs: vec![PortDef::new(10, "out", SignalKind::Gate)],
1709            },
1710        }
1711    }
1712}
1713
1714impl Default for LogicNot {
1715    fn default() -> Self {
1716        Self::new()
1717    }
1718}
1719
1720impl GraphModule for LogicNot {
1721    fn port_spec(&self) -> &PortSpec {
1722        &self.spec
1723    }
1724
1725    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1726        let input = inputs.get_or(0, 0.0) > GATE_THRESHOLD_V;
1727        outputs.set(10, if input { 0.0 } else { GATE_HIGH_V });
1728    }
1729
1730    fn reset(&mut self) {}
1731
1732    fn set_sample_rate(&mut self, _: f64) {}
1733
1734    fn type_id(&self) -> &'static str {
1735        "logic_not"
1736    }
1737}
1738
1739/// Comparator
1740///
1741/// Compares two CV inputs and outputs a gate based on the comparison.
1742/// Outputs high (+5V) when A > B, otherwise low (0V).
1743/// Also provides inverted output (A <= B).
1744pub struct Comparator {
1745    /// Last committed comparison state: `1` = gt, `-1` = lt, `0` = eq. Used for
1746    /// true stateful hysteresis so a signal dithering around B does not toggle
1747    /// every sample (Q041).
1748    state: i8,
1749    spec: PortSpec,
1750}
1751
1752impl Comparator {
1753    /// Deadband half-width defining the equality region (volts).
1754    const DEADBAND_V: f64 = 0.01;
1755    /// Extra margin, beyond the deadband edge, the input must cross to flip
1756    /// state. A dither smaller than this can no longer cause chatter.
1757    const HYSTERESIS_V: f64 = 0.02;
1758
1759    pub fn new() -> Self {
1760        Self {
1761            state: 0,
1762            spec: PortSpec {
1763                inputs: vec![
1764                    PortDef::new(0, "a", SignalKind::CvBipolar),
1765                    PortDef::new(1, "b", SignalKind::CvBipolar),
1766                ],
1767                outputs: vec![
1768                    PortDef::new(10, "gt", SignalKind::Gate), // A > B
1769                    PortDef::new(11, "lt", SignalKind::Gate), // A < B
1770                    PortDef::new(12, "eq", SignalKind::Gate), // A ≈ B (within threshold)
1771                ],
1772            },
1773        }
1774    }
1775}
1776
1777impl Default for Comparator {
1778    fn default() -> Self {
1779        Self::new()
1780    }
1781}
1782
1783impl GraphModule for Comparator {
1784    fn port_spec(&self) -> &PortSpec {
1785        &self.spec
1786    }
1787
1788    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1789        let a = inputs.get_or(0, 0.0);
1790        let b = inputs.get_or(1, 0.0);
1791        let d = a - b;
1792
1793        let t = Self::DEADBAND_V;
1794        let hy = Self::HYSTERESIS_V;
1795
1796        // Stateful hysteresis: turning an output ON needs the input to cross the
1797        // deadband edge plus the hysteresis margin; turning it OFF happens back
1798        // at the deadband edge. Once committed, a dither smaller than `hy`
1799        // cannot flip the state, eliminating boundary chatter (Q041).
1800        let mut gt = self.state == 1;
1801        let mut lt = self.state == -1;
1802
1803        if gt {
1804            if d < t {
1805                gt = false;
1806            }
1807        } else if d >= t + hy {
1808            gt = true;
1809        }
1810
1811        if lt {
1812            if d > -t {
1813                lt = false;
1814            }
1815        } else if d <= -t - hy {
1816            lt = true;
1817        }
1818
1819        // `gt` and `lt` are mutually exclusive: their ON conditions require
1820        // |d| >= t + hy of opposite sign.
1821        self.state = if gt {
1822            1
1823        } else if lt {
1824            -1
1825        } else {
1826            0
1827        };
1828
1829        outputs.set(10, if gt { GATE_HIGH_V } else { 0.0 });
1830        outputs.set(11, if lt { GATE_HIGH_V } else { 0.0 });
1831        outputs.set(12, if self.state == 0 { GATE_HIGH_V } else { 0.0 });
1832    }
1833
1834    fn reset(&mut self) {
1835        self.state = 0;
1836    }
1837
1838    fn set_sample_rate(&mut self, _: f64) {}
1839
1840    fn type_id(&self) -> &'static str {
1841        "comparator"
1842    }
1843}
1844
1845/// Rectifier
1846///
1847/// Performs full-wave and half-wave rectification of audio/CV signals.
1848/// Also provides absolute value output.
1849pub struct Rectifier {
1850    spec: PortSpec,
1851}
1852
1853impl Rectifier {
1854    pub fn new() -> Self {
1855        Self {
1856            spec: PortSpec {
1857                inputs: vec![PortDef::new(0, "in", SignalKind::Audio)],
1858                outputs: vec![
1859                    PortDef::new(10, "full", SignalKind::Audio), // Full-wave rectified
1860                    PortDef::new(11, "half_pos", SignalKind::Audio), // Half-wave (positive)
1861                    PortDef::new(12, "half_neg", SignalKind::Audio), // Half-wave (negative, inverted)
1862                    PortDef::new(13, "abs", SignalKind::CvUnipolar), // Absolute value (0-10V)
1863                ],
1864            },
1865        }
1866    }
1867}
1868
1869impl Default for Rectifier {
1870    fn default() -> Self {
1871        Self::new()
1872    }
1873}
1874
1875impl GraphModule for Rectifier {
1876    fn port_spec(&self) -> &PortSpec {
1877        &self.spec
1878    }
1879
1880    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1881        let input = inputs.get_or(0, 0.0);
1882
1883        // Full-wave rectification: absolute value, keeps ±5V range as 0-5V
1884        outputs.set(10, Libm::<f64>::fabs(input));
1885
1886        // Half-wave positive: pass positive, block negative
1887        outputs.set(11, Libm::<f64>::fmax(input, 0.0));
1888
1889        // Half-wave negative: pass negative inverted, block positive
1890        outputs.set(12, Libm::<f64>::fmax(-input, 0.0));
1891
1892        // Absolute value scaled to 0-10V unipolar (input ±5V -> output 0-10V)
1893        outputs.set(13, Libm::<f64>::fabs(input) * 2.0);
1894    }
1895
1896    fn reset(&mut self) {}
1897
1898    fn set_sample_rate(&mut self, _: f64) {}
1899
1900    fn type_id(&self) -> &'static str {
1901        "rectifier"
1902    }
1903}
1904
1905/// Precision Adder
1906///
1907/// A high-precision CV adder/mixer with multiple inputs.
1908/// Useful for combining V/Oct signals for transposition.
1909/// Includes a precision 1V/octave offset output for tuning.
1910pub struct PrecisionAdder {
1911    spec: PortSpec,
1912}
1913
1914impl PrecisionAdder {
1915    pub fn new() -> Self {
1916        Self {
1917            spec: PortSpec {
1918                inputs: vec![
1919                    PortDef::new(0, "in1", SignalKind::VoltPerOctave),
1920                    PortDef::new(1, "in2", SignalKind::VoltPerOctave),
1921                    PortDef::new(2, "in3", SignalKind::CvBipolar),
1922                    PortDef::new(3, "in4", SignalKind::CvBipolar),
1923                ],
1924                outputs: vec![
1925                    PortDef::new(10, "sum", SignalKind::VoltPerOctave),
1926                    PortDef::new(11, "inv", SignalKind::VoltPerOctave), // Inverted sum
1927                ],
1928            },
1929        }
1930    }
1931}
1932
1933impl Default for PrecisionAdder {
1934    fn default() -> Self {
1935        Self::new()
1936    }
1937}
1938
1939impl GraphModule for PrecisionAdder {
1940    fn port_spec(&self) -> &PortSpec {
1941        &self.spec
1942    }
1943
1944    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1945        let sum = inputs.get_or(0, 0.0)
1946            + inputs.get_or(1, 0.0)
1947            + inputs.get_or(2, 0.0)
1948            + inputs.get_or(3, 0.0);
1949
1950        outputs.set(10, sum);
1951        outputs.set(11, -sum);
1952    }
1953
1954    fn reset(&mut self) {}
1955
1956    fn set_sample_rate(&mut self, _: f64) {}
1957
1958    fn type_id(&self) -> &'static str {
1959        "precision_adder"
1960    }
1961}
1962
1963/// Voltage-Controlled Switch
1964///
1965/// Routes one of two inputs to the output based on a control signal.
1966/// When CV > 2.5V, output = B; otherwise output = A.
1967/// Also provides complementary outputs.
1968pub struct VcSwitch {
1969    spec: PortSpec,
1970}
1971
1972impl VcSwitch {
1973    pub fn new() -> Self {
1974        Self {
1975            spec: PortSpec {
1976                inputs: vec![
1977                    PortDef::new(0, "a", SignalKind::Audio),
1978                    PortDef::new(1, "b", SignalKind::Audio),
1979                    PortDef::new(2, "cv", SignalKind::Gate).with_default(0.0),
1980                ],
1981                outputs: vec![
1982                    PortDef::new(10, "out", SignalKind::Audio), // Selected input
1983                    PortDef::new(11, "a_out", SignalKind::Audio), // A when selected, else 0
1984                    PortDef::new(12, "b_out", SignalKind::Audio), // B when selected, else 0
1985                ],
1986            },
1987        }
1988    }
1989}
1990
1991impl Default for VcSwitch {
1992    fn default() -> Self {
1993        Self::new()
1994    }
1995}
1996
1997impl GraphModule for VcSwitch {
1998    fn port_spec(&self) -> &PortSpec {
1999        &self.spec
2000    }
2001
2002    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
2003        let a = inputs.get_or(0, 0.0);
2004        let b = inputs.get_or(1, 0.0);
2005        let cv = inputs.get_or(2, 0.0);
2006
2007        let select_b = cv > GATE_THRESHOLD_V;
2008
2009        if select_b {
2010            outputs.set(10, b);
2011            outputs.set(11, 0.0);
2012            outputs.set(12, b);
2013        } else {
2014            outputs.set(10, a);
2015            outputs.set(11, a);
2016            outputs.set(12, 0.0);
2017        }
2018    }
2019
2020    fn reset(&mut self) {}
2021
2022    fn set_sample_rate(&mut self, _: f64) {}
2023
2024    fn type_id(&self) -> &'static str {
2025        "vc_switch"
2026    }
2027}
2028
2029/// Bernoulli Gate
2030///
2031/// A probabilistic gate router. On each trigger, randomly routes the signal
2032/// to one of two outputs based on a probability parameter.
2033/// Inspired by Mutable Instruments Branches.
2034pub struct BernoulliGate {
2035    prev_trigger: f64,
2036    /// Latched gate A state, persisted in the struct because the engine hands
2037    /// `tick` a fresh output buffer each sample (Q036).
2038    gate_a: f64,
2039    /// Latched gate B state (see `gate_a`).
2040    gate_b: f64,
2041    spec: PortSpec,
2042}
2043
2044impl BernoulliGate {
2045    pub fn new() -> Self {
2046        Self {
2047            prev_trigger: 0.0,
2048            gate_a: 0.0,
2049            gate_b: 0.0,
2050            spec: PortSpec {
2051                inputs: vec![
2052                    PortDef::new(0, "trig", SignalKind::Trigger),
2053                    PortDef::new(1, "prob", SignalKind::CvUnipolar).with_default(5.0), // 50% default
2054                ],
2055                outputs: vec![
2056                    PortDef::new(10, "a", SignalKind::Trigger),   // Output A
2057                    PortDef::new(11, "b", SignalKind::Trigger),   // Output B
2058                    PortDef::new(12, "gate_a", SignalKind::Gate), // Latched gate A
2059                    PortDef::new(13, "gate_b", SignalKind::Gate), // Latched gate B
2060                ],
2061            },
2062        }
2063    }
2064}
2065
2066impl Default for BernoulliGate {
2067    fn default() -> Self {
2068        Self::new()
2069    }
2070}
2071
2072impl GraphModule for BernoulliGate {
2073    fn port_spec(&self) -> &PortSpec {
2074        &self.spec
2075    }
2076
2077    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
2078        let trigger = inputs.get_or(0, 0.0);
2079        let prob = (inputs.get_or(1, 5.0) / 10.0).clamp(0.0, 1.0); // Normalize to 0-1
2080
2081        let rising_edge = trigger > GATE_THRESHOLD_V && self.prev_trigger <= GATE_THRESHOLD_V;
2082        self.prev_trigger = trigger;
2083
2084        // Default: no trigger output
2085        let mut trig_a = 0.0;
2086        let mut trig_b = 0.0;
2087
2088        if rising_edge {
2089            // Random decision based on probability
2090            let rand_val: f64 = rng::random();
2091            if rand_val < prob {
2092                trig_a = GATE_HIGH_V;
2093            } else {
2094                trig_b = GATE_HIGH_V;
2095            }
2096        }
2097
2098        // Trigger outputs (momentary)
2099        outputs.set(10, trig_a);
2100        outputs.set(11, trig_b);
2101
2102        // Gate outputs track which side was last triggered and latch until the
2103        // other side is triggered. State lives in struct fields (Q036) because
2104        // the output buffer is not persisted across ticks by the engine.
2105        if trig_a > 0.0 {
2106            self.gate_a = GATE_HIGH_V;
2107            self.gate_b = 0.0;
2108        } else if trig_b > 0.0 {
2109            self.gate_a = 0.0;
2110            self.gate_b = GATE_HIGH_V;
2111        }
2112
2113        outputs.set(12, self.gate_a);
2114        outputs.set(13, self.gate_b);
2115    }
2116
2117    fn reset(&mut self) {
2118        self.prev_trigger = 0.0;
2119        self.gate_a = 0.0;
2120        self.gate_b = 0.0;
2121    }
2122
2123    fn set_sample_rate(&mut self, _: f64) {}
2124
2125    fn type_id(&self) -> &'static str {
2126        "bernoulli_gate"
2127    }
2128}
2129
2130/// Min module
2131///
2132/// Outputs the minimum of two input signals.
2133pub struct Min {
2134    spec: PortSpec,
2135}
2136
2137impl Min {
2138    pub fn new() -> Self {
2139        Self {
2140            spec: PortSpec {
2141                inputs: vec![
2142                    PortDef::new(0, "a", SignalKind::CvBipolar),
2143                    PortDef::new(1, "b", SignalKind::CvBipolar),
2144                ],
2145                outputs: vec![PortDef::new(10, "out", SignalKind::CvBipolar)],
2146            },
2147        }
2148    }
2149}
2150
2151impl Default for Min {
2152    fn default() -> Self {
2153        Self::new()
2154    }
2155}
2156
2157impl GraphModule for Min {
2158    fn port_spec(&self) -> &PortSpec {
2159        &self.spec
2160    }
2161
2162    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
2163        let a = inputs.get_or(0, 0.0);
2164        let b = inputs.get_or(1, 0.0);
2165        outputs.set(10, Libm::<f64>::fmin(a, b));
2166    }
2167
2168    fn reset(&mut self) {}
2169
2170    fn set_sample_rate(&mut self, _: f64) {}
2171
2172    fn type_id(&self) -> &'static str {
2173        "min"
2174    }
2175}
2176
2177/// Max module
2178///
2179/// Outputs the maximum of two input signals.
2180pub struct Max {
2181    spec: PortSpec,
2182}
2183
2184impl Max {
2185    pub fn new() -> Self {
2186        Self {
2187            spec: PortSpec {
2188                inputs: vec![
2189                    PortDef::new(0, "a", SignalKind::CvBipolar),
2190                    PortDef::new(1, "b", SignalKind::CvBipolar),
2191                ],
2192                outputs: vec![PortDef::new(10, "out", SignalKind::CvBipolar)],
2193            },
2194        }
2195    }
2196}
2197
2198impl Default for Max {
2199    fn default() -> Self {
2200        Self::new()
2201    }
2202}
2203
2204impl GraphModule for Max {
2205    fn port_spec(&self) -> &PortSpec {
2206        &self.spec
2207    }
2208
2209    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
2210        let a = inputs.get_or(0, 0.0);
2211        let b = inputs.get_or(1, 0.0);
2212        outputs.set(10, Libm::<f64>::fmax(a, b));
2213    }
2214
2215    fn reset(&mut self) {}
2216
2217    fn set_sample_rate(&mut self, _: f64) {}
2218
2219    fn type_id(&self) -> &'static str {
2220        "max"
2221    }
2222}
2223
2224// ============================================================================
2225// Planned Modules: ChordMemory
2226// ============================================================================
2227
2228/// Chord type for the ChordMemory module
2229#[derive(Debug, Clone, Copy, PartialEq)]
2230pub enum ChordType {
2231    Major,
2232    Minor,
2233    Seventh,
2234    MajorSeventh,
2235    MinorSeventh,
2236    Diminished,
2237    Augmented,
2238    Sus2,
2239    Sus4,
2240}
2241
2242impl ChordType {
2243    /// Returns the semitone intervals for this chord type (relative to root)
2244    fn intervals(&self) -> &'static [i32] {
2245        match self {
2246            ChordType::Major => &[0, 4, 7],
2247            ChordType::Minor => &[0, 3, 7],
2248            ChordType::Seventh => &[0, 4, 7, 10],
2249            ChordType::MajorSeventh => &[0, 4, 7, 11],
2250            ChordType::MinorSeventh => &[0, 3, 7, 10],
2251            ChordType::Diminished => &[0, 3, 6],
2252            ChordType::Augmented => &[0, 4, 8],
2253            ChordType::Sus2 => &[0, 2, 7],
2254            ChordType::Sus4 => &[0, 5, 7],
2255        }
2256    }
2257
2258    /// Select chord type from CV value (0.0-1.0)
2259    fn from_cv(cv: f64) -> Self {
2260        match (cv * 8.99) as u8 {
2261            0 => ChordType::Major,
2262            1 => ChordType::Minor,
2263            2 => ChordType::Seventh,
2264            3 => ChordType::MajorSeventh,
2265            4 => ChordType::MinorSeventh,
2266            5 => ChordType::Diminished,
2267            6 => ChordType::Augmented,
2268            7 => ChordType::Sus2,
2269            _ => ChordType::Sus4,
2270        }
2271    }
2272}
2273
2274/// Chord Memory
2275///
2276/// Generates chord voicings from a root note. Outputs 4 V/Oct signals
2277/// representing chord voices. Supports 9 chord types with inversions
2278/// and voice spreading.
2279///
2280/// **Chord types** (selected via CV 0-1):
2281/// - Major, Minor, 7th, Maj7, Min7, Dim, Aug, Sus2, Sus4
2282///
2283/// **Inversion**: Rotates which note is the bass
2284/// **Spread**: Distributes voices across octaves
2285pub struct ChordMemory {
2286    spec: PortSpec,
2287}
2288
2289impl ChordMemory {
2290    pub fn new() -> Self {
2291        Self {
2292            spec: PortSpec {
2293                inputs: vec![
2294                    PortDef::new(0, "root", SignalKind::VoltPerOctave),
2295                    PortDef::new(1, "chord", SignalKind::CvUnipolar)
2296                        .with_default(0.0)
2297                        .with_attenuverter(),
2298                    PortDef::new(2, "inversion", SignalKind::CvUnipolar)
2299                        .with_default(0.0)
2300                        .with_attenuverter(),
2301                    PortDef::new(3, "spread", SignalKind::CvUnipolar)
2302                        .with_default(0.0)
2303                        .with_attenuverter(),
2304                ],
2305                outputs: vec![
2306                    PortDef::new(10, "voice1", SignalKind::VoltPerOctave),
2307                    PortDef::new(11, "voice2", SignalKind::VoltPerOctave),
2308                    PortDef::new(12, "voice3", SignalKind::VoltPerOctave),
2309                    PortDef::new(13, "voice4", SignalKind::VoltPerOctave),
2310                ],
2311            },
2312        }
2313    }
2314}
2315
2316impl Default for ChordMemory {
2317    fn default() -> Self {
2318        Self::new()
2319    }
2320}
2321
2322impl GraphModule for ChordMemory {
2323    fn port_spec(&self) -> &PortSpec {
2324        &self.spec
2325    }
2326
2327    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
2328        let root = inputs.get_or(0, 0.0);
2329        let chord_cv = inputs.get_or(1, 0.0).clamp(0.0, 1.0);
2330        let inversion_cv = inputs.get_or(2, 0.0).clamp(0.0, 1.0);
2331        let spread = inputs.get_or(3, 0.0).clamp(0.0, 1.0);
2332
2333        let chord_type = ChordType::from_cv(chord_cv);
2334        let intervals = chord_type.intervals();
2335        let num_notes = intervals.len();
2336
2337        // Calculate inversion (0, 1, 2, or 3)
2338        let inversion = ((inversion_cv * num_notes as f64) as usize) % num_notes;
2339
2340        // Build chord voices
2341        let mut voices = [0.0f64; 4];
2342        for (i, voice) in voices.iter_mut().enumerate() {
2343            if i < num_notes {
2344                let interval_idx = (i + inversion) % num_notes;
2345                let semitones = intervals[interval_idx];
2346
2347                // Add octave if the interval wrapped around due to inversion
2348                let octave_offset = if i + inversion >= num_notes { 1.0 } else { 0.0 };
2349
2350                // Apply spread (voices spread across octaves)
2351                let spread_offset = spread * (i as f64 / 3.0);
2352
2353                // Convert semitones to V/Oct (1V = 1 octave, so 1 semitone = 1/12 V)
2354                *voice = root + semitones as f64 / 12.0 + octave_offset + spread_offset;
2355            } else {
2356                // For 3-note chords, duplicate the root an octave up for voice 4
2357                // Apply spread to the duplicated voice as well
2358                let spread_offset = spread * (i as f64 / 3.0);
2359                *voice = root + 1.0 + spread_offset;
2360            }
2361        }
2362
2363        outputs.set(10, voices[0]);
2364        outputs.set(11, voices[1]);
2365        outputs.set(12, voices[2]);
2366        outputs.set(13, voices[3]);
2367    }
2368
2369    fn reset(&mut self) {}
2370
2371    fn set_sample_rate(&mut self, _: f64) {}
2372
2373    fn type_id(&self) -> &'static str {
2374        "chord_memory"
2375    }
2376}
2377
2378// ============================================================================
2379// Planned Modules: ParametricEq
2380// ============================================================================
2381
2382/// Arpeggiator pattern types
2383#[derive(Debug, Clone, Copy, PartialEq)]
2384pub enum ArpPattern {
2385    /// Play notes ascending
2386    Up,
2387    /// Play notes descending
2388    Down,
2389    /// Play notes up then down
2390    UpDown,
2391    /// Play notes in random order
2392    Random,
2393}
2394
2395impl ArpPattern {
2396    /// Get pattern from CV (0-1 maps to 4 patterns)
2397    fn from_cv(cv: f64) -> Self {
2398        let cv = cv.clamp(0.0, 1.0);
2399        if cv < 0.25 {
2400            ArpPattern::Up
2401        } else if cv < 0.5 {
2402            ArpPattern::Down
2403        } else if cv < 0.75 {
2404            ArpPattern::UpDown
2405        } else {
2406            ArpPattern::Random
2407        }
2408    }
2409}
2410
2411/// Pattern-based arpeggiator
2412///
2413/// Captures held notes and plays them back in sequence on each clock pulse.
2414/// Supports multiple octave ranges and different playback patterns.
2415///
2416/// # Ports
2417/// - Input 0: V/Oct input note
2418/// - Input 1: Gate input (captures notes on rising edge)
2419/// - Input 2: Clock input (advances sequence)
2420/// - Input 3: Pattern select (0-1 CV maps to Up/Down/UpDown/Random)
2421/// - Input 4: Octave range (0-1 CV maps to 1-4 octaves)
2422/// - Input 5: Reset input (gate)
2423/// - Output 10: V/Oct output
2424/// - Output 11: Gate output
2425/// - Output 12: Trigger output (pulse on each step)
2426pub struct Arpeggiator {
2427    /// Held notes buffer (V/Oct values)
2428    held_notes: [f64; 8],
2429    /// Number of held notes
2430    num_notes: usize,
2431    /// Current step in sequence
2432    current_step: usize,
2433    /// Direction for up-down pattern (true = up)
2434    direction_up: bool,
2435    /// Previous gate state for edge detection
2436    prev_gate: f64,
2437    /// Note captured on the current gate's rising edge, removed on its falling
2438    /// edge so held notes are actually released (Q040).
2439    captured_note: Option<f64>,
2440    /// Previous clock state for edge detection
2441    prev_clock: f64,
2442    /// Previous reset state for edge detection
2443    prev_reset: f64,
2444    /// Random number generator
2445    rng: crate::rng::Rng,
2446    /// Output gate state
2447    gate_out: f64,
2448    /// Trigger countdown (samples remaining)
2449    trigger_countdown: usize,
2450    sample_rate: f64,
2451    spec: PortSpec,
2452}
2453
2454impl Arpeggiator {
2455    /// Trigger pulse length in ms
2456    const TRIGGER_MS: f64 = 1.0;
2457
2458    pub fn new(sample_rate: f64) -> Self {
2459        let spec = PortSpec {
2460            inputs: vec![
2461                PortDef::new(0, "v_oct", SignalKind::VoltPerOctave).with_default(0.0),
2462                PortDef::new(1, "gate", SignalKind::Gate).with_default(0.0),
2463                PortDef::new(2, "clock", SignalKind::Clock).with_default(0.0),
2464                PortDef::new(3, "pattern", SignalKind::CvUnipolar).with_default(0.0),
2465                PortDef::new(4, "octaves", SignalKind::CvUnipolar).with_default(0.0),
2466                PortDef::new(5, "reset", SignalKind::Gate).with_default(0.0),
2467            ],
2468            outputs: vec![
2469                PortDef::new(10, "v_oct_out", SignalKind::VoltPerOctave),
2470                PortDef::new(11, "gate_out", SignalKind::Gate),
2471                PortDef::new(12, "trigger", SignalKind::Trigger),
2472            ],
2473        };
2474
2475        Self {
2476            held_notes: [0.0; 8],
2477            num_notes: 0,
2478            current_step: 0,
2479            direction_up: true,
2480            prev_gate: 0.0,
2481            captured_note: None,
2482            prev_clock: 0.0,
2483            prev_reset: 0.0,
2484            rng: crate::rng::Rng::from_seed(42),
2485            gate_out: 0.0,
2486            trigger_countdown: 0,
2487            sample_rate,
2488            spec,
2489        }
2490    }
2491
2492    /// Add a note to the held notes buffer (keeps sorted)
2493    fn add_note(&mut self, note: f64) {
2494        if self.num_notes >= 8 {
2495            return;
2496        }
2497
2498        // Insert in sorted order
2499        let mut insert_pos = self.num_notes;
2500        for i in 0..self.num_notes {
2501            if note < self.held_notes[i] {
2502                insert_pos = i;
2503                break;
2504            }
2505        }
2506
2507        // Shift notes up
2508        for i in (insert_pos..self.num_notes).rev() {
2509            self.held_notes[i + 1] = self.held_notes[i];
2510        }
2511
2512        self.held_notes[insert_pos] = note;
2513        self.num_notes += 1;
2514    }
2515
2516    /// Remove a note from the held notes buffer
2517    pub fn remove_note(&mut self, note: f64) {
2518        // Find the note (with small tolerance for floating point)
2519        let mut found_idx = None;
2520        for i in 0..self.num_notes {
2521            if (self.held_notes[i] - note).abs() < 0.001 {
2522                found_idx = Some(i);
2523                break;
2524            }
2525        }
2526
2527        if let Some(idx) = found_idx {
2528            // Shift notes down
2529            for i in idx..self.num_notes - 1 {
2530                self.held_notes[i] = self.held_notes[i + 1];
2531            }
2532            self.num_notes -= 1;
2533        }
2534    }
2535
2536    /// Get the current note based on step and pattern
2537    fn get_current_note(&mut self, pattern: ArpPattern, octaves: usize) -> f64 {
2538        if self.num_notes == 0 {
2539            return 0.0;
2540        }
2541
2542        let total_steps = self.num_notes * octaves;
2543        let step = self.current_step % total_steps;
2544
2545        let note_idx = match pattern {
2546            ArpPattern::Up => step % self.num_notes,
2547            ArpPattern::Down => (self.num_notes - 1) - (step % self.num_notes),
2548            ArpPattern::UpDown => {
2549                // Calculate position in up-down cycle
2550                let cycle_len = if self.num_notes > 1 {
2551                    (self.num_notes - 1) * 2
2552                } else {
2553                    1
2554                };
2555                let pos = step % cycle_len;
2556                if pos < self.num_notes {
2557                    pos
2558                } else {
2559                    (self.num_notes - 1) * 2 - pos
2560                }
2561            }
2562            ArpPattern::Random => (self.rng.next_u64() as usize) % self.num_notes,
2563        };
2564
2565        let octave = step / self.num_notes;
2566        let base_note = self.held_notes[note_idx % self.num_notes];
2567
2568        base_note + octave as f64 // Add octave offset (1V per octave)
2569    }
2570}
2571
2572impl Default for Arpeggiator {
2573    fn default() -> Self {
2574        Self::new(44100.0)
2575    }
2576}
2577
2578impl GraphModule for Arpeggiator {
2579    fn port_spec(&self) -> &PortSpec {
2580        &self.spec
2581    }
2582
2583    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
2584        let v_oct = inputs.get_or(0, 0.0);
2585        let gate = inputs.get_or(1, 0.0);
2586        let clock = inputs.get_or(2, 0.0);
2587        let pattern_cv = inputs.get_or(3, 0.0);
2588        let octaves_cv = inputs.get_or(4, 0.0);
2589        let reset = inputs.get_or(5, 0.0);
2590
2591        let pattern = ArpPattern::from_cv(pattern_cv);
2592        let octaves = (1.0 + octaves_cv.clamp(0.0, 1.0) * 3.0) as usize; // 1-4 octaves
2593
2594        // Handle gate input (note capture/release).
2595        // A note is added on the gate's rising edge and released on its falling
2596        // edge (Q040), so the held set reflects the currently-held note rather
2597        // than growing monotonically to the 8-note cap.
2598        if gate > GATE_THRESHOLD_V && self.prev_gate <= GATE_THRESHOLD_V {
2599            // Rising edge - add note and remember it for the matching release.
2600            self.add_note(v_oct);
2601            self.captured_note = Some(v_oct);
2602        } else if gate <= GATE_THRESHOLD_V && self.prev_gate > GATE_THRESHOLD_V {
2603            // Falling edge - remove the note captured on the rising edge.
2604            if let Some(note) = self.captured_note.take() {
2605                self.remove_note(note);
2606            }
2607        }
2608        self.prev_gate = gate;
2609
2610        // Handle reset - also clears the held-note buffer (Q040).
2611        if reset > GATE_THRESHOLD_V && self.prev_reset <= GATE_THRESHOLD_V {
2612            self.current_step = 0;
2613            self.direction_up = true;
2614            self.held_notes = [0.0; 8];
2615            self.num_notes = 0;
2616            self.captured_note = None;
2617        }
2618        self.prev_reset = reset;
2619
2620        // Handle clock (advance sequence)
2621        let mut trigger_out = 0.0;
2622        let clock_rising =
2623            clock > GATE_THRESHOLD_V && self.prev_clock <= GATE_THRESHOLD_V && self.num_notes > 0;
2624
2625        if clock_rising {
2626            self.gate_out = GATE_HIGH_V;
2627            // Start trigger pulse
2628            self.trigger_countdown = (Self::TRIGGER_MS * self.sample_rate / 1000.0) as usize;
2629            trigger_out = GATE_HIGH_V;
2630        }
2631        self.prev_clock = clock;
2632
2633        // Update trigger
2634        if self.trigger_countdown > 0 {
2635            self.trigger_countdown -= 1;
2636            trigger_out = GATE_HIGH_V;
2637        }
2638
2639        // Gate follows clock (simplified - stays high while clock is high)
2640        if clock <= GATE_THRESHOLD_V {
2641            self.gate_out = 0.0;
2642        }
2643
2644        // Get current note
2645        let v_oct_out = if self.num_notes > 0 {
2646            self.get_current_note(pattern, octaves)
2647        } else {
2648            0.0
2649        };
2650
2651        // Advance step AFTER outputting current note
2652        if clock_rising {
2653            self.current_step += 1;
2654        }
2655
2656        outputs.set(10, v_oct_out);
2657        outputs.set(
2658            11,
2659            if self.num_notes > 0 {
2660                self.gate_out
2661            } else {
2662                0.0
2663            },
2664        );
2665        outputs.set(12, trigger_out);
2666    }
2667
2668    fn reset(&mut self) {
2669        self.held_notes = [0.0; 8];
2670        self.num_notes = 0;
2671        self.captured_note = None;
2672        self.current_step = 0;
2673        self.direction_up = true;
2674        self.prev_gate = 0.0;
2675        self.prev_clock = 0.0;
2676        self.prev_reset = 0.0;
2677        self.gate_out = 0.0;
2678        self.trigger_countdown = 0;
2679    }
2680
2681    fn set_sample_rate(&mut self, sample_rate: f64) {
2682        self.sample_rate = sample_rate;
2683    }
2684
2685    fn type_id(&self) -> &'static str {
2686        "arpeggiator"
2687    }
2688}
2689
2690// =============================================================================
2691// Reverb - Algorithmic Reverb (Freeverb Style)
2692// =============================================================================
2693
2694#[cfg(test)]
2695mod tests {
2696    use super::*;
2697    use crate::modules::common::SAFE_AUDIO_LIMIT;
2698
2699    #[test]
2700    fn test_mixer() {
2701        let mut mixer = Mixer::new(4);
2702        let mut inputs = PortValues::new();
2703        let mut outputs = PortValues::new();
2704
2705        inputs.set(0, 1.0);
2706        inputs.set(1, 2.0);
2707        inputs.set(2, 3.0);
2708        inputs.set(3, 4.0);
2709
2710        mixer.tick(&inputs, &mut outputs);
2711
2712        let out = outputs.get(100).unwrap();
2713        assert!((out - 10.0).abs() < 0.01);
2714    }
2715    #[test]
2716    fn test_step_sequencer() {
2717        let mut seq = StepSequencer::new();
2718        seq.set_step(0, 0.0, true);
2719        seq.set_step(1, 0.5, true);
2720        seq.set_step(2, 1.0, true);
2721
2722        let mut inputs = PortValues::new();
2723        let mut outputs = PortValues::new();
2724
2725        // Initial state
2726        seq.tick(&inputs, &mut outputs);
2727        assert!((outputs.get(10).unwrap() - 0.0).abs() < 0.01);
2728
2729        // Clock rising edge
2730        inputs.set(0, 5.0);
2731        seq.tick(&inputs, &mut outputs);
2732        assert!((outputs.get(10).unwrap() - 0.5).abs() < 0.01);
2733
2734        // Clock falling edge, then rising again
2735        inputs.set(0, 0.0);
2736        seq.tick(&inputs, &mut outputs);
2737        inputs.set(0, 5.0);
2738        seq.tick(&inputs, &mut outputs);
2739        assert!((outputs.get(10).unwrap() - 1.0).abs() < 0.01);
2740    }
2741    #[test]
2742    fn test_sample_and_hold() {
2743        let mut sh = SampleAndHold::new();
2744        let mut inputs = PortValues::new();
2745        let mut outputs = PortValues::new();
2746
2747        // Set input value, no trigger
2748        inputs.set(0, 3.0);
2749        inputs.set(1, 0.0);
2750        sh.tick(&inputs, &mut outputs);
2751        // Initial held value should be 0
2752        assert!((outputs.get(10).unwrap() - 0.0).abs() < 0.01);
2753
2754        // Trigger rising edge - should sample input
2755        inputs.set(1, 5.0);
2756        sh.tick(&inputs, &mut outputs);
2757        assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01);
2758
2759        // Change input, but no new trigger - should hold previous value
2760        inputs.set(0, 7.0);
2761        sh.tick(&inputs, &mut outputs);
2762        assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01);
2763
2764        // New trigger - should sample new value
2765        inputs.set(1, 0.0);
2766        sh.tick(&inputs, &mut outputs);
2767        inputs.set(1, 5.0);
2768        sh.tick(&inputs, &mut outputs);
2769        assert!((outputs.get(10).unwrap() - 7.0).abs() < 0.01);
2770    }
2771    #[test]
2772    fn test_slew_limiter() {
2773        let mut slew = SlewLimiter::new(1000.0); // 1kHz sample rate
2774        let mut inputs = PortValues::new();
2775        let mut outputs = PortValues::new();
2776
2777        // Set rise/fall rates (normalized 0-1)
2778        inputs.set(1, 0.5); // Rise rate
2779        inputs.set(2, 0.5); // Fall rate
2780
2781        // Step input from 0 to 5V
2782        inputs.set(0, 5.0);
2783        slew.tick(&inputs, &mut outputs);
2784        let first = outputs.get(10).unwrap();
2785
2786        // Should start rising but not instantly reach target
2787        assert!(first > 0.0);
2788        assert!(first < 5.0);
2789
2790        // Continue rising
2791        for _ in 0..100 {
2792            slew.tick(&inputs, &mut outputs);
2793        }
2794        // Should be close to target now
2795        let after_100 = outputs.get(10).unwrap();
2796        assert!(after_100 > first);
2797    }
2798    #[test]
2799    fn test_quantizer_chromatic() {
2800        let mut quant = Quantizer::new(Scale::Chromatic);
2801        let mut inputs = PortValues::new();
2802        let mut outputs = PortValues::new();
2803
2804        // Exactly on a note
2805        inputs.set(0, 0.0); // C
2806        quant.tick(&inputs, &mut outputs);
2807        assert!((outputs.get(10).unwrap() - 0.0).abs() < 0.01);
2808
2809        // Between C and C#
2810        inputs.set(0, 0.04); // 1/25 of a semitone above C
2811        quant.tick(&inputs, &mut outputs);
2812        // Should quantize to C (0.0)
2813        assert!((outputs.get(10).unwrap() - 0.0).abs() < 0.01);
2814
2815        // Closer to C#
2816        inputs.set(0, 0.07);
2817        quant.tick(&inputs, &mut outputs);
2818        // Should quantize to C# (1/12 = 0.0833...)
2819        let expected_csharp = 1.0 / 12.0;
2820        assert!((outputs.get(10).unwrap() - expected_csharp).abs() < 0.01);
2821    }
2822    #[test]
2823    fn test_quantizer_major_scale() {
2824        let mut quant = Quantizer::new(Scale::Major);
2825        let mut inputs = PortValues::new();
2826        let mut outputs = PortValues::new();
2827
2828        // C# (1 semitone) should snap to C or D
2829        inputs.set(0, 1.0 / 12.0); // C#
2830        quant.tick(&inputs, &mut outputs);
2831        let out = outputs.get(10).unwrap();
2832        // Should be C (0) or D (2/12)
2833        assert!(out.abs() < 0.01 || (out - 2.0 / 12.0).abs() < 0.01);
2834    }
2835    #[test]
2836    fn test_clock() {
2837        let mut clock = Clock::new(1000.0); // 1kHz sample rate
2838        let mut inputs = PortValues::new();
2839        let mut outputs = PortValues::new();
2840
2841        // Set tempo CV: 10V maps to 300 BPM (5 Hz), so 200 samples per beat
2842        inputs.set(0, 10.0); // Maximum tempo
2843
2844        let mut trigger_count = 0;
2845        let mut last_trigger = 0.0;
2846
2847        for _ in 0..1000 {
2848            clock.tick(&inputs, &mut outputs);
2849            let trigger = outputs.get(10).unwrap(); // Main clock output
2850            if trigger > 2.5 && last_trigger <= 2.5 {
2851                trigger_count += 1;
2852            }
2853            last_trigger = trigger;
2854        }
2855
2856        // At 300 BPM (5 Hz), should get ~5 triggers per second
2857        // In 1000 samples at 1kHz, that's 5 triggers
2858        assert!(trigger_count >= 3);
2859    }
2860    #[test]
2861    fn test_attenuverter() {
2862        let mut att = Attenuverter::new();
2863        let mut inputs = PortValues::new();
2864        let mut outputs = PortValues::new();
2865
2866        // Test unity gain (5V = unity in 0-10V range)
2867        inputs.set(0, 5.0); // Input
2868        inputs.set(1, 5.0); // 5V = unity (1.0 multiplier)
2869        att.tick(&inputs, &mut outputs);
2870        assert!((outputs.get(10).unwrap() - 5.0).abs() < 0.1);
2871
2872        // Test half attenuation (2.5V = 0.5 multiplier)
2873        inputs.set(1, 2.5);
2874        att.tick(&inputs, &mut outputs);
2875        assert!((outputs.get(10).unwrap() - 2.5).abs() < 0.1);
2876
2877        // Test zero (0V = 0 multiplier)
2878        inputs.set(1, 0.0);
2879        att.tick(&inputs, &mut outputs);
2880        assert!((outputs.get(10).unwrap() - 0.0).abs() < 0.1);
2881    }
2882    #[test]
2883    fn test_multiple() {
2884        let mut mult = Multiple::new();
2885        let mut inputs = PortValues::new();
2886        let mut outputs = PortValues::new();
2887
2888        inputs.set(0, 3.5);
2889        mult.tick(&inputs, &mut outputs);
2890
2891        // All 4 outputs should have the same value
2892        assert!((outputs.get(10).unwrap() - 3.5).abs() < 0.0001);
2893        assert!((outputs.get(11).unwrap() - 3.5).abs() < 0.0001);
2894        assert!((outputs.get(12).unwrap() - 3.5).abs() < 0.0001);
2895        assert!((outputs.get(13).unwrap() - 3.5).abs() < 0.0001);
2896    }
2897    #[test]
2898    fn test_crossfader() {
2899        let mut xf = Crossfader::new();
2900        let mut inputs = PortValues::new();
2901        let mut outputs = PortValues::new();
2902
2903        inputs.set(0, 5.0); // A
2904        inputs.set(1, -5.0); // B
2905
2906        // Full A (pos = -5V)
2907        inputs.set(2, -5.0);
2908        xf.tick(&inputs, &mut outputs);
2909        assert!((outputs.get(10).unwrap() - 5.0).abs() < 0.1);
2910
2911        // Full B (pos = +5V)
2912        inputs.set(2, 5.0);
2913        xf.tick(&inputs, &mut outputs);
2914        assert!((outputs.get(10).unwrap() - (-5.0)).abs() < 0.1);
2915
2916        // Center (pos = 0V): equal mix
2917        inputs.set(2, 0.0);
2918        xf.tick(&inputs, &mut outputs);
2919        // Equal power mix at center
2920        let out = outputs.get(10).unwrap();
2921        assert!(out.abs() < 1.0); // Should be near zero (equal mix of +5 and -5)
2922    }
2923    #[test]
2924    fn test_logic_and() {
2925        let mut gate = LogicAnd::new();
2926        let mut inputs = PortValues::new();
2927        let mut outputs = PortValues::new();
2928
2929        // Both low
2930        inputs.set(0, 0.0);
2931        inputs.set(1, 0.0);
2932        gate.tick(&inputs, &mut outputs);
2933        assert!(outputs.get(10).unwrap() < 2.5);
2934
2935        // One high
2936        inputs.set(0, 5.0);
2937        inputs.set(1, 0.0);
2938        gate.tick(&inputs, &mut outputs);
2939        assert!(outputs.get(10).unwrap() < 2.5);
2940
2941        // Both high
2942        inputs.set(0, 5.0);
2943        inputs.set(1, 5.0);
2944        gate.tick(&inputs, &mut outputs);
2945        assert!(outputs.get(10).unwrap() > 2.5);
2946    }
2947    #[test]
2948    fn test_logic_or() {
2949        let mut gate = LogicOr::new();
2950        let mut inputs = PortValues::new();
2951        let mut outputs = PortValues::new();
2952
2953        // Both low
2954        inputs.set(0, 0.0);
2955        inputs.set(1, 0.0);
2956        gate.tick(&inputs, &mut outputs);
2957        assert!(outputs.get(10).unwrap() < 2.5);
2958
2959        // One high
2960        inputs.set(0, 5.0);
2961        inputs.set(1, 0.0);
2962        gate.tick(&inputs, &mut outputs);
2963        assert!(outputs.get(10).unwrap() > 2.5);
2964
2965        // Both high
2966        inputs.set(0, 5.0);
2967        inputs.set(1, 5.0);
2968        gate.tick(&inputs, &mut outputs);
2969        assert!(outputs.get(10).unwrap() > 2.5);
2970    }
2971    #[test]
2972    fn test_logic_xor() {
2973        let mut gate = LogicXor::new();
2974        let mut inputs = PortValues::new();
2975        let mut outputs = PortValues::new();
2976
2977        // Both low
2978        inputs.set(0, 0.0);
2979        inputs.set(1, 0.0);
2980        gate.tick(&inputs, &mut outputs);
2981        assert!(outputs.get(10).unwrap() < 2.5);
2982
2983        // One high
2984        inputs.set(0, 5.0);
2985        inputs.set(1, 0.0);
2986        gate.tick(&inputs, &mut outputs);
2987        assert!(outputs.get(10).unwrap() > 2.5);
2988
2989        // Both high
2990        inputs.set(0, 5.0);
2991        inputs.set(1, 5.0);
2992        gate.tick(&inputs, &mut outputs);
2993        assert!(outputs.get(10).unwrap() < 2.5);
2994    }
2995    #[test]
2996    fn test_logic_not() {
2997        let mut gate = LogicNot::new();
2998        let mut inputs = PortValues::new();
2999        let mut outputs = PortValues::new();
3000
3001        // Low input
3002        inputs.set(0, 0.0);
3003        gate.tick(&inputs, &mut outputs);
3004        assert!(outputs.get(10).unwrap() > 2.5);
3005
3006        // High input
3007        inputs.set(0, 5.0);
3008        gate.tick(&inputs, &mut outputs);
3009        assert!(outputs.get(10).unwrap() < 2.5);
3010    }
3011    #[test]
3012    fn test_comparator() {
3013        let mut cmp = Comparator::new();
3014        let mut inputs = PortValues::new();
3015        let mut outputs = PortValues::new();
3016
3017        // A > B
3018        inputs.set(0, 3.0);
3019        inputs.set(1, 1.0);
3020        cmp.tick(&inputs, &mut outputs);
3021        assert!(outputs.get(10).unwrap() > 2.5); // gt
3022        assert!(outputs.get(11).unwrap() < 2.5); // lt
3023        assert!(outputs.get(12).unwrap() < 2.5); // eq
3024
3025        // A < B
3026        inputs.set(0, 1.0);
3027        inputs.set(1, 3.0);
3028        cmp.tick(&inputs, &mut outputs);
3029        assert!(outputs.get(10).unwrap() < 2.5); // gt
3030        assert!(outputs.get(11).unwrap() > 2.5); // lt
3031        assert!(outputs.get(12).unwrap() < 2.5); // eq
3032
3033        // A ≈ B
3034        inputs.set(0, 2.0);
3035        inputs.set(1, 2.0);
3036        cmp.tick(&inputs, &mut outputs);
3037        assert!(outputs.get(10).unwrap() < 2.5); // gt
3038        assert!(outputs.get(11).unwrap() < 2.5); // lt
3039        assert!(outputs.get(12).unwrap() > 2.5); // eq
3040    }
3041    #[test]
3042    fn test_rectifier() {
3043        let mut rect = Rectifier::new();
3044        let mut inputs = PortValues::new();
3045        let mut outputs = PortValues::new();
3046
3047        // Positive input
3048        inputs.set(0, 3.0);
3049        rect.tick(&inputs, &mut outputs);
3050        assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01); // full
3051        assert!((outputs.get(11).unwrap() - 3.0).abs() < 0.01); // half_pos
3052        assert!((outputs.get(12).unwrap()).abs() < 0.01); // half_neg
3053
3054        // Negative input
3055        inputs.set(0, -3.0);
3056        rect.tick(&inputs, &mut outputs);
3057        assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01); // full (abs)
3058        assert!((outputs.get(11).unwrap()).abs() < 0.01); // half_pos
3059        assert!((outputs.get(12).unwrap() - 3.0).abs() < 0.01); // half_neg (inverted)
3060    }
3061    #[test]
3062    fn test_precision_adder() {
3063        let mut adder = PrecisionAdder::new();
3064        let mut inputs = PortValues::new();
3065        let mut outputs = PortValues::new();
3066
3067        inputs.set(0, 1.0);
3068        inputs.set(1, 2.0);
3069        inputs.set(2, 0.5);
3070        inputs.set(3, -0.5);
3071        adder.tick(&inputs, &mut outputs);
3072
3073        assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01); // sum
3074        assert!((outputs.get(11).unwrap() - (-3.0)).abs() < 0.01); // inverted
3075    }
3076    #[test]
3077    fn test_vc_switch() {
3078        let mut sw = VcSwitch::new();
3079        let mut inputs = PortValues::new();
3080        let mut outputs = PortValues::new();
3081
3082        inputs.set(0, 3.0); // A
3083        inputs.set(1, 7.0); // B
3084
3085        // CV low: select A
3086        inputs.set(2, 0.0);
3087        sw.tick(&inputs, &mut outputs);
3088        assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01);
3089        assert!((outputs.get(11).unwrap() - 3.0).abs() < 0.01);
3090        assert!((outputs.get(12).unwrap()).abs() < 0.01);
3091
3092        // CV high: select B
3093        inputs.set(2, 5.0);
3094        sw.tick(&inputs, &mut outputs);
3095        assert!((outputs.get(10).unwrap() - 7.0).abs() < 0.01);
3096        assert!((outputs.get(11).unwrap()).abs() < 0.01);
3097        assert!((outputs.get(12).unwrap() - 7.0).abs() < 0.01);
3098    }
3099    #[test]
3100    fn test_bernoulli_gate() {
3101        let mut bg = BernoulliGate::new();
3102        let mut inputs = PortValues::new();
3103        let mut outputs = PortValues::new();
3104
3105        // Set probability to 100%
3106        inputs.set(1, 10.0);
3107
3108        // Trigger rising edge
3109        inputs.set(0, 0.0);
3110        bg.tick(&inputs, &mut outputs);
3111        inputs.set(0, 5.0);
3112        bg.tick(&inputs, &mut outputs);
3113
3114        // At 100% prob, should always go to A
3115        assert!(outputs.get(10).unwrap() > 2.5); // trig_a
3116        assert!(outputs.get(11).unwrap() < 2.5); // trig_b
3117
3118        // Reset and test 0% probability
3119        bg.reset();
3120        inputs.set(1, 0.0);
3121        inputs.set(0, 0.0);
3122        bg.tick(&inputs, &mut outputs);
3123        inputs.set(0, 5.0);
3124        bg.tick(&inputs, &mut outputs);
3125
3126        // At 0% prob, should always go to B
3127        assert!(outputs.get(10).unwrap() < 2.5); // trig_a
3128        assert!(outputs.get(11).unwrap() > 2.5); // trig_b
3129    }
3130    #[test]
3131    fn test_min() {
3132        let mut m = Min::new();
3133        let mut inputs = PortValues::new();
3134        let mut outputs = PortValues::new();
3135
3136        inputs.set(0, 3.0);
3137        inputs.set(1, 5.0);
3138        m.tick(&inputs, &mut outputs);
3139        assert!((outputs.get(10).unwrap() - 3.0).abs() < 0.01);
3140
3141        inputs.set(0, 7.0);
3142        inputs.set(1, 2.0);
3143        m.tick(&inputs, &mut outputs);
3144        assert!((outputs.get(10).unwrap() - 2.0).abs() < 0.01);
3145    }
3146    #[test]
3147    fn test_max() {
3148        let mut m = Max::new();
3149        let mut inputs = PortValues::new();
3150        let mut outputs = PortValues::new();
3151
3152        inputs.set(0, 3.0);
3153        inputs.set(1, 5.0);
3154        m.tick(&inputs, &mut outputs);
3155        assert!((outputs.get(10).unwrap() - 5.0).abs() < 0.01);
3156
3157        inputs.set(0, 7.0);
3158        inputs.set(1, 2.0);
3159        m.tick(&inputs, &mut outputs);
3160        assert!((outputs.get(10).unwrap() - 7.0).abs() < 0.01);
3161    }
3162    #[test]
3163    fn test_mixer_default_reset_sample_rate() {
3164        let mut mixer = Mixer::default();
3165        mixer.reset();
3166        mixer.set_sample_rate(48000.0);
3167        assert_eq!(mixer.type_id(), "mixer");
3168    }
3169    #[test]
3170    fn test_stereo_output_default_reset_sample_rate() {
3171        let mut stereo = StereoOutput::default();
3172        stereo.reset();
3173        stereo.set_sample_rate(48000.0);
3174        assert_eq!(stereo.type_id(), "stereo_output");
3175    }
3176    #[test]
3177    fn test_offset_default_reset_sample_rate() {
3178        let mut offset = Offset::default();
3179        offset.reset();
3180        offset.set_sample_rate(48000.0);
3181        assert_eq!(offset.type_id(), "offset");
3182    }
3183    #[test]
3184    fn test_scale_enum_semitones() {
3185        let scale = Scale::Chromatic;
3186        assert!(scale.semitones().len() == 12);
3187
3188        let scale = Scale::Major;
3189        assert!(scale.semitones().len() == 7);
3190
3191        let scale = Scale::PentatonicMajor;
3192        assert!(scale.semitones().len() == 5);
3193    }
3194    #[test]
3195    fn test_step_sequencer_default_reset_sample_rate() {
3196        let mut seq = StepSequencer::default();
3197        seq.set_step(0, 1.0, true);
3198        let mut inputs = PortValues::new();
3199        let mut outputs = PortValues::new();
3200        inputs.set(0, 5.0);
3201        seq.tick(&inputs, &mut outputs);
3202
3203        seq.reset();
3204        assert!(seq.current == 0);
3205        assert!(seq.prev_clock == 0.0);
3206
3207        seq.set_sample_rate(48000.0);
3208        assert_eq!(seq.type_id(), "step_sequencer");
3209    }
3210    #[test]
3211    fn test_sample_and_hold_default_reset_sample_rate() {
3212        let mut sh = SampleAndHold::default();
3213        let mut inputs = PortValues::new();
3214        let mut outputs = PortValues::new();
3215        inputs.set(0, 5.0);
3216        inputs.set(1, 5.0);
3217        sh.tick(&inputs, &mut outputs);
3218
3219        sh.reset();
3220        assert!(sh.held_value == 0.0);
3221
3222        sh.set_sample_rate(48000.0);
3223        assert_eq!(sh.type_id(), "sample_hold");
3224    }
3225    #[test]
3226    fn test_slew_limiter_default_reset_sample_rate() {
3227        let mut slew = SlewLimiter::default();
3228        assert!(slew.sample_rate == 44100.0);
3229
3230        let mut inputs = PortValues::new();
3231        let mut outputs = PortValues::new();
3232        inputs.set(0, 5.0);
3233        slew.tick(&inputs, &mut outputs);
3234
3235        slew.reset();
3236        assert!(slew.current == 0.0);
3237
3238        slew.set_sample_rate(48000.0);
3239        assert!(slew.sample_rate == 48000.0);
3240
3241        assert_eq!(slew.type_id(), "slew_limiter");
3242    }
3243    #[test]
3244    fn test_quantizer_default_reset_sample_rate() {
3245        let mut quant = Quantizer::default();
3246        quant.reset();
3247        quant.set_sample_rate(48000.0);
3248        assert_eq!(quant.type_id(), "quantizer");
3249    }
3250    #[test]
3251    fn test_clock_default_reset_sample_rate() {
3252        let mut clock = Clock::default();
3253        assert!(clock.sample_rate == 44100.0);
3254
3255        let inputs = PortValues::new();
3256        let mut outputs = PortValues::new();
3257        for _ in 0..100 {
3258            clock.tick(&inputs, &mut outputs);
3259        }
3260
3261        clock.reset();
3262        assert!(clock.phase == 0.0);
3263
3264        clock.set_sample_rate(48000.0);
3265        assert!(clock.sample_rate == 48000.0);
3266
3267        assert_eq!(clock.type_id(), "clock");
3268    }
3269    #[test]
3270    fn test_attenuverter_default_reset_sample_rate() {
3271        let mut att = Attenuverter::default();
3272        att.reset();
3273        att.set_sample_rate(48000.0);
3274        assert_eq!(att.type_id(), "attenuverter");
3275    }
3276    #[test]
3277    fn test_multiple_default_reset_sample_rate() {
3278        let mut mult = Multiple::default();
3279        mult.reset();
3280        mult.set_sample_rate(48000.0);
3281        assert_eq!(mult.type_id(), "multiple");
3282    }
3283    #[test]
3284    fn test_crossfader_default_reset_sample_rate() {
3285        let mut xf = Crossfader::default();
3286        xf.reset();
3287        xf.set_sample_rate(48000.0);
3288        assert_eq!(xf.type_id(), "crossfader");
3289    }
3290    #[test]
3291    fn test_logic_and_default_reset_sample_rate() {
3292        let mut gate = LogicAnd::default();
3293        gate.reset();
3294        gate.set_sample_rate(48000.0);
3295        assert_eq!(gate.type_id(), "logic_and");
3296    }
3297    #[test]
3298    fn test_logic_or_default_reset_sample_rate() {
3299        let mut gate = LogicOr::default();
3300        gate.reset();
3301        gate.set_sample_rate(48000.0);
3302        assert_eq!(gate.type_id(), "logic_or");
3303    }
3304    #[test]
3305    fn test_logic_xor_default_reset_sample_rate() {
3306        let mut gate = LogicXor::default();
3307        gate.reset();
3308        gate.set_sample_rate(48000.0);
3309        assert_eq!(gate.type_id(), "logic_xor");
3310    }
3311    #[test]
3312    fn test_logic_not_default_reset_sample_rate() {
3313        let mut gate = LogicNot::default();
3314        gate.reset();
3315        gate.set_sample_rate(48000.0);
3316        assert_eq!(gate.type_id(), "logic_not");
3317    }
3318    #[test]
3319    fn test_comparator_default_reset_sample_rate() {
3320        let mut cmp = Comparator::default();
3321        cmp.reset();
3322        cmp.set_sample_rate(48000.0);
3323        assert_eq!(cmp.type_id(), "comparator");
3324    }
3325    #[test]
3326    fn test_rectifier_default_reset_sample_rate() {
3327        let mut rect = Rectifier::default();
3328        rect.reset();
3329        rect.set_sample_rate(48000.0);
3330        assert_eq!(rect.type_id(), "rectifier");
3331    }
3332    #[test]
3333    fn test_precision_adder_default_reset_sample_rate() {
3334        let mut adder = PrecisionAdder::default();
3335        adder.reset();
3336        adder.set_sample_rate(48000.0);
3337        assert_eq!(adder.type_id(), "precision_adder");
3338    }
3339    #[test]
3340    fn test_vc_switch_default_reset_sample_rate() {
3341        let mut sw = VcSwitch::default();
3342        sw.reset();
3343        sw.set_sample_rate(48000.0);
3344        assert_eq!(sw.type_id(), "vc_switch");
3345    }
3346    #[test]
3347    fn test_bernoulli_gate_default_reset_sample_rate() {
3348        let mut bg = BernoulliGate::default();
3349        let mut inputs = PortValues::new();
3350        let mut outputs = PortValues::new();
3351        inputs.set(0, 5.0);
3352        bg.tick(&inputs, &mut outputs);
3353
3354        bg.reset();
3355        assert!(bg.prev_trigger == 0.0);
3356
3357        bg.set_sample_rate(48000.0);
3358        assert_eq!(bg.type_id(), "bernoulli_gate");
3359    }
3360    #[test]
3361    fn test_min_default_reset_sample_rate() {
3362        let mut m = Min::default();
3363        m.reset();
3364        m.set_sample_rate(48000.0);
3365        assert_eq!(m.type_id(), "min");
3366    }
3367    #[test]
3368    fn test_max_default_reset_sample_rate() {
3369        let mut m = Max::default();
3370        m.reset();
3371        m.set_sample_rate(48000.0);
3372        assert_eq!(m.type_id(), "max");
3373    }
3374    #[test]
3375    fn test_step_sequencer_skip_disabled() {
3376        let mut seq = StepSequencer::new();
3377        seq.set_step(0, 1.0, true);
3378        seq.set_step(1, 2.0, false); // Disabled step
3379        seq.set_step(2, 3.0, true);
3380
3381        let mut inputs = PortValues::new();
3382        let mut outputs = PortValues::new();
3383
3384        // Initial step
3385        seq.tick(&inputs, &mut outputs);
3386        let _out = outputs.get(10).unwrap_or(0.0);
3387
3388        // Clock to next step
3389        inputs.set(0, 5.0);
3390        seq.tick(&inputs, &mut outputs);
3391    }
3392    #[test]
3393    fn test_quantizer_pentatonic_scale() {
3394        let mut quant = Quantizer::new(Scale::PentatonicMajor);
3395        let mut inputs = PortValues::new();
3396        let mut outputs = PortValues::new();
3397
3398        // Pentatonic scale has notes: 0, 2, 4, 7, 9 semitones
3399        inputs.set(0, 0.0);
3400        quant.tick(&inputs, &mut outputs);
3401        assert!(outputs.get(10).unwrap().abs() < 0.01);
3402    }
3403    #[test]
3404    fn test_quantizer_blues_scale() {
3405        let mut quant = Quantizer::new(Scale::Blues);
3406        let mut inputs = PortValues::new();
3407        let mut outputs = PortValues::new();
3408
3409        inputs.set(0, 0.0);
3410        quant.tick(&inputs, &mut outputs);
3411        assert!(outputs.get(10).is_some());
3412    }
3413    #[test]
3414    fn test_slew_limiter_falling() {
3415        let mut slew = SlewLimiter::new(1000.0);
3416        let mut inputs = PortValues::new();
3417        let mut outputs = PortValues::new();
3418
3419        // First, set to high value
3420        inputs.set(0, 5.0);
3421        inputs.set(1, 10.0); // Fast rise
3422        inputs.set(2, 0.5); // Slower fall
3423        for _ in 0..1000 {
3424            slew.tick(&inputs, &mut outputs);
3425        }
3426
3427        // Now set to low value and observe falling behavior
3428        inputs.set(0, 0.0);
3429        slew.tick(&inputs, &mut outputs);
3430        let falling = outputs.get(10).unwrap();
3431        assert!(falling < 5.0);
3432        assert!(falling > 0.0);
3433    }
3434    #[test]
3435    fn test_scale_dorian_and_mixolydian() {
3436        let scale = Scale::Dorian;
3437        assert!(scale.semitones().len() == 7);
3438
3439        let scale = Scale::Mixolydian;
3440        assert!(scale.semitones().len() == 7);
3441    }
3442    #[test]
3443    fn test_clock_subdivisions() {
3444        let mut clock = Clock::new(1000.0);
3445        let mut inputs = PortValues::new();
3446        let mut outputs = PortValues::new();
3447
3448        inputs.set(0, 5.0); // Medium tempo
3449
3450        // Run and check all outputs exist
3451        for _ in 0..1000 {
3452            clock.tick(&inputs, &mut outputs);
3453        }
3454
3455        // Should have all clock subdivision outputs
3456        assert!(outputs.get(10).is_some()); // Main
3457        assert!(outputs.get(11).is_some()); // /2
3458        assert!(outputs.get(12).is_some()); // /4
3459    }
3460    #[test]
3461    fn test_chord_memory_major() {
3462        let mut cm = ChordMemory::new();
3463        let mut inputs = PortValues::new();
3464        let mut outputs = PortValues::new();
3465
3466        // Root at C4 (0V), major chord (cv=0)
3467        inputs.set(0, 0.0);
3468        inputs.set(1, 0.0); // Major
3469        inputs.set(2, 0.0); // No inversion
3470        inputs.set(3, 0.0); // No spread
3471
3472        cm.tick(&inputs, &mut outputs);
3473
3474        // Major chord: root, major 3rd (+4 semitones), perfect 5th (+7 semitones)
3475        let voice1 = outputs.get(10).unwrap();
3476        let voice2 = outputs.get(11).unwrap();
3477        let voice3 = outputs.get(12).unwrap();
3478        let voice4 = outputs.get(13).unwrap();
3479
3480        assert!((voice1 - 0.0).abs() < 0.01); // Root (C)
3481        assert!((voice2 - 4.0 / 12.0).abs() < 0.01); // Major 3rd (E)
3482        assert!((voice3 - 7.0 / 12.0).abs() < 0.01); // Perfect 5th (G)
3483        assert!((voice4 - 1.0).abs() < 0.01); // Octave (for 3-note chord, voice4 = root+1)
3484    }
3485    #[test]
3486    fn test_chord_memory_minor() {
3487        let mut cm = ChordMemory::new();
3488        let mut inputs = PortValues::new();
3489        let mut outputs = PortValues::new();
3490
3491        inputs.set(0, 0.0);
3492        inputs.set(1, 0.15); // Minor (second chord type, cv ~0.111-0.222)
3493
3494        cm.tick(&inputs, &mut outputs);
3495
3496        // Minor chord: root, minor 3rd (+3 semitones), perfect 5th (+7 semitones)
3497        let voice2 = outputs.get(11).unwrap();
3498        assert!((voice2 - 3.0 / 12.0).abs() < 0.01); // Minor 3rd (Eb)
3499    }
3500    #[test]
3501    fn test_chord_memory_seventh() {
3502        let mut cm = ChordMemory::new();
3503        let mut inputs = PortValues::new();
3504        let mut outputs = PortValues::new();
3505
3506        inputs.set(0, 0.0);
3507        inputs.set(1, 0.26); // Dominant 7th (cv ~0.222-0.333)
3508
3509        cm.tick(&inputs, &mut outputs);
3510
3511        // Dom7 chord: root, major 3rd, perfect 5th, minor 7th (+10 semitones)
3512        let voice4 = outputs.get(13).unwrap();
3513        assert!((voice4 - 10.0 / 12.0).abs() < 0.01); // Minor 7th (Bb)
3514    }
3515    #[test]
3516    fn test_chord_memory_inversion() {
3517        let mut cm = ChordMemory::new();
3518        let mut inputs = PortValues::new();
3519        let mut outputs = PortValues::new();
3520
3521        inputs.set(0, 0.0);
3522        inputs.set(1, 0.0); // Major
3523        inputs.set(2, 0.4); // First inversion (for 3-note chord: ~1/3)
3524
3525        cm.tick(&inputs, &mut outputs);
3526
3527        // First inversion: E in bass, G, C (octave up)
3528        let voice1 = outputs.get(10).unwrap();
3529        let voice2 = outputs.get(11).unwrap();
3530        let voice3 = outputs.get(12).unwrap();
3531
3532        // Voice 1 should be the 3rd (4 semitones = major 3rd)
3533        assert!((voice1 - 4.0 / 12.0).abs() < 0.01);
3534        // Voice 2 should be the 5th (7 semitones)
3535        assert!((voice2 - 7.0 / 12.0).abs() < 0.01);
3536        // Voice 3 should be root + octave (wrapped)
3537        assert!((voice3 - 1.0).abs() < 0.01);
3538    }
3539    #[test]
3540    fn test_chord_memory_spread() {
3541        let mut cm = ChordMemory::new();
3542        let mut inputs = PortValues::new();
3543        let mut outputs = PortValues::new();
3544
3545        inputs.set(0, 0.0);
3546        inputs.set(1, 0.0); // Major
3547        inputs.set(2, 0.0); // No inversion
3548        inputs.set(3, 1.0); // Full spread
3549
3550        cm.tick(&inputs, &mut outputs);
3551
3552        let voice1 = outputs.get(10).unwrap();
3553        let voice2 = outputs.get(11).unwrap();
3554        let voice3 = outputs.get(12).unwrap();
3555        let voice4 = outputs.get(13).unwrap();
3556
3557        // With spread=1.0, voice4 should be ~1 octave higher than without spread
3558        // voice1: 0 + 0/3 = 0
3559        // voice2: 4/12 + 1/3 ≈ 0.666
3560        // voice3: 7/12 + 2/3 ≈ 1.25
3561        // voice4: 1.0 + 1.0 = 2.0 (for 3-note chord)
3562        assert!(voice1 < voice2);
3563        assert!(voice2 < voice3);
3564        assert!(voice3 < voice4);
3565    }
3566    #[test]
3567    fn test_chord_memory_all_chord_types() {
3568        let mut cm = ChordMemory::new();
3569        let mut inputs = PortValues::new();
3570        let mut outputs = PortValues::new();
3571
3572        // Test all 9 chord types produce valid output
3573        for i in 0..9 {
3574            let chord_cv = i as f64 / 9.0;
3575            inputs.set(0, 0.0);
3576            inputs.set(1, chord_cv);
3577
3578            cm.tick(&inputs, &mut outputs);
3579
3580            // All voices should have valid output
3581            assert!(outputs.get(10).is_some());
3582            assert!(outputs.get(11).is_some());
3583            assert!(outputs.get(12).is_some());
3584            assert!(outputs.get(13).is_some());
3585        }
3586    }
3587    #[test]
3588    fn test_chord_memory_default_reset_sample_rate() {
3589        let mut cm = ChordMemory::default();
3590        cm.reset();
3591        cm.set_sample_rate(48000.0);
3592        assert_eq!(cm.type_id(), "chord_memory");
3593
3594        // Verify port spec
3595        assert_eq!(cm.port_spec().inputs.len(), 4);
3596        assert_eq!(cm.port_spec().outputs.len(), 4);
3597    }
3598    #[test]
3599    fn test_chord_type_intervals() {
3600        // Test that all chord types return valid intervals
3601        assert_eq!(ChordType::Major.intervals(), &[0, 4, 7]);
3602        assert_eq!(ChordType::Minor.intervals(), &[0, 3, 7]);
3603        assert_eq!(ChordType::Seventh.intervals(), &[0, 4, 7, 10]);
3604        assert_eq!(ChordType::MajorSeventh.intervals(), &[0, 4, 7, 11]);
3605        assert_eq!(ChordType::MinorSeventh.intervals(), &[0, 3, 7, 10]);
3606        assert_eq!(ChordType::Diminished.intervals(), &[0, 3, 6]);
3607        assert_eq!(ChordType::Augmented.intervals(), &[0, 4, 8]);
3608        assert_eq!(ChordType::Sus2.intervals(), &[0, 2, 7]);
3609        assert_eq!(ChordType::Sus4.intervals(), &[0, 5, 7]);
3610    }
3611    #[test]
3612    fn test_chord_type_from_cv() {
3613        assert_eq!(ChordType::from_cv(0.0), ChordType::Major);
3614        assert_eq!(ChordType::from_cv(0.12), ChordType::Minor);
3615        assert_eq!(ChordType::from_cv(0.23), ChordType::Seventh);
3616        assert_eq!(ChordType::from_cv(1.0), ChordType::Sus4);
3617    }
3618    #[test]
3619    fn test_arp_pattern_from_cv() {
3620        assert_eq!(ArpPattern::from_cv(0.0), ArpPattern::Up);
3621        assert_eq!(ArpPattern::from_cv(0.1), ArpPattern::Up);
3622        assert_eq!(ArpPattern::from_cv(0.3), ArpPattern::Down);
3623        assert_eq!(ArpPattern::from_cv(0.6), ArpPattern::UpDown);
3624        assert_eq!(ArpPattern::from_cv(0.9), ArpPattern::Random);
3625        assert_eq!(ArpPattern::from_cv(1.0), ArpPattern::Random);
3626    }
3627    #[test]
3628    fn test_arpeggiator_default_reset_sample_rate() {
3629        let mut arp = Arpeggiator::default();
3630        assert_eq!(arp.sample_rate, 44100.0);
3631
3632        // Add a note
3633        arp.add_note(0.0);
3634        assert_eq!(arp.num_notes, 1);
3635
3636        // Reset should clear notes
3637        arp.reset();
3638        assert_eq!(arp.num_notes, 0);
3639        assert_eq!(arp.current_step, 0);
3640
3641        // Set sample rate
3642        arp.set_sample_rate(48000.0);
3643        assert_eq!(arp.sample_rate, 48000.0);
3644
3645        assert_eq!(arp.type_id(), "arpeggiator");
3646        assert_eq!(arp.port_spec().inputs.len(), 6);
3647        assert_eq!(arp.port_spec().outputs.len(), 3);
3648    }
3649    #[test]
3650    fn test_arpeggiator_add_remove_notes() {
3651        let mut arp = Arpeggiator::new(44100.0);
3652
3653        // Add notes
3654        arp.add_note(0.0); // C4
3655        arp.add_note(0.5); // F#4
3656        arp.add_note(0.25); // D#4
3657
3658        assert_eq!(arp.num_notes, 3);
3659        // Notes should be sorted
3660        assert_eq!(arp.held_notes[0], 0.0);
3661        assert_eq!(arp.held_notes[1], 0.25);
3662        assert_eq!(arp.held_notes[2], 0.5);
3663
3664        // Remove middle note
3665        arp.remove_note(0.25);
3666        assert_eq!(arp.num_notes, 2);
3667        assert_eq!(arp.held_notes[0], 0.0);
3668        assert_eq!(arp.held_notes[1], 0.5);
3669    }
3670    #[test]
3671    fn test_arpeggiator_up_pattern() {
3672        let mut arp = Arpeggiator::new(44100.0);
3673        let mut inputs = PortValues::new();
3674        let mut outputs = PortValues::new();
3675
3676        // Populate the held chord directly. (A single gate+pitch input can only
3677        // hold one note at a time now that releases remove notes — Q040 — so a
3678        // three-note chord is set up via add_note.)
3679        arp.add_note(0.0); // C4
3680        arp.add_note(0.333); // E4
3681        arp.add_note(0.583); // G4
3682        inputs.set(1, 0.0); // Gate low throughout
3683
3684        assert_eq!(arp.num_notes, 3);
3685
3686        // Send clock pulses and check output
3687        inputs.set(3, 0.0); // Up pattern
3688        let mut notes_out = Vec::new();
3689
3690        for _ in 0..6 {
3691            inputs.set(2, 5.0); // Clock high
3692            arp.tick(&inputs, &mut outputs);
3693            notes_out.push(outputs.get(10).unwrap());
3694
3695            inputs.set(2, 0.0); // Clock low
3696            arp.tick(&inputs, &mut outputs);
3697        }
3698
3699        // Should cycle through notes in ascending order
3700        assert!(notes_out[0] < notes_out[1]);
3701        assert!(notes_out[1] < notes_out[2]);
3702        // Then repeat
3703        assert!((notes_out[3] - notes_out[0]).abs() < 0.01);
3704    }
3705    #[test]
3706    fn test_arpeggiator_trigger_output() {
3707        let mut arp = Arpeggiator::new(44100.0);
3708        let mut inputs = PortValues::new();
3709        let mut outputs = PortValues::new();
3710
3711        // Add a note
3712        inputs.set(0, 0.0);
3713        inputs.set(1, 5.0);
3714        arp.tick(&inputs, &mut outputs);
3715
3716        // Clock pulse should produce trigger
3717        inputs.set(2, 5.0);
3718        arp.tick(&inputs, &mut outputs);
3719        let trigger = outputs.get(12).unwrap();
3720        assert!(trigger > 0.0, "Should output trigger on clock");
3721
3722        // Trigger should continue for a short time
3723        inputs.set(2, 0.0);
3724        arp.tick(&inputs, &mut outputs);
3725        let trigger2 = outputs.get(12).unwrap();
3726        assert!(trigger2 > 0.0, "Trigger should persist briefly");
3727    }
3728    #[test]
3729    fn test_arpeggiator_reset_input() {
3730        let mut arp = Arpeggiator::new(44100.0);
3731        let mut inputs = PortValues::new();
3732        let mut outputs = PortValues::new();
3733
3734        // Add notes and advance steps
3735        inputs.set(0, 0.0);
3736        inputs.set(1, 5.0);
3737        arp.tick(&inputs, &mut outputs);
3738
3739        for _ in 0..5 {
3740            inputs.set(2, 5.0);
3741            arp.tick(&inputs, &mut outputs);
3742            inputs.set(2, 0.0);
3743            arp.tick(&inputs, &mut outputs);
3744        }
3745
3746        let step_before = arp.current_step;
3747        assert!(step_before > 0);
3748
3749        // Send reset
3750        inputs.set(5, 5.0);
3751        arp.tick(&inputs, &mut outputs);
3752
3753        assert_eq!(arp.current_step, 0, "Reset should clear step");
3754    }
3755    #[test]
3756    fn test_arpeggiator_octaves() {
3757        let mut arp = Arpeggiator::new(44100.0);
3758
3759        // Add one note
3760        arp.add_note(0.0); // C4
3761
3762        // With 2 octaves, step 0 should give 0.0, step 1 should give 1.0 (octave higher)
3763        let note1 = arp.get_current_note(ArpPattern::Up, 2);
3764        arp.current_step = 1;
3765        let note2 = arp.get_current_note(ArpPattern::Up, 2);
3766
3767        assert!(
3768            (note2 - note1 - 1.0).abs() < 0.01,
3769            "Second note should be 1 octave higher"
3770        );
3771    }
3772    #[test]
3773    fn test_mixer_summation_bounded() {
3774        // Mixer should not produce unbounded output when summing multiple channels
3775        let mut mixer = Mixer::new(4);
3776        let mut inputs = PortValues::new();
3777        let mut outputs = PortValues::new();
3778
3779        // 4 channels at full scale
3780        for i in 0..4 {
3781            inputs.set(i as u32, 5.0);
3782        }
3783
3784        mixer.tick(&inputs, &mut outputs);
3785        let out = outputs.get(100).unwrap_or(0.0);
3786
3787        // Note: This test documents current behavior (20V output)
3788        // If mixer adds limiting, update this test
3789        assert!(
3790            out.abs() <= SAFE_AUDIO_LIMIT * 2.0,
3791            "Mixer output {} is very high - consider adding limiting",
3792            out
3793        );
3794    }
3795
3796    // ---- Q034: ScaleQuantizer octave-wrap ----
3797
3798    #[test]
3799    fn test_scale_quantizer_octave_wrap_minor_11() {
3800        // Semitone 11 (B above the root) must snap UP to 12, not drop to 0.
3801        assert_eq!(
3802            ScaleQuantizer::quantize_to_scale(11, &ScaleQuantizer::MINOR),
3803            12
3804        );
3805    }
3806
3807    #[test]
3808    fn test_scale_quantizer_monotonic_sweep() {
3809        // A chromatic sweep across two octaves must map to a monotonically
3810        // nondecreasing sequence for every scale — the old code dropped
3811        // top-of-octave notes ~an octave (non-monotonic).
3812        for scale in [
3813            &ScaleQuantizer::MINOR[..],
3814            &ScaleQuantizer::PENT_MAJOR[..],
3815            &ScaleQuantizer::BLUES[..],
3816        ] {
3817            let mut prev = i32::MIN;
3818            for note in 0..=24 {
3819                let q = ScaleQuantizer::quantize_to_scale(note, scale);
3820                assert!(
3821                    q >= prev,
3822                    "non-monotonic: note {} -> {} after {}",
3823                    note,
3824                    q,
3825                    prev
3826                );
3827                prev = q;
3828            }
3829        }
3830    }
3831
3832    // ---- Q041: quantizer / comparator hysteresis ----
3833
3834    #[test]
3835    fn test_quantizer_hysteresis_no_chatter() {
3836        // A slow ramp across two semitone boundaries with a tiny dither must
3837        // cross each boundary exactly once (two committed note changes), not
3838        // chatter every sample.
3839        let mut quant = Quantizer::new(Scale::Chromatic);
3840        let mut inputs = PortValues::new();
3841        let mut outputs = PortValues::new();
3842
3843        let n = 4000;
3844        let mut changes = 0;
3845        let mut prev: Option<f64> = None;
3846        for i in 0..=n {
3847            let base = 2.0 * i as f64 / n as f64; // semitones, 0..2
3848            let dither = if i % 2 == 0 { 0.1 } else { -0.1 };
3849            inputs.set(0, (base + dither) / 12.0);
3850            quant.tick(&inputs, &mut outputs);
3851            let out = outputs.get(10).unwrap();
3852            if let Some(p) = prev {
3853                if (out - p).abs() > 1e-9 {
3854                    changes += 1;
3855                }
3856            }
3857            prev = Some(out);
3858        }
3859        assert_eq!(changes, 2, "expected exactly two boundary crossings");
3860    }
3861
3862    #[test]
3863    fn test_scale_quantizer_trigger_once_per_boundary() {
3864        // The change-trigger must fire once per committed note change, not
3865        // continuously while quantization is active.
3866        let mut sq = ScaleQuantizer::new(44100.0);
3867        let mut inputs = PortValues::new();
3868        let mut outputs = PortValues::new();
3869        inputs.set(2, 0.0); // chromatic scale
3870
3871        let n = 4000;
3872        let mut triggers = 0;
3873        for i in 0..=n {
3874            let base = 2.0 * i as f64 / n as f64;
3875            let dither = if i % 2 == 0 { 0.1 } else { -0.1 };
3876            inputs.set(0, (base + dither) / 12.0);
3877            sq.tick(&inputs, &mut outputs);
3878            if outputs.get(11).unwrap() > 2.5 {
3879                triggers += 1;
3880            }
3881        }
3882        assert_eq!(triggers, 2, "trigger should fire once per boundary");
3883    }
3884
3885    #[test]
3886    fn test_comparator_hysteresis_no_chatter() {
3887        // A signal dithering around B (amplitude between the deadband and the
3888        // hysteresis margin) must not toggle the outputs.
3889        let mut cmp = Comparator::new();
3890        let mut inputs = PortValues::new();
3891        let mut outputs = PortValues::new();
3892        inputs.set(1, 0.0); // B = 0
3893
3894        // Establish equality first.
3895        inputs.set(0, 0.0);
3896        cmp.tick(&inputs, &mut outputs);
3897
3898        let mut gt_high = 0;
3899        let mut lt_high = 0;
3900        for i in 0..200 {
3901            let a = if i % 2 == 0 { 0.02 } else { -0.02 };
3902            inputs.set(0, a);
3903            cmp.tick(&inputs, &mut outputs);
3904            if outputs.get(10).unwrap() > 2.5 {
3905                gt_high += 1;
3906            }
3907            if outputs.get(11).unwrap() > 2.5 {
3908                lt_high += 1;
3909            }
3910            assert!(outputs.get(12).unwrap() > 2.5, "should stay equal");
3911        }
3912        assert_eq!(gt_high, 0, "gt should never fire on sub-band dither");
3913        assert_eq!(lt_high, 0, "lt should never fire on sub-band dither");
3914    }
3915
3916    #[test]
3917    fn test_comparator_still_compares() {
3918        // Decisive inputs still resolve correctly through the hysteresis.
3919        let mut cmp = Comparator::new();
3920        let mut inputs = PortValues::new();
3921        let mut outputs = PortValues::new();
3922
3923        inputs.set(0, 3.0);
3924        inputs.set(1, 1.0);
3925        cmp.tick(&inputs, &mut outputs);
3926        assert!(outputs.get(10).unwrap() > 2.5);
3927
3928        inputs.set(0, 1.0);
3929        inputs.set(1, 3.0);
3930        cmp.tick(&inputs, &mut outputs);
3931        assert!(outputs.get(11).unwrap() > 2.5);
3932
3933        inputs.set(0, 2.0);
3934        inputs.set(1, 2.0);
3935        cmp.tick(&inputs, &mut outputs);
3936        assert!(outputs.get(12).unwrap() > 2.5);
3937    }
3938
3939    // ---- Q035 / Q038: Clock divided outputs and default tempo ----
3940
3941    #[test]
3942    fn test_clock_divided_outputs() {
3943        // Over N main cycles, div2 pulses N/2 times and div4 pulses N/4 times.
3944        let mut clock = Clock::new(1000.0);
3945        let mut inputs = PortValues::new();
3946        let mut outputs = PortValues::new();
3947        inputs.set(0, 5.0); // medium tempo
3948
3949        let (mut main_c, mut div2_c, mut div4_c) = (0, 0, 0);
3950        let (mut pm, mut p2, mut p4) = (0.0, 0.0, 0.0);
3951        for _ in 0..100_000 {
3952            clock.tick(&inputs, &mut outputs);
3953            let m = outputs.get(10).unwrap();
3954            let d2 = outputs.get(11).unwrap();
3955            let d4 = outputs.get(12).unwrap();
3956            let m_rise = m > 2.5 && pm <= 2.5;
3957            if m_rise {
3958                main_c += 1;
3959            }
3960            if d2 > 2.5 && p2 <= 2.5 {
3961                div2_c += 1;
3962            }
3963            if d4 > 2.5 && p4 <= 2.5 {
3964                div4_c += 1;
3965            }
3966            pm = m;
3967            p2 = d2;
3968            p4 = d4;
3969            if m_rise && main_c == 8 {
3970                break;
3971            }
3972        }
3973        assert_eq!(main_c, 8, "main should pulse every cycle");
3974        assert_eq!(div2_c, 4, "div2 should pulse at half rate");
3975        assert_eq!(div4_c, 2, "div4 should pulse at quarter rate");
3976    }
3977
3978    #[test]
3979    fn test_clock_default_tempo_120_bpm() {
3980        // With no bpm input, the port default must yield ~120 BPM.
3981        let mut clock = Clock::default();
3982        let inputs = PortValues::new(); // empty -> use port default
3983        let mut outputs = PortValues::new();
3984
3985        let mut edges = Vec::new();
3986        let mut prev = 0.0;
3987        for i in 0..100_000 {
3988            clock.tick(&inputs, &mut outputs);
3989            let m = outputs.get(10).unwrap();
3990            if m > 2.5 && prev <= 2.5 {
3991                edges.push(i);
3992            }
3993            prev = m;
3994            if edges.len() >= 2 {
3995                break;
3996            }
3997        }
3998        assert!(edges.len() >= 2, "expected at least two clock pulses");
3999        let period = (edges[1] - edges[0]) as f64;
4000        let bpm = 60.0 * 44100.0 / period;
4001        assert!(
4002            (bpm - 120.0).abs() < 1.0,
4003            "default tempo {} BPM should be ~120",
4004            bpm
4005        );
4006    }
4007
4008    // ---- Q036: BernoulliGate latched gates ----
4009
4010    #[test]
4011    fn test_bernoulli_gate_latches() {
4012        let mut bg = BernoulliGate::new();
4013        let mut inputs = PortValues::new();
4014
4015        // Deterministic route to A: 100% probability.
4016        inputs.set(1, 10.0);
4017
4018        // A fresh output buffer every tick (as the engine provides) — the latch
4019        // must survive without reading back the output buffer.
4020        let tick = |bg: &mut BernoulliGate, inputs: &PortValues| {
4021            let mut o = PortValues::new();
4022            bg.tick(inputs, &mut o);
4023            o
4024        };
4025
4026        inputs.set(0, 0.0);
4027        tick(&mut bg, &inputs);
4028        inputs.set(0, 5.0);
4029        let o = tick(&mut bg, &inputs); // rising edge -> A
4030        assert!(o.get(12).unwrap() > 2.5, "gate_a should latch high");
4031        assert!(o.get(13).unwrap() < 2.5);
4032
4033        // Hold across many non-trigger ticks (gate low, no rising edge).
4034        inputs.set(0, 0.0);
4035        for _ in 0..20 {
4036            let o = tick(&mut bg, &inputs);
4037            assert!(o.get(12).unwrap() > 2.5, "gate_a must stay latched");
4038            assert!(o.get(13).unwrap() < 2.5);
4039        }
4040
4041        // Now route to B: 0% probability, new rising edge.
4042        inputs.set(1, 0.0);
4043        inputs.set(0, 0.0);
4044        tick(&mut bg, &inputs);
4045        inputs.set(0, 5.0);
4046        let o = tick(&mut bg, &inputs); // rising edge -> B
4047        assert!(o.get(12).unwrap() < 2.5, "gate_a should release");
4048        assert!(o.get(13).unwrap() > 2.5, "gate_b should latch high");
4049    }
4050
4051    // ---- Q037 / Q042 / Q129: Euclidean ----
4052
4053    #[test]
4054    fn test_euclidean_pulses_control_live() {
4055        // Changing pulses at a constant step count must change the pattern.
4056        let mut euc = Euclidean::new(44100.0);
4057        let mut inputs = PortValues::new();
4058        let mut outputs = PortValues::new();
4059        inputs.set(1, 0.5); // steps -> 9
4060
4061        inputs.set(2, 0.25); // pulses -> 2
4062        euc.tick(&inputs, &mut outputs);
4063        let active_low = euc.pattern.iter().filter(|&&x| x).count();
4064        assert_eq!(active_low, 2);
4065
4066        inputs.set(2, 0.75); // pulses -> 6
4067        euc.tick(&inputs, &mut outputs);
4068        let active_high = euc.pattern.iter().filter(|&&x| x).count();
4069        assert_eq!(active_high, 6);
4070        assert_ne!(active_low, active_high, "pulses control must be live");
4071    }
4072
4073    #[test]
4074    fn test_euclidean_accent_on_rotated_pulse() {
4075        // With rotation, the accent must fire exactly once per cycle and always
4076        // coincide with an actual pulse.
4077        let mut euc = Euclidean::new(44100.0);
4078        let mut inputs = PortValues::new();
4079        let mut outputs = PortValues::new();
4080        inputs.set(1, 0.4003); // steps -> 8
4081        inputs.set(2, 0.5); // pulses -> 4
4082        inputs.set(3, 0.3); // rotation -> 2
4083
4084        let mut accents = 0;
4085        for _ in 0..8 {
4086            inputs.set(0, 5.0); // clock high (rising)
4087            euc.tick(&inputs, &mut outputs);
4088            let out = outputs.get(10).unwrap();
4089            let accent = outputs.get(11).unwrap();
4090            if accent > 2.5 {
4091                accents += 1;
4092                assert!(out > 2.5, "accent must coincide with a pulse");
4093            }
4094            inputs.set(0, 0.0); // clock low
4095            euc.tick(&inputs, &mut outputs);
4096        }
4097        assert_eq!(accents, 1, "exactly one accent per cycle");
4098    }
4099
4100    #[test]
4101    fn test_euclidean_gate_threshold() {
4102        // Clock pulses below the canonical 2.5V threshold must be ignored;
4103        // 5V pulses must produce output (Q129).
4104        let mut euc = Euclidean::new(44100.0);
4105        let mut inputs = PortValues::new();
4106        let mut outputs = PortValues::new();
4107        inputs.set(1, 0.4003); // steps -> 8
4108        inputs.set(2, 1.0); // all pulses active
4109
4110        // 1.0V clock: never crosses threshold -> no pulses.
4111        let mut low_pulses = 0;
4112        for _ in 0..8 {
4113            inputs.set(0, 1.0);
4114            euc.tick(&inputs, &mut outputs);
4115            if outputs.get(10).unwrap() > 2.5 {
4116                low_pulses += 1;
4117            }
4118            inputs.set(0, 0.0);
4119            euc.tick(&inputs, &mut outputs);
4120        }
4121        assert_eq!(low_pulses, 0, "1.0V clock must not trigger");
4122
4123        // 5V clock: produces pulses.
4124        let mut high_pulses = 0;
4125        for _ in 0..8 {
4126            inputs.set(0, 5.0);
4127            euc.tick(&inputs, &mut outputs);
4128            if outputs.get(10).unwrap() > 2.5 {
4129                high_pulses += 1;
4130            }
4131            inputs.set(0, 0.0);
4132            euc.tick(&inputs, &mut outputs);
4133        }
4134        assert!(high_pulses > 0, "5V clock must trigger");
4135    }
4136
4137    // ---- Q040: Arpeggiator note release ----
4138
4139    #[test]
4140    fn test_arpeggiator_releases_notes() {
4141        let mut arp = Arpeggiator::new(44100.0);
4142        let mut inputs = PortValues::new();
4143        let mut outputs = PortValues::new();
4144
4145        // Press a note (rising edge).
4146        inputs.set(0, 0.25);
4147        inputs.set(1, 5.0);
4148        arp.tick(&inputs, &mut outputs);
4149        assert_eq!(arp.num_notes, 1);
4150
4151        // Hold: still one note.
4152        for _ in 0..5 {
4153            arp.tick(&inputs, &mut outputs);
4154        }
4155        assert_eq!(arp.num_notes, 1);
4156
4157        // Release (falling edge) removes the note.
4158        inputs.set(1, 0.0);
4159        arp.tick(&inputs, &mut outputs);
4160        assert_eq!(arp.num_notes, 0, "release must remove the held note");
4161
4162        // Another press/release cycle keeps the count correct.
4163        inputs.set(0, 0.5);
4164        inputs.set(1, 5.0);
4165        arp.tick(&inputs, &mut outputs);
4166        assert_eq!(arp.num_notes, 1);
4167        inputs.set(1, 0.0);
4168        arp.tick(&inputs, &mut outputs);
4169        assert_eq!(arp.num_notes, 0);
4170    }
4171
4172    #[test]
4173    fn test_arpeggiator_reset_clears_held_notes() {
4174        let mut arp = Arpeggiator::new(44100.0);
4175        let mut inputs = PortValues::new();
4176        let mut outputs = PortValues::new();
4177
4178        // Hold a note (gate stays high).
4179        inputs.set(0, 0.0);
4180        inputs.set(1, 5.0);
4181        arp.tick(&inputs, &mut outputs);
4182        assert_eq!(arp.num_notes, 1);
4183
4184        // Reset input (rising edge) empties the held set.
4185        inputs.set(5, 5.0);
4186        arp.tick(&inputs, &mut outputs);
4187        assert_eq!(arp.num_notes, 0, "reset must clear held notes");
4188    }
4189
4190    // ================================================================
4191    // Q146: microtuning / custom scales on ScaleQuantizer
4192    // ================================================================
4193
4194    #[cfg(feature = "alloc")]
4195    #[test]
4196    fn test_custom_scale_snaps_to_degrees() {
4197        // Whole-tone scale: degrees every 200 cents.
4198        let mut sq = ScaleQuantizer::new(44100.0);
4199        assert!(!sq.has_custom_scale());
4200        sq.set_custom_scale(&[0.0, 200.0, 400.0, 600.0, 800.0, 1000.0]);
4201        assert!(sq.has_custom_scale());
4202
4203        let mut inputs = PortValues::new();
4204        let mut outputs = PortValues::new();
4205
4206        // Input just above the 200-cent degree (200 cents = 1/6 V) should snap to
4207        // it, not to a chromatic semitone.
4208        // 210 cents = 0.175 V.
4209        inputs.set(0, 0.175);
4210        // Run a few ticks so hysteresis commits.
4211        for _ in 0..4 {
4212            sq.tick(&inputs, &mut outputs);
4213        }
4214        let out_v = outputs.get(10).unwrap();
4215        // Expect ~200 cents = 0.16667 V.
4216        assert!(
4217            (out_v - 200.0 / 1200.0).abs() < 1e-6,
4218            "custom scale should snap to 200 cents, got {} cents",
4219            out_v * 1200.0
4220        );
4221    }
4222
4223    #[cfg(feature = "alloc")]
4224    #[test]
4225    fn test_load_scala_and_quantize() {
4226        let scl = "\
4227whole tone
42286
4229200.0
4230400.0
4231600.0
4232800.0
42331000.0
42341200.0
4235";
4236        let mut sq = ScaleQuantizer::new(44100.0);
4237        sq.load_scala(scl).unwrap();
4238        assert!(sq.has_custom_scale());
4239
4240        let mut inputs = PortValues::new();
4241        let mut outputs = PortValues::new();
4242        // 390 cents -> nearest whole-tone degree is 400 cents.
4243        inputs.set(0, 390.0 / 1200.0);
4244        for _ in 0..4 {
4245            sq.tick(&inputs, &mut outputs);
4246        }
4247        let out_v = outputs.get(10).unwrap();
4248        assert!(
4249            (out_v - 400.0 / 1200.0).abs() < 1e-6,
4250            "expected 400 cents, got {} cents",
4251            out_v * 1200.0
4252        );
4253    }
4254
4255    #[cfg(feature = "alloc")]
4256    #[test]
4257    fn test_clear_custom_scale_restores_enum() {
4258        let mut sq = ScaleQuantizer::new(44100.0);
4259        sq.set_custom_scale(&[0.0, 200.0, 400.0]);
4260        assert!(sq.has_custom_scale());
4261        sq.clear_custom_scale();
4262        assert!(!sq.has_custom_scale());
4263    }
4264
4265    #[cfg(feature = "alloc")]
4266    #[test]
4267    fn test_load_scala_malformed_leaves_scale_unchanged() {
4268        let mut sq = ScaleQuantizer::new(44100.0);
4269        sq.set_custom_scale(&[0.0, 500.0]);
4270        // Malformed: declares 3 pitches but supplies one.
4271        let err = sq.load_scala("bad\n3\n100.0\n");
4272        assert!(err.is_err());
4273        // Previous custom scale is retained.
4274        assert!(sq.has_custom_scale());
4275    }
4276
4277    // ---- Q161: Quantizer with negative V/Oct (notes below C4) ----
4278
4279    #[test]
4280    fn test_quantizer_negative_voct_chromatic() {
4281        // A fresh quantizer per input avoids boundary hysteresis carrying over.
4282        let cases = [
4283            (-0.5, -0.5),                 // exactly F#3 -> stays
4284            (-1.0, -1.0),                 // C3 -> octave floor, stays
4285            (-13.0 / 12.0, -13.0 / 12.0), // B2 -> negative octave, chromatic passthrough
4286            (-1.0 / 24.0, 0.0),           // half a semitone below C4 -> wraps UP to C4
4287        ];
4288        for (input, expected) in cases {
4289            let mut q = Quantizer::new(Scale::Chromatic);
4290            let mut inputs = PortValues::new();
4291            let mut outputs = PortValues::new();
4292            inputs.set(0, input);
4293            q.tick(&inputs, &mut outputs);
4294            let out = outputs.get(10).unwrap();
4295            assert!(
4296                (out - expected).abs() < 1e-9,
4297                "chromatic {input}V -> {out}V, expected {expected}V"
4298            );
4299        }
4300    }
4301
4302    #[test]
4303    fn test_quantizer_negative_voct_major_scale() {
4304        // -0.5V is F#3; in a major scale it snaps to the nearest degree, F3
4305        // (-7/12 V), proving the negative-octave scale-wrap path works.
4306        let mut q = Quantizer::new(Scale::Major);
4307        let mut inputs = PortValues::new();
4308        let mut outputs = PortValues::new();
4309        inputs.set(0, -0.5);
4310        q.tick(&inputs, &mut outputs);
4311        let out = outputs.get(10).unwrap();
4312        assert!(
4313            (out - (-7.0 / 12.0)).abs() < 1e-9,
4314            "major -0.5V should snap to F3 (-7/12 V), got {out}V"
4315        );
4316
4317        // -1.0V is exactly C3, a scale degree, so it is preserved.
4318        let mut q2 = Quantizer::new(Scale::Major);
4319        inputs.set(0, -1.0);
4320        q2.tick(&inputs, &mut outputs);
4321        assert!((outputs.get(10).unwrap() - (-1.0)).abs() < 1e-9);
4322    }
4323
4324    #[test]
4325    fn test_scale_quantizer_negative_voct() {
4326        // Chromatic passthrough below C4 with the div_euclid/rem_euclid path.
4327        let cases = [(-1.0, -1.0), (-13.0 / 12.0, -13.0 / 12.0)];
4328        for (input, expected) in cases {
4329            let mut sq = ScaleQuantizer::new(44100.0);
4330            let mut inputs = PortValues::new();
4331            let mut outputs = PortValues::new();
4332            inputs.set(0, input);
4333            inputs.set(2, 0.0); // chromatic
4334            sq.tick(&inputs, &mut outputs);
4335            let out = outputs.get(10).unwrap();
4336            assert!(
4337                (out - expected).abs() < 1e-9,
4338                "scale-quantizer chromatic {input}V -> {out}V, expected {expected}V"
4339            );
4340        }
4341
4342        // Minor scale, -0.5V (F#3) snaps to F3 (-7/12 V) below C4.
4343        let mut sq = ScaleQuantizer::new(44100.0);
4344        let mut inputs = PortValues::new();
4345        let mut outputs = PortValues::new();
4346        inputs.set(0, -0.5);
4347        inputs.set(2, 0.3); // scale index 2 == minor
4348        sq.tick(&inputs, &mut outputs);
4349        let out = outputs.get(10).unwrap();
4350        assert!(
4351            (out - (-7.0 / 12.0)).abs() < 1e-9,
4352            "minor -0.5V should snap to F3 (-7/12 V), got {out}V"
4353        );
4354    }
4355
4356    // ---- Q157: Euclidean + ScaleQuantizer reset / sample-rate ----
4357
4358    #[test]
4359    fn test_euclidean_reset_and_sample_rate() {
4360        let mut euc = Euclidean::new(44100.0);
4361        assert_eq!(euc.type_id(), "euclidean");
4362        let mut inputs = PortValues::new();
4363        let mut outputs = PortValues::new();
4364        // Advance the sequencer a few clock pulses so `step` moves off zero.
4365        for _ in 0..3 {
4366            inputs.set(0, 5.0);
4367            euc.tick(&inputs, &mut outputs);
4368            inputs.set(0, 0.0);
4369            euc.tick(&inputs, &mut outputs);
4370        }
4371        assert!(euc.step != 0, "clock pulses should advance the step");
4372        euc.reset();
4373        assert_eq!(euc.step, 0);
4374        assert!(!euc.cycle_accented);
4375        // set_sample_rate is a no-op but must not panic and keep it usable.
4376        euc.set_sample_rate(48000.0);
4377        inputs.set(0, 5.0);
4378        euc.tick(&inputs, &mut outputs);
4379        assert!(outputs.get(10).unwrap().is_finite());
4380    }
4381
4382    #[test]
4383    fn test_scale_quantizer_reset_and_sample_rate() {
4384        let mut sq = ScaleQuantizer::new(44100.0);
4385        assert_eq!(sq.type_id(), "scale_quantizer");
4386        let mut inputs = PortValues::new();
4387        let mut outputs = PortValues::new();
4388        inputs.set(0, 0.25); // some note
4389        sq.tick(&inputs, &mut outputs);
4390        assert!(sq.last_output.is_some());
4391        sq.reset();
4392        assert!(sq.last_output.is_none());
4393        sq.set_sample_rate(48000.0);
4394        sq.tick(&inputs, &mut outputs);
4395        assert!(outputs.get(10).unwrap().is_finite());
4396    }
4397
4398    // ---- Coefficient memoization (perf) ------------------------------------
4399
4400    /// Memoization must be observationally invisible: a slew limiter and a
4401    /// clock whose memos are invalidated before every tick execute the
4402    /// pre-memoization computation every sample and must agree bit-for-bit
4403    /// with the memoized modules under both constant and per-sample-modulated
4404    /// parameters.
4405    #[test]
4406    fn test_utilities_memos_bit_identical() {
4407        let mut slew_m = SlewLimiter::new(44100.0);
4408        let mut slew_f = SlewLimiter::new(44100.0);
4409        let mut clk_m = Clock::new(44100.0);
4410        let mut clk_f = Clock::new(44100.0);
4411        let mut inputs = PortValues::new();
4412        let mut out_m = PortValues::new();
4413        let mut out_f = PortValues::new();
4414
4415        for n in 0..20_000u32 {
4416            let t = n as f64;
4417            let sweep = if n < 10_000 {
4418                0.5
4419            } else {
4420                0.5 + 0.3 * Libm::<f64>::sin(t * 0.002)
4421            };
4422
4423            // SlewLimiter: square-wave target exercises both rise and fall.
4424            inputs.set(0, if (n / 500) % 2 == 0 { 4.0 } else { -4.0 });
4425            inputs.set(1, sweep);
4426            inputs.set(2, 0.3);
4427            slew_m.tick(&inputs, &mut out_m);
4428            slew_f.rise_memo.invalidate();
4429            slew_f.fall_memo.invalidate();
4430            slew_f.tick(&inputs, &mut out_f);
4431            assert_eq!(
4432                out_m.get(10).unwrap().to_bits(),
4433                out_f.get(10).unwrap().to_bits(),
4434                "SlewLimiter diverged at sample {n}"
4435            );
4436
4437            // Clock: bpm CV modulated in the second half.
4438            inputs.set(0, 5.0 + sweep);
4439            clk_m.tick(&inputs, &mut out_m);
4440            clk_f.bpm_memo.invalidate();
4441            clk_f.tick(&inputs, &mut out_f);
4442            for &id in &[10u32, 11, 12] {
4443                assert_eq!(
4444                    out_m.get(id).unwrap().to_bits(),
4445                    out_f.get(id).unwrap().to_bits(),
4446                    "Clock output {id} diverged at sample {n}"
4447                );
4448            }
4449        }
4450        assert!(clk_m.bpm_memo.recompute_count() <= 10_001);
4451    }
4452}