1#![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#[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
218pub use pedalkernel_rt::PedalProcessor;
220
221pub use pedalkernel_rt::Wave;
225
226#[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 struct CouplingCap {
257 x_prev: f64,
258 y_prev: f64,
259 alpha: f64,
260 }
261
262 impl CouplingCap {
263 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 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 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 struct OutputStage {
306 coupling_cap: CouplingCap,
307 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 output_headroom: 0.956,
322 }
323 }
324
325 #[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 x
341 } else if abs_x >= 1.0 {
342 x.signum()
344 } else {
345 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 struct ControlSlot {
365 label: String,
366 value: AtomicU64,
368 generation: AtomicU64,
371 last_read_gen: std::cell::UnsafeCell<u64>,
373 }
374
375 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 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 #[inline]
399 fn load_if_changed(&self) -> Option<f64> {
400 let gen = self.generation.load(Ordering::Acquire);
401 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 pub type ActiveJackClient<P> = (jack::AsyncClient<(), JackProcessorLive<P>>, String, String);
415
416 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 pub struct SharedControls {
445 slots: Vec<ControlSlot>,
446 pending: Mutex<Vec<(String, f64)>>,
448 bypassed: AtomicBool,
449 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 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 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 if let Ok(mut pending) = self.pending.lock() {
499 pending.push((label.to_string(), value));
500 }
501 }
502
503 #[inline]
506 fn drain_into(&self, processor: &mut dyn PedalProcessor) {
507 for slot in &self.slots {
509 if let Some(value) = slot.load_if_changed() {
510 processor.set_control(&slot.label, value);
511 }
512 }
513 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 processor.set_supply_voltage(self.supply_voltage());
521 }
522
523 pub fn set_supply_voltage(&self, voltage: f64) {
525 self.supply_voltage_bits
526 .store(voltage.to_bits(), Ordering::Relaxed);
527 }
528
529 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 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 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 *out = self.output_stage.process(processed) as f32;
569 }
570 }
571
572 Control::Continue
573 }
574 }
575
576 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 pub struct AudioEngine;
628
629 impl AudioEngine {
630 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 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 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 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 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 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#[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 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 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 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 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 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 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 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 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 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 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 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 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 assert!(p
1015 .components
1016 .iter()
1017 .any(|c| c.kind.is_modulation_source() && c.kind.type_tag() == "LFO"));
1018 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 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 #[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 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 #[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 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 #[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 assert_eq!(count_kind(&p, |k| k.op_amp_type().is_some()), 0);
1136 }
1137
1138 #[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 #[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 #[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 assert_eq!(count_kind(&p, |k| k.is_bjt()), 0);
1205 }
1206
1207 #[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 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 #[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 #[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 #[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 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}