Skip to main content

quiver/
combinator.rs

1//! # Layer 1: Typed Module Combinators
2//!
3//! This module provides **Arrow-style combinators** for composing signal processing
4//! modules with *structural* (arity/shape) compile-time type checking. These
5//! combinators enable functional composition of DSP chains that compile down to tight,
6//! inlinable loops with zero runtime overhead.
7//!
8//! ## What "type-safe" means here (and what it does not)
9//!
10//! The combinators give **structural** type safety: the compiler guarantees that the
11//! *shape* of a signal matches — a mono `f64` connects to a mono `f64`, a stereo
12//! `(f64, f64)` to a stereo `(f64, f64)`, and tuple arities line up. This is real and
13//! useful: `.then`, `.parallel`, and `.fanout` cannot be mis-wired shape-wise.
14//!
15//! It is **not** *semantic* signal-kind safety. [`Module::In`] / [`Module::Out`] are bare
16//! Rust types (in practice `f64`), so an `Audio` output and a `VoltPerOctave` pitch input
17//! are the *same* type and will chain silently. Semantic [`SignalKind`] checking (Audio vs
18//! CV vs V/Oct vs Gate) is enforced by the **graph layer** (Layer 2 / Layer 3, see
19//! [`crate::port`] and [`crate::graph`]), not by these combinators.
20//!
21//! [`SignalKind`]: crate::port::SignalKind
22//!
23//! ## Category Theory Background
24//!
25//! In category theory, an **Arrow** is a generalization of functions that allows for
26//! composition while carrying additional structure (like state). The combinators here
27//! implement the Arrow interface. The left column below is the **conceptual
28//! Haskell/`Control.Arrow` notation** used throughout the functional-programming
29//! literature — it is **not** Rust syntax. `>>>`, `***`, and `&&&` are *not* Rust
30//! operators, and this crate deliberately ships **no operator overloads** for them.
31//! Use the method in the right column instead:
32//!
33//! | Conceptual (Haskell) | Real Quiver API      | Meaning                         |
34//! |----------------------|----------------------|---------------------------------|
35//! | `arr f`              | `arr(f)`             | Lift a pure `Fn` into a module  |
36//! | `f >>> g`            | `f.then(g)`          | Sequential composition          |
37//! | `first f`            | `f.first()`          | Apply to first tuple element    |
38//! | `f *** g`            | `f.parallel(g)`      | Independent parallel processing |
39//! | `f &&& g`            | `f.fanout(g)`        | Split one input to two          |
40//!
41//! ```text
42//! arr:     (a -> b) -> Arrow a b                         // Lift pure function
43//! (>>>):   Arrow a b -> Arrow b c -> Arrow a c           // Sequential composition
44//! first:   Arrow a b -> Arrow (a,c) (b,c)                // Apply to first element
45//! (***):   Arrow a b -> Arrow c d -> Arrow (a,c) (b,d)   // Parallel
46//! (&&&):   Arrow a b -> Arrow a c -> Arrow a (b,c)       // Fanout
47//! ```
48//!
49//! The real methods live on [`ModuleExt`]; the `arr` primitive is the free function
50//! [`arr`].
51//!
52//! ## Arrow Laws
53//!
54//! These combinators satisfy the Arrow laws, ensuring predictable behavior. They are
55//! checked by the `arrow_law_*` tests in this module (using the real `.then`/`.first`
56//! API over deterministic input sequences against stateful test modules):
57//!
58//! - **Identity**: `id.then(f)` behaves as `f` (and `f.then(id)` as `f`)
59//! - **Associativity**: `(f.then(g)).then(h)` behaves as `f.then(g.then(h))`
60//! - **First distributes**: `f.then(g).first()` behaves as `f.first().then(g.first())`
61//!
62//! ## Zero-Cost Abstraction
63//!
64//! Due to Rust's monomorphization, combinator chains compile to the same code as
65//! hand-written loops:
66//!
67//! ```text
68//! // This combinator chain...
69//! let synth = vco.then(vcf).then(vca);
70//!
71//! // ...compiles to essentially:
72//! fn tick(&mut self) -> f64 {
73//!     self.vca.tick(self.vcf.tick(self.vco.tick(())))
74//! }
75//! ```
76//!
77//! ## Example: Composing with the real API
78//!
79//! ```rust
80//! use quiver::combinator::{arr, Module, ModuleExt};
81//!
82//! // `.then` is sequential composition (conceptually `>>>`).
83//! let mut chain = arr(|x: f64| x + 1.0).then(arr(|x: f64| x * 2.0));
84//! assert_eq!(chain.tick(3.0), 8.0); // (3 + 1) * 2
85//!
86//! // `.parallel` (conceptually `***`) processes two independent signals.
87//! let mut stereo = arr(|l: f64| l * 0.5).parallel(arr(|r: f64| r * 0.25));
88//! assert_eq!(stereo.tick((2.0, 4.0)), (1.0, 1.0));
89//!
90//! // `.fanout` (conceptually `&&&`) sends one input to two processors.
91//! let mut split = arr(|x: f64| x + 1.0).fanout(arr(|x: f64| x - 1.0));
92//! assert_eq!(split.tick(5.0), (6.0, 4.0));
93//! ```
94//!
95//! ## Bridging to the Patch Graph (Layer 3)
96//!
97//! The combinator [`Module`] trait and the engine's [`GraphModule`] trait are distinct
98//! worlds: shipped DSP modules (`Vco`, `Svf`, `Vca`, …) implement [`GraphModule`], so they
99//! cannot be dropped into `.then(...)` directly. Two adapters bridge them:
100//!
101//! - [`GraphModuleAdapter`] wraps any [`GraphModule`] (choosing one input and one output
102//!   port) and exposes it as a `Module<In = f64, Out = f64>`, so real modules *can* be
103//!   composed with `.then`/`.parallel`/`.fanout`.
104//! - [`ModuleGraphAdapter`] wraps any `Module<In = f64, Out = f64>` as a one-in/one-out
105//!   [`GraphModule`], so a combinator chain can be `patch.add(...)`-ed into a [`Patch`].
106//!
107//! ```rust
108//! use quiver::combinator::{GraphModuleAdapter, Module, ModuleExt};
109//! use quiver::modules::{Svf, Vco};
110//!
111//! // Drive the Vco's `voct` input (port 0) and take its `saw` output (port 12).
112//! let vco = GraphModuleAdapter::new(Vco::new(44_100.0), 0, 12);
113//! // Svf: pick its first audio in/out automatically (`in` -> `lp`).
114//! let svf = GraphModuleAdapter::from_audio_ports(Svf::new(44_100.0)).unwrap();
115//!
116//! let mut voice = vco.then(svf);
117//! let mut peak = 0.0_f64;
118//! for _ in 0..256 {
119//!     peak = peak.max(voice.tick(0.0).abs()); // 0 V/oct = middle C
120//! }
121//! assert!(peak > 0.0, "a real Vco -> Svf combinator chain should make sound");
122//! ```
123//!
124//! [`Module`]: crate::combinator::Module
125//! [`GraphModule`]: crate::port::GraphModule
126//! [`GraphModuleAdapter`]: crate::combinator::GraphModuleAdapter
127//! [`ModuleGraphAdapter`]: crate::combinator::ModuleGraphAdapter
128//! [`Patch`]: crate::graph::Patch
129
130use crate::port::{GraphModule, PortDef, PortId, PortSpec, PortValues, SignalKind};
131use alloc::string::String;
132use alloc::vec;
133use core::marker::PhantomData;
134
135/// A signal processing module with typed input and output.
136///
137/// This is the fundamental abstraction for DSP processing in Quiver. Modules are
138/// **stateful processors** that transform input samples to output samples. The
139/// associated types `In` and `Out` enable compile-time verification of signal *shape*
140/// (arity): a `Module<Out = f64>` only chains into a `Module<In = f64>`, a stereo
141/// `(f64, f64)` only into a `(f64, f64)`, and so on. This is *structural* type safety;
142/// it does **not** check semantic [`SignalKind`] (Audio vs CV
143/// vs V/Oct) — both are `f64` here. Signal-kind compatibility is validated by the graph
144/// layer ([`crate::port`] / [`crate::graph`]).
145///
146/// # Mathematical Model
147///
148/// A module represents a morphism in the category of signals:
149///
150/// ```text
151/// M : In → Out
152/// ```
153///
154/// The `tick` method computes one step of this transformation, potentially updating
155/// internal state (like oscillator phase or filter memory).
156///
157/// # Implementing Module
158///
159/// ```
160/// use quiver::prelude::*;
161///
162/// struct Amplifier { gain: f64 }
163///
164/// impl Module for Amplifier {
165///     type In = f64;
166///     type Out = f64;
167///
168///     fn tick(&mut self, input: f64) -> f64 {
169///         input * self.gain
170///     }
171///
172///     fn reset(&mut self) {
173///         // Amplifier is stateless, nothing to reset
174///     }
175/// }
176///
177/// let mut amp = Amplifier { gain: 2.0 };
178/// assert_eq!(amp.tick(0.5), 1.0);
179/// ```
180///
181/// # Thread Safety
182///
183/// All modules must be `Send` to allow audio processing on dedicated threads.
184pub trait Module: Send {
185    /// Input signal type (e.g., `f64` for mono, `(f64, f64)` for stereo)
186    type In;
187    /// Output signal type
188    type Out;
189
190    /// Process a single sample, advancing internal state by one time step.
191    ///
192    /// This is the core DSP function. For a VCO, this updates phase and outputs
193    /// a waveform sample. For a filter, this processes through the filter stages.
194    fn tick(&mut self, input: Self::In) -> Self::Out;
195
196    /// Process a block of samples for efficiency.
197    ///
198    /// Override this method for SIMD optimization or when block processing is
199    /// more efficient than sample-by-sample. The default implementation simply
200    /// calls `tick` in a loop.
201    fn process(&mut self, input: &[Self::In], output: &mut [Self::Out])
202    where
203        Self::In: Clone,
204    {
205        for (i, o) in input.iter().zip(output.iter_mut()) {
206            *o = self.tick(i.clone());
207        }
208    }
209
210    /// Reset internal state to initial conditions.
211    ///
212    /// Called when starting a new note, reinitializing the synth, etc.
213    /// For oscillators, this typically resets phase. For filters, clears memory.
214    fn reset(&mut self);
215
216    /// Notify module of sample rate changes.
217    ///
218    /// Modules with time-dependent behavior (filters, delays, envelopes) should
219    /// recalculate coefficients here.
220    fn set_sample_rate(&mut self, _sample_rate: f64) {}
221}
222
223/// Extension trait providing combinator methods for all modules
224pub trait ModuleExt: Module + Sized {
225    /// Chain this module with another (sequential composition: `>>>`)
226    fn then<M: Module<In = Self::Out>>(self, next: M) -> Chain<Self, M> {
227        Chain {
228            first: self,
229            second: next,
230        }
231    }
232
233    /// Run two modules in parallel (`***`)
234    fn parallel<M: Module>(self, other: M) -> Parallel<Self, M> {
235        Parallel {
236            left: self,
237            right: other,
238        }
239    }
240
241    /// Split input to two parallel processors (`&&&`)
242    fn fanout<M: Module<In = Self::In>>(self, other: M) -> Fanout<Self, M>
243    where
244        Self::In: Clone,
245    {
246        Fanout {
247            left: self,
248            right: other,
249        }
250    }
251
252    /// Transform output with a pure function
253    fn map<F, U>(self, f: F) -> Map<Self, F>
254    where
255        F: Fn(Self::Out) -> U,
256    {
257        Map { module: self, f }
258    }
259
260    /// Transform input with a pure function
261    fn contramap<F, U>(self, f: F) -> Contramap<Self, F, U>
262    where
263        F: Fn(U) -> Self::In,
264    {
265        Contramap {
266            module: self,
267            f,
268            _phantom: PhantomData,
269        }
270    }
271
272    /// Create a feedback loop with a single-sample unit delay.
273    ///
274    /// The `combine` closure is called as `combine(external_input, previous_output)`:
275    ///
276    /// - the **first** argument is this tick's external input,
277    /// - the **second** argument is the module's output from the *previous* tick,
278    ///   delayed by exactly one sample for causality.
279    ///
280    /// On the very first tick (and after [`reset`](Module::reset)), the previous-output
281    /// argument is `Self::Out::default()` (i.e. `0.0` for `f64`). The combined value is
282    /// fed to the wrapped module, and its output is both returned and stored as the next
283    /// tick's feedback signal.
284    ///
285    /// # Example: a one-pole low-pass via feedback
286    ///
287    /// ```
288    /// use quiver::combinator::{Identity, Module, ModuleExt};
289    ///
290    /// let coeff = 0.5;
291    /// // y[n] = input * (1 - coeff) + previous_output * coeff
292    /// let mut one_pole = Identity::<f64>::new()
293    ///     .feedback(move |input, previous| input * (1.0 - coeff) + previous * coeff);
294    ///
295    /// // First tick: previous_output defaults to 0.0.
296    /// assert!((one_pole.tick(1.0) - 0.5).abs() < 1e-12); // 1*0.5 + 0*0.5
297    /// // Second tick: previous_output is now 0.5.
298    /// assert!((one_pole.tick(1.0) - 0.75).abs() < 1e-12); // 1*0.5 + 0.5*0.5
299    /// ```
300    fn feedback<F>(self, combine: F) -> Feedback<Self, F>
301    where
302        Self::Out: Default + Clone,
303    {
304        Feedback {
305            module: self,
306            combine,
307            delay_buffer: Self::Out::default(),
308        }
309    }
310
311    /// Apply this module only to the first element of a tuple
312    fn first<C>(self) -> First<Self, C> {
313        First {
314            module: self,
315            _phantom: PhantomData,
316        }
317    }
318
319    /// Apply this module only to the second element of a tuple
320    fn second<C>(self) -> Second<Self, C> {
321        Second {
322            module: self,
323            _phantom: PhantomData,
324        }
325    }
326}
327
328// Blanket implementation for all modules
329impl<M: Module> ModuleExt for M {}
330
331/// Sequential composition: processes through first module, then second
332pub struct Chain<A, B> {
333    pub first: A,
334    pub second: B,
335}
336
337impl<A, B> Module for Chain<A, B>
338where
339    A: Module,
340    B: Module<In = A::Out>,
341{
342    type In = A::In;
343    type Out = B::Out;
344
345    #[inline]
346    fn tick(&mut self, input: Self::In) -> Self::Out {
347        self.second.tick(self.first.tick(input))
348    }
349
350    fn reset(&mut self) {
351        self.first.reset();
352        self.second.reset();
353    }
354
355    fn set_sample_rate(&mut self, sample_rate: f64) {
356        self.first.set_sample_rate(sample_rate);
357        self.second.set_sample_rate(sample_rate);
358    }
359}
360
361/// Parallel composition: processes two independent signals simultaneously
362pub struct Parallel<A, B> {
363    pub left: A,
364    pub right: B,
365}
366
367impl<A, B> Module for Parallel<A, B>
368where
369    A: Module,
370    B: Module,
371{
372    type In = (A::In, B::In);
373    type Out = (A::Out, B::Out);
374
375    #[inline]
376    fn tick(&mut self, (a, b): Self::In) -> Self::Out {
377        (self.left.tick(a), self.right.tick(b))
378    }
379
380    fn reset(&mut self) {
381        self.left.reset();
382        self.right.reset();
383    }
384
385    fn set_sample_rate(&mut self, sample_rate: f64) {
386        self.left.set_sample_rate(sample_rate);
387        self.right.set_sample_rate(sample_rate);
388    }
389}
390
391/// Fanout: splits a single input to two parallel processors
392pub struct Fanout<A, B> {
393    pub left: A,
394    pub right: B,
395}
396
397impl<A, B> Module for Fanout<A, B>
398where
399    A: Module,
400    B: Module<In = A::In>,
401    A::In: Clone,
402{
403    type In = A::In;
404    type Out = (A::Out, B::Out);
405
406    #[inline]
407    fn tick(&mut self, input: Self::In) -> Self::Out {
408        (self.left.tick(input.clone()), self.right.tick(input))
409    }
410
411    fn reset(&mut self) {
412        self.left.reset();
413        self.right.reset();
414    }
415
416    fn set_sample_rate(&mut self, sample_rate: f64) {
417        self.left.set_sample_rate(sample_rate);
418        self.right.set_sample_rate(sample_rate);
419    }
420}
421
422/// Feedback loop with a mandatory single-sample delay for causality.
423///
424/// Each tick computes `combine(external_input, previous_output)` and feeds the result to
425/// the wrapped module. `previous_output` is the module's output from the previous tick
426/// (the `delay_buffer`), which starts at `M::Out::default()` on the first tick and after
427/// [`reset`](Module::reset). See [`ModuleExt::feedback`] for the argument-order contract
428/// and a runnable example.
429pub struct Feedback<M: Module, F> {
430    pub module: M,
431    pub combine: F,
432    /// The previous tick's output, delayed one sample. `Default` on the first tick.
433    pub delay_buffer: M::Out,
434}
435
436impl<M, F, Combined> Module for Feedback<M, F>
437where
438    M: Module<In = Combined>,
439    F: Fn(M::Out, M::Out) -> Combined + Send,
440    M::Out: Default + Clone + Send,
441{
442    type In = M::Out;
443    type Out = M::Out;
444
445    fn tick(&mut self, input: Self::In) -> Self::Out {
446        let combined = (self.combine)(input, self.delay_buffer.clone());
447        let output = self.module.tick(combined);
448        self.delay_buffer = output.clone();
449        output
450    }
451
452    fn reset(&mut self) {
453        self.module.reset();
454        self.delay_buffer = M::Out::default();
455    }
456
457    fn set_sample_rate(&mut self, sample_rate: f64) {
458        self.module.set_sample_rate(sample_rate);
459    }
460}
461
462/// Transform output with a pure function
463pub struct Map<M, F> {
464    pub module: M,
465    pub f: F,
466}
467
468impl<M, F, U> Module for Map<M, F>
469where
470    M: Module,
471    F: Fn(M::Out) -> U + Send,
472{
473    type In = M::In;
474    type Out = U;
475
476    #[inline]
477    fn tick(&mut self, input: Self::In) -> Self::Out {
478        (self.f)(self.module.tick(input))
479    }
480
481    fn reset(&mut self) {
482        self.module.reset();
483    }
484
485    fn set_sample_rate(&mut self, sample_rate: f64) {
486        self.module.set_sample_rate(sample_rate);
487    }
488}
489
490/// Transform input with a pure function
491pub struct Contramap<M, F, U> {
492    pub module: M,
493    pub f: F,
494    pub _phantom: PhantomData<U>,
495}
496
497impl<M, F, U> Module for Contramap<M, F, U>
498where
499    M: Module,
500    F: Fn(U) -> M::In + Send,
501    U: Send,
502{
503    type In = U;
504    type Out = M::Out;
505
506    #[inline]
507    fn tick(&mut self, input: Self::In) -> Self::Out {
508        self.module.tick((self.f)(input))
509    }
510
511    fn reset(&mut self) {
512        self.module.reset();
513    }
514
515    fn set_sample_rate(&mut self, sample_rate: f64) {
516        self.module.set_sample_rate(sample_rate);
517    }
518}
519
520/// Duplicate a signal
521pub struct Split<T> {
522    _phantom: PhantomData<T>,
523}
524
525impl<T> Split<T> {
526    pub fn new() -> Self {
527        Self {
528            _phantom: PhantomData,
529        }
530    }
531}
532
533impl<T> Default for Split<T> {
534    fn default() -> Self {
535        Self::new()
536    }
537}
538
539impl<T: Clone + Send> Module for Split<T> {
540    type In = T;
541    type Out = (T, T);
542
543    #[inline]
544    fn tick(&mut self, input: Self::In) -> Self::Out {
545        (input.clone(), input)
546    }
547
548    fn reset(&mut self) {}
549}
550
551/// Combine two signals with a function
552pub struct Merge<T, F> {
553    pub f: F,
554    _phantom: PhantomData<T>,
555}
556
557impl<T, F> Merge<T, F>
558where
559    F: Fn(T, T) -> T,
560{
561    pub fn new(f: F) -> Self {
562        Self {
563            f,
564            _phantom: PhantomData,
565        }
566    }
567}
568
569impl<T, F> Module for Merge<T, F>
570where
571    T: Send,
572    F: Fn(T, T) -> T + Send,
573{
574    type In = (T, T);
575    type Out = T;
576
577    #[inline]
578    fn tick(&mut self, (a, b): Self::In) -> Self::Out {
579        (self.f)(a, b)
580    }
581
582    fn reset(&mut self) {}
583}
584
585/// Swap tuple elements
586pub struct Swap<A, B> {
587    _phantom: PhantomData<(A, B)>,
588}
589
590impl<A, B> Swap<A, B> {
591    pub fn new() -> Self {
592        Self {
593            _phantom: PhantomData,
594        }
595    }
596}
597
598impl<A, B> Default for Swap<A, B> {
599    fn default() -> Self {
600        Self::new()
601    }
602}
603
604impl<A: Send, B: Send> Module for Swap<A, B> {
605    type In = (A, B);
606    type Out = (B, A);
607
608    #[inline]
609    fn tick(&mut self, (a, b): Self::In) -> Self::Out {
610        (b, a)
611    }
612
613    fn reset(&mut self) {}
614}
615
616/// Process first element, pass through second
617pub struct First<M, C> {
618    pub module: M,
619    pub _phantom: PhantomData<C>,
620}
621
622impl<M, C> Module for First<M, C>
623where
624    M: Module,
625    C: Send,
626{
627    type In = (M::In, C);
628    type Out = (M::Out, C);
629
630    #[inline]
631    fn tick(&mut self, (a, c): Self::In) -> Self::Out {
632        (self.module.tick(a), c)
633    }
634
635    fn reset(&mut self) {
636        self.module.reset();
637    }
638
639    fn set_sample_rate(&mut self, sample_rate: f64) {
640        self.module.set_sample_rate(sample_rate);
641    }
642}
643
644/// Pass through first element, process second
645pub struct Second<M, C> {
646    pub module: M,
647    pub _phantom: PhantomData<C>,
648}
649
650impl<M, C> Module for Second<M, C>
651where
652    M: Module,
653    C: Send,
654{
655    type In = (C, M::In);
656    type Out = (C, M::Out);
657
658    #[inline]
659    fn tick(&mut self, (c, a): Self::In) -> Self::Out {
660        (c, self.module.tick(a))
661    }
662
663    fn reset(&mut self) {
664        self.module.reset();
665    }
666
667    fn set_sample_rate(&mut self, sample_rate: f64) {
668        self.module.set_sample_rate(sample_rate);
669    }
670}
671
672/// Identity: pass-through module (categorical identity)
673pub struct Identity<T> {
674    _phantom: PhantomData<T>,
675}
676
677impl<T> Identity<T> {
678    pub fn new() -> Self {
679        Self {
680            _phantom: PhantomData,
681        }
682    }
683}
684
685impl<T> Default for Identity<T> {
686    fn default() -> Self {
687        Self::new()
688    }
689}
690
691impl<T: Send> Module for Identity<T> {
692    type In = T;
693    type Out = T;
694
695    #[inline]
696    fn tick(&mut self, input: Self::In) -> Self::Out {
697        input
698    }
699
700    fn reset(&mut self) {}
701}
702
703/// Constant: emit a constant value (ignores input)
704pub struct Constant<T> {
705    pub value: T,
706}
707
708impl<T> Constant<T> {
709    pub fn new(value: T) -> Self {
710        Self { value }
711    }
712}
713
714impl<T: Clone + Send> Module for Constant<T> {
715    type In = ();
716    type Out = T;
717
718    #[inline]
719    fn tick(&mut self, _input: Self::In) -> Self::Out {
720        self.value.clone()
721    }
722
723    fn reset(&mut self) {}
724}
725
726/// A stateless module that lifts a pure function into the [`Module`] world.
727///
728/// This is the Arrow `arr` primitive: `arr : (a -> b) -> Arrow a b`. Unlike
729/// [`ModuleExt::map`] (which post-composes a function onto an *existing* module), `arr`
730/// builds a standalone module directly from a function, carrying no state. Construct it
731/// with the free function [`arr`].
732pub struct Arr<F, A> {
733    f: F,
734    _phantom: PhantomData<A>,
735}
736
737/// Lift a pure function into a [`Module`] (the Arrow `arr` primitive).
738///
739/// `arr(f)` produces a stateless module whose `tick` is exactly `f`. This is the missing
740/// Arrow primitive that lets combinator laws be expressed and tested against plain
741/// functions.
742///
743/// # Examples
744///
745/// ```
746/// use quiver::combinator::{arr, Module};
747///
748/// let mut double = arr(|x: f64| x * 2.0);
749/// assert_eq!(double.tick(21.0), 42.0);
750/// ```
751pub fn arr<F, A, B>(f: F) -> Arr<F, A>
752where
753    F: Fn(A) -> B,
754{
755    Arr {
756        f,
757        _phantom: PhantomData,
758    }
759}
760
761impl<F, A, B> Module for Arr<F, A>
762where
763    F: Fn(A) -> B + Send,
764    A: Send,
765{
766    type In = A;
767    type Out = B;
768
769    #[inline]
770    fn tick(&mut self, input: Self::In) -> Self::Out {
771        (self.f)(input)
772    }
773
774    fn reset(&mut self) {}
775}
776
777/// Adapts a [`GraphModule`] (the engine's multi-port, type-erased module trait) into a
778/// single-in / single-out combinator [`Module`], so real DSP modules such as
779/// [`Vco`](crate::modules::Vco) or [`Svf`](crate::modules::Svf) can be composed with
780/// `.then`, `.parallel`, and `.fanout`.
781///
782/// One input port and one output port of the wrapped module are chosen at construction
783/// (by id, via [`new`](GraphModuleAdapter::new), or automatically from the first audio
784/// ports, via [`from_audio_ports`](GraphModuleAdapter::from_audio_ports)). Every other
785/// input port keeps its [`PortDef`] default; the wrapped module still reads those on each
786/// `tick`, so pulse-width, cutoff, etc. behave as if unpatched in a graph.
787pub struct GraphModuleAdapter<G: GraphModule> {
788    module: G,
789    input_port: PortId,
790    output_port: PortId,
791    inputs: PortValues,
792    outputs: PortValues,
793}
794
795impl<G: GraphModule> GraphModuleAdapter<G> {
796    /// Wrap `module`, driving input port `input_port` and reading output port
797    /// `output_port` on every `tick`.
798    pub fn new(module: G, input_port: PortId, output_port: PortId) -> Self {
799        let mut inputs = PortValues::new();
800        for port in &module.port_spec().inputs {
801            inputs.set(port.id, port.default);
802        }
803        Self {
804            module,
805            input_port,
806            output_port,
807            inputs,
808            outputs: PortValues::new(),
809        }
810    }
811
812    /// Wrap `module`, automatically choosing its first [`SignalKind::Audio`] input and
813    /// first [`SignalKind::Audio`] output port.
814    ///
815    /// Returns `None` if the module exposes no audio input or no audio output (e.g. a
816    /// `Vco`, whose only inputs are CV/pitch — use [`new`](GraphModuleAdapter::new) with
817    /// an explicit port id for those).
818    pub fn from_audio_ports(module: G) -> Option<Self> {
819        let (input_port, output_port) = {
820            let spec = module.port_spec();
821            let input_port = spec.inputs.iter().find(|p| p.kind == SignalKind::Audio)?.id;
822            let output_port = spec
823                .outputs
824                .iter()
825                .find(|p| p.kind == SignalKind::Audio)?
826                .id;
827            (input_port, output_port)
828        };
829        Some(Self::new(module, input_port, output_port))
830    }
831
832    /// Borrow the wrapped [`GraphModule`].
833    pub fn inner(&self) -> &G {
834        &self.module
835    }
836
837    /// Consume the adapter, returning the wrapped [`GraphModule`].
838    pub fn into_inner(self) -> G {
839        self.module
840    }
841}
842
843impl<G: GraphModule> Module for GraphModuleAdapter<G> {
844    type In = f64;
845    type Out = f64;
846
847    #[inline]
848    fn tick(&mut self, input: Self::In) -> Self::Out {
849        self.inputs.set(self.input_port, input);
850        // Clear the output buffer before ticking so an output the wrapped module
851        // does NOT write this tick reads back as the `get_or` default (0.0),
852        // matching the graph engine's per-sample `scratch_out.clear()`
853        // (see `Patch::tick_step`). Without this, a module that conditionally
854        // omits writing its selected port (e.g. an event/trigger-driven module)
855        // would return a stale prior-tick value here but 0.0 inside a `Patch`.
856        self.outputs.clear();
857        self.module.tick(&self.inputs, &mut self.outputs);
858        self.outputs.get_or(self.output_port, 0.0)
859    }
860
861    fn reset(&mut self) {
862        self.module.reset();
863    }
864
865    fn set_sample_rate(&mut self, sample_rate: f64) {
866        self.module.set_sample_rate(sample_rate);
867    }
868}
869
870/// Adapts a single-in / single-out combinator [`Module`] into a [`GraphModule`], so a
871/// combinator chain (e.g. `a.then(b).then(c)`) can be added to a
872/// [`Patch`](crate::graph::Patch) via `patch.add(...)`.
873///
874/// The generated [`PortSpec`] has exactly one input port (id `0`) and one output port
875/// (id `10`), both [`SignalKind::Audio`], following the crate's input-ids-from-0 /
876/// output-ids-from-10 convention.
877pub struct ModuleGraphAdapter<M> {
878    module: M,
879    spec: PortSpec,
880}
881
882impl<M> ModuleGraphAdapter<M>
883where
884    M: Module<In = f64, Out = f64>,
885{
886    /// Wrap `module` with a one-in (`"in"`, id `0`) / one-out (`"out"`, id `10`) audio
887    /// port spec.
888    pub fn new(module: M) -> Self {
889        Self::with_ports(module, "in", "out")
890    }
891
892    /// Wrap `module`, naming its single input (id `0`) and output (id `10`) ports.
893    pub fn with_ports(
894        module: M,
895        input_name: impl Into<String>,
896        output_name: impl Into<String>,
897    ) -> Self {
898        let spec = PortSpec {
899            inputs: vec![PortDef::new(0, input_name, SignalKind::Audio)],
900            outputs: vec![PortDef::new(10, output_name, SignalKind::Audio)],
901        };
902        Self { module, spec }
903    }
904}
905
906impl<M> GraphModule for ModuleGraphAdapter<M>
907where
908    M: Module<In = f64, Out = f64> + Sync,
909{
910    fn port_spec(&self) -> &PortSpec {
911        &self.spec
912    }
913
914    fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
915        let x = inputs.get_or(0, 0.0);
916        let y = self.module.tick(x);
917        outputs.set(10, y);
918    }
919
920    fn reset(&mut self) {
921        self.module.reset();
922    }
923
924    fn set_sample_rate(&mut self, sample_rate: f64) {
925        self.module.set_sample_rate(sample_rate);
926    }
927
928    fn type_id(&self) -> &'static str {
929        "combinator_chain"
930    }
931}
932
933#[cfg(test)]
934mod tests {
935    use super::*;
936    use crate::port::{GraphModule, PortDef, PortSpec, PortValues, SignalKind};
937
938    // Simple test module that multiplies by a constant
939    struct Gain {
940        factor: f64,
941    }
942
943    impl Module for Gain {
944        type In = f64;
945        type Out = f64;
946
947        fn tick(&mut self, input: Self::In) -> Self::Out {
948            input * self.factor
949        }
950
951        fn reset(&mut self) {}
952    }
953
954    #[test]
955    fn test_chain() {
956        let mut chain = Gain { factor: 2.0 }.then(Gain { factor: 3.0 });
957        assert!((chain.tick(1.0) - 6.0).abs() < 1e-10);
958    }
959
960    #[test]
961    fn test_parallel() {
962        let mut par = Gain { factor: 2.0 }.parallel(Gain { factor: 3.0 });
963        let (a, b) = par.tick((1.0, 1.0));
964        assert!((a - 2.0).abs() < 1e-10);
965        assert!((b - 3.0).abs() < 1e-10);
966    }
967
968    #[test]
969    fn test_fanout() {
970        let mut fan = Gain { factor: 2.0 }.fanout(Gain { factor: 3.0 });
971        let (a, b) = fan.tick(1.0);
972        assert!((a - 2.0).abs() < 1e-10);
973        assert!((b - 3.0).abs() < 1e-10);
974    }
975
976    #[test]
977    fn test_map() {
978        let mut mapped = Gain { factor: 2.0 }.map(|x| x + 1.0);
979        assert!((mapped.tick(1.0) - 3.0).abs() < 1e-10);
980    }
981
982    #[test]
983    fn test_identity() {
984        let mut id = Identity::<f64>::new();
985        assert!((id.tick(42.0) - 42.0).abs() < 1e-10);
986    }
987
988    #[test]
989    fn test_constant() {
990        let mut c = Constant::new(42.0_f64);
991        assert!((c.tick(()) - 42.0).abs() < 1e-10);
992    }
993
994    #[test]
995    fn test_split() {
996        let mut split = Split::<f64>::new();
997        let (a, b) = split.tick(5.0);
998        assert!((a - 5.0).abs() < 1e-10);
999        assert!((b - 5.0).abs() < 1e-10);
1000    }
1001
1002    #[test]
1003    fn test_merge() {
1004        let mut merge = Merge::new(|a: f64, b: f64| a + b);
1005        assert!((merge.tick((2.0, 3.0)) - 5.0).abs() < 1e-10);
1006    }
1007
1008    #[test]
1009    fn test_swap() {
1010        let mut swap = Swap::<i32, f64>::new();
1011        assert_eq!(swap.tick((1, 2.0)), (2.0, 1));
1012    }
1013
1014    // Additional tests for 100% coverage
1015
1016    // Test module with sample_rate awareness
1017    struct SampleRateAware {
1018        sample_rate: f64,
1019        count: u32,
1020    }
1021
1022    impl SampleRateAware {
1023        fn new() -> Self {
1024            Self {
1025                sample_rate: 44100.0,
1026                count: 0,
1027            }
1028        }
1029    }
1030
1031    impl Module for SampleRateAware {
1032        type In = f64;
1033        type Out = f64;
1034
1035        fn tick(&mut self, input: Self::In) -> Self::Out {
1036            self.count += 1;
1037            input * self.sample_rate / 44100.0
1038        }
1039
1040        fn reset(&mut self) {
1041            self.count = 0;
1042        }
1043
1044        fn set_sample_rate(&mut self, sample_rate: f64) {
1045            self.sample_rate = sample_rate;
1046        }
1047    }
1048
1049    #[test]
1050    fn test_chain_reset_and_sample_rate() {
1051        let mut chain = SampleRateAware::new().then(SampleRateAware::new());
1052
1053        chain.tick(1.0);
1054        chain.tick(1.0);
1055
1056        // Reset should reset both modules
1057        chain.reset();
1058        assert_eq!(chain.first.count, 0);
1059        assert_eq!(chain.second.count, 0);
1060
1061        // Set sample rate should propagate
1062        chain.set_sample_rate(48000.0);
1063        assert_eq!(chain.first.sample_rate, 48000.0);
1064        assert_eq!(chain.second.sample_rate, 48000.0);
1065    }
1066
1067    #[test]
1068    fn test_parallel_reset_and_sample_rate() {
1069        let mut par = SampleRateAware::new().parallel(SampleRateAware::new());
1070
1071        par.tick((1.0, 1.0));
1072        par.tick((1.0, 1.0));
1073
1074        par.reset();
1075        par.set_sample_rate(48000.0);
1076
1077        let result = par.tick((1.0, 1.0));
1078        assert!(result.0.abs() < 10.0);
1079    }
1080
1081    #[test]
1082    fn test_fanout_reset_and_sample_rate() {
1083        let mut fan = SampleRateAware::new().fanout(SampleRateAware::new());
1084
1085        fan.tick(1.0);
1086        fan.tick(1.0);
1087
1088        fan.reset();
1089        fan.set_sample_rate(48000.0);
1090
1091        let result = fan.tick(1.0);
1092        assert!(result.0.abs() < 10.0);
1093    }
1094
1095    #[test]
1096    fn test_feedback_reset_and_sample_rate() {
1097        let feedback_fn = |x: f64, prev: f64| x + prev * 0.5;
1098        let mut fb = SampleRateAware::new().feedback(feedback_fn);
1099
1100        for _ in 0..10 {
1101            fb.tick(1.0);
1102        }
1103
1104        fb.reset();
1105        fb.set_sample_rate(48000.0);
1106    }
1107
1108    #[test]
1109    fn test_map_reset_and_sample_rate() {
1110        let mut mapped = SampleRateAware::new().map(|x| x + 1.0);
1111
1112        mapped.tick(1.0);
1113        mapped.tick(1.0);
1114
1115        mapped.reset();
1116        mapped.set_sample_rate(48000.0);
1117
1118        let result = mapped.tick(1.0);
1119        assert!(result.abs() < 10.0);
1120    }
1121
1122    #[test]
1123    fn test_contramap() {
1124        let mut contra = Gain { factor: 2.0 }.contramap(|x: f64| x + 1.0);
1125        assert!((contra.tick(1.0) - 4.0).abs() < 1e-10); // (1+1) * 2 = 4
1126
1127        // Test reset and sample_rate
1128        contra.reset();
1129        contra.set_sample_rate(48000.0);
1130    }
1131
1132    #[test]
1133    fn test_contramap_reset_and_sample_rate() {
1134        let mut contra = SampleRateAware::new().contramap(|x: f64| x * 2.0);
1135
1136        contra.tick(1.0);
1137        contra.reset();
1138        contra.set_sample_rate(48000.0);
1139
1140        let result = contra.tick(1.0);
1141        assert!(result.abs() < 10.0);
1142    }
1143
1144    #[test]
1145    fn test_first() {
1146        let mut first = Gain { factor: 2.0 }.first::<i32>();
1147        let (a, b) = first.tick((3.0, 42));
1148        assert!((a - 6.0).abs() < 1e-10);
1149        assert_eq!(b, 42);
1150    }
1151
1152    #[test]
1153    fn test_first_reset_and_sample_rate() {
1154        let mut first = SampleRateAware::new().first::<i32>();
1155
1156        first.tick((1.0, 0));
1157        first.reset();
1158        first.set_sample_rate(48000.0);
1159
1160        let (result, _) = first.tick((1.0, 0));
1161        assert!(result.abs() < 10.0);
1162    }
1163
1164    #[test]
1165    fn test_second() {
1166        let mut second = Gain { factor: 2.0 }.second::<i32>();
1167        let (a, b) = second.tick((42, 3.0));
1168        assert_eq!(a, 42);
1169        assert!((b - 6.0).abs() < 1e-10);
1170    }
1171
1172    #[test]
1173    fn test_second_reset_and_sample_rate() {
1174        let mut second = SampleRateAware::new().second::<i32>();
1175
1176        second.tick((0, 1.0));
1177        second.reset();
1178        second.set_sample_rate(48000.0);
1179
1180        let (_, result) = second.tick((0, 1.0));
1181        assert!(result.abs() < 10.0);
1182    }
1183
1184    #[test]
1185    fn test_identity_reset() {
1186        let mut id = Identity::<f64>::new();
1187        id.reset(); // Should not panic
1188        assert!((id.tick(42.0) - 42.0).abs() < 1e-10);
1189    }
1190
1191    #[test]
1192    fn test_identity_default() {
1193        let id: Identity<f64> = Identity::default();
1194        assert!(std::mem::size_of_val(&id) == 0);
1195    }
1196
1197    #[test]
1198    fn test_constant_reset() {
1199        let mut c = Constant::new(42.0_f64);
1200        c.reset(); // Should not panic
1201        assert!((c.tick(()) - 42.0).abs() < 1e-10);
1202    }
1203
1204    #[test]
1205    fn test_split_reset() {
1206        let mut split = Split::<f64>::new();
1207        split.reset(); // Should not panic
1208    }
1209
1210    #[test]
1211    fn test_split_default() {
1212        let split: Split<f64> = Split::default();
1213        let (a, b) = Split::<f64>::new().tick(1.0);
1214        assert!((a - 1.0).abs() < 1e-10);
1215        assert!((b - 1.0).abs() < 1e-10);
1216        let _ = split;
1217    }
1218
1219    #[test]
1220    fn test_merge_reset() {
1221        let mut merge = Merge::new(|a: f64, b: f64| a + b);
1222        merge.reset(); // Should not panic
1223    }
1224
1225    #[test]
1226    fn test_swap_reset() {
1227        let mut swap = Swap::<i32, f64>::new();
1228        swap.reset(); // Should not panic
1229    }
1230
1231    #[test]
1232    fn test_swap_default() {
1233        let swap: Swap<i32, f64> = Swap::default();
1234        let _ = swap;
1235    }
1236
1237    #[test]
1238    fn test_process_block() {
1239        let mut gain = Gain { factor: 2.0 };
1240        let input = vec![1.0, 2.0, 3.0, 4.0];
1241        let mut output = vec![0.0; 4];
1242        gain.process(&input, &mut output);
1243        assert_eq!(output, vec![2.0, 4.0, 6.0, 8.0]);
1244    }
1245
1246    // =========================================================================
1247    // Q052: `arr` primitive and Arrow-law tests
1248    // =========================================================================
1249
1250    // Stateful test module: running sum. Makes law tests non-trivial (an
1251    // associativity/identity check on a stateless module would be vacuous).
1252    struct Accum {
1253        sum: f64,
1254    }
1255
1256    impl Accum {
1257        fn new() -> Self {
1258            Self { sum: 0.0 }
1259        }
1260    }
1261
1262    impl Module for Accum {
1263        type In = f64;
1264        type Out = f64;
1265
1266        fn tick(&mut self, input: Self::In) -> Self::Out {
1267            self.sum += input;
1268            self.sum
1269        }
1270
1271        fn reset(&mut self) {
1272            self.sum = 0.0;
1273        }
1274    }
1275
1276    // Deterministic input sequence used by all law checks.
1277    const SEQ: [f64; 8] = [0.5, -0.3, 1.0, 2.0, -1.5, 0.25, 3.0, -0.7];
1278
1279    fn run_mono<M: Module<In = f64, Out = f64>>(mut m: M) -> Vec<f64> {
1280        SEQ.iter().map(|&x| m.tick(x)).collect()
1281    }
1282
1283    // Feeds SEQ as the processed element and the sample index as the pass-through
1284    // element, so `first`/`second` pass-through behavior is also exercised.
1285    fn run_pair<M: Module<In = (f64, f64), Out = (f64, f64)>>(mut m: M) -> Vec<(f64, f64)> {
1286        SEQ.iter()
1287            .enumerate()
1288            .map(|(i, &x)| m.tick((x, i as f64)))
1289            .collect()
1290    }
1291
1292    fn assert_seq_close(a: &[f64], b: &[f64]) {
1293        assert_eq!(a.len(), b.len());
1294        for (x, y) in a.iter().zip(b) {
1295            assert!((x - y).abs() < 1e-12, "sequence mismatch: {x} != {y}");
1296        }
1297    }
1298
1299    #[test]
1300    fn test_arr_basic() {
1301        let mut m = arr(|x: f64| x * 2.0 + 1.0);
1302        assert!((m.tick(3.0) - 7.0).abs() < 1e-12);
1303        // Stateless: reset / set_sample_rate are no-ops but must not panic.
1304        m.reset();
1305        m.set_sample_rate(48_000.0);
1306        assert!((m.tick(0.0) - 1.0).abs() < 1e-12);
1307    }
1308
1309    #[test]
1310    fn arrow_law_identity() {
1311        // id.then(f) == f  and  f.then(id) == f
1312        let reference = run_mono(Accum::new());
1313        let left_id = run_mono(Identity::<f64>::new().then(Accum::new()));
1314        let right_id = run_mono(Accum::new().then(Identity::<f64>::new()));
1315        assert_seq_close(&left_id, &reference);
1316        assert_seq_close(&right_id, &reference);
1317    }
1318
1319    #[test]
1320    fn arrow_law_associativity() {
1321        // (f.then(g)).then(h) == f.then(g.then(h))
1322        // f, h are stateful (Accum); g is a scaling module.
1323        let lhs = run_mono((Accum::new().then(Gain { factor: 2.0 })).then(Accum::new()));
1324        let rhs = run_mono(Accum::new().then(Gain { factor: 2.0 }.then(Accum::new())));
1325        assert_seq_close(&lhs, &rhs);
1326    }
1327
1328    #[test]
1329    fn arrow_law_first_distributes() {
1330        // first(f.then(g)) == first(f).then(first(g))
1331        let lhs = run_pair(Accum::new().then(Gain { factor: 3.0 }).first::<f64>());
1332        let rhs = run_pair(
1333            Accum::new()
1334                .first::<f64>()
1335                .then(Gain { factor: 3.0 }.first::<f64>()),
1336        );
1337        assert_eq!(lhs.len(), rhs.len());
1338        for (a, b) in lhs.iter().zip(&rhs) {
1339            assert!((a.0 - b.0).abs() < 1e-12, "processed element differs");
1340            assert!((a.1 - b.1).abs() < 1e-12, "pass-through element differs");
1341        }
1342        // Sanity: the pass-through element is carried unchanged (equals the index).
1343        for (i, pair) in lhs.iter().enumerate() {
1344            assert!((pair.1 - i as f64).abs() < 1e-12);
1345        }
1346    }
1347
1348    // =========================================================================
1349    // Q050: GraphModule <-> Module bridge adapters
1350    // =========================================================================
1351
1352    // A minimal GraphModule for precise port-selection checks: out(10) = 2 * in(0).
1353    struct DoublerGm {
1354        spec: PortSpec,
1355    }
1356
1357    impl DoublerGm {
1358        fn new() -> Self {
1359            Self {
1360                spec: PortSpec {
1361                    inputs: vec![PortDef::new(0, "in", SignalKind::Audio)],
1362                    outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
1363                },
1364            }
1365        }
1366    }
1367
1368    impl GraphModule for DoublerGm {
1369        fn port_spec(&self) -> &PortSpec {
1370            &self.spec
1371        }
1372        fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1373            outputs.set(10, inputs.get_or(0, 0.0) * 2.0);
1374        }
1375        fn reset(&mut self) {}
1376        fn set_sample_rate(&mut self, _sample_rate: f64) {}
1377    }
1378
1379    #[test]
1380    fn test_graph_module_adapter_drives_selected_ports() {
1381        // GraphModule -> Module: chosen input (0) is driven, chosen output (10) is read.
1382        let mut adapter = GraphModuleAdapter::new(DoublerGm::new(), 0, 10);
1383        assert!((adapter.tick(3.0) - 6.0).abs() < 1e-12);
1384        assert!((adapter.tick(-2.5) - (-5.0)).abs() < 1e-12);
1385        // inner() exposes the wrapped module.
1386        assert_eq!(adapter.inner().port_spec().outputs[0].id, 10);
1387        // reset / set_sample_rate forward without panic.
1388        adapter.reset();
1389        adapter.set_sample_rate(48_000.0);
1390    }
1391
1392    // A trigger-style GraphModule that writes its output ONLY when its input is
1393    // high, exercising the conditionally-writing / event-driven contract: an
1394    // unwritten output must read back as the default (0.0), not a stale value.
1395    struct GatedEmit {
1396        spec: PortSpec,
1397    }
1398    impl GatedEmit {
1399        fn new() -> Self {
1400            Self {
1401                spec: PortSpec {
1402                    inputs: vec![PortDef::new(0, "trig", SignalKind::Audio)],
1403                    outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
1404                },
1405            }
1406        }
1407    }
1408    impl GraphModule for GatedEmit {
1409        fn port_spec(&self) -> &PortSpec {
1410            &self.spec
1411        }
1412        fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1413            // Only emit on a high trigger; otherwise write nothing this tick.
1414            if inputs.get_or(0, 0.0) > 0.5 {
1415                outputs.set(10, 1.0);
1416            }
1417        }
1418        fn reset(&mut self) {}
1419        fn set_sample_rate(&mut self, _sample_rate: f64) {}
1420    }
1421
1422    // Regression: the adapter must clear its output buffer each tick so a module
1423    // that omits writing its output this tick reads back as the default (0.0),
1424    // matching the graph engine's per-sample `scratch_out.clear()`. Pre-fix the
1425    // adapter retained the prior tick's value and returned a stale 1.0.
1426    #[test]
1427    fn test_graph_module_adapter_clears_stale_outputs() {
1428        let mut adapter = GraphModuleAdapter::new(GatedEmit::new(), 0, 10);
1429        // Trigger high -> module emits 1.0.
1430        assert!((adapter.tick(1.0) - 1.0).abs() < 1e-12);
1431        // Trigger low -> module writes nothing; adapter must report 0.0, not 1.0.
1432        assert!(
1433            adapter.tick(0.0).abs() < 1e-12,
1434            "unwritten output must read as default 0.0, not the stale prior value"
1435        );
1436        // And it recovers when the trigger returns high.
1437        assert!((adapter.tick(1.0) - 1.0).abs() < 1e-12);
1438    }
1439
1440    #[test]
1441    fn test_graph_module_adapter_from_audio_ports() {
1442        use crate::modules::{Svf, Vco};
1443        // Svf has an audio input ("in") and audio outputs -> Some.
1444        let svf = GraphModuleAdapter::from_audio_ports(Svf::new(44_100.0));
1445        assert!(svf.is_some());
1446        // Vco's inputs are all CV/pitch/gate (no Audio input) -> None.
1447        assert!(GraphModuleAdapter::from_audio_ports(Vco::new(44_100.0)).is_none());
1448    }
1449
1450    #[test]
1451    fn test_graph_module_adapter_vco_svf_chain_produces_audio() {
1452        use crate::modules::{Svf, Vco};
1453        // Flagship Q050 check: a REAL Vco adapted and chained into a REAL Svf,
1454        // via the combinator `.then`, produces nonzero audio.
1455        let vco = GraphModuleAdapter::new(Vco::new(44_100.0), 0, 12); // voct in, saw out
1456        let svf = GraphModuleAdapter::from_audio_ports(Svf::new(44_100.0)).unwrap();
1457        let mut chain = vco.then(svf);
1458
1459        let mut peak = 0.0_f64;
1460        for _ in 0..512 {
1461            peak = peak.max(chain.tick(0.0).abs()); // 0 V/oct = middle C
1462        }
1463        assert!(
1464            peak > 1e-6,
1465            "expected nonzero audio from Vco -> Svf combinator chain, got {peak}"
1466        );
1467    }
1468
1469    #[test]
1470    fn test_module_graph_adapter_direct() {
1471        // Module -> GraphModule: one-in/one-out spec, tick maps in(0) -> out(10).
1472        let mut node = ModuleGraphAdapter::new(arr(|x: f64| x * 3.0));
1473        assert_eq!(node.port_spec().inputs.len(), 1);
1474        assert_eq!(node.port_spec().outputs.len(), 1);
1475        assert_eq!(node.port_spec().inputs[0].id, 0);
1476        assert_eq!(node.port_spec().outputs[0].id, 10);
1477        assert_eq!(node.type_id(), "combinator_chain");
1478
1479        let mut inputs = PortValues::new();
1480        inputs.set(0, 2.0);
1481        let mut outputs = PortValues::new();
1482        node.tick(&inputs, &mut outputs);
1483        assert!((outputs.get_or(10, 0.0) - 6.0).abs() < 1e-12);
1484
1485        node.reset();
1486        node.set_sample_rate(48_000.0);
1487    }
1488
1489    #[test]
1490    fn test_module_graph_adapter_in_patch() {
1491        use crate::graph::Patch;
1492        use crate::modules::{StereoOutput, Vco};
1493
1494        // A combinator chain wrapped as a GraphModule node, added to a Patch.
1495        let mut patch = Patch::new(44_100.0);
1496        let chain = arr(|x: f64| x * 0.5).then(arr(|x: f64| x + 0.1));
1497        let node = patch.add("chain", ModuleGraphAdapter::new(chain));
1498        let vco = patch.add("vco", Vco::new(44_100.0));
1499        let out = patch.add("out", StereoOutput::new());
1500
1501        patch.connect(vco.out("saw"), node.in_("in")).unwrap();
1502        patch.connect(node.out("out"), out.in_("left")).unwrap();
1503        patch.set_output(out.id());
1504        patch.compile().unwrap();
1505
1506        let mut peak = 0.0_f64;
1507        for _ in 0..512 {
1508            let (left, _right) = patch.tick();
1509            peak = peak.max(left.abs());
1510        }
1511        assert!(
1512            peak > 1e-6,
1513            "combinator chain wrapped as a GraphModule should tick inside a Patch, got {peak}"
1514        );
1515    }
1516}