1use crate::board::{find_global_board, BoardDef, BoardEntry};
11use crate::compiler::{compile_pedal, compile_split_pedal, CompiledPedal, SplitCompiledPedal};
12use crate::dsl::parse_pedal_file;
13use crate::loading::InterstageLoading;
14use crate::PedalProcessor;
15use std::path::Path;
16
17struct PedalSlot {
23 name: String,
24 processor: Option<CompiledPedal>,
25 bypassed: bool,
26 failed: bool,
27}
28
29struct FxLoopSlot {
33 amp: SplitCompiledPedal,
35 effects: Vec<PedalSlot>,
37 mix: f64,
39}
40
41enum ChainSlot {
43 Pedal(PedalSlot),
44 FxLoop(FxLoopSlot),
45}
46
47pub struct PedalboardProcessor {
53 chain: Vec<ChainSlot>,
54 loading: Vec<InterstageLoading>,
57}
58
59impl PedalboardProcessor {
60 pub fn from_board(
69 board: &BoardDef,
70 board_dir: &Path,
71 sample_rate: f64,
72 ) -> Result<Self, String> {
73 let (pb, warnings) = Self::from_board_with_warnings(board, board_dir, sample_rate);
74 for w in &warnings {
75 eprintln!("WARNING: {w}");
76 }
77 Ok(pb)
78 }
79
80 pub fn from_board_with_warnings(
89 board: &BoardDef,
90 board_dir: &Path,
91 sample_rate: f64,
92 ) -> (Self, Vec<String>) {
93 let mut chain = Vec::new();
94 let mut warnings = Vec::new();
95
96 let global = find_global_board(board_dir);
98 if let Some((ref def, ref dir)) = global {
99 let n_start = def.start.len();
100 let n_end = def.end.len();
101 eprintln!(
102 "Global: {} ({} start, {} end)",
103 dir.join(".global.board").display(),
104 n_start,
105 n_end,
106 );
107 }
108
109 if let Some((ref def, ref global_dir)) = global {
111 for entry in &def.start {
112 let slot = Self::compile_pedal_entry(entry, global_dir, sample_rate, &mut warnings);
113 chain.push(ChainSlot::Pedal(slot));
114 }
115 }
116
117 for entry in &board.entries {
119 match entry {
120 BoardEntry::Pedal(pedal_entry) => {
121 let slot = Self::compile_pedal_entry(
122 pedal_entry,
123 board_dir,
124 sample_rate,
125 &mut warnings,
126 );
127 chain.push(ChainSlot::Pedal(slot));
128 }
129 BoardEntry::FxLoop(group) => {
130 let amp_entry = board.entries.iter().find_map(|e| {
134 if let BoardEntry::Pedal(p) = e {
135 if p.id == group.target_id {
136 return Some(p);
137 }
138 }
139 None
140 });
141
142 let amp_result: Result<(String, SplitCompiledPedal), String> = match amp_entry {
143 None => Err(format!(
144 "fx_loop references '{}' but no pedal with that ID exists",
145 group.target_id
146 )),
147 Some(entry) => {
148 let pedal_path = {
149 let direct = board_dir.join(&entry.path);
150 if direct.exists() {
151 direct
152 } else if let Some(found) =
153 Self::find_in_subdirs(board_dir, &entry.path)
154 {
155 found
156 } else {
157 direct
158 }
159 };
160 (|| {
161 let source = std::fs::read_to_string(&pedal_path).map_err(|e| {
162 format!(
163 "Error reading amp '{}' at {}: {e}",
164 entry.id,
165 pedal_path.display()
166 )
167 })?;
168 let pedal_def = parse_pedal_file(&source)
169 .map_err(|e| format!("Parse error in '{}': {e}", entry.id))?;
170
171 if !pedal_def.has_fx_loop() {
172 return Err(format!(
173 "Pedal '{}' has no fx_send/fx_return nodes",
174 entry.id
175 ));
176 }
177
178 let mut split = compile_split_pedal(&pedal_def, sample_rate)
179 .map_err(|e| {
180 format!("Failed to split-compile '{}': {e}", entry.id)
181 })?;
182
183 for ctrl in &pedal_def.controls {
185 split.set_control(&ctrl.label, ctrl.default);
186 }
187 for (label, val) in &entry.overrides {
188 split.set_control(label, *val);
189 }
190
191 Ok((pedal_def.name.clone(), split))
192 })()
193 }
194 };
195
196 match amp_result {
197 Ok((name, split)) => {
198 chain.retain(|slot| {
201 if let ChainSlot::Pedal(p) = slot {
202 p.name != name || p.failed
203 } else {
204 true
205 }
206 });
207
208 let mut effects = Vec::with_capacity(group.pedals.len());
209 for pedal_entry in &group.pedals {
210 let slot = Self::compile_pedal_entry(
211 pedal_entry,
212 board_dir,
213 sample_rate,
214 &mut warnings,
215 );
216 effects.push(slot);
217 }
218 chain.push(ChainSlot::FxLoop(FxLoopSlot {
219 amp: split,
220 effects,
221 mix: group.mix,
222 }));
223 }
224 Err(msg) => {
225 warnings
226 .push(format!("FX loop for '{}' failed: {msg}", group.target_id));
227 for pedal_entry in &group.pedals {
229 let slot = Self::compile_pedal_entry(
230 pedal_entry,
231 board_dir,
232 sample_rate,
233 &mut warnings,
234 );
235 chain.push(ChainSlot::Pedal(slot));
236 }
237 }
238 }
239 }
240 }
241 }
242
243 if let Some((ref def, ref global_dir)) = global {
245 for entry in &def.end {
246 let slot = Self::compile_pedal_entry(entry, global_dir, sample_rate, &mut warnings);
247 chain.push(ChainSlot::Pedal(slot));
248 }
249 }
250
251 let n = chain.len();
255 let loading = if n > 1 {
256 (0..n - 1)
257 .map(|_| InterstageLoading::transparent(sample_rate as crate::Wave))
258 .collect()
259 } else {
260 Vec::new()
261 };
262
263 (Self { chain, loading }, warnings)
264 }
265
266 pub fn from_board_with_resolver(
277 board: &BoardDef,
278 sample_rate: f64,
279 resolver: impl Fn(&str) -> Option<String>,
280 ) -> (Self, Vec<String>) {
281 let mut chain = Vec::new();
282 let mut warnings = Vec::new();
283
284 for entry in &board.entries {
285 match entry {
286 BoardEntry::Pedal(pedal_entry) => {
287 let slot = Self::compile_pedal_entry_from_source(
288 pedal_entry,
289 &resolver,
290 sample_rate,
291 &mut warnings,
292 );
293 chain.push(ChainSlot::Pedal(slot));
294 }
295 BoardEntry::FxLoop(group) => {
296 warnings.push(format!(
297 "FX loop '{}' skipped — not supported in resolver mode",
298 group.target_id,
299 ));
300 for pedal_entry in &group.pedals {
302 let slot = Self::compile_pedal_entry_from_source(
303 pedal_entry,
304 &resolver,
305 sample_rate,
306 &mut warnings,
307 );
308 chain.push(ChainSlot::Pedal(slot));
309 }
310 }
311 }
312 }
313
314 let n = chain.len();
315 let loading = if n > 1 {
316 (0..n - 1)
317 .map(|_| InterstageLoading::transparent(sample_rate as crate::Wave))
318 .collect()
319 } else {
320 Vec::new()
321 };
322
323 (Self { chain, loading }, warnings)
324 }
325
326 fn compile_pedal_entry_from_source(
328 entry: &crate::board::BoardPedalEntry,
329 resolver: &impl Fn(&str) -> Option<String>,
330 sample_rate: f64,
331 warnings: &mut Vec<String>,
332 ) -> PedalSlot {
333 let result: Result<(String, CompiledPedal), String> = (|| {
334 let source = resolver(&entry.path)
335 .ok_or_else(|| format!("Pedal '{}' not found: {}", entry.id, entry.path))?;
336 let pedal_def = parse_pedal_file(&source)
337 .map_err(|e| format!("Parse error in '{}' ({}): {e}", entry.id, entry.path))?;
338 let mut proc = compile_pedal(&pedal_def, sample_rate).map_err(|e| {
339 format!("Compilation error in '{}' ({}): {e}", entry.id, entry.path)
340 })?;
341
342 for ctrl in &pedal_def.controls {
343 proc.set_control(&ctrl.label, ctrl.default as crate::Wave);
344 }
345 for (label, val) in &entry.overrides {
346 proc.set_control(label, *val as crate::Wave);
347 }
348
349 Ok((pedal_def.name.clone(), proc))
350 })();
351
352 match result {
353 Ok((name, proc)) => PedalSlot {
354 name,
355 processor: Some(proc),
356 bypassed: false,
357 failed: false,
358 },
359 Err(msg) => {
360 warnings.push(format!(
361 "Pedal '{}' failed — using dry passthrough: {msg}",
362 entry.id
363 ));
364 PedalSlot {
365 name: format!("{} [FAILED]", entry.id),
366 processor: None,
367 bypassed: false,
368 failed: true,
369 }
370 }
371 }
372 }
373
374 fn compile_pedal_entry(
376 entry: &crate::board::BoardPedalEntry,
377 board_dir: &Path,
378 sample_rate: f64,
379 warnings: &mut Vec<String>,
380 ) -> PedalSlot {
381 let pedal_path = {
383 let direct = board_dir.join(&entry.path);
384 if direct.exists() {
385 direct
386 } else if let Some(found) = Self::find_in_subdirs(board_dir, &entry.path) {
387 found
388 } else {
389 direct }
391 };
392
393 let result: Result<(String, CompiledPedal), String> = (|| {
394 let source = std::fs::read_to_string(&pedal_path).map_err(|e| {
395 format!(
396 "Error reading pedal '{}' at {}: {e}",
397 entry.id,
398 pedal_path.display()
399 )
400 })?;
401 let pedal_def = parse_pedal_file(&source)
402 .map_err(|e| format!("Parse error in '{}' ({}): {e}", entry.id, entry.path))?;
403 let mut proc = compile_pedal(&pedal_def, sample_rate).map_err(|e| {
404 format!("Compilation error in '{}' ({}): {e}", entry.id, entry.path)
405 })?;
406
407 for ctrl in &pedal_def.controls {
409 proc.set_control(&ctrl.label, ctrl.default as crate::Wave);
410 }
411
412 for (label, val) in &entry.overrides {
414 proc.set_control(label, *val as crate::Wave);
415 }
416
417 Ok((pedal_def.name.clone(), proc))
418 })();
419
420 match result {
421 Ok((name, proc)) => PedalSlot {
422 name,
423 processor: Some(proc),
424 bypassed: false,
425 failed: false,
426 },
427 Err(msg) => {
428 warnings.push(format!(
429 "Pedal '{}' failed — using dry passthrough: {msg}",
430 entry.id
431 ));
432 PedalSlot {
433 name: format!("{} [FAILED]", entry.id),
434 processor: None,
435 bypassed: false,
436 failed: true,
437 }
438 }
439 }
440 }
441
442 fn find_in_subdirs(base: &Path, filename: &str) -> Option<std::path::PathBuf> {
444 let target = Path::new(filename).file_name()?.to_str()?;
445 Self::walk_for_file(base, target)
446 }
447
448 fn walk_for_file(dir: &Path, target: &str) -> Option<std::path::PathBuf> {
449 for entry in std::fs::read_dir(dir).ok()?.flatten() {
450 let path = entry.path();
451 if path.is_dir() {
452 if let Some(found) = Self::walk_for_file(&path, target) {
453 return Some(found);
454 }
455 } else if path.file_name().and_then(|n| n.to_str()) == Some(target) {
456 return Some(path);
457 }
458 }
459 None
460 }
461
462 fn flat_pedals(&self) -> Vec<&PedalSlot> {
465 let mut out = Vec::new();
466 for slot in &self.chain {
467 match slot {
468 ChainSlot::Pedal(p) => out.push(p),
469 ChainSlot::FxLoop(lp) => {
470 for p in &lp.effects {
471 out.push(p);
472 }
473 }
474 }
475 }
476 out
477 }
478
479 fn flat_pedals_mut(&mut self) -> Vec<&mut PedalSlot> {
481 let mut out = Vec::new();
482 for slot in &mut self.chain {
483 match slot {
484 ChainSlot::Pedal(p) => out.push(p),
485 ChainSlot::FxLoop(lp) => {
486 for p in &mut lp.effects {
487 out.push(p);
488 }
489 }
490 }
491 }
492 out
493 }
494
495 pub fn len(&self) -> usize {
497 self.flat_pedals().len()
498 }
499
500 pub fn is_empty(&self) -> bool {
502 self.chain.is_empty()
503 }
504
505 pub fn pedal_name(&self, index: usize) -> Option<&str> {
507 self.flat_pedals().get(index).map(|s| s.name.as_str())
508 }
509
510 pub fn is_pedal_bypassed(&self, index: usize) -> bool {
512 self.flat_pedals().get(index).is_some_and(|s| s.bypassed)
513 }
514
515 pub fn is_pedal_failed(&self, index: usize) -> bool {
517 self.flat_pedals().get(index).is_some_and(|s| s.failed)
518 }
519
520 pub fn set_interstage_loading(
532 &mut self,
533 junction_index: usize,
534 source: crate::loading::ImpedanceModel,
535 load: crate::loading::ImpedanceModel,
536 sample_rate: f64,
537 ) {
538 if junction_index < self.loading.len() {
539 self.loading[junction_index] =
540 InterstageLoading::new(source, load, sample_rate as crate::Wave);
541 }
542 }
543
544 pub fn input_impedance(&self) -> f64 {
549 for slot in &self.chain {
550 match slot {
551 ChainSlot::Pedal(p) => {
552 if !p.bypassed {
553 if let Some(ref proc) = p.processor {
554 return proc.input_impedance() as f64;
555 }
556 }
557 }
558 ChainSlot::FxLoop(fx) => {
559 return fx.amp.input_impedance();
561 }
562 }
563 }
564 1_000_000.0 }
566
567 pub fn output_impedance(&self) -> f64 {
572 for slot in self.chain.iter().rev() {
573 match slot {
574 ChainSlot::Pedal(p) => {
575 if !p.bypassed {
576 if let Some(ref proc) = p.processor {
577 return proc.output_impedance() as f64;
578 }
579 }
580 }
581 ChainSlot::FxLoop(fx) => {
582 return fx.amp.output_impedance();
584 }
585 }
586 }
587 1_000.0 }
589}
590
591fn process_slot(slot: &mut PedalSlot, signal: f64) -> f64 {
593 if slot.bypassed {
594 signal
595 } else if let Some(ref mut proc) = slot.processor {
596 proc.process(signal as crate::Wave) as f64
597 } else {
598 signal }
600}
601
602impl PedalProcessor for PedalboardProcessor {
603 fn process(&mut self, input: crate::Wave) -> crate::Wave {
604 let mut signal = input as f64;
605 let n = self.chain.len();
606 for i in 0..n {
607 match &mut self.chain[i] {
608 ChainSlot::Pedal(slot) => {
609 signal = process_slot(slot, signal);
610 }
611 ChainSlot::FxLoop(fx) => {
612 let send = fx.amp.process_pre(signal);
614 let dry = send;
616 let mut wet = send;
617 for slot in &mut fx.effects {
618 wet = process_slot(slot, wet);
619 }
620 let fx_return = dry * (1.0 - fx.mix) + wet * fx.mix;
621 signal = fx.amp.process_post(fx_return);
623 }
624 }
625 if i < self.loading.len() {
627 signal = self.loading[i].process(signal as crate::Wave) as f64;
628 }
629 }
630 signal as crate::Wave
631 }
632
633 fn set_sample_rate(&mut self, rate: crate::Wave) {
634 for chain_slot in &mut self.chain {
635 match chain_slot {
636 ChainSlot::Pedal(slot) => {
637 if let Some(ref mut proc) = slot.processor {
638 proc.set_sample_rate(rate);
639 }
640 }
641 ChainSlot::FxLoop(fx) => {
642 fx.amp.set_sample_rate(rate as f64);
643 for slot in &mut fx.effects {
644 if let Some(ref mut proc) = slot.processor {
645 proc.set_sample_rate(rate);
646 }
647 }
648 }
649 }
650 }
651 for loading in &mut self.loading {
652 loading.set_sample_rate(rate);
653 }
654 }
655
656 fn reset(&mut self) {
657 for chain_slot in &mut self.chain {
658 match chain_slot {
659 ChainSlot::Pedal(slot) => {
660 if let Some(ref mut proc) = slot.processor {
661 proc.reset();
662 }
663 }
664 ChainSlot::FxLoop(fx) => {
665 fx.amp.reset();
666 for slot in &mut fx.effects {
667 if let Some(ref mut proc) = slot.processor {
668 proc.reset();
669 }
670 }
671 }
672 }
673 }
674 for loading in &mut self.loading {
675 loading.reset();
676 }
677 }
678
679 fn set_control(&mut self, label: &str, value: crate::Wave) {
687 let value = value as f64;
688 if let Some(rest) = label.strip_prefix("fx_loop") {
690 let (loop_idx, knob) = if let Some(knob_label) = rest.strip_prefix(':') {
692 (0usize, knob_label)
693 } else if let Some(idx_and_label) = rest.strip_prefix('_') {
694 if let Some((idx_str, knob_label)) = idx_and_label.split_once(':') {
695 if let Ok(idx) = idx_str.parse::<usize>() {
696 (idx, knob_label)
697 } else {
698 return;
699 }
700 } else {
701 return;
702 }
703 } else {
704 return;
705 };
706
707 let mut cur_loop = 0;
708 for slot in &mut self.chain {
709 if let ChainSlot::FxLoop(fx) = slot {
710 if cur_loop == loop_idx {
711 if knob == "mix" {
712 fx.mix = value.clamp(0.0, 1.0);
713 } else {
714 fx.amp.set_control(knob, value);
716 }
717 return;
718 }
719 cur_loop += 1;
720 }
721 }
722 return;
723 }
724
725 if let Some((prefix, remainder)) = label.split_once(':') {
727 if let Ok(idx) = prefix.parse::<usize>() {
728 let mut flat = self.flat_pedals_mut();
729 if let Some(slot) = flat.get_mut(idx) {
730 if remainder == "__bypass__" {
731 slot.bypassed = value > 0.5;
732 } else if let Some(ref mut proc) = slot.processor {
733 proc.set_control(remainder, value as crate::Wave);
734 }
735 }
736 }
737 }
738 }
739}
740
741pub struct BoardSwitcherProcessor {
750 boards: Vec<PedalboardProcessor>,
751 active: usize,
752}
753
754impl BoardSwitcherProcessor {
755 pub fn new(boards: Vec<PedalboardProcessor>) -> Self {
757 Self { boards, active: 0 }
758 }
759
760 pub fn len(&self) -> usize {
762 self.boards.len()
763 }
764
765 pub fn is_empty(&self) -> bool {
767 self.boards.is_empty()
768 }
769
770 pub fn active(&self) -> usize {
772 self.active
773 }
774}
775
776impl PedalProcessor for BoardSwitcherProcessor {
777 fn process(&mut self, input: crate::Wave) -> crate::Wave {
778 if let Some(board) = self.boards.get_mut(self.active) {
779 board.process(input)
780 } else {
781 input
782 }
783 }
784
785 fn set_sample_rate(&mut self, rate: crate::Wave) {
786 for board in &mut self.boards {
787 board.set_sample_rate(rate);
788 }
789 }
790
791 fn reset(&mut self) {
792 for board in &mut self.boards {
793 board.reset();
794 }
795 }
796
797 fn set_control(&mut self, label: &str, value: crate::Wave) {
798 if label == "__switch_board__" {
799 let idx = value as usize;
800 if idx < self.boards.len() {
801 self.active = idx;
802 }
803 } else if let Some(board) = self.boards.get_mut(self.active) {
804 board.set_control(label, value);
805 }
806 }
807}
808
809pub struct PedalSwitcherProcessor {
819 pedals: Vec<CompiledPedal>,
820 active: usize,
821}
822
823impl PedalSwitcherProcessor {
824 pub fn new(pedals: Vec<CompiledPedal>) -> Self {
826 Self { pedals, active: 0 }
827 }
828
829 pub fn len(&self) -> usize {
831 self.pedals.len()
832 }
833
834 pub fn is_empty(&self) -> bool {
836 self.pedals.is_empty()
837 }
838
839 pub fn active(&self) -> usize {
841 self.active
842 }
843}
844
845impl PedalProcessor for PedalSwitcherProcessor {
846 fn process(&mut self, input: crate::Wave) -> crate::Wave {
847 if let Some(pedal) = self.pedals.get_mut(self.active) {
848 pedal.process(input)
849 } else {
850 input
851 }
852 }
853
854 fn set_sample_rate(&mut self, rate: crate::Wave) {
855 for pedal in &mut self.pedals {
856 pedal.set_sample_rate(rate);
857 }
858 }
859
860 fn reset(&mut self) {
861 for pedal in &mut self.pedals {
862 pedal.reset();
863 }
864 }
865
866 fn set_control(&mut self, label: &str, value: crate::Wave) {
867 if label == "__switch_pedal__" {
868 let idx = value as usize;
869 if idx < self.pedals.len() {
870 self.active = idx;
871 }
872 } else if let Some(pedal) = self.pedals.get_mut(self.active) {
873 pedal.set_control(label, value);
874 }
875 }
876}
877
878#[cfg(test)]
883mod tests {
884 use super::*;
885 use crate::board::parse_board_file;
886
887 #[test]
888 fn parse_and_build_pedalboard() {
889 let src = r#"
890board "Test Board" {
891 ts: "tube_screamer.pedal" { Drive = 0.6, Level = 0.7 }
892}
893"#;
894 let board = parse_board_file(src).unwrap();
895 let board_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples");
896 let pb = PedalboardProcessor::from_board(&board, &board_dir, 48000.0).unwrap();
897 assert_eq!(pb.len(), 1);
898 assert_eq!(pb.pedal_name(0), Some("Tube Screamer"));
899 assert!(!pb.is_pedal_bypassed(0));
900 }
901
902 #[test]
903 fn pedalboard_processes_audio() {
904 let src = r#"
905board "Chain" {
906 ts: "tube_screamer.pedal"
907 bd: "blues_driver.pedal"
908}
909"#;
910 let board = parse_board_file(src).unwrap();
911 let board_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples");
912 let mut pb = PedalboardProcessor::from_board(&board, &board_dir, 48000.0).unwrap();
913
914 let mut max_out = 0.0_f64;
916 for i in 0..1000 {
917 let input = (i as f64 * 440.0 * 2.0 * std::f64::consts::PI / 48000.0).sin() * 0.1;
918 let output = pb.process(input);
919 max_out = max_out.max(output.abs());
920 }
921 assert!(max_out > 0.0001, "pedalboard should produce output");
922 }
923
924 #[test]
925 fn pedalboard_bypass_control() {
926 let src = r#"
927board "Bypass Test" {
928 ts: "tube_screamer.pedal"
929}
930"#;
931 let board = parse_board_file(src).unwrap();
932 let board_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples");
933 let mut pb = PedalboardProcessor::from_board(&board, &board_dir, 48000.0).unwrap();
934
935 pb.set_control("0:__bypass__", 1.0);
937 assert!(pb.is_pedal_bypassed(0));
938
939 let input = 0.42;
941 let output = pb.process(input);
942 assert!((output - input).abs() < f64::EPSILON);
943
944 pb.set_control("0:__bypass__", 0.0);
946 assert!(!pb.is_pedal_bypassed(0));
947 }
948
949 #[test]
950 fn pedalboard_knob_control_routing() {
951 let src = r#"
952board "Knob Test" {
953 ts: "tube_screamer.pedal"
954}
955"#;
956 let board = parse_board_file(src).unwrap();
957 let board_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples");
958 let mut pb = PedalboardProcessor::from_board(&board, &board_dir, 48000.0).unwrap();
959
960 pb.set_control("0:Drive", 0.9);
962 pb.set_control("0:Level", 0.3);
963
964 pb.set_control("99:Drive", 0.5);
966 }
967
968 #[test]
969 fn pedalboard_missing_pedal_file_uses_dry_passthrough() {
970 let src = r#"
971board "Bad" {
972 x: "nonexistent.pedal"
973}
974"#;
975 let board = parse_board_file(src).unwrap();
976 let board_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples");
977 let (pb, warnings) =
978 PedalboardProcessor::from_board_with_warnings(&board, &board_dir, 48000.0);
979
980 assert_eq!(pb.len(), 1);
982 assert!(pb.is_pedal_failed(0));
983 assert!(!warnings.is_empty());
984
985 let mut pb = pb;
987 let output = pb.process(0.42);
988 assert!((output - 0.42).abs() < f64::EPSILON);
989 }
990
991 fn make_test_board(pedal_file: &str) -> PedalboardProcessor {
994 let src = format!(
995 r#"board "Test" {{
996 p: "{pedal_file}"
997}}"#
998 );
999 let board = parse_board_file(&src).unwrap();
1000 let board_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples");
1001 PedalboardProcessor::from_board(&board, &board_dir, 48000.0).unwrap()
1002 }
1003
1004 #[test]
1005 fn switcher_delegates_to_active_board() {
1006 let board0 = make_test_board("tube_screamer.pedal");
1007 let board1 = make_test_board("blues_driver.pedal");
1008 let mut switcher = BoardSwitcherProcessor::new(vec![board0, board1]);
1009
1010 assert_eq!(switcher.len(), 2);
1011 assert_eq!(switcher.active(), 0);
1012
1013 let out0 = switcher.process(0.1);
1015
1016 switcher.set_control("__switch_board__", 1.0);
1018 assert_eq!(switcher.active(), 1);
1019
1020 let out1 = switcher.process(0.1);
1022 assert!(out0.is_finite());
1025 assert!(out1.is_finite());
1026 }
1027
1028 #[test]
1029 fn switcher_bounds_check() {
1030 let board0 = make_test_board("tube_screamer.pedal");
1031 let mut switcher = BoardSwitcherProcessor::new(vec![board0]);
1032
1033 switcher.set_control("__switch_board__", 99.0);
1035 assert_eq!(switcher.active(), 0);
1036 }
1037
1038 #[test]
1039 fn switcher_forwards_controls_to_active() {
1040 let board0 = make_test_board("tube_screamer.pedal");
1041 let board1 = make_test_board("blues_driver.pedal");
1042 let mut switcher = BoardSwitcherProcessor::new(vec![board0, board1]);
1043
1044 switcher.set_control("0:Drive", 0.9);
1046
1047 switcher.set_control("__switch_board__", 1.0);
1049 switcher.set_control("0:Gain", 0.8);
1050 }
1051
1052 #[test]
1053 fn switcher_set_sample_rate_all_boards() {
1054 let board0 = make_test_board("tube_screamer.pedal");
1055 let board1 = make_test_board("blues_driver.pedal");
1056 let mut switcher = BoardSwitcherProcessor::new(vec![board0, board1]);
1057
1058 switcher.set_sample_rate(44100.0);
1060 switcher.reset();
1061 }
1062
1063 #[test]
1066 fn global_board_prepends_and_appends() {
1067 let examples = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples");
1068 let ts_path = examples
1069 .join("pedals/overdrive/tube_screamer.pedal")
1070 .canonicalize()
1071 .unwrap();
1072 let comp_path = examples
1073 .join("pedals/compressor/dyna_comp.pedal")
1074 .canonicalize()
1075 .unwrap();
1076 let bd_path = examples
1077 .join("pedals/overdrive/blues_driver.pedal")
1078 .canonicalize()
1079 .unwrap();
1080
1081 let tmp = tempfile::tempdir().unwrap();
1082
1083 let global_content = format!(
1084 "global {{\n start {{\n comp: \"{}\" {{ Sensitivity = 0.3 }}\n }}\n end {{\n bd: \"{}\"\n }}\n}}",
1085 comp_path.display(),
1086 bd_path.display(),
1087 );
1088 std::fs::write(tmp.path().join(".global.board"), &global_content).unwrap();
1089
1090 let sub = tmp.path().join("boards");
1091 std::fs::create_dir(&sub).unwrap();
1092 let board_content = format!(
1093 "board \"Test\" {{\n ts: \"{}\" {{ Drive = 0.6 }}\n}}",
1094 ts_path.display(),
1095 );
1096 std::fs::write(sub.join("test.board"), &board_content).unwrap();
1097
1098 let board = parse_board_file(&board_content).unwrap();
1099 let (pb, warnings) = PedalboardProcessor::from_board_with_warnings(&board, &sub, 48000.0);
1100
1101 assert_eq!(pb.len(), 3, "warnings: {warnings:?}");
1102 assert_eq!(pb.pedal_name(0), Some("MXR Dyna Comp"));
1103 assert_eq!(pb.pedal_name(1), Some("Tube Screamer"));
1104 assert_eq!(pb.pedal_name(2), Some("Boss Blues Driver"));
1105 }
1106
1107 #[test]
1108 fn global_board_start_only() {
1109 let examples = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples");
1110 let ts_path = examples
1111 .join("pedals/overdrive/tube_screamer.pedal")
1112 .canonicalize()
1113 .unwrap();
1114 let comp_path = examples
1115 .join("pedals/compressor/dyna_comp.pedal")
1116 .canonicalize()
1117 .unwrap();
1118
1119 let tmp = tempfile::tempdir().unwrap();
1120
1121 let global_content = format!(
1122 "global {{\n start {{\n comp: \"{}\"\n }}\n}}",
1123 comp_path.display(),
1124 );
1125 std::fs::write(tmp.path().join(".global.board"), &global_content).unwrap();
1126
1127 let board_content = format!("board \"Test\" {{\n ts: \"{}\"\n}}", ts_path.display(),);
1128
1129 let board = parse_board_file(&board_content).unwrap();
1130 let (pb, _) = PedalboardProcessor::from_board_with_warnings(&board, tmp.path(), 48000.0);
1131
1132 assert_eq!(pb.len(), 2);
1133 assert_eq!(pb.pedal_name(0), Some("MXR Dyna Comp"));
1134 assert_eq!(pb.pedal_name(1), Some("Tube Screamer"));
1135 }
1136
1137 #[test]
1138 fn global_board_signal_chain_order() {
1139 let examples = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples");
1140 let ts_path = examples
1141 .join("pedals/overdrive/tube_screamer.pedal")
1142 .canonicalize()
1143 .unwrap();
1144 let comp_path = examples
1145 .join("pedals/compressor/dyna_comp.pedal")
1146 .canonicalize()
1147 .unwrap();
1148 let bd_path = examples
1149 .join("pedals/overdrive/blues_driver.pedal")
1150 .canonicalize()
1151 .unwrap();
1152
1153 let tmp = tempfile::tempdir().unwrap();
1154
1155 let global_content = format!(
1156 "global {{\n start {{\n comp: \"{}\"\n }}\n end {{\n bd: \"{}\"\n }}\n}}",
1157 comp_path.display(),
1158 bd_path.display(),
1159 );
1160 std::fs::write(tmp.path().join(".global.board"), &global_content).unwrap();
1161
1162 let board_content = format!(
1163 "board \"Chain\" {{\n ts: \"{}\" {{ Drive = 0.8 }}\n}}",
1164 ts_path.display(),
1165 );
1166
1167 let board = parse_board_file(&board_content).unwrap();
1168 let (pb, _) = PedalboardProcessor::from_board_with_warnings(&board, tmp.path(), 48000.0);
1169
1170 assert_eq!(pb.len(), 3);
1171
1172 let mut pb = pb;
1173 let mut max_out = 0.0_f64;
1174 for i in 0..1000 {
1175 let input = (i as f64 * 440.0 * 2.0 * std::f64::consts::PI / 48000.0).sin() * 0.1;
1176 let output = pb.process(input);
1177 assert!(output.is_finite());
1178 max_out = max_out.max(output.abs());
1179 }
1180 assert!(
1181 max_out > 0.0001,
1182 "pedalboard with global should produce output"
1183 );
1184 }
1185
1186 #[test]
1187 fn no_global_board_unchanged() {
1188 let tmp = tempfile::tempdir().unwrap();
1189 let examples = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples");
1190 let ts_path = examples
1191 .join("pedals/overdrive/tube_screamer.pedal")
1192 .canonicalize()
1193 .unwrap();
1194
1195 let board_content = format!("board \"Solo\" {{\n ts: \"{}\"\n}}", ts_path.display(),);
1196
1197 let board = parse_board_file(&board_content).unwrap();
1198 let (pb, warnings) =
1199 PedalboardProcessor::from_board_with_warnings(&board, tmp.path(), 48000.0);
1200
1201 assert_eq!(pb.len(), 1);
1202 assert!(warnings.is_empty());
1203 assert_eq!(pb.pedal_name(0), Some("Tube Screamer"));
1204 }
1205
1206 #[test]
1207 fn global_board_control_routing_flat_index() {
1208 let examples = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples");
1209 let ts_path = examples
1210 .join("pedals/overdrive/tube_screamer.pedal")
1211 .canonicalize()
1212 .unwrap();
1213 let comp_path = examples
1214 .join("pedals/compressor/dyna_comp.pedal")
1215 .canonicalize()
1216 .unwrap();
1217
1218 let tmp = tempfile::tempdir().unwrap();
1219
1220 let global_content = format!(
1221 "global {{\n start {{\n comp: \"{}\"\n }}\n}}",
1222 comp_path.display(),
1223 );
1224 std::fs::write(tmp.path().join(".global.board"), &global_content).unwrap();
1225
1226 let board_content = format!("board \"Test\" {{\n ts: \"{}\"\n}}", ts_path.display(),);
1227
1228 let board = parse_board_file(&board_content).unwrap();
1229 let (mut pb, _) =
1230 PedalboardProcessor::from_board_with_warnings(&board, tmp.path(), 48000.0);
1231
1232 pb.set_control("0:__bypass__", 1.0);
1233 assert!(pb.is_pedal_bypassed(0));
1234 assert!(!pb.is_pedal_bypassed(1));
1235
1236 pb.set_control("1:Drive", 0.9);
1237 }
1238
1239 #[test]
1242 fn pedalboard_fx_loop_processes_audio() {
1243 let src = r#"
1244board "FX Loop" {
1245 amp: "tweed_deluxe_5e3.pedal" { Volume = 0.7, Tone = 0.5 }
1246 fx_loop(amp) {
1247 ts: "tube_screamer.pedal" { Drive = 0.3, Level = 0.5 }
1248 }
1249}
1250"#;
1251 let board = parse_board_file(src).unwrap();
1252 let board_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples");
1253 let (mut pb, warnings) =
1254 PedalboardProcessor::from_board_with_warnings(&board, &board_dir, 48000.0);
1255 assert!(
1256 warnings.is_empty(),
1257 "FX loop board should load without warnings: {warnings:?}"
1258 );
1259
1260 let mut max_out = 0.0_f64;
1262 for i in 0..1000 {
1263 let input = (i as f64 * 440.0 * 2.0 * std::f64::consts::PI / 48000.0).sin() * 0.1;
1264 let output = pb.process(input);
1265 assert!(output.is_finite(), "FX loop output should be finite");
1266 max_out = max_out.max(output.abs());
1267 }
1268 assert!(max_out > 0.0001, "FX loop board should produce output");
1269 }
1270
1271 #[test]
1272 fn pedalboard_fx_loop_mix_control() {
1273 let src = r#"
1274board "Mix" {
1275 amp: "tweed_deluxe_5e3.pedal" { Volume = 0.5, Tone = 0.5 }
1276 fx_loop(amp) {
1277 ts: "tube_screamer.pedal" { Drive = 0.5 }
1278 }
1279}
1280"#;
1281 let board = parse_board_file(src).unwrap();
1282 let board_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples");
1283 let mut pb = PedalboardProcessor::from_board(&board, &board_dir, 48000.0).unwrap();
1284
1285 pb.set_control("fx_loop:mix", 0.5);
1287
1288 pb.set_control("fx_loop:Volume", 0.8);
1290
1291 let output = pb.process(0.1);
1293 assert!(output.is_finite());
1294 }
1295
1296 #[test]
1297 fn pedalboard_fx_loop_missing_amp_falls_back() {
1298 let src = r#"
1299board "Bad FX Loop" {
1300 ts: "tube_screamer.pedal"
1301 fx_loop(nonexistent) {
1302 bd: "blues_driver.pedal"
1303 }
1304}
1305"#;
1306 let board = parse_board_file(src).unwrap();
1307 let board_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples");
1308 let (pb, warnings) =
1309 PedalboardProcessor::from_board_with_warnings(&board, &board_dir, 48000.0);
1310
1311 assert!(
1313 warnings.iter().any(|w| w.contains("nonexistent")),
1314 "Should warn about missing amp: {warnings:?}"
1315 );
1316 assert!(pb.len() >= 2, "Fallback should still have pedals");
1318 }
1319
1320 #[test]
1321 fn from_board_with_resolver_builds_chain() {
1322 let board_src = r#"
1323board "Resolver Test" {
1324 ts: "tube_screamer.pedal" { Drive = 0.7 }
1325 bd: "blues_driver.pedal"
1326}
1327"#;
1328 let ts_src = std::fs::read_to_string(
1329 Path::new(env!("CARGO_MANIFEST_DIR"))
1330 .join("examples/pedals/overdrive/tube_screamer.pedal"),
1331 )
1332 .unwrap();
1333 let bd_src = std::fs::read_to_string(
1334 Path::new(env!("CARGO_MANIFEST_DIR"))
1335 .join("examples/pedals/overdrive/blues_driver.pedal"),
1336 )
1337 .unwrap();
1338
1339 let sources: Vec<(&str, String)> = vec![
1340 ("tube_screamer.pedal", ts_src),
1341 ("blues_driver.pedal", bd_src),
1342 ];
1343
1344 let board = parse_board_file(board_src).unwrap();
1345 let (pb, warnings) =
1346 PedalboardProcessor::from_board_with_resolver(&board, 48000.0, |path| {
1347 sources
1348 .iter()
1349 .find(|(p, _)| *p == path)
1350 .map(|(_, s)| s.clone())
1351 });
1352
1353 assert!(warnings.is_empty(), "Unexpected warnings: {warnings:?}");
1354 assert_eq!(pb.len(), 2);
1355 assert_eq!(pb.pedal_name(0), Some("Tube Screamer"));
1356 assert_eq!(pb.pedal_name(1), Some("Boss Blues Driver"));
1357
1358 let mut pb = pb;
1360 let input = crate::wav::sine_wave(330.0, 0.02, 48000);
1361 let output: Vec<f64> = input.iter().map(|&s| pb.process(s)).collect();
1362 let max_out = output.iter().copied().fold(0.0_f64, |a, b| a.max(b.abs()));
1363 assert!(max_out > 1e-6, "Board should produce non-silent output");
1364 }
1365
1366 #[test]
1367 fn from_board_with_resolver_missing_pedal_warns() {
1368 let board_src = r#"
1369board "Missing Test" {
1370 ts: "nonexistent.pedal"
1371}
1372"#;
1373 let board = parse_board_file(board_src).unwrap();
1374 let (pb, warnings) =
1375 PedalboardProcessor::from_board_with_resolver(&board, 48000.0, |_| None);
1376
1377 assert_eq!(pb.len(), 1);
1378 assert!(pb.is_pedal_failed(0));
1379 assert!(
1380 warnings.iter().any(|w| w.contains("not found")),
1381 "Should warn about missing pedal: {warnings:?}"
1382 );
1383 }
1384}