Skip to main content

pedalkernel/
lib.rs

1// Pre-existing lint suppressions — these cover known technical debt throughout
2// the codebase. New code should not rely on these allows.
3#![allow(dead_code)]
4#![allow(unused_variables)]
5#![allow(unused_mut)]
6#![allow(unused_imports)]
7#![allow(clippy::too_many_arguments)]
8#![allow(clippy::type_complexity)]
9#![allow(clippy::needless_doctest_main)]
10#![allow(clippy::only_used_in_recursion)]
11#![allow(clippy::if_same_then_else)]
12#![allow(clippy::redundant_closure)]
13#![allow(clippy::unnecessary_to_owned)]
14#![allow(clippy::unnecessary_unwrap)]
15#![allow(clippy::useless_vec)]
16#![allow(clippy::needless_range_loop)]
17#![allow(clippy::while_let_loop)]
18#![allow(clippy::map_flatten)]
19#![allow(clippy::large_enum_variant)]
20#![allow(clippy::enum_variant_names)]
21#![allow(clippy::approx_constant)]
22#![allow(clippy::boxed_local)]
23#![allow(clippy::collapsible_else_if)]
24#![allow(clippy::collapsible_if)]
25#![allow(clippy::redundant_pattern_matching)]
26#![allow(clippy::ptr_arg)]
27#![allow(clippy::vec_box)]
28#![allow(clippy::unnecessary_get_then_check)]
29#![allow(clippy::unnecessary_literal_unwrap)]
30#![allow(clippy::should_implement_trait)]
31#![allow(clippy::unused_enumerate_index)]
32#![allow(clippy::for_kv_map)]
33#![allow(clippy::io_other_error)]
34#![allow(clippy::manual_is_multiple_of)]
35#![allow(clippy::manual_map)]
36#![allow(clippy::needless_borrows_for_generic_args)]
37#![allow(clippy::redundant_field_names)]
38#![allow(clippy::single_match)]
39#![allow(clippy::unnecessary_map_or)]
40#![allow(clippy::write_literal)]
41#![allow(private_interfaces)]
42//! # PedalKernel
43//!
44//! A DSL compiler that transforms `.pedal` circuit netlists into real-time audio
45//! processors using Wave Digital Filter (WDF) theory. PedalKernel models the
46//! complete analog signal path of guitar effect pedals — resistors, capacitors,
47//! op-amps, transistors, vacuum tubes, diodes — and synthesizes a per-sample
48//! processing kernel that runs at audio rate with deterministic, bounded latency.
49//!
50//! # Compilation Pipeline
51//!
52//! The compiler transforms a human-readable circuit description into an optimized
53//! real-time audio processor through five stages:
54//!
55//! ```text
56//!   .pedal DSL         nom parser           Circuit graph
57//!  ┌──────────┐      ┌──────────┐         ┌──────────────┐
58//!  │ pedal    │      │ PedalDef │         │ CircuitGraph │
59//!  │ "RAT" { │─────>│ (AST)    │────────>│ (nodes+edges)│
60//!  │  R1: ... │ parse│          │ graph   │              │
61//!  └──────────┘      └──────────┘ build   └──────┬───────┘
62//!                                                │
63//!                         SPQR decomposition     │
64//!                        ┌───────────────────────┘
65//!                        │
66//!                        v
67//!            ┌───────────────────────┐
68//!            │  S / P / Q / R nodes  │
69//!            │  (unique tree decomp) │
70//!            └───────────┬───────────┘
71//!                        │
72//!                        v
73//!              ┌──────────────────┐       ┌─────────────────┐
74//!              │  SpqrStage list  │──────>│  CompiledPedal  │
75//!              │  (WDF, IIR, MNA) │ build │  (PedalProcessor│
76//!              └──────────────────┘       │   impl)         │
77//!                                         └─────────────────┘
78//! ```
79//!
80//! 1. **Parse**: The [`dsl`] module uses nom to parse `.pedal` files into a
81//!    [`dsl::PedalDef`] AST containing components, nets, and controls.
82//!
83//! 2. **Graph build**: The [`compiler`] constructs a [`CircuitGraph`](compiler::component::Component)
84//!    from the AST. Each component declares its own topology via the
85//!    [`Component`](compiler::Component) trait — the compiler never pattern-matches
86//!    on component type.
87//!
88//! 3. **SPQR decomposition**: The circuit graph is decomposed into a unique
89//!    SPQR tree (Series / Parallel / Q-leaf / Rigid). S and P nodes map
90//!    directly to WDF adaptors. R nodes require matrix-based solvers.
91//!
92//! 4. **Stage assignment**: Each SPQR subtree becomes a processing stage with
93//!    complexity determined by its topology and component classification.
94//!
95//! 5. **Build**: Stages are compiled into concrete runtime processors and
96//!    chained into a [`CompiledPedal`](compiler::CompiledPedal) that implements
97//!    [`PedalProcessor`].
98//!
99//! # Key Concepts
100//!
101//! ## Wave Digital Filters (WDF)
102//!
103//! WDFs model analog circuits by propagating *wave variables* (incident and
104//! reflected voltage waves) through a binary tree of adaptors. Each passive
105//! component (R, C, L) becomes a leaf node; series and parallel connections
106//! become adaptor nodes. The tree is traversed once per sample — O(1) per
107//! component — making WDFs ideal for real-time audio.
108//!
109//! ## SPQR Decomposition
110//!
111//! Every 2-connected graph has a unique decomposition into Series, Parallel,
112//! and Rigid (triconnected) components. PedalKernel exploits this to
113//! automatically identify which parts of a circuit can be solved with cheap
114//! WDF tree traversal (S/P) and which require heavier matrix solvers (R).
115//!
116//! ## The Component Trait
117//!
118//! [`Component`](compiler::Component) is the single source of truth for all
119//! component behavior. Each concrete component (resistor, op-amp, triode, etc.)
120//! declares its own ports, edge kinds, signal flow, non-idealities, and MNA
121//! stamps. The compiler reacts to these declarations — it never hard-codes
122//! knowledge about specific component types. This makes adding new components
123//! a matter of implementing one trait, with no changes to the compilation
124//! pipeline.
125//!
126//! ## Stage Types and Complexity
127//!
128//! | Stage | Solver | Complexity | When Used |
129//! |-------|--------|------------|-----------|
130//! | **PassiveWdf** | WDF tree traversal | O(1) | All-passive S/P subtree |
131//! | **NlWdf** | WDF + scalar Newton-Raphson | O(1) | S/P subtree with one nonlinear element |
132//! | **IIR** | Biquad from MNA poles/zeros | O(1) | All-passive R-node (e.g., tone stack) |
133//! | **BlackFeedback** | Harold Black's formula | O(1) | Op-amp + passive feedback network |
134//! | **StateSpace** | State-space MNA integration | O(N) | Reactive R-node with active elements |
135//! | **MultiNl** | Multi-dimensional NR | O(N^2) | R-node with multiple nonlinear elements |
136//!
137//! ## Non-Ideal Effects
138//!
139//! Components declare their non-ideal behaviors via
140//! [`NonIdealFx`](compiler::component::NonIdealFx) — gain-bandwidth product
141//! limiting, slew rate, rail saturation — using values from SPICE models and
142//! datasheets. The stage builder applies these as post-processing filters,
143//! keeping the core WDF/MNA solver ideal and the non-ideal modeling composable.
144//!
145//! # Quick Start
146//!
147//! ```rust
148//! use pedalkernel::{dsl, compiler, PedalProcessor};
149//!
150//! // Parse a .pedal circuit definition
151//! let src = r#"
152//! pedal "Example" {
153//!   components {
154//!     R1: resistor(10k)
155//!     C1: cap(100n)
156//!   }
157//!   nets {
158//!     in -> C1.a
159//!     C1.b -> R1.a
160//!     R1.b -> gnd
161//!   }
162//! }
163//! "#;
164//! let pedal = dsl::parse_pedal_file(src).unwrap();
165//!
166//! // Compile to a real-time processor
167//! let mut processor = compiler::compile_pedal(&pedal, 48000.0).unwrap();
168//!
169//! // Process audio sample-by-sample
170//! let output = processor.process(0.5);
171//! ```
172//!
173//! See the [`compiler`] module for implementation details, and the
174//! [`compiler::component`] module for the trait that drives the entire pipeline.
175//!
176//! # Modules
177//!
178//! - [`dsl`] — nom-based parser for `.pedal` circuit definition files
179//! - [`compiler`] — netlist-to-WDF compiler: graph building, SPQR decomposition, stage synthesis
180//! - [`elements`] — WDF one-port elements (R, C, L) and nonlinear roots (diodes)
181//! - [`tree`] — WDF adaptors (series, parallel) and the processing engine
182//! - [`kicad`] — KiCad netlist export from the parsed AST
183//! - [`wav`] — WAV file I/O for offline rendering and testing
184//! - [`oversampling`] — antialiasing via oversampling at nonlinear stages
185//! - [`loading`] — electrical loading and impedance interaction between stages
186//! - [`tolerance`] — component tolerance randomization for realistic variation
187//! - [`thermal`] — thermal drift model for temperature-dependent behavior
188//! - [`metering`] — lock-free audio-to-UI metrics for VU meters and visualizations
189
190#[cfg(feature = "analysis")]
191pub mod analysis;
192pub mod board;
193pub mod bom;
194pub mod compiler;
195#[cfg(feature = "diag")]
196pub mod diag_ipc;
197pub mod dsl;
198pub mod dsl_expand;
199pub mod elements;
200pub mod fast_math;
201#[cfg(feature = "fault-injection")]
202pub mod fault_injection;
203pub mod kicad;
204pub mod loading;
205pub mod metering;
206pub mod model_lookup;
207pub mod models;
208pub mod nonideal_fx;
209pub mod oversampling;
210pub mod pedalboard;
211pub mod pot_taper;
212pub mod precompute;
213pub mod thermal;
214pub mod tolerance;
215pub mod tree;
216pub mod wav;
217
218/// Audio processor trait for pedals — re-exported from pedalkernel-rt.
219pub use pedalkernel_rt::PedalProcessor;
220
221/// Runtime audio scalar (`f64` on the host, `f32` under `wave-f32` / on device),
222/// re-exported so `crate::Wave` resolves in the compiler when it casts f64
223/// computations to the runtime types' scalar at the storage boundary.
224pub use pedalkernel_rt::Wave;
225
226// ---------------------------------------------------------------------------
227// JACK real-time audio engine (requires `jack-rt` feature)
228// ---------------------------------------------------------------------------
229
230#[cfg(feature = "jack-rt")]
231mod jack_engine {
232    use crate::PedalProcessor;
233    use jack::{AudioIn, AudioOut, Client, ClientOptions, Control, ProcessHandler, ProcessScope};
234    use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
235    use std::sync::{Arc, Mutex};
236
237    // ── Output coupling capacitor ─────────────────────────────────────
238    //
239    // Models the physical output coupling capacitor found in every real
240    // pedal and amp.  A typical coupling cap (1µF–10µF electrolytic in
241    // series with the output) forms a first-order high-pass with the
242    // load impedance, blocking DC bias from reaching the next stage.
243    //
244    // In a real circuit: fc = 1 / (2π × R_load × C_coupling)
245    // With R_load = 1MΩ (typical guitar amp input) and C = 1µF: fc ≈ 0.16 Hz
246    // With R_load = 10kΩ (typical pedal input) and C = 1µF: fc ≈ 16 Hz
247    //
248    // We model the conservative case (pedal-to-pedal coupling) at ~10 Hz,
249    // which matches a 1µF cap into a 15kΩ load — a very common real value.
250
251    /// Output coupling capacitor model (first-order high-pass).
252    ///
253    /// Equivalent circuit: series capacitor + shunt load resistor.
254    /// Transfer function: H(z) = (1 - z⁻¹) / (1 - α·z⁻¹)
255    /// where α = exp(-2π·fc/fs), fc = 1/(2π·R·C).
256    struct CouplingCap {
257        x_prev: f64,
258        y_prev: f64,
259        alpha: f64,
260    }
261
262    impl CouplingCap {
263        /// Create a coupling cap with the specified R·C time constant.
264        ///
265        /// `r_ohms`: load resistance in ohms (typ. 10k–1M)
266        /// `c_farads`: coupling capacitor in farads (typ. 1µF–10µF)
267        fn new(sample_rate: f64, r_ohms: f64, c_farads: f64) -> Self {
268            let fc = 1.0 / (2.0 * std::f64::consts::PI * r_ohms * c_farads);
269            let alpha = (-2.0 * std::f64::consts::PI * fc / sample_rate).exp();
270            Self {
271                x_prev: 0.0,
272                y_prev: 0.0,
273                alpha,
274            }
275        }
276
277        /// Standard pedal output coupling: 1µF into 15kΩ load (~10.6 Hz).
278        fn pedal_output(sample_rate: f64) -> Self {
279            Self::new(sample_rate, 15_000.0, 1.0e-6)
280        }
281
282        #[inline]
283        fn process(&mut self, x: f64) -> f64 {
284            // First-order HP: y[n] = α·(y[n-1] + x[n] - x[n-1])
285            // This is the exact discrete-time model of a series capacitor.
286            self.y_prev = self.alpha * (self.y_prev + x - self.x_prev);
287            self.x_prev = x;
288            self.y_prev
289        }
290    }
291
292    // ── Output stage ──────────────────────────────────────────────────
293    //
294    // Models the physical output stage that exists between the pedal's
295    // internal circuitry and the external world.  In a real pedal this
296    // includes the output coupling cap (DC blocking) and the output
297    // buffer's current-limiting behavior (soft saturation when driving
298    // low-impedance loads near the supply rails).
299    //
300    // The output buffer model is based on a BJT emitter-follower output
301    // stage: linear below the supply rails, with soft compression as the
302    // output transistor runs out of collector current near the rails.
303
304    /// Output stage: coupling cap + emitter-follower output buffer model.
305    struct OutputStage {
306        coupling_cap: CouplingCap,
307        /// Emitter-follower output buffer saturation current limit.
308        /// In a real BJT emitter follower, Iout_max = Ibias × β.
309        /// When the load demands more current, the output compresses.
310        /// Normalized: 1.0 = full-scale DAC output.
311        output_headroom: f64,
312    }
313
314    impl OutputStage {
315        fn new(sample_rate: f64) -> Self {
316            Self {
317                coupling_cap: CouplingCap::pedal_output(sample_rate),
318                // Emitter-follower headroom: the output transistor can swing
319                // to within ~0.2V of the rail.  At 9V single-supply biased
320                // at 4.5V, that's ±4.3V swing = 0.956 of half-supply.
321                output_headroom: 0.956,
322            }
323        }
324
325        /// Process a sample through coupling cap + output buffer.
326        ///
327        /// The output buffer uses the emitter-follower saturation model:
328        /// - Linear region: |x| < headroom → passthrough
329        /// - Saturation region: |x| ≥ headroom → collector current limiting
330        ///   causes soft compression following 1/√(1 + (x/headroom)²),
331        ///   which matches the measured behavior of a BJT output stage
332        ///   running out of current near the rails.
333        /// - Hard ceiling at ±1.0 (DAC full scale, should rarely reach).
334        #[inline]
335        fn process(&mut self, sample: f64) -> f64 {
336            let x = self.coupling_cap.process(sample);
337            let abs_x = x.abs();
338            if abs_x <= self.output_headroom * 0.9 {
339                // Well within linear region: clean passthrough.
340                x
341            } else if abs_x >= 1.0 {
342                // Beyond DAC full scale: hard ceiling (safety).
343                x.signum()
344            } else {
345                // Emitter-follower current limiting: soft compression.
346                // Models Ic rolling off as Vce → Vce_sat.
347                // Uses inverse-sqrt saturation: smoother than cubic,
348                // matches BJT output stage I-V curves.
349                let norm = abs_x / self.output_headroom;
350                let compressed = self.output_headroom * norm / (1.0 + (norm - 1.0).powi(2)).sqrt();
351                compressed.min(1.0).copysign(x)
352            }
353        }
354    }
355
356    // ── Lock-free control slot ────────────────────────────────────────
357    //
358    // Replaces the `Mutex<Vec<(String, f64)>>` on the hot path.  The UI
359    // thread writes control updates into fixed slots keyed by label.
360    // The RT callback reads them without locking — no priority inversion,
361    // no heap allocation, no xruns.
362
363    /// A single lock-free control value slot using atomic f64.
364    struct ControlSlot {
365        label: String,
366        /// Atomic storage for the f64 value (bit-cast via u64).
367        value: AtomicU64,
368        /// Generation counter: bumped on each write so the reader knows
369        /// whether a new value is available without a separate dirty flag.
370        generation: AtomicU64,
371        /// Last generation the reader saw.
372        last_read_gen: std::cell::UnsafeCell<u64>,
373    }
374
375    // SAFETY: The AtomicU64 fields are inherently thread-safe.
376    // `last_read_gen` is only accessed from the RT thread (reader side).
377    unsafe impl Send for ControlSlot {}
378    unsafe impl Sync for ControlSlot {}
379
380    impl ControlSlot {
381        fn new(label: String, initial: f64) -> Self {
382            Self {
383                label,
384                value: AtomicU64::new(initial.to_bits()),
385                generation: AtomicU64::new(0),
386                last_read_gen: std::cell::UnsafeCell::new(0),
387            }
388        }
389
390        /// Write a new value (UI thread).
391        fn store(&self, value: f64) {
392            self.value.store(value.to_bits(), Ordering::Release);
393            self.generation.fetch_add(1, Ordering::Release);
394        }
395
396        /// Check if a new value is available and read it (RT thread).
397        /// Returns `Some(value)` if updated since last read.
398        #[inline]
399        fn load_if_changed(&self) -> Option<f64> {
400            let gen = self.generation.load(Ordering::Acquire);
401            // SAFETY: only called from the single RT thread
402            let last = unsafe { &mut *self.last_read_gen.get() };
403            if gen != *last {
404                *last = gen;
405                Some(f64::from_bits(self.value.load(Ordering::Acquire)))
406            } else {
407                None
408            }
409        }
410    }
411
412    /// Result of activating a JACK client: the async handle plus registered
413    /// input/output port names.
414    pub type ActiveJackClient<P> = (jack::AsyncClient<(), JackProcessorLive<P>>, String, String);
415
416    /// JACK process handler wrapping a PedalProcessor.
417    pub struct JackProcessor<P: PedalProcessor> {
418        processor: P,
419        in_port: jack::Port<AudioIn>,
420        out_port: jack::Port<AudioOut>,
421    }
422
423    impl<P: PedalProcessor + Send> ProcessHandler for JackProcessor<P> {
424        fn process(&mut self, _client: &Client, ps: &ProcessScope) -> Control {
425            let input = self.in_port.as_slice(ps);
426            let output = self.out_port.as_mut_slice(ps);
427
428            for (out, &inp) in output.iter_mut().zip(input.iter()) {
429                *out = self.processor.process(inp as f64) as f32;
430            }
431
432            Control::Continue
433        }
434    }
435
436    // ── Shared controls for real-time TUI ↔ JACK communication ──────────
437
438    /// Shared control state for real-time parameter updates between a UI
439    /// thread and the JACK process callback.
440    ///
441    /// Uses lock-free atomic slots instead of a `Mutex<Vec<>>` to avoid
442    /// priority inversion and heap allocation in the RT callback — the
443    /// most common cause of xruns (crackle/static) in JACK applications.
444    pub struct SharedControls {
445        slots: Vec<ControlSlot>,
446        /// Fallback for controls not pre-registered as slots.
447        pending: Mutex<Vec<(String, f64)>>,
448        bypassed: AtomicBool,
449        /// Supply voltage in f64 bits (default 9.0V).  Lock-free path for
450        /// headroom adjustment from the TUI.
451        supply_voltage_bits: AtomicU64,
452    }
453
454    impl Default for SharedControls {
455        fn default() -> Self {
456            Self::new()
457        }
458    }
459
460    impl SharedControls {
461        pub fn new() -> Self {
462            Self {
463                slots: Vec::new(),
464                pending: Mutex::new(Vec::new()),
465                bypassed: AtomicBool::new(false),
466                supply_voltage_bits: AtomicU64::new(9.0_f64.to_bits()),
467            }
468        }
469
470        /// Create SharedControls with pre-registered lock-free slots for
471        /// known control labels.  This is the preferred constructor — it
472        /// eliminates all locking from the RT callback for these controls.
473        pub fn with_controls(labels: &[(String, f64)]) -> Self {
474            let slots = labels
475                .iter()
476                .map(|(label, default)| ControlSlot::new(label.clone(), *default))
477                .collect();
478            Self {
479                slots,
480                pending: Mutex::new(Vec::new()),
481                bypassed: AtomicBool::new(false),
482                supply_voltage_bits: AtomicU64::new(9.0_f64.to_bits()),
483            }
484        }
485
486        /// Set a control value (called from the UI thread).
487        ///
488        /// If the label matches a pre-registered slot, uses lock-free
489        /// atomic write.  Otherwise falls back to the Mutex path.
490        pub fn set_control(&self, label: &str, value: f64) {
491            for slot in &self.slots {
492                if slot.label == label {
493                    slot.store(value);
494                    return;
495                }
496            }
497            // Fallback for unknown labels
498            if let Ok(mut pending) = self.pending.lock() {
499                pending.push((label.to_string(), value));
500            }
501        }
502
503        /// Drain changed controls into the processor (called from RT thread).
504        /// Lock-free for pre-registered slots; `try_lock` for the fallback.
505        #[inline]
506        fn drain_into(&self, processor: &mut dyn PedalProcessor) {
507            // Lock-free path: check each atomic slot
508            for slot in &self.slots {
509                if let Some(value) = slot.load_if_changed() {
510                    processor.set_control(&slot.label, value);
511                }
512            }
513            // Fallback path (rare): drain any Mutex-queued controls
514            if let Ok(mut pending) = self.pending.try_lock() {
515                for (label, value) in pending.drain(..) {
516                    processor.set_control(&label, value);
517                }
518            }
519            // Supply voltage (lock-free, always applied)
520            processor.set_supply_voltage(self.supply_voltage());
521        }
522
523        /// Set the supply voltage (called from UI thread, lock-free).
524        pub fn set_supply_voltage(&self, voltage: f64) {
525            self.supply_voltage_bits
526                .store(voltage.to_bits(), Ordering::Relaxed);
527        }
528
529        /// Read the current supply voltage.
530        pub fn supply_voltage(&self) -> f64 {
531            f64::from_bits(self.supply_voltage_bits.load(Ordering::Relaxed))
532        }
533
534        pub fn set_bypassed(&self, bypassed: bool) {
535            self.bypassed.store(bypassed, Ordering::Relaxed);
536        }
537
538        pub fn is_bypassed(&self) -> bool {
539            self.bypassed.load(Ordering::Relaxed)
540        }
541    }
542
543    /// JACK process handler with live control updates from a shared state.
544    pub struct JackProcessorLive<P: PedalProcessor> {
545        processor: P,
546        in_port: jack::Port<AudioIn>,
547        out_port: jack::Port<AudioOut>,
548        controls: Arc<SharedControls>,
549        output_stage: OutputStage,
550    }
551
552    impl<P: PedalProcessor + Send> ProcessHandler for JackProcessorLive<P> {
553        fn process(&mut self, _client: &Client, ps: &ProcessScope) -> Control {
554            // Drain pending control updates (lock-free for registered slots).
555            self.controls.drain_into(&mut self.processor);
556
557            let input = self.in_port.as_slice(ps);
558            let output = self.out_port.as_mut_slice(ps);
559
560            if self.controls.bypassed.load(Ordering::Relaxed) {
561                for (out, &inp) in output.iter_mut().zip(input.iter()) {
562                    *out = inp;
563                }
564            } else {
565                for (out, &inp) in output.iter_mut().zip(input.iter()) {
566                    let processed = self.processor.process(inp as f64);
567                    // DC-block + soft-limit to prevent DAC hard clipping.
568                    *out = self.output_stage.process(processed) as f32;
569                }
570            }
571
572            Control::Continue
573        }
574    }
575
576    /// A `PedalProcessor` wrapper that ignores JACK input and feeds samples
577    /// from a pre-loaded circular buffer (looping WAV file) into the inner processor.
578    pub struct WavLoopProcessor<P: PedalProcessor> {
579        samples: Vec<f64>,
580        position: usize,
581        inner: P,
582    }
583
584    impl<P: PedalProcessor> WavLoopProcessor<P> {
585        pub fn new(samples: Vec<f64>, inner: P) -> Self {
586            Self {
587                samples,
588                position: 0,
589                inner,
590            }
591        }
592    }
593
594    impl<P: PedalProcessor + Send> PedalProcessor for WavLoopProcessor<P> {
595        fn process(&mut self, _input: f64) -> f64 {
596            let sample = if self.samples.is_empty() {
597                0.0
598            } else {
599                let s = self.samples[self.position];
600                self.position = (self.position + 1) % self.samples.len();
601                s
602            };
603            self.inner.process(sample)
604        }
605
606        fn set_sample_rate(&mut self, rate: f64) {
607            self.inner.set_sample_rate(rate);
608        }
609
610        fn reset(&mut self) {
611            self.position = 0;
612            self.inner.reset();
613        }
614
615        fn set_control(&mut self, label: &str, value: f64) {
616            self.inner.set_control(label, value);
617        }
618    }
619
620    /// JACK-based real-time audio engine.
621    ///
622    /// Connects a `PedalProcessor` to the system audio graph via JACK.
623    /// Target: sub-5 ms total latency through a Scarlett 2i2 on Linux
624    /// (48 kHz, 64-sample buffer).
625    ///
626    /// Enable with: `cargo build --features jack-rt`
627    pub struct AudioEngine;
628
629    impl AudioEngine {
630        /// Create a JACK client, register ports, and start processing.
631        ///
632        /// Blocks until the returned `AsyncClient` is dropped.
633        pub fn run<P: PedalProcessor + Send + 'static>(
634            name: &str,
635            mut processor: P,
636        ) -> Result<jack::AsyncClient<(), JackProcessor<P>>, jack::Error> {
637            let (client, _status) = Client::new(name, ClientOptions::NO_START_SERVER)?;
638            processor.set_sample_rate(client.sample_rate() as f64);
639
640            let in_port = client.register_port("in", AudioIn)?;
641            let out_port = client.register_port("out", AudioOut)?;
642
643            let handler = JackProcessor {
644                processor,
645                in_port,
646                out_port,
647            };
648            client.activate_async((), handler)
649        }
650
651        /// Create a JACK client (for port enumeration before activation).
652        ///
653        /// Tries to connect to an existing JACK server first; falls back to
654        /// allowing the server to auto-start if `NO_START_SERVER` fails.
655        pub fn create_client(name: &str) -> Result<Client, jack::Error> {
656            match Client::new(name, ClientOptions::NO_START_SERVER) {
657                Ok((client, _status)) => Ok(client),
658                Err(_) => {
659                    let (client, _status) = Client::new(name, ClientOptions::empty())?;
660                    Ok(client)
661                }
662            }
663        }
664
665        /// List available audio input sources (JACK ports that produce audio).
666        /// These are ports you can read from — typically `system:capture_*`.
667        pub fn list_input_sources(client: &Client) -> Vec<String> {
668            client.ports(
669                None,
670                Some("32 bit float mono audio"),
671                jack::PortFlags::IS_OUTPUT,
672            )
673        }
674
675        /// List available audio output destinations (JACK ports that consume audio).
676        /// These are ports you can write to — typically `system:playback_*`.
677        pub fn list_output_destinations(client: &Client) -> Vec<String> {
678            client.ports(
679                None,
680                Some("32 bit float mono audio"),
681                jack::PortFlags::IS_INPUT,
682            )
683        }
684
685        /// Activate a processor with shared controls for live parameter updates.
686        ///
687        /// Returns the async client and the full names of the registered
688        /// input and output ports (for connecting to other JACK ports).
689        pub fn start<P: PedalProcessor + Send + 'static>(
690            client: Client,
691            mut processor: P,
692            controls: Arc<SharedControls>,
693        ) -> Result<ActiveJackClient<P>, jack::Error> {
694            let sample_rate = client.sample_rate() as f64;
695            processor.set_sample_rate(sample_rate);
696
697            let in_port = client.register_port("in", AudioIn)?;
698            let out_port = client.register_port("out", AudioOut)?;
699
700            // Build full port names: "<client_name>:<port_name>"
701            let client_name = client.name().to_string();
702            let in_name = format!("{client_name}:in");
703            let out_name = format!("{client_name}:out");
704
705            let handler = JackProcessorLive {
706                processor,
707                in_port,
708                out_port,
709                controls,
710                output_stage: OutputStage::new(sample_rate),
711            };
712            let async_client = client.activate_async((), handler)?;
713
714            Ok((async_client, in_name, out_name))
715        }
716    }
717}
718
719#[cfg(feature = "jack-rt")]
720pub use jack_engine::{AudioEngine, SharedControls, WavLoopProcessor};
721
722// ---------------------------------------------------------------------------
723// Integration tests
724// ---------------------------------------------------------------------------
725
726#[cfg(test)]
727mod tests {
728    use super::*;
729    use crate::compiler::component::Component;
730
731    #[test]
732    fn dsl_to_kicad_roundtrip() {
733        let src = r#"
734pedal "Test Pedal" {
735  components {
736    R1: resistor(10k)
737    C1: cap(100n)
738    D1: diode_pair(silicon)
739  }
740  nets {
741    in -> C1.a
742    C1.b -> R1.a, D1.a
743    D1.b -> gnd
744    R1.b -> out
745  }
746  controls {
747    R1.position -> "Tone" [0.0, 1.0] = 0.5
748  }
749}
750"#;
751        let pedal = dsl::parse_pedal_file(src).unwrap();
752        assert_eq!(pedal.name, "Test Pedal");
753        assert_eq!(pedal.components.len(), 3);
754        assert_eq!(pedal.nets.len(), 4);
755        assert_eq!(pedal.controls.len(), 1);
756
757        let netlist = kicad::export_kicad_netlist(&pedal);
758        assert!(netlist.contains("(export (version D)"));
759        assert!(netlist.contains("10.0kΩ"));
760    }
761
762    #[test]
763    fn compiled_pedal_pipeline() {
764        // Compile a .pedal file and verify it produces output
765        let pedal = dsl::parse_pedal_file(include_str!(
766            "../examples/pedals/distortion/proco_rat.pedal"
767        ))
768        .expect("should parse RAT pedal");
769        let mut compiled =
770            compiler::compile_pedal(&pedal, 48000.0).expect("should compile RAT pedal");
771
772        let input = wav::sine_wave(440.0, 0.1, 48000);
773        let output: Vec<f64> = input.iter().map(|&s| compiled.process(s)).collect();
774
775        let max_out = output.iter().copied().fold(0.0_f64, |a, b| a.max(b.abs()));
776        assert!(max_out > 0.001, "compiled pedal should produce output");
777    }
778
779    // -----------------------------------------------------------------------
780    // Per-pedal .pedal file parse tests
781    // -----------------------------------------------------------------------
782
783    /// Helper: read and parse a .pedal example file, panicking with context on failure.
784    fn parse_example(filename: &str) -> dsl::PedalDef {
785        let path = find_example_file(filename);
786        let src =
787            std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read {path}: {e}"));
788        dsl::parse_pedal_file(&src).unwrap_or_else(|e| panic!("failed to parse {path}: {e}"))
789    }
790
791    /// Search examples/ subdirectories for a file by name.
792    fn find_example_file(filename: &str) -> String {
793        fn walk(dir: &str, target: &str) -> Option<String> {
794            for entry in std::fs::read_dir(dir).ok()?.flatten() {
795                let path = entry.path();
796                if path.is_dir() {
797                    if let Some(found) = walk(path.to_str()?, target) {
798                        return Some(found);
799                    }
800                } else if path.file_name().and_then(|n| n.to_str()) == Some(target) {
801                    return Some(path.to_string_lossy().to_string());
802                }
803            }
804            None
805        }
806        walk("examples", filename).unwrap_or_else(|| panic!("example file not found: {filename}"))
807    }
808
809    #[test]
810    fn pedal_tube_screamer() {
811        let p = parse_example("tube_screamer.pedal");
812        assert_eq!(p.name, "Tube Screamer");
813        assert_eq!(p.components.len(), 20);
814        assert_eq!(p.nets.len(), 27);
815        assert_eq!(p.controls.len(), 3);
816        let labels: Vec<&str> = p.controls.iter().map(|c| c.label.as_str()).collect();
817        assert!(labels.contains(&"Drive"));
818        assert!(labels.contains(&"Tone"));
819        assert!(labels.contains(&"Level"));
820        // Verify dual JRC4558 opamps + feedback clipping diodes
821        let opamp_count = p
822            .components
823            .iter()
824            .filter(|c| c.kind.op_amp_type() == Some(dsl::OpAmpType::Jrc4558))
825            .count();
826        assert_eq!(opamp_count, 2, "TS808 uses dual JRC4558D");
827        assert!(p.components.iter().any(|c| c.kind.type_tag() == "diode"
828            && c.kind.diode_type() == Some(dsl::DiodeType::Silicon)));
829    }
830
831    #[test]
832    fn pedal_fuzz_face() {
833        let p = parse_example("fuzz_face.pedal");
834        assert_eq!(p.name, "Fuzz Face");
835        assert_eq!(p.components.len(), 11);
836        assert_eq!(p.nets.len(), 17);
837        assert_eq!(p.controls.len(), 2);
838        let labels: Vec<&str> = p.controls.iter().map(|c| c.label.as_str()).collect();
839        assert!(labels.contains(&"Fuzz"));
840        assert!(labels.contains(&"Volume"));
841        // Verify PNP transistors (germanium)
842        let pnp_count = p
843            .components
844            .iter()
845            .filter(|c| c.kind.is_bjt() && c.kind.type_tag() == "PNP transistor")
846            .count();
847        assert_eq!(pnp_count, 2, "Fuzz Face uses 2 PNP transistors");
848    }
849
850    #[test]
851    fn pedal_big_muff() {
852        let p = parse_example("big_muff.pedal");
853        assert_eq!(p.name, "MUFF");
854        assert_eq!(p.controls.len(), 3);
855        let labels: Vec<&str> = p.controls.iter().map(|c| c.label.as_str()).collect();
856        assert!(labels.contains(&"Sustain"));
857        assert!(labels.contains(&"Tone"));
858        assert!(labels.contains(&"Volume"));
859        // Verify 4 NPN gain stages + 2 clipping diode pairs
860        let npn_count = p
861            .components
862            .iter()
863            .filter(|c| c.kind.is_bjt() && c.kind.type_tag() == "NPN transistor")
864            .count();
865        assert_eq!(npn_count, 4, "Big Muff uses 4 NPN transistor stages");
866    }
867
868    #[test]
869    fn pedal_dyna_comp() {
870        let p = parse_example("dyna_comp.pedal");
871        assert_eq!(p.name, "MXR Dyna Comp");
872        assert_eq!(p.components.len(), 10);
873        assert_eq!(p.nets.len(), 14);
874        assert_eq!(p.controls.len(), 2);
875        let labels: Vec<&str> = p.controls.iter().map(|c| c.label.as_str()).collect();
876        assert!(labels.contains(&"Sensitivity"));
877        assert!(labels.contains(&"Output"));
878        // Verify opamp is present (OTA topology)
879        assert!(p.components.iter().any(|c| c.kind.op_amp_type().is_some()));
880    }
881
882    #[test]
883    fn pedal_proco_rat() {
884        let p = parse_example("proco_rat.pedal");
885        assert_eq!(p.name, "ProCo RAT");
886        assert_eq!(p.components.len(), 22);
887        assert_eq!(p.nets.len(), 29);
888        assert_eq!(p.controls.len(), 3);
889        let labels: Vec<&str> = p.controls.iter().map(|c| c.label.as_str()).collect();
890        assert!(labels.contains(&"Distortion"));
891        assert!(labels.contains(&"Filter"));
892        assert!(labels.contains(&"Volume"));
893        // Verify LM308 opamp + hard clipping diodes
894        assert!(p
895            .components
896            .iter()
897            .any(|c| c.kind.op_amp_type() == Some(dsl::OpAmpType::Lm308)));
898        let diode_count = p
899            .components
900            .iter()
901            .filter(|c| {
902                c.kind.type_tag() == "diode" && c.kind.diode_type() == Some(dsl::DiodeType::Silicon)
903            })
904            .count();
905        assert_eq!(diode_count, 2, "RAT uses 2 silicon clipping diodes");
906    }
907
908    #[test]
909    fn pedal_blues_driver() {
910        let p = parse_example("blues_driver.pedal");
911        assert_eq!(p.name, "Boss Blues Driver");
912        assert_eq!(p.components.len(), 32);
913        assert_eq!(p.nets.len(), 40);
914        assert_eq!(p.controls.len(), 3);
915        let labels: Vec<&str> = p.controls.iter().map(|c| c.label.as_str()).collect();
916        assert!(labels.contains(&"Gain"));
917        assert!(labels.contains(&"Tone"));
918        assert!(labels.contains(&"Level"));
919        // Verify JFET input buffer + dual TL072 + asymmetric diodes
920        assert!(p
921            .components
922            .iter()
923            .any(|c| c.kind.is_jfet() && c.kind.type_tag() == "N-channel JFET"));
924        let opamp_count = p
925            .components
926            .iter()
927            .filter(|c| c.kind.op_amp_type().is_some())
928            .count();
929        assert_eq!(opamp_count, 2, "Blues Driver uses dual TL072");
930        let diode_count = p
931            .components
932            .iter()
933            .filter(|c| {
934                c.kind.type_tag() == "diode" && c.kind.diode_type() == Some(dsl::DiodeType::Silicon)
935            })
936            .count();
937        assert_eq!(
938            diode_count, 3,
939            "Blues Driver uses 3 asymmetric clipping diodes"
940        );
941    }
942
943    #[test]
944    fn pedal_klon_centaur() {
945        let p = parse_example("klon_centaur.pedal");
946        assert_eq!(p.name, "Klon Centaur");
947        assert_eq!(p.components.len(), 29);
948        assert_eq!(p.nets.len(), 38);
949        assert_eq!(p.controls.len(), 3);
950        let labels: Vec<&str> = p.controls.iter().map(|c| c.label.as_str()).collect();
951        assert!(labels.contains(&"Gain"));
952        assert!(labels.contains(&"Treble"));
953        assert!(labels.contains(&"Output"));
954        // Verify 3 TL072 opamps + germanium feedback clipping diodes
955        let opamp_count = p
956            .components
957            .iter()
958            .filter(|c| c.kind.op_amp_type() == Some(dsl::OpAmpType::Tl072))
959            .count();
960        assert_eq!(
961            opamp_count, 3,
962            "Klon uses 3 opamps (2 gain + 1 output buffer)"
963        );
964        let ge_diode_count = p
965            .components
966            .iter()
967            .filter(|c| {
968                c.kind.type_tag() == "diode"
969                    && c.kind.diode_type() == Some(dsl::DiodeType::Germanium)
970            })
971            .count();
972        assert_eq!(
973            ge_diode_count, 2,
974            "Klon uses 2 germanium clipping diodes in feedback"
975        );
976    }
977
978    #[test]
979    fn pedal_fulltone_ocd() {
980        let p = parse_example("fulltone_ocd.pedal");
981        assert_eq!(p.name, "Fulltone OCD");
982        assert_eq!(p.components.len(), 18);
983        assert_eq!(p.nets.len(), 24);
984        assert_eq!(p.controls.len(), 3);
985        let labels: Vec<&str> = p.controls.iter().map(|c| c.label.as_str()).collect();
986        assert!(labels.contains(&"Drive"));
987        assert!(labels.contains(&"Tone"));
988        assert!(labels.contains(&"Volume"));
989        // Verify MOSFET clipping (the OCD's signature)
990        let mosfet_count = p
991            .components
992            .iter()
993            .filter(|c| c.kind.is_mosfet() && c.kind.type_tag() == "N-channel MOSFET")
994            .count();
995        assert_eq!(mosfet_count, 2, "OCD uses 2 NMOS MOSFETs for clipping");
996    }
997
998    #[test]
999    fn pedal_boss_ce2() {
1000        let p = parse_example("boss_ce2.pedal");
1001        assert_eq!(p.name, "Boss CE-2");
1002        assert_eq!(p.components.len(), 16);
1003        assert_eq!(p.controls.len(), 2);
1004        let labels: Vec<&str> = p.controls.iter().map(|c| c.label.as_str()).collect();
1005        assert!(labels.contains(&"Rate"));
1006        assert!(labels.contains(&"Depth"));
1007        // Verify BBD delay line component
1008        assert!(p.components.iter().any(|c| c
1009            .kind
1010            .as_any()
1011            .downcast_ref::<crate::compiler::components::Bbd>()
1012            .map_or(false, |b| b.bbd_type == dsl::BbdType::Mn3207)));
1013        // Verify LFO for chorus modulation
1014        assert!(p
1015            .components
1016            .iter()
1017            .any(|c| c.kind.is_modulation_source() && c.kind.type_tag() == "LFO"));
1018        // Verify TL072 op-amp (input buffer)
1019        assert!(p
1020            .components
1021            .iter()
1022            .any(|c| c.kind.op_amp_type() == Some(dsl::OpAmpType::Tl072)));
1023    }
1024
1025    #[test]
1026    fn all_pedal_files_export_kicad() {
1027        let files = [
1028            "tube_screamer.pedal",
1029            "fuzz_face.pedal",
1030            "big_muff.pedal",
1031            "dyna_comp.pedal",
1032            "proco_rat.pedal",
1033            "blues_driver.pedal",
1034            "klon_centaur.pedal",
1035            "fulltone_ocd.pedal",
1036            "boss_ce2.pedal",
1037        ];
1038        for f in files {
1039            let p = parse_example(f);
1040            let netlist = kicad::export_kicad_netlist(&p);
1041            assert!(
1042                netlist.contains("(export (version D)"),
1043                "{f} KiCad export missing header"
1044            );
1045            assert!(
1046                netlist.contains(&p.name),
1047                "{f} KiCad export missing pedal name"
1048            );
1049        }
1050    }
1051
1052    // -----------------------------------------------------------------------
1053    // Schematic accuracy: verify component type counts match real circuits
1054    // -----------------------------------------------------------------------
1055
1056    /// Count components by predicate.
1057    fn count_kind(p: &dsl::PedalDef, f: impl Fn(&dyn Component) -> bool) -> usize {
1058        p.components.iter().filter(|c| f(c.kind.as_ref())).count()
1059    }
1060
1061    /// TS808: real schematic has 2 JRC4558 op-amps (dual package),
1062    /// 2 silicon diodes (1N914) in anti-parallel in feedback, 3 pots.
1063    #[test]
1064    fn schematic_ts808_component_types() {
1065        let p = parse_example("tube_screamer.pedal");
1066        assert_eq!(
1067            count_kind(&p, |k| k.op_amp_type() == Some(dsl::OpAmpType::Jrc4558)),
1068            2,
1069            "TS808: 2x JRC4558D (dual op-amp package used as 2 separate amps)"
1070        );
1071        assert_eq!(
1072            count_kind(&p, |k| k.type_tag() == "diode"
1073                && k.diode_type() == Some(dsl::DiodeType::Silicon)),
1074            2,
1075            "TS808: 2x 1N914 silicon diodes in anti-parallel in feedback loop"
1076        );
1077        assert_eq!(
1078            count_kind(&p, |k| k.is_pot()),
1079            3,
1080            "TS808: 3 pots (Drive, Tone, Level)"
1081        );
1082        // Verify NO MOSFETs, NO transistors, NO JFETs — pure op-amp circuit
1083        assert_eq!(count_kind(&p, |k| k.is_bjt()), 0);
1084        assert_eq!(count_kind(&p, |k| k.is_jfet()), 0);
1085        assert_eq!(
1086            count_kind(&p, |k| k.is_mosfet() && k.type_tag() == "N-channel MOSFET"),
1087            0
1088        );
1089    }
1090
1091    /// Fuzz Face: real schematic has 2 PNP germanium transistors,
1092    /// NO op-amps, NO diodes — gain comes from transistor saturation.
1093    #[test]
1094    fn schematic_fuzz_face_component_types() {
1095        let p = parse_example("fuzz_face.pedal");
1096        assert_eq!(
1097            count_kind(&p, |k| k.is_bjt() && k.type_tag() == "PNP transistor"),
1098            2,
1099            "Fuzz Face: 2x AC128/NKT275 PNP germanium transistors"
1100        );
1101        assert_eq!(
1102            count_kind(&p, |k| k.is_pot()),
1103            2,
1104            "Fuzz Face: 2 pots (Fuzz, Volume)"
1105        );
1106        // No op-amps or diodes in a Fuzz Face
1107        assert_eq!(count_kind(&p, |k| k.op_amp_type().is_some()), 0);
1108        assert_eq!(
1109            count_kind(&p, |k| k.type_tag() == "diode"
1110                || k.type_tag() == "diode pair"),
1111            0
1112        );
1113    }
1114
1115    /// Big Muff: 4 NPN transistor gain stages + 2 diode pairs for clipping.
1116    #[test]
1117    fn schematic_big_muff_component_types() {
1118        let p = parse_example("big_muff.pedal");
1119        assert_eq!(
1120            count_kind(&p, |k| k.is_bjt() && k.type_tag() == "NPN transistor"),
1121            4,
1122            "Big Muff: 4x 2N5088 NPN transistor stages"
1123        );
1124        assert_eq!(
1125            count_kind(&p, |k| k.type_tag() == "diode pair"),
1126            2,
1127            "Big Muff: 2x anti-parallel diode pairs for clipping"
1128        );
1129        assert_eq!(
1130            count_kind(&p, |k| k.is_pot()),
1131            3,
1132            "Big Muff: 3 pots (Sustain, Tone, Volume)"
1133        );
1134        // No op-amps in a Big Muff — all discrete
1135        assert_eq!(count_kind(&p, |k| k.op_amp_type().is_some()), 0);
1136    }
1137
1138    /// ProCo RAT: LM308 op-amp (unique slow slew rate), 2 silicon diodes
1139    /// to ground (hard clipping), 3 pots.
1140    #[test]
1141    fn schematic_rat_component_types() {
1142        let p = parse_example("proco_rat.pedal");
1143        assert_eq!(
1144            count_kind(&p, |k| k.op_amp_type() == Some(dsl::OpAmpType::Lm308)),
1145            1,
1146            "RAT: 1x LM308 op-amp (the slow slew rate shapes the RAT's tone)"
1147        );
1148        assert_eq!(
1149            count_kind(&p, |k| k.type_tag() == "diode"
1150                && k.diode_type() == Some(dsl::DiodeType::Silicon)),
1151            2,
1152            "RAT: 2x 1N914 diodes to ground (hard clipping — NOT in feedback)"
1153        );
1154        assert_eq!(
1155            count_kind(&p, |k| k.is_pot()),
1156            3,
1157            "RAT: 3 pots (Distortion, Filter, Volume)"
1158        );
1159    }
1160
1161    /// Blues Driver: JFET input buffer + 2 TL072 op-amps + 3 asymmetric diodes.
1162    #[test]
1163    fn schematic_blues_driver_component_types() {
1164        let p = parse_example("blues_driver.pedal");
1165        assert_eq!(
1166            count_kind(&p, |k| k.is_jfet() && k.type_tag() == "N-channel JFET"),
1167            1,
1168            "BD-2: 1x 2N5457 N-JFET input buffer"
1169        );
1170        assert_eq!(
1171            count_kind(&p, |k| k.op_amp_type() == Some(dsl::OpAmpType::Tl072)),
1172            2,
1173            "BD-2: 2x TL072 op-amp gain stages"
1174        );
1175        assert_eq!(
1176            count_kind(&p, |k| k.type_tag() == "diode"
1177                && k.diode_type() == Some(dsl::DiodeType::Silicon)),
1178            3,
1179            "BD-2: 3x asymmetric clipping diodes (2+1 for asymmetry)"
1180        );
1181    }
1182
1183    /// Klon Centaur: 3 TL072 op-amps + 2 germanium diodes in feedback.
1184    #[test]
1185    fn schematic_klon_component_types() {
1186        let p = parse_example("klon_centaur.pedal");
1187        assert_eq!(
1188            count_kind(&p, |k| k.op_amp_type() == Some(dsl::OpAmpType::Tl072)),
1189            3,
1190            "Klon: 3x TL072 (input buffer + clipping amp + output buffer)"
1191        );
1192        assert_eq!(
1193            count_kind(&p, |k| k.type_tag() == "diode"
1194                && k.diode_type() == Some(dsl::DiodeType::Germanium)),
1195            2,
1196            "Klon: 2x germanium diodes (MA856) in anti-parallel in feedback"
1197        );
1198        assert_eq!(
1199            count_kind(&p, |k| k.is_pot()),
1200            3,
1201            "Klon: 3 pots (Gain, Treble, Output)"
1202        );
1203        // No transistors — pure op-amp design
1204        assert_eq!(count_kind(&p, |k| k.is_bjt()), 0);
1205    }
1206
1207    /// OCD: 2 NMOS MOSFETs (2N7000) + 1 TL072 op-amp — NO silicon diodes.
1208    #[test]
1209    fn schematic_ocd_component_types() {
1210        let p = parse_example("fulltone_ocd.pedal");
1211        assert_eq!(
1212            count_kind(&p, |k| k.is_mosfet() && k.type_tag() == "N-channel MOSFET"),
1213            2,
1214            "OCD: 2x 2N7000 NMOS MOSFETs for clipping (the OCD's signature)"
1215        );
1216        assert_eq!(
1217            count_kind(&p, |k| k.op_amp_type() == Some(dsl::OpAmpType::Tl072)),
1218            1,
1219            "OCD: 1x TL072 op-amp gain stage"
1220        );
1221        // OCD uses MOSFETs, NOT silicon diodes
1222        assert_eq!(
1223            count_kind(&p, |k| k.type_tag() == "diode"),
1224            0,
1225            "OCD: no diodes — MOSFETs do the clipping"
1226        );
1227    }
1228
1229    /// Dyna Comp: CA3080 OTA (not a generic op-amp).
1230    #[test]
1231    fn schematic_dyna_comp_component_types() {
1232        let p = parse_example("dyna_comp.pedal");
1233        assert_eq!(
1234            count_kind(&p, |k| k.op_amp_type() == Some(dsl::OpAmpType::Ca3080)),
1235            1,
1236            "Dyna Comp: 1x CA3080 OTA for current-controlled gain"
1237        );
1238        assert_eq!(
1239            count_kind(&p, |k| k.is_pot()),
1240            2,
1241            "Dyna Comp: 2 pots (Sensitivity, Output)"
1242        );
1243    }
1244
1245    /// CE-2: MN3207 BBD + TL072 op-amp + triangle LFO.
1246    #[test]
1247    fn schematic_ce2_component_types() {
1248        let p = parse_example("boss_ce2.pedal");
1249        assert_eq!(
1250            count_kind(&p, |k| k
1251                .as_any()
1252                .downcast_ref::<crate::compiler::components::Bbd>()
1253                .map_or(false, |b| b.bbd_type == dsl::BbdType::Mn3207)),
1254            1,
1255            "CE-2: 1x MN3207 BBD (1024 stages for chorus delay)"
1256        );
1257        assert_eq!(
1258            count_kind(&p, |k| k.op_amp_type() == Some(dsl::OpAmpType::Tl072)),
1259            1,
1260            "CE-2: 1x TL072 input buffer"
1261        );
1262        assert_eq!(
1263            count_kind(&p, |k| k.is_modulation_source() && k.type_tag() == "LFO"),
1264            1,
1265            "CE-2: 1x triangle LFO for chorus sweep"
1266        );
1267        assert_eq!(
1268            count_kind(&p, |k| k.is_pot()),
1269            2,
1270            "CE-2: 2 pots (Rate, Depth)"
1271        );
1272    }
1273
1274    // -----------------------------------------------------------------------
1275    // Compile + process: every pedal file produces non-silent output
1276    // -----------------------------------------------------------------------
1277
1278    #[test]
1279    fn all_pedals_compile_and_produce_audio() {
1280        let files = [
1281            "tube_screamer.pedal",
1282            "fuzz_face.pedal",
1283            "big_muff.pedal",
1284            "dyna_comp.pedal",
1285            "proco_rat.pedal",
1286            "blues_driver.pedal",
1287            "klon_centaur.pedal",
1288            "fulltone_ocd.pedal",
1289            "boss_ce2.pedal",
1290        ];
1291        let sample_rate = 48000.0;
1292        let input = wav::sine_wave(330.0, 0.05, sample_rate as u32);
1293
1294        for filename in files {
1295            let p = parse_example(filename);
1296            let mut proc = compiler::compile_pedal(&p, sample_rate)
1297                .unwrap_or_else(|e| panic!("{filename}: compile failed: {e}"));
1298
1299            // Apply default controls
1300            for ctrl in &p.controls {
1301                proc.set_control(&ctrl.label, ctrl.default);
1302            }
1303
1304            let output: Vec<f64> = input.iter().map(|&s| proc.process(s)).collect();
1305            let max_out = output.iter().copied().fold(0.0_f64, |a, b| a.max(b.abs()));
1306            assert!(
1307                max_out > 1e-6,
1308                "{filename}: pedal produced silent output (max={max_out})"
1309            );
1310        }
1311    }
1312}