1use crate::analog::{AnalogVco, Saturator, Wavefolder};
7use crate::graph::{NodeHandle, Patch, PatchError};
8use crate::modules::*;
9use crate::port::{GraphModule, PortSpec};
10use crate::StdMap;
11use alloc::boxed::Box;
12use alloc::format;
13use alloc::string::{String, ToString};
14use alloc::vec;
15use alloc::vec::Vec;
16use serde::{Deserialize, Serialize};
17
18pub const CURRENT_PATCH_VERSION: u32 = 1;
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
30#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
31pub struct PatchDef {
32 pub version: u32,
35
36 pub name: String,
38 pub author: Option<String>,
39 pub description: Option<String>,
40
41 #[serde(default)]
45 pub tags: Vec<String>,
46
47 #[serde(default)]
53 pub output: Option<String>,
54
55 pub modules: Vec<ModuleDef>,
57
58 pub cables: Vec<CableDef>,
60
61 #[serde(default)]
71 pub parameters: StdMap<String, f64>,
72}
73
74impl PatchDef {
75 pub fn new(name: impl Into<String>) -> Self {
77 Self {
78 version: CURRENT_PATCH_VERSION,
79 name: name.into(),
80 author: None,
81 description: None,
82 tags: vec![],
83 output: None,
84 modules: vec![],
85 cables: vec![],
86 parameters: StdMap::new(),
87 }
88 }
89
90 pub fn with_author(mut self, author: impl Into<String>) -> Self {
92 self.author = Some(author.into());
93 self
94 }
95
96 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
98 self.description = Some(desc.into());
99 self
100 }
101
102 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
104 self.tags.push(tag.into());
105 self
106 }
107
108 pub fn to_json(&self) -> Result<String, serde_json::Error> {
110 serde_json::to_string_pretty(self)
111 }
112
113 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
115 serde_json::from_str(json)
116 }
117}
118
119impl Default for PatchDef {
120 fn default() -> Self {
121 Self::new("Untitled")
122 }
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
128pub struct ModuleDef {
129 pub name: String,
131
132 pub module_type: String,
134
135 pub position: Option<(f32, f32)>,
137
138 pub state: Option<serde_json::Value>,
140}
141
142impl ModuleDef {
143 pub fn new(name: impl Into<String>, module_type: impl Into<String>) -> Self {
144 Self {
145 name: name.into(),
146 module_type: module_type.into(),
147 position: None,
148 state: None,
149 }
150 }
151
152 pub fn with_position(mut self, x: f32, y: f32) -> Self {
153 self.position = Some((x, y));
154 self
155 }
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
161pub struct CableDef {
162 pub from: String,
164
165 pub to: String,
167
168 pub attenuation: Option<f64>,
170
171 pub offset: Option<f64>,
173}
174
175impl CableDef {
176 pub fn new(from: impl Into<String>, to: impl Into<String>) -> Self {
177 Self {
178 from: from.into(),
179 to: to.into(),
180 attenuation: None,
181 offset: None,
182 }
183 }
184
185 pub fn with_attenuation(mut self, attenuation: f64) -> Self {
186 self.attenuation = Some(attenuation);
187 self
188 }
189
190 pub fn with_offset(mut self, offset: f64) -> Self {
191 self.offset = Some(offset);
192 self
193 }
194
195 pub fn with_modulation(mut self, attenuation: f64, offset: f64) -> Self {
196 self.attenuation = Some(attenuation);
197 self.offset = Some(offset);
198 self
199 }
200}
201
202pub type ModuleFactory = Box<dyn Fn(f64) -> Box<dyn GraphModule> + Send + Sync>;
204
205#[derive(Debug, Clone)]
207pub struct ModuleMetadata {
208 pub type_id: String,
209 pub name: String,
210 pub category: String,
211 pub description: String,
212 pub port_spec: PortSpec,
213 pub keywords: Vec<String>,
215 pub tags: Vec<String>,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
225#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
226pub struct PortSummary {
227 pub inputs: u8,
229 pub outputs: u8,
231 pub has_audio_in: bool,
233 pub has_audio_out: bool,
235}
236
237impl PortSummary {
238 pub fn from_port_spec(spec: &PortSpec) -> Self {
240 use crate::port::SignalKind;
241
242 let has_audio_in = spec.inputs.iter().any(|p| p.kind == SignalKind::Audio);
243 let has_audio_out = spec.outputs.iter().any(|p| p.kind == SignalKind::Audio);
244
245 Self {
246 inputs: spec.inputs.len() as u8,
247 outputs: spec.outputs.len() as u8,
248 has_audio_in,
249 has_audio_out,
250 }
251 }
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
256#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
257pub struct ModuleCatalogEntry {
258 pub type_id: String,
260 pub name: String,
262 pub category: String,
264 pub description: String,
266 pub keywords: Vec<String>,
268 pub ports: PortSummary,
270 pub tags: Vec<String>,
272}
273
274impl ModuleCatalogEntry {
275 pub fn from_metadata(metadata: &ModuleMetadata) -> Self {
277 Self {
278 type_id: metadata.type_id.clone(),
279 name: metadata.name.clone(),
280 category: metadata.category.clone(),
281 description: metadata.description.clone(),
282 keywords: metadata.keywords.clone(),
283 ports: PortSummary::from_port_spec(&metadata.port_spec),
284 tags: metadata.tags.clone(),
285 }
286 }
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
291#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
292#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
293pub struct CatalogResponse {
294 pub modules: Vec<ModuleCatalogEntry>,
296 pub categories: Vec<String>,
298}
299
300pub struct ModuleRegistry {
302 factories: StdMap<String, ModuleFactory>,
303 metadata: StdMap<String, ModuleMetadata>,
304}
305
306impl ModuleRegistry {
307 pub fn new() -> Self {
309 let mut registry = Self {
310 factories: StdMap::new(),
311 metadata: StdMap::new(),
312 };
313
314 registry.register_builtin();
316 registry
317 }
318
319 fn register_builtin(&mut self) {
320 self.register_factory_with_keywords(
324 "vco",
325 "VCO",
326 "Oscillators",
327 "Voltage-controlled oscillator with multiple waveforms",
328 &[
329 "oscillator",
330 "sine",
331 "saw",
332 "pulse",
333 "triangle",
334 "waveform",
335 "pitch",
336 ],
337 &["essential"],
338 |sr| Box::new(Vco::new(sr)),
339 );
340
341 self.register_factory_with_keywords(
342 "analog_vco",
343 "Analog VCO",
344 "Oscillators",
345 "VCO with analog modeling (drift, saturation)",
346 &[
347 "oscillator",
348 "analog",
349 "drift",
350 "warm",
351 "vintage",
352 "detuned",
353 ],
354 &["analog"],
355 |sr| Box::new(AnalogVco::new(sr)),
356 );
357
358 self.register_factory_with_keywords(
359 "lfo",
360 "LFO",
361 "Modulation",
362 "Low-frequency oscillator for modulation",
363 &[
364 "oscillator",
365 "modulation",
366 "vibrato",
367 "tremolo",
368 "slow",
369 "sweep",
370 ],
371 &["essential"],
372 |sr| Box::new(Lfo::new(sr)),
373 );
374
375 self.register_factory_with_keywords(
379 "svf",
380 "SVF",
381 "Filters",
382 "State-variable filter with LP/BP/HP/Notch outputs",
383 &[
384 "filter",
385 "lowpass",
386 "highpass",
387 "bandpass",
388 "notch",
389 "resonance",
390 "cutoff",
391 ],
392 &["essential"],
393 |sr| Box::new(Svf::new(sr)),
394 );
395
396 self.register_factory_with_keywords(
397 "diode_ladder",
398 "Diode Ladder Filter",
399 "Filters",
400 "24dB/oct ladder filter with diode saturation",
401 &[
402 "filter",
403 "ladder",
404 "moog",
405 "lowpass",
406 "resonance",
407 "saturation",
408 "analog",
409 ],
410 &["analog"],
411 |sr| Box::new(DiodeLadderFilter::new(sr)),
412 );
413
414 self.register_factory_with_keywords(
418 "adsr",
419 "ADSR",
420 "Envelopes",
421 "Attack-Decay-Sustain-Release envelope generator",
422 &[
423 "envelope", "attack", "decay", "sustain", "release", "eg", "contour",
424 ],
425 &["essential"],
426 |sr| Box::new(Adsr::new(sr)),
427 );
428
429 self.register_factory_with_keywords(
433 "vca",
434 "VCA",
435 "Utilities",
436 "Voltage-controlled amplifier",
437 &["amplifier", "gain", "volume", "level", "cv"],
438 &["essential"],
439 |_| Box::new(Vca::new()),
440 );
441
442 self.register_factory_with_keywords(
446 "mixer",
447 "Mixer",
448 "Utilities",
449 "4-channel audio mixer",
450 &["mix", "combine", "sum", "blend", "audio"],
451 &["essential"],
452 |_| Box::new(Mixer::new(4)),
453 );
454
455 self.register_factory_with_keywords(
456 "mixer8",
457 "Mixer 8",
458 "Utilities",
459 "8-channel audio mixer for polyphony",
460 &["mix", "combine", "sum", "blend", "audio", "poly", "voices"],
461 &[],
462 |_| Box::new(Mixer::new(8)),
463 );
464
465 self.register_factory_with_keywords(
466 "offset",
467 "Offset",
468 "Utilities",
469 "DC offset / voltage source",
470 &["dc", "voltage", "constant", "bias", "source"],
471 &[],
472 |_| Box::new(Offset::new(0.0)),
473 );
474
475 self.register_factory_with_keywords(
476 "unit_delay",
477 "Unit Delay",
478 "Utilities",
479 "Single-sample delay for feedback",
480 &["delay", "feedback", "sample", "z-1"],
481 &["advanced"],
482 |_| Box::new(UnitDelay::new()),
483 );
484
485 self.register_factory_with_keywords(
486 "delay_line",
487 "Delay Line",
488 "Effects",
489 "Multi-tap delay with feedback and wet/dry mix",
490 &["delay", "echo", "feedback", "time", "effect"],
491 &[],
492 |sr| Box::new(DelayLine::new(sr)),
493 );
494
495 self.register_factory_with_keywords(
496 "tape_delay",
497 "Tape Delay",
498 "Effects",
499 "Long tape-style delay: time in seconds (up to 12 s), feedback past unity with saturation in the loop",
500 &["tape", "delay", "echo", "loop", "feedback", "self-oscillation"],
501 &[],
502 |sr| Box::new(DelayLine::tape(sr)),
503 );
504
505 self.register_factory_with_keywords(
506 "chorus",
507 "Chorus",
508 "Effects",
509 "Classic chorus effect with modulated delay lines",
510 &[
511 "chorus",
512 "modulation",
513 "detune",
514 "ensemble",
515 "effect",
516 "stereo",
517 ],
518 &[],
519 |sr| Box::new(Chorus::new(sr)),
520 );
521
522 self.register_factory_with_keywords(
523 "flanger",
524 "Flanger",
525 "Effects",
526 "Classic flanging effect with modulated delay",
527 &["flanger", "modulation", "sweep", "jet", "effect"],
528 &[],
529 |sr| Box::new(Flanger::new(sr)),
530 );
531
532 self.register_factory_with_keywords(
533 "phaser",
534 "Phaser",
535 "Effects",
536 "Classic phaser effect with all-pass filters",
537 &["phaser", "modulation", "sweep", "effect", "allpass"],
538 &[],
539 |sr| Box::new(Phaser::new(sr)),
540 );
541
542 self.register_factory_with_keywords(
543 "limiter",
544 "Limiter",
545 "Dynamics",
546 "Prevents signals from exceeding threshold",
547 &["limiter", "dynamics", "ceiling", "clip", "loudness"],
548 &[],
549 |sr| Box::new(Limiter::new(sr)),
550 );
551
552 self.register_factory_with_keywords(
553 "noise_gate",
554 "Noise Gate",
555 "Dynamics",
556 "Attenuates signals below threshold",
557 &["gate", "dynamics", "noise", "threshold", "mute"],
558 &[],
559 |sr| Box::new(NoiseGate::new(sr)),
560 );
561
562 self.register_factory_with_keywords(
563 "compressor",
564 "Compressor",
565 "Dynamics",
566 "Dynamic range compression with sidechain",
567 &["compressor", "dynamics", "squeeze", "punch", "sidechain"],
568 &[],
569 |sr| Box::new(Compressor::new(sr)),
570 );
571
572 self.register_factory_with_keywords(
573 "envelope_follower",
574 "Envelope Follower",
575 "Utilities",
576 "Extracts amplitude envelope from audio",
577 &["envelope", "follower", "detector", "cv", "ducking"],
578 &[],
579 |sr| Box::new(EnvelopeFollower::new(sr)),
580 );
581
582 self.register_factory_with_keywords(
583 "ducker",
584 "Ducker",
585 "Dynamics",
586 "Sidechain ducking driven by a key input",
587 &["ducker", "duck", "sidechain", "key", "dynamics", "pump"],
588 &[],
589 |sr| Box::new(Ducker::new(sr)),
590 );
591
592 self.register_factory_with_keywords(
593 "sample_player",
594 "Sample Player",
595 "Oscillators",
596 "Mono sample playback with V/Oct pitch and looping",
597 &["sample", "player", "playback", "sampler", "wav", "loop"],
598 &[],
599 |sr| Box::new(SamplePlayer::empty(sr)),
602 );
603
604 self.register_factory_with_keywords(
605 "mid_side_encode",
606 "Mid/Side Encode",
607 "Utilities",
608 "Encode left/right stereo to mid/side",
609 &["mid", "side", "ms", "stereo", "encode", "matrix"],
610 &[],
611 |_| Box::new(MidSideEncode::new()),
612 );
613
614 self.register_factory_with_keywords(
615 "mid_side_decode",
616 "Mid/Side Decode",
617 "Utilities",
618 "Decode mid/side to left/right with width control",
619 &["mid", "side", "ms", "stereo", "decode", "width"],
620 &[],
621 |_| Box::new(MidSideDecode::new()),
622 );
623
624 self.register_factory_with_keywords(
625 "bitcrusher",
626 "Bitcrusher",
627 "Effects",
628 "Lo-fi bit depth and sample rate reduction",
629 &["bitcrusher", "lofi", "distortion", "digital", "retro"],
630 &[],
631 |_| Box::new(Bitcrusher::new()),
632 );
633
634 self.register_factory_with_keywords(
636 "tremolo",
637 "Tremolo",
638 "Effects",
639 "Amplitude modulation effect with rate and depth control",
640 &["tremolo", "amplitude", "modulation", "wobble", "lfo"],
641 &[],
642 |sr| Box::new(Tremolo::new(sr)),
643 );
644
645 self.register_factory_with_keywords(
646 "vibrato",
647 "Vibrato",
648 "Effects",
649 "Pitch modulation effect using modulated delay",
650 &["vibrato", "pitch", "modulation", "wobble", "lfo"],
651 &[],
652 |sr| Box::new(Vibrato::new(sr)),
653 );
654
655 self.register_factory_with_keywords(
656 "distortion",
657 "Distortion",
658 "Effects",
659 "Waveshaping distortion with multiple modes",
660 &["distortion", "overdrive", "fuzz", "saturation", "clip"],
661 &[],
662 |sr| Box::new(Distortion::new(sr)),
663 );
664
665 self.register_factory_with_keywords(
667 "supersaw",
668 "Supersaw",
669 "Oscillators",
670 "JP-8000 style 7-voice detuned supersaw oscillator",
671 &["supersaw", "trance", "unison", "detune", "thick"],
672 &[],
673 |sr| Box::new(Supersaw::new(sr)),
674 );
675
676 self.register_factory_with_keywords(
677 "karplus_strong",
678 "Karplus-Strong",
679 "Oscillators",
680 "Physical modeling plucked string synthesis",
681 &["karplus", "string", "pluck", "physical", "modeling"],
682 &[],
683 |sr| Box::new(KarplusStrong::new(sr)),
684 );
685
686 self.register_factory_with_keywords(
688 "scale_quantizer",
689 "Scale Quantizer",
690 "Utilities",
691 "Quantize CV to musical scale notes",
692 &["quantizer", "scale", "music", "notes", "pitch"],
693 &[],
694 |sr| Box::new(ScaleQuantizer::new(sr)),
695 );
696
697 self.register_factory_with_keywords(
698 "euclidean",
699 "Euclidean Rhythm",
700 "Sequencers",
701 "Euclidean rhythm generator for evenly distributed pulses",
702 &["euclidean", "rhythm", "pattern", "trigger", "clock"],
703 &[],
704 |sr| Box::new(Euclidean::new(sr)),
705 );
706
707 self.register_factory_with_keywords(
708 "attenuverter",
709 "Attenuverter",
710 "Utilities",
711 "Attenuate, invert, and offset signals",
712 &["attenuator", "invert", "scale", "offset", "gain"],
713 &["essential"],
714 |_| Box::new(Attenuverter::new()),
715 );
716
717 self.register_factory_with_keywords(
718 "multiple",
719 "Multiple",
720 "Utilities",
721 "Signal splitter (1 input to 4 outputs)",
722 &["split", "copy", "mult", "buffer", "distribute"],
723 &["essential"],
724 |_| Box::new(Multiple::new()),
725 );
726
727 self.register_factory_with_keywords(
728 "crossfader",
729 "Crossfader/Panner",
730 "Utilities",
731 "Crossfade between inputs or pan stereo",
732 &["crossfade", "pan", "stereo", "balance", "mix"],
733 &[],
734 |_| Box::new(Crossfader::new()),
735 );
736
737 self.register_factory_with_keywords(
738 "precision_adder",
739 "Precision Adder",
740 "Utilities",
741 "High-precision CV adder for V/Oct signals",
742 &["add", "sum", "transpose", "octave", "voct", "pitch"],
743 &[],
744 |_| Box::new(PrecisionAdder::new()),
745 );
746
747 self.register_factory_with_keywords(
748 "vc_switch",
749 "VC Switch",
750 "Utilities",
751 "Voltage-controlled signal router",
752 &["switch", "router", "selector", "mux", "demux"],
753 &[],
754 |_| Box::new(VcSwitch::new()),
755 );
756
757 self.register_factory_with_keywords(
758 "min",
759 "Min",
760 "Utilities",
761 "Output minimum of two signals",
762 &["minimum", "compare", "math", "lowest"],
763 &[],
764 |_| Box::new(Min::new()),
765 );
766
767 self.register_factory_with_keywords(
768 "max",
769 "Max",
770 "Utilities",
771 "Output maximum of two signals",
772 &["maximum", "compare", "math", "highest"],
773 &[],
774 |_| Box::new(Max::new()),
775 );
776
777 self.register_factory_with_keywords(
778 "sample_and_hold",
779 "Sample & Hold",
780 "Utilities",
781 "Sample input value on trigger",
782 &["sample", "hold", "trigger", "freeze", "snapshot"],
783 &[],
784 |_| Box::new(SampleAndHold::new()),
785 );
786
787 self.register_factory_with_keywords(
788 "slew_limiter",
789 "Slew Limiter",
790 "Utilities",
791 "Limits rate of change (portamento/glide)",
792 &["slew", "portamento", "glide", "lag", "smooth"],
793 &[],
794 |sr| Box::new(SlewLimiter::new(sr)),
795 );
796
797 self.register_factory_with_keywords(
798 "quantizer",
799 "Quantizer",
800 "Utilities",
801 "Quantize V/Oct to musical scales",
802 &["quantize", "scale", "pitch", "chromatic", "note", "tune"],
803 &[],
804 |_| Box::new(Quantizer::new(Scale::Chromatic)),
805 );
806
807 self.register_factory_with_keywords(
811 "noise",
812 "Noise",
813 "Sources",
814 "White and pink noise generator",
815 &["noise", "white", "pink", "random", "hiss"],
816 &["essential"],
817 |_| Box::new(NoiseGenerator::new()),
818 );
819
820 self.register_factory_with_keywords(
824 "step_sequencer",
825 "Step Sequencer",
826 "Sequencing",
827 "8-step CV/gate sequencer",
828 &["sequencer", "step", "pattern", "melody", "cv", "gate"],
829 &["essential"],
830 |_| Box::new(StepSequencer::new()),
831 );
832
833 self.register_factory_with_keywords(
834 "clock",
835 "Clock",
836 "Sequencing",
837 "Master clock with tempo control",
838 &["clock", "tempo", "bpm", "trigger", "pulse", "sync"],
839 &["essential"],
840 |sr| Box::new(Clock::new(sr)),
841 );
842
843 self.register_factory_with_keywords(
847 "stereo_output",
848 "Stereo Output",
849 "I/O",
850 "Final stereo audio output",
851 &["output", "stereo", "main", "master", "speaker", "audio"],
852 &["essential"],
853 |_| Box::new(StereoOutput::new()),
854 );
855
856 self.register_factory_with_keywords(
860 "saturator",
861 "Saturator",
862 "Effects",
863 "Soft saturation / overdrive",
864 &[
865 "saturation",
866 "overdrive",
867 "distortion",
868 "warm",
869 "tube",
870 "tape",
871 ],
872 &["analog"],
873 |_| Box::new(Saturator::default()),
874 );
875
876 self.register_factory_with_keywords(
877 "wavefolder",
878 "Wavefolder",
879 "Effects",
880 "Wavefolder for complex harmonics",
881 &["wavefolder", "fold", "harmonics", "timbre", "west coast"],
882 &[],
883 |_| Box::new(Wavefolder::default()),
884 );
885
886 self.register_factory_with_keywords(
887 "ring_mod",
888 "Ring Modulator",
889 "Effects",
890 "Multiplies two signals for metallic/bell sounds",
891 &["ring", "modulator", "multiply", "bell", "metallic", "am"],
892 &[],
893 |_| Box::new(RingModulator::new()),
894 );
895
896 self.register_factory_with_keywords(
897 "rectifier",
898 "Rectifier",
899 "Effects",
900 "Full-wave and half-wave rectification",
901 &["rectify", "absolute", "waveshape", "fold"],
902 &[],
903 |_| Box::new(Rectifier::new()),
904 );
905
906 self.register_factory_with_keywords(
910 "logic_and",
911 "Logic AND",
912 "Logic",
913 "Output high when both inputs are high",
914 &["and", "gate", "boolean", "logic", "digital"],
915 &[],
916 |_| Box::new(LogicAnd::new()),
917 );
918
919 self.register_factory_with_keywords(
920 "logic_or",
921 "Logic OR",
922 "Logic",
923 "Output high when either input is high",
924 &["or", "gate", "boolean", "logic", "digital"],
925 &[],
926 |_| Box::new(LogicOr::new()),
927 );
928
929 self.register_factory_with_keywords(
930 "logic_xor",
931 "Logic XOR",
932 "Logic",
933 "Output high when exactly one input is high",
934 &["xor", "exclusive", "gate", "boolean", "logic", "digital"],
935 &[],
936 |_| Box::new(LogicXor::new()),
937 );
938
939 self.register_factory_with_keywords(
940 "logic_not",
941 "Logic NOT",
942 "Logic",
943 "Invert gate signal",
944 &["not", "invert", "gate", "boolean", "logic", "digital"],
945 &[],
946 |_| Box::new(LogicNot::new()),
947 );
948
949 self.register_factory_with_keywords(
950 "comparator",
951 "Comparator",
952 "Logic",
953 "Compare two CVs, output gates for greater/less/equal",
954 &["compare", "greater", "less", "equal", "threshold", "cv"],
955 &[],
956 |_| Box::new(Comparator::new()),
957 );
958
959 self.register_factory_with_keywords(
963 "bernoulli_gate",
964 "Bernoulli Gate",
965 "Random",
966 "Probabilistic trigger router",
967 &[
968 "random",
969 "probability",
970 "chance",
971 "coin",
972 "trigger",
973 "router",
974 ],
975 &[],
976 |_| Box::new(BernoulliGate::new()),
977 );
978
979 self.register_factory_with_keywords(
983 "crosstalk",
984 "Crosstalk",
985 "Analog Modeling",
986 "Channel crosstalk simulation",
987 &["crosstalk", "bleed", "stereo", "channel", "analog"],
988 &["analog"],
989 |sr| Box::new(Crosstalk::new(sr)),
990 );
991
992 self.register_factory_with_keywords(
993 "ground_loop",
994 "Ground Loop",
995 "Analog Modeling",
996 "Ground loop hum simulation (50/60 Hz)",
997 &["ground", "hum", "buzz", "50hz", "60hz", "mains", "analog"],
998 &["analog"],
999 |sr| Box::new(GroundLoop::new(sr)),
1000 );
1001
1002 self.register_factory_with_keywords(
1008 "wavetable",
1009 "Wavetable",
1010 "Oscillators",
1011 "Wavetable oscillator with 8 tables and morphing",
1012 &["wavetable", "oscillator", "morph", "digital", "synthesis"],
1013 &[],
1014 |sr| Box::new(Wavetable::new(sr)),
1015 );
1016
1017 self.register_factory_with_keywords(
1018 "formant_osc",
1019 "Formant Oscillator",
1020 "Oscillators",
1021 "Formant oscillator for vocal synthesis (a/e/i/o/u)",
1022 &["formant", "vocal", "vowel", "voice", "speech", "oscillator"],
1023 &[],
1024 |sr| Box::new(FormantOsc::new(sr)),
1025 );
1026
1027 self.register_factory_with_keywords(
1029 "reverb",
1030 "Reverb",
1031 "Effects",
1032 "Algorithmic reverb (Freeverb-style) with stereo output",
1033 &["reverb", "room", "hall", "space", "ambience", "freeverb"],
1034 &["essential"],
1035 |sr| Box::new(Reverb::new(sr)),
1036 );
1037
1038 self.register_factory_with_keywords(
1039 "parametric_eq",
1040 "Parametric EQ",
1041 "Effects",
1042 "3-band parametric equalizer (low shelf, mid peak, high shelf)",
1043 &["eq", "equalizer", "tone", "parametric", "shelf", "filter"],
1044 &[],
1045 |sr| Box::new(ParametricEq::new(sr)),
1046 );
1047
1048 self.register_factory_with_keywords(
1049 "vocoder",
1050 "Vocoder",
1051 "Effects",
1052 "16-band vocoder with carrier/modulator inputs",
1053 &["vocoder", "voice", "robot", "spectral", "filter", "bands"],
1054 &[],
1055 |sr| Box::new(Vocoder::new(sr)),
1056 );
1057
1058 self.register_factory_with_keywords(
1059 "pitch_shifter",
1060 "Pitch Shifter",
1061 "Effects",
1062 "Granular pitch shifter (±24 semitones)",
1063 &["pitch", "shift", "transpose", "semitone", "granular"],
1064 &[],
1065 |sr| Box::new(PitchShifter::new(sr)),
1066 );
1067
1068 self.register_factory_with_keywords(
1069 "granular",
1070 "Granular",
1071 "Effects",
1072 "Granular synthesis/processing with 16 concurrent grains",
1073 &[
1074 "granular", "grain", "texture", "freeze", "clouds", "ambient",
1075 ],
1076 &["advanced"],
1077 |sr| Box::new(Granular::new(sr)),
1078 );
1079
1080 self.register_factory_with_keywords(
1082 "chord_memory",
1083 "Chord Memory",
1084 "Utilities",
1085 "Generate chord voicings from root note (9 chord types)",
1086 &["chord", "harmony", "voicing", "major", "minor", "seventh"],
1087 &[],
1088 |_| Box::new(ChordMemory::new()),
1089 );
1090
1091 self.register_factory_with_keywords(
1092 "arpeggiator",
1093 "Arpeggiator",
1094 "Sequencers",
1095 "Pattern-based arpeggiator (up/down/up-down/random)",
1096 &[
1097 "arpeggiator",
1098 "arp",
1099 "pattern",
1100 "sequence",
1101 "melody",
1102 "clock",
1103 ],
1104 &[],
1105 |sr| Box::new(Arpeggiator::new(sr)),
1106 );
1107 }
1108
1109 pub fn register_factory<F>(
1111 &mut self,
1112 type_id: &str,
1113 name: &str,
1114 category: &str,
1115 description: &str,
1116 factory: F,
1117 ) where
1118 F: Fn(f64) -> Box<dyn GraphModule> + Send + Sync + 'static,
1119 {
1120 self.register_factory_with_keywords(
1121 type_id,
1122 name,
1123 category,
1124 description,
1125 &[],
1126 &[],
1127 factory,
1128 );
1129 }
1130
1131 #[allow(clippy::too_many_arguments)]
1133 pub fn register_factory_with_keywords<F>(
1134 &mut self,
1135 type_id: &str,
1136 name: &str,
1137 category: &str,
1138 description: &str,
1139 keywords: &[&str],
1140 tags: &[&str],
1141 factory: F,
1142 ) where
1143 F: Fn(f64) -> Box<dyn GraphModule> + Send + Sync + 'static,
1144 {
1145 let temp_instance = factory(44100.0);
1147 let port_spec = temp_instance.port_spec().clone();
1148
1149 self.factories
1150 .insert(type_id.to_string(), Box::new(factory));
1151
1152 self.metadata.insert(
1153 type_id.to_string(),
1154 ModuleMetadata {
1155 type_id: type_id.to_string(),
1156 name: name.to_string(),
1157 category: category.to_string(),
1158 description: description.to_string(),
1159 port_spec,
1160 keywords: keywords.iter().map(|s| s.to_string()).collect(),
1161 tags: tags.iter().map(|s| s.to_string()).collect(),
1162 },
1163 );
1164 }
1165
1166 pub fn instantiate(&self, type_id: &str, sample_rate: f64) -> Option<Box<dyn GraphModule>> {
1168 self.factories.get(type_id).map(|f| f(sample_rate))
1169 }
1170
1171 pub fn list_modules(&self) -> impl Iterator<Item = &ModuleMetadata> {
1173 self.metadata.values()
1174 }
1175
1176 pub fn get_metadata(&self, type_id: &str) -> Option<&ModuleMetadata> {
1178 self.metadata.get(type_id)
1179 }
1180
1181 pub fn list_by_category<'a>(
1183 &'a self,
1184 category: &'a str,
1185 ) -> impl Iterator<Item = &'a ModuleMetadata> {
1186 self.metadata
1187 .values()
1188 .filter(move |m| m.category == category)
1189 }
1190
1191 pub fn categories(&self) -> Vec<String> {
1193 let mut cats: Vec<_> = self.metadata.values().map(|m| m.category.clone()).collect();
1194 cats.sort();
1195 cats.dedup();
1196 cats
1197 }
1198
1199 pub fn catalog(&self) -> CatalogResponse {
1205 let mut modules: Vec<ModuleCatalogEntry> = self
1206 .metadata
1207 .values()
1208 .map(ModuleCatalogEntry::from_metadata)
1209 .collect();
1210
1211 modules.sort_by(|a, b| (&a.category, &a.name).cmp(&(&b.category, &b.name)));
1213
1214 CatalogResponse {
1215 modules,
1216 categories: self.categories(),
1217 }
1218 }
1219
1220 pub fn search(&self, query: &str) -> Vec<ModuleCatalogEntry> {
1224 let query_lower = query.to_lowercase();
1225
1226 let mut results: Vec<(ModuleCatalogEntry, u8)> = self
1227 .metadata
1228 .values()
1229 .filter_map(|m| {
1230 let mut score: u8 = 0;
1232
1233 if m.type_id.to_lowercase() == query_lower {
1235 score += 100;
1236 }
1237 else if m.name.to_lowercase() == query_lower {
1239 score += 90;
1240 }
1241 else if m.type_id.to_lowercase().contains(&query_lower) {
1243 score += 70;
1244 }
1245 else if m.name.to_lowercase().contains(&query_lower) {
1247 score += 60;
1248 }
1249 else if m.keywords.iter().any(|k| k.to_lowercase() == query_lower) {
1251 score += 50;
1252 }
1253 else if m
1255 .keywords
1256 .iter()
1257 .any(|k| k.to_lowercase().contains(&query_lower))
1258 {
1259 score += 40;
1260 }
1261 else if m.description.to_lowercase().contains(&query_lower) {
1263 score += 20;
1264 }
1265 else if m.category.to_lowercase().contains(&query_lower) {
1267 score += 10;
1268 }
1269
1270 if score > 0 {
1271 Some((ModuleCatalogEntry::from_metadata(m), score))
1272 } else {
1273 None
1274 }
1275 })
1276 .collect();
1277
1278 results.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.name.cmp(&b.0.name)));
1280
1281 results.into_iter().map(|(entry, _)| entry).collect()
1282 }
1283
1284 pub fn by_category(&self, category: &str) -> Vec<ModuleCatalogEntry> {
1286 let mut modules: Vec<ModuleCatalogEntry> = self
1287 .metadata
1288 .values()
1289 .filter(|m| m.category.eq_ignore_ascii_case(category))
1290 .map(ModuleCatalogEntry::from_metadata)
1291 .collect();
1292
1293 modules.sort_by(|a, b| a.name.cmp(&b.name));
1295 modules
1296 }
1297}
1298
1299impl Default for ModuleRegistry {
1300 fn default() -> Self {
1301 Self::new()
1302 }
1303}
1304
1305impl Patch {
1307 pub fn to_def(&self, name: &str) -> PatchDef {
1309 let modules: Vec<ModuleDef> = self
1310 .nodes()
1311 .map(|(node_id, node_name, module)| ModuleDef {
1312 name: node_name.to_string(),
1313 module_type: module.type_id().to_string(),
1314 position: self.get_position(node_id),
1315 state: module.serialize_state(),
1316 })
1317 .collect();
1318
1319 let cables: Vec<CableDef> = self
1320 .cables()
1321 .iter()
1322 .filter_map(|cable| {
1323 let from_name = self.get_name(cable.from.node)?;
1325 let to_name = self.get_name(cable.to.node)?;
1326
1327 let (_, _, from_module) = self.nodes().find(|(id, _, _)| *id == cable.from.node)?;
1329 let (_, _, to_module) = self.nodes().find(|(id, _, _)| *id == cable.to.node)?;
1330
1331 let from_port = from_module
1332 .port_spec()
1333 .outputs
1334 .iter()
1335 .find(|p| p.id == cable.from.port)
1336 .map(|p| p.name.as_str())?;
1337
1338 let to_port = to_module
1339 .port_spec()
1340 .inputs
1341 .iter()
1342 .find(|p| p.id == cable.to.port)
1343 .map(|p| p.name.as_str())?;
1344
1345 Some(CableDef {
1346 from: format!("{}.{}", from_name, from_port),
1347 to: format!("{}.{}", to_name, to_port),
1348 attenuation: cable.attenuation,
1349 offset: cable.offset,
1350 })
1351 })
1352 .collect();
1353
1354 let mut parameters: StdMap<String, f64> = StdMap::new();
1358 for (node_id, node_name, _) in self.nodes() {
1359 for p in self.param_infos(node_id) {
1360 if (p.value - p.default).abs() > f64::EPSILON {
1361 parameters.insert(format!("{}.{}", node_name, p.id), p.value);
1362 }
1363 }
1364 }
1365
1366 let output = self
1369 .output_node()
1370 .and_then(|id| self.get_name(id))
1371 .map(|n| n.to_string());
1372
1373 let meta = self.meta();
1374 PatchDef {
1375 version: CURRENT_PATCH_VERSION,
1376 name: name.to_string(),
1377 author: meta.author.clone(),
1378 description: meta.description.clone(),
1379 tags: meta.tags.clone(),
1380 output,
1381 modules,
1382 cables,
1383 parameters,
1384 }
1385 }
1386
1387 pub fn from_def(
1389 def: &PatchDef,
1390 registry: &ModuleRegistry,
1391 sample_rate: f64,
1392 ) -> Result<Self, PatchError> {
1393 if def.version > CURRENT_PATCH_VERSION {
1396 return Err(PatchError::CompilationFailed(format!(
1397 "Unsupported patch version {} (this build supports up to {})",
1398 def.version, CURRENT_PATCH_VERSION
1399 )));
1400 }
1401
1402 let mut patch = Patch::new(sample_rate);
1403 let mut name_to_handle: StdMap<String, NodeHandle> = StdMap::new();
1404
1405 for module_def in &def.modules {
1407 let module = registry
1408 .instantiate(&module_def.module_type, sample_rate)
1409 .ok_or_else(|| {
1410 PatchError::CompilationFailed(format!(
1411 "Unknown module type: {}",
1412 module_def.module_type
1413 ))
1414 })?;
1415
1416 let handle = patch.add_boxed(&module_def.name, module);
1417
1418 if let Some((x, y)) = module_def.position {
1420 patch.set_position(handle.id(), (x, y));
1421 }
1422
1423 if let Some(state) = &module_def.state {
1427 patch
1428 .deserialize_module_state(handle.id(), state)
1429 .map_err(PatchError::CompilationFailed)?;
1430 }
1431
1432 name_to_handle.insert(module_def.name.clone(), handle);
1433 }
1434
1435 for cable_def in &def.cables {
1437 let (from_module, from_port) = parse_port_ref(&cable_def.from)?;
1438 let (to_module, to_port) = parse_port_ref(&cable_def.to)?;
1439
1440 let from_handle = name_to_handle.get(from_module).ok_or_else(|| {
1441 PatchError::CompilationFailed(format!("Unknown module: {}", from_module))
1442 })?;
1443
1444 let to_handle = name_to_handle.get(to_module).ok_or_else(|| {
1445 PatchError::CompilationFailed(format!("Unknown module: {}", to_module))
1446 })?;
1447
1448 let from_ref = from_handle.output(from_port).map_err(|_| {
1453 PatchError::CompilationFailed(format!(
1454 "Unknown output port '{}' on module '{}' (available: {})",
1455 from_port,
1456 from_module,
1457 from_handle.output_names().join(", ")
1458 ))
1459 })?;
1460 let to_ref = to_handle.input(to_port).map_err(|_| {
1461 PatchError::CompilationFailed(format!(
1462 "Unknown input port '{}' on module '{}' (available: {})",
1463 to_port,
1464 to_module,
1465 to_handle.input_names().join(", ")
1466 ))
1467 })?;
1468
1469 match (cable_def.attenuation, cable_def.offset) {
1470 (Some(attenuation), Some(offset)) => {
1471 patch.connect_modulated(from_ref, to_ref, attenuation, offset)?;
1472 }
1473 (Some(attenuation), None) => {
1474 patch.connect_attenuated(from_ref, to_ref, attenuation)?;
1475 }
1476 (None, Some(offset)) => {
1477 patch.connect_modulated(
1478 from_ref, to_ref, 1.0, offset,
1480 )?;
1481 }
1482 (None, None) => {
1483 patch.connect(from_ref, to_ref)?;
1484 }
1485 }
1486 }
1487
1488 for (key, &value) in &def.parameters {
1493 if let Some((module_name, param_id)) = key.split_once('.') {
1494 if let Some(handle) = name_to_handle.get(module_name) {
1495 patch.set_param_by_id(handle.id(), param_id, value);
1496 }
1497 }
1498 }
1499
1500 let output_set = def
1503 .output
1504 .as_ref()
1505 .and_then(|name| name_to_handle.get(name))
1506 .map(|handle| patch.set_output(handle.id()))
1507 .is_some();
1508 if !output_set {
1509 if let Some(handle) = name_to_handle.get("output") {
1510 patch.set_output(handle.id());
1511 } else if let Some(handle) = name_to_handle.values().find(|h| {
1512 h.spec()
1513 .outputs
1514 .iter()
1515 .any(|p| p.name == "left" || p.name == "right")
1516 }) {
1517 patch.set_output(handle.id());
1518 }
1519 }
1520
1521 patch.set_meta(crate::graph::PatchMeta {
1523 name: Some(def.name.clone()),
1524 author: def.author.clone(),
1525 description: def.description.clone(),
1526 tags: def.tags.clone(),
1527 });
1528
1529 patch.compile()?;
1530 Ok(patch)
1531 }
1532}
1533
1534fn parse_port_ref(s: &str) -> Result<(&str, &str), PatchError> {
1535 let parts: Vec<&str> = s.splitn(2, '.').collect();
1536 if parts.len() != 2 {
1537 return Err(PatchError::CompilationFailed(format!(
1538 "Invalid port reference: {}",
1539 s
1540 )));
1541 }
1542 Ok((parts[0], parts[1]))
1543}
1544
1545#[derive(Debug, Clone, Serialize, Deserialize)]
1551#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
1552pub struct ValidationError {
1553 pub path: String,
1555 pub message: String,
1557}
1558
1559impl ValidationError {
1560 pub fn new(path: impl Into<String>, message: impl Into<String>) -> Self {
1561 Self {
1562 path: path.into(),
1563 message: message.into(),
1564 }
1565 }
1566}
1567
1568impl core::fmt::Display for ValidationError {
1569 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1570 write!(f, "{}: {}", self.path, self.message)
1571 }
1572}
1573
1574#[derive(Debug, Clone, Serialize, Deserialize)]
1576#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
1577#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1578pub struct ValidationResult {
1579 pub valid: bool,
1581 pub errors: Vec<ValidationError>,
1583}
1584
1585impl ValidationResult {
1586 pub fn ok() -> Self {
1587 Self {
1588 valid: true,
1589 errors: Vec::new(),
1590 }
1591 }
1592
1593 pub fn with_errors(errors: Vec<ValidationError>) -> Self {
1594 Self {
1595 valid: errors.is_empty(),
1596 errors,
1597 }
1598 }
1599}
1600
1601impl PatchDef {
1602 pub fn validate(&self) -> ValidationResult {
1608 let mut errors = Vec::new();
1609
1610 if self.version < 1 {
1612 errors.push(ValidationError::new(
1613 "version",
1614 "Version must be a positive integer",
1615 ));
1616 }
1617
1618 if self.name.is_empty() {
1620 errors.push(ValidationError::new(
1621 "name",
1622 "Name must be a non-empty string",
1623 ));
1624 }
1625
1626 let mut module_names = alloc::collections::BTreeSet::new();
1628
1629 for (i, module) in self.modules.iter().enumerate() {
1631 let path = format!("modules[{}]", i);
1632
1633 if module.name.is_empty() {
1634 errors.push(ValidationError::new(
1635 format!("{}.name", path),
1636 "Module name must be a non-empty string",
1637 ));
1638 } else if !module_names.insert(&module.name) {
1639 errors.push(ValidationError::new(
1640 format!("{}.name", path),
1641 format!("Duplicate module name: {}", module.name),
1642 ));
1643 }
1644
1645 if module.module_type.is_empty() {
1646 errors.push(ValidationError::new(
1647 format!("{}.module_type", path),
1648 "Module type must be a non-empty string",
1649 ));
1650 }
1651 }
1652
1653 for (i, cable) in self.cables.iter().enumerate() {
1655 let path = format!("cables[{}]", i);
1656
1657 if !is_valid_port_ref(&cable.from) {
1659 errors.push(ValidationError::new(
1660 format!("{}.from", path),
1661 "From must be a port reference in format 'module_name.port_name'",
1662 ));
1663 }
1664
1665 if !is_valid_port_ref(&cable.to) {
1666 errors.push(ValidationError::new(
1667 format!("{}.to", path),
1668 "To must be a port reference in format 'module_name.port_name'",
1669 ));
1670 }
1671
1672 if let Some(attenuation) = cable.attenuation {
1674 if !(-2.0..=2.0).contains(&attenuation) {
1675 errors.push(ValidationError::new(
1676 format!("{}.attenuation", path),
1677 "Attenuation must be between -2.0 and 2.0",
1678 ));
1679 }
1680 }
1681
1682 if let Some(offset) = cable.offset {
1684 if !(-10.0..=10.0).contains(&offset) {
1685 errors.push(ValidationError::new(
1686 format!("{}.offset", path),
1687 "Offset must be between -10.0 and 10.0",
1688 ));
1689 }
1690 }
1691 }
1692
1693 ValidationResult::with_errors(errors)
1694 }
1695
1696 pub fn validate_with_registry(&self, registry: &ModuleRegistry) -> ValidationResult {
1703 let mut result = self.validate();
1705 if !result.valid {
1706 return result;
1707 }
1708
1709 let mut errors = Vec::new();
1710
1711 let module_names: alloc::collections::BTreeSet<_> =
1713 self.modules.iter().map(|m| m.name.as_str()).collect();
1714
1715 for (i, module) in self.modules.iter().enumerate() {
1717 if registry.get_metadata(&module.module_type).is_none() {
1718 errors.push(ValidationError::new(
1719 format!("modules[{}].module_type", i),
1720 format!("Unknown module type: {}", module.module_type),
1721 ));
1722 }
1723 }
1724
1725 for (i, cable) in self.cables.iter().enumerate() {
1727 let path = format!("cables[{}]", i);
1728
1729 if let Ok((from_module, from_port)) = parse_port_ref(&cable.from) {
1731 if !module_names.contains(from_module) {
1732 errors.push(ValidationError::new(
1733 format!("{}.from", path),
1734 format!("Unknown module: {}", from_module),
1735 ));
1736 } else {
1737 if let Some(module_def) = self.modules.iter().find(|m| m.name == from_module) {
1739 if let Some(metadata) = registry.get_metadata(&module_def.module_type) {
1740 if metadata.port_spec.output_by_name(from_port).is_none() {
1741 errors.push(ValidationError::new(
1742 format!("{}.from", path),
1743 format!(
1744 "Unknown output port '{}' on module '{}'",
1745 from_port, from_module
1746 ),
1747 ));
1748 }
1749 }
1750 }
1751 }
1752 }
1753
1754 if let Ok((to_module, to_port)) = parse_port_ref(&cable.to) {
1756 if !module_names.contains(to_module) {
1757 errors.push(ValidationError::new(
1758 format!("{}.to", path),
1759 format!("Unknown module: {}", to_module),
1760 ));
1761 } else {
1762 if let Some(module_def) = self.modules.iter().find(|m| m.name == to_module) {
1764 if let Some(metadata) = registry.get_metadata(&module_def.module_type) {
1765 if metadata.port_spec.input_by_name(to_port).is_none() {
1766 errors.push(ValidationError::new(
1767 format!("{}.to", path),
1768 format!(
1769 "Unknown input port '{}' on module '{}'",
1770 to_port, to_module
1771 ),
1772 ));
1773 }
1774 }
1775 }
1776 }
1777 }
1778 }
1779
1780 if errors.is_empty() {
1781 result
1782 } else {
1783 result.valid = false;
1784 result.errors.extend(errors);
1785 result
1786 }
1787 }
1788}
1789
1790fn is_valid_port_ref(s: &str) -> bool {
1792 let parts: Vec<&str> = s.splitn(2, '.').collect();
1793 if parts.len() != 2 {
1794 return false;
1795 }
1796
1797 let valid_chars = |s: &str| {
1799 !s.is_empty()
1800 && s.chars()
1801 .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
1802 };
1803
1804 valid_chars(parts[0]) && valid_chars(parts[1])
1805}
1806
1807#[cfg(test)]
1808mod tests {
1809 use super::*;
1810
1811 #[test]
1812 fn test_patch_def_serialization() {
1813 let def = PatchDef::new("Test Patch")
1814 .with_author("Test Author")
1815 .with_description("A test patch")
1816 .with_tag("test");
1817
1818 let json = def.to_json().unwrap();
1819 let loaded = PatchDef::from_json(&json).unwrap();
1820
1821 assert_eq!(loaded.name, "Test Patch");
1822 assert_eq!(loaded.author, Some("Test Author".to_string()));
1823 }
1824
1825 #[test]
1826 fn test_cable_def() {
1827 let cable = CableDef::new("vco.saw", "vcf.in").with_attenuation(0.5);
1828 assert_eq!(cable.from, "vco.saw");
1829 assert_eq!(cable.to, "vcf.in");
1830 assert_eq!(cable.attenuation, Some(0.5));
1831 }
1832
1833 #[test]
1834 fn test_patch_def_default() {
1835 let def = PatchDef::default();
1836 assert_eq!(def.name, "Untitled");
1837 }
1838
1839 #[test]
1840 fn test_module_def_with_position() {
1841 let def = ModuleDef::new("vco1", "vco").with_position(100.0, 200.0);
1842 assert_eq!(def.position, Some((100.0, 200.0)));
1843 }
1844
1845 #[test]
1846 fn test_cable_def_with_offset() {
1847 let cable = CableDef::new("a.out", "b.in").with_offset(2.5);
1848 assert_eq!(cable.offset, Some(2.5));
1849 }
1850
1851 #[test]
1852 fn test_cable_def_with_modulation() {
1853 let cable = CableDef::new("a.out", "b.in").with_modulation(0.5, 1.0);
1854 assert_eq!(cable.attenuation, Some(0.5));
1855 assert_eq!(cable.offset, Some(1.0));
1856 }
1857
1858 #[test]
1863 fn test_valid_patch_validation() {
1864 let mut def = PatchDef::new("Test Patch");
1865 def.modules.push(ModuleDef::new("vco1", "vco"));
1866 def.modules.push(ModuleDef::new("output", "stereo_output"));
1867 def.cables.push(CableDef::new("vco1.saw", "output.left"));
1868
1869 let result = def.validate();
1870 assert!(
1871 result.valid,
1872 "Expected valid patch, got errors: {:?}",
1873 result.errors
1874 );
1875 assert!(result.errors.is_empty());
1876 }
1877
1878 #[test]
1879 fn test_empty_name_validation() {
1880 let mut def = PatchDef::new("");
1881 def.modules.push(ModuleDef::new("vco1", "vco"));
1882
1883 let result = def.validate();
1884 assert!(!result.valid);
1885 assert!(result.errors.iter().any(|e| e.path == "name"));
1886 }
1887
1888 #[test]
1889 fn test_duplicate_module_name_validation() {
1890 let mut def = PatchDef::new("Test");
1891 def.modules.push(ModuleDef::new("vco1", "vco"));
1892 def.modules.push(ModuleDef::new("vco1", "vco")); let result = def.validate();
1895 assert!(!result.valid);
1896 assert!(result
1897 .errors
1898 .iter()
1899 .any(|e| e.message.contains("Duplicate")));
1900 }
1901
1902 #[test]
1903 fn test_invalid_port_reference_validation() {
1904 let mut def = PatchDef::new("Test");
1905 def.modules.push(ModuleDef::new("vco1", "vco"));
1906 def.cables.push(CableDef::new("invalid", "also_invalid")); let result = def.validate();
1909 assert!(!result.valid);
1910 assert!(result.errors.len() >= 2);
1911 }
1912
1913 #[test]
1914 fn test_attenuation_range_validation() {
1915 let mut def = PatchDef::new("Test");
1916 def.modules.push(ModuleDef::new("vco1", "vco"));
1917 def.cables
1918 .push(CableDef::new("a.out", "b.in").with_attenuation(5.0)); let result = def.validate();
1921 assert!(!result.valid);
1922 assert!(result.errors.iter().any(|e| e.path.contains("attenuation")));
1923 }
1924
1925 #[test]
1926 fn test_offset_range_validation() {
1927 let mut def = PatchDef::new("Test");
1928 def.modules.push(ModuleDef::new("vco1", "vco"));
1929 def.cables
1930 .push(CableDef::new("a.out", "b.in").with_offset(15.0)); let result = def.validate();
1933 assert!(!result.valid);
1934 assert!(result.errors.iter().any(|e| e.path.contains("offset")));
1935 }
1936
1937 #[test]
1938 fn test_validate_with_registry_unknown_module_type() {
1939 let registry = ModuleRegistry::new();
1940
1941 let mut def = PatchDef::new("Test");
1942 def.modules.push(ModuleDef::new("foo", "nonexistent_type"));
1943
1944 let result = def.validate_with_registry(®istry);
1945 assert!(!result.valid);
1946 assert!(result
1947 .errors
1948 .iter()
1949 .any(|e| e.message.contains("Unknown module type")));
1950 }
1951
1952 #[test]
1953 fn test_validate_with_registry_unknown_module_reference() {
1954 let registry = ModuleRegistry::new();
1955
1956 let mut def = PatchDef::new("Test");
1957 def.modules.push(ModuleDef::new("vco1", "vco"));
1958 def.cables
1959 .push(CableDef::new("nonexistent.out", "vco1.voct"));
1960
1961 let result = def.validate_with_registry(®istry);
1962 assert!(!result.valid);
1963 assert!(result
1964 .errors
1965 .iter()
1966 .any(|e| e.message.contains("Unknown module")));
1967 }
1968
1969 #[test]
1970 fn test_validate_with_registry_unknown_port() {
1971 let registry = ModuleRegistry::new();
1972
1973 let mut def = PatchDef::new("Test");
1974 def.modules.push(ModuleDef::new("vco1", "vco"));
1975 def.modules.push(ModuleDef::new("output", "stereo_output"));
1976 def.cables
1977 .push(CableDef::new("vco1.nonexistent_port", "output.left"));
1978
1979 let result = def.validate_with_registry(®istry);
1980 assert!(!result.valid);
1981 assert!(result
1982 .errors
1983 .iter()
1984 .any(|e| e.message.contains("Unknown output port")));
1985 }
1986
1987 #[test]
1988 fn test_validate_with_registry_valid_patch() {
1989 let registry = ModuleRegistry::new();
1990
1991 let mut def = PatchDef::new("Valid Patch");
1992 def.modules.push(ModuleDef::new("vco1", "vco"));
1993 def.modules.push(ModuleDef::new("output", "stereo_output"));
1994 def.cables.push(CableDef::new("vco1.saw", "output.left"));
1995 def.cables.push(CableDef::new("vco1.sin", "output.right"));
1996
1997 let result = def.validate_with_registry(®istry);
1998 assert!(
1999 result.valid,
2000 "Expected valid patch, got errors: {:?}",
2001 result.errors
2002 );
2003 }
2004
2005 #[test]
2006 fn test_is_valid_port_ref() {
2007 assert!(is_valid_port_ref("vco1.out"));
2008 assert!(is_valid_port_ref("module_name.port_name"));
2009 assert!(is_valid_port_ref("a.b"));
2010 assert!(is_valid_port_ref("my-module.my-port"));
2011
2012 assert!(!is_valid_port_ref("nodot"));
2013 assert!(!is_valid_port_ref(".startswithdot"));
2014 assert!(!is_valid_port_ref("endswithdot."));
2015 assert!(!is_valid_port_ref(""));
2016 assert!(!is_valid_port_ref("has spaces.port"));
2017 }
2018
2019 #[test]
2020 fn test_validation_error_display() {
2021 let error = ValidationError::new("modules[0].name", "Name is empty");
2022 let display = format!("{}", error);
2023 assert_eq!(display, "modules[0].name: Name is empty");
2024 }
2025
2026 #[test]
2031 fn test_catalog_returns_all_modules() {
2032 let registry = ModuleRegistry::new();
2033 let catalog = registry.catalog();
2034
2035 assert!(catalog.modules.len() >= 36, "Expected at least 36 modules");
2037
2038 assert!(catalog.modules.iter().any(|m| m.type_id == "vco"));
2040 assert!(catalog.modules.iter().any(|m| m.type_id == "svf"));
2041 assert!(catalog.modules.iter().any(|m| m.type_id == "adsr"));
2042 }
2043
2044 #[test]
2045 fn test_remediation_modules_registered_and_instantiate() {
2046 let registry = ModuleRegistry::new();
2047 for id in [
2050 "sample_player",
2051 "ducker",
2052 "mid_side_encode",
2053 "mid_side_decode",
2054 ] {
2055 let module = registry
2056 .instantiate(id, 44100.0)
2057 .unwrap_or_else(|| panic!("module '{id}' not registered"));
2058 assert_eq!(module.type_id(), id, "type_id mismatch for '{id}'");
2059 }
2060 }
2061
2062 #[test]
2063 fn test_catalog_categories() {
2064 let registry = ModuleRegistry::new();
2065 let catalog = registry.catalog();
2066
2067 assert!(catalog.categories.contains(&"Oscillators".to_string()));
2069 assert!(catalog.categories.contains(&"Filters".to_string()));
2070 assert!(catalog.categories.contains(&"Utilities".to_string()));
2071 assert!(catalog.categories.contains(&"Effects".to_string()));
2072
2073 let mut sorted_cats = catalog.categories.clone();
2075 sorted_cats.sort();
2076 assert_eq!(catalog.categories, sorted_cats);
2077 }
2078
2079 #[test]
2080 fn test_catalog_entry_has_port_summary() {
2081 let registry = ModuleRegistry::new();
2082 let catalog = registry.catalog();
2083
2084 let vco = catalog.modules.iter().find(|m| m.type_id == "vco").unwrap();
2085 assert!(vco.ports.outputs > 0, "VCO should have outputs");
2086 assert!(vco.ports.has_audio_out, "VCO should have audio output");
2087
2088 let stereo_out = catalog
2089 .modules
2090 .iter()
2091 .find(|m| m.type_id == "stereo_output")
2092 .unwrap();
2093 assert!(
2094 stereo_out.ports.inputs > 0,
2095 "Stereo output should have inputs"
2096 );
2097 assert!(
2098 stereo_out.ports.has_audio_in,
2099 "Stereo output should have audio input"
2100 );
2101 }
2102
2103 #[test]
2104 fn test_search_exact_type_id_match() {
2105 let registry = ModuleRegistry::new();
2106 let results = registry.search("vco");
2107
2108 assert!(!results.is_empty());
2109 assert_eq!(results[0].type_id, "vco");
2111 }
2112
2113 #[test]
2114 fn test_search_by_keyword() {
2115 let registry = ModuleRegistry::new();
2116 let results = registry.search("oscillator");
2117
2118 assert!(results.len() >= 3);
2120 assert!(results.iter().any(|m| m.type_id == "vco"));
2121 assert!(results.iter().any(|m| m.type_id == "analog_vco"));
2122 assert!(results.iter().any(|m| m.type_id == "lfo"));
2123 }
2124
2125 #[test]
2126 fn test_search_case_insensitive() {
2127 let registry = ModuleRegistry::new();
2128 let results_lower = registry.search("filter");
2129 let results_upper = registry.search("FILTER");
2130 let results_mixed = registry.search("FiLtEr");
2131
2132 assert_eq!(results_lower.len(), results_upper.len());
2133 assert_eq!(results_lower.len(), results_mixed.len());
2134 }
2135
2136 #[test]
2137 fn test_search_by_description() {
2138 let registry = ModuleRegistry::new();
2139 let results = registry.search("saturation");
2140
2141 assert!(!results.is_empty());
2143 assert!(results.iter().any(|m| m.type_id == "saturator"));
2144 }
2145
2146 #[test]
2147 fn test_search_no_results() {
2148 let registry = ModuleRegistry::new();
2149 let results = registry.search("nonexistent_xyz_123");
2150
2151 assert!(results.is_empty());
2152 }
2153
2154 #[test]
2155 fn test_by_category() {
2156 let registry = ModuleRegistry::new();
2157 let oscillators = registry.by_category("Oscillators");
2158
2159 assert!(oscillators.len() >= 2);
2160 assert!(oscillators.iter().all(|m| m.category == "Oscillators"));
2161 assert!(oscillators.iter().any(|m| m.type_id == "vco"));
2162 assert!(oscillators.iter().any(|m| m.type_id == "analog_vco"));
2163 }
2164
2165 #[test]
2166 fn test_by_category_case_insensitive() {
2167 let registry = ModuleRegistry::new();
2168 let filters1 = registry.by_category("Filters");
2169 let filters2 = registry.by_category("filters");
2170 let filters3 = registry.by_category("FILTERS");
2171
2172 assert_eq!(filters1.len(), filters2.len());
2173 assert_eq!(filters1.len(), filters3.len());
2174 }
2175
2176 #[test]
2177 fn test_by_category_sorted_by_name() {
2178 let registry = ModuleRegistry::new();
2179 let utilities = registry.by_category("Utilities");
2180
2181 let names: Vec<_> = utilities.iter().map(|m| &m.name).collect();
2183 let mut sorted_names = names.clone();
2184 sorted_names.sort();
2185 assert_eq!(names, sorted_names);
2186 }
2187
2188 #[test]
2189 fn test_catalog_entry_serialization() {
2190 let registry = ModuleRegistry::new();
2191 let catalog = registry.catalog();
2192
2193 let json = serde_json::to_string(&catalog).unwrap();
2195 assert!(json.contains("\"type_id\""));
2196 assert!(json.contains("\"category\""));
2197 assert!(json.contains("\"keywords\""));
2198
2199 let deserialized: CatalogResponse = serde_json::from_str(&json).unwrap();
2201 assert_eq!(deserialized.modules.len(), catalog.modules.len());
2202 }
2203
2204 #[test]
2205 fn test_module_has_keywords_and_tags() {
2206 let registry = ModuleRegistry::new();
2207 let metadata = registry.get_metadata("vco").unwrap();
2208
2209 assert!(!metadata.keywords.is_empty());
2211 assert!(metadata.keywords.contains(&"oscillator".to_string()));
2212
2213 assert!(metadata.tags.contains(&"essential".to_string()));
2215 }
2216
2217 #[test]
2218 fn test_from_def_mistyped_cable_port_returns_err() {
2219 let registry = ModuleRegistry::new();
2222 let mut def = PatchDef::new("Bad Ports");
2223 def.modules.push(ModuleDef::new("vco", "vco"));
2224 def.modules.push(ModuleDef::new("output", "stereo_output"));
2225 def.cables
2227 .push(CableDef::new("vco.definitely_not_a_port", "output.left"));
2228
2229 let result = Patch::from_def(&def, ®istry, 44100.0);
2230 assert!(result.is_err(), "expected Err for mistyped cable port");
2231 match result {
2232 Err(PatchError::CompilationFailed(msg)) => {
2233 assert!(
2234 msg.contains("definitely_not_a_port") && msg.contains("vco"),
2235 "error should name the bad port and module: {}",
2236 msg
2237 );
2238 }
2239 other => panic!("expected CompilationFailed, got {:?}", other),
2240 }
2241 }
2242
2243 #[test]
2244 fn test_from_def_unknown_module_type_returns_err() {
2245 let registry = ModuleRegistry::new();
2247 let mut def = PatchDef::new("Bad Type");
2248 def.modules.push(ModuleDef::new("x", "not_a_real_module"));
2249 match Patch::from_def(&def, ®istry, 44100.0) {
2250 Err(PatchError::CompilationFailed(msg)) => {
2251 assert!(msg.contains("not_a_real_module"), "msg: {msg}");
2252 }
2253 other => panic!("expected CompilationFailed, got {other:?}"),
2254 }
2255 }
2256
2257 #[test]
2258 fn test_from_def_malformed_cable_ref_returns_err() {
2259 let registry = ModuleRegistry::new();
2261 let mut def = PatchDef::new("Malformed");
2262 def.modules.push(ModuleDef::new("vco", "vco"));
2263 def.modules.push(ModuleDef::new("output", "stereo_output"));
2264 def.cables.push(CableDef::new("no_dot_here", "output.left"));
2265 assert!(Patch::from_def(&def, ®istry, 44100.0).is_err());
2266 }
2267
2268 #[test]
2269 fn test_from_def_rejects_newer_version() {
2270 let registry = ModuleRegistry::new();
2272 let mut def = PatchDef::new("Future");
2273 def.version = CURRENT_PATCH_VERSION + 1;
2274 def.modules.push(ModuleDef::new("output", "stereo_output"));
2275 match Patch::from_def(&def, ®istry, 44100.0) {
2276 Err(PatchError::CompilationFailed(msg)) => {
2277 assert!(msg.contains("version"), "msg: {msg}");
2278 }
2279 other => panic!("expected version rejection, got {other:?}"),
2280 }
2281 def.version = CURRENT_PATCH_VERSION;
2283 assert!(Patch::from_def(&def, ®istry, 44100.0).is_ok());
2284 }
2285
2286 #[test]
2287 fn test_roundtrip_preserves_params_and_output() {
2288 use crate::modules::{Distortion, StereoOutput, Svf, Vco};
2291
2292 let build = || -> (Patch, crate::graph::NodeId, crate::graph::NodeId, crate::graph::NodeId) {
2293 let mut patch = Patch::new(44100.0);
2294 let osc = patch.add("osc", Vco::new(44100.0));
2295 let dist = patch.add("dist", Distortion::new(44100.0));
2296 let flt = patch.add("flt", Svf::new(44100.0));
2297 let out = patch.add("output", StereoOutput::new());
2298 patch.connect(osc.out("saw"), dist.in_("in")).unwrap();
2299 patch.connect(dist.out("out"), flt.in_("in")).unwrap();
2300 patch.connect(flt.out("lp"), out.in_("left")).unwrap();
2301 patch.connect(flt.out("lp"), out.in_("right")).unwrap();
2302 patch.set_output(out.id());
2303 (patch, osc.id(), dist.id(), flt.id())
2304 };
2305
2306 let (mut original, osc_id, dist_id, flt_id) = build();
2307 assert!(original.set_param_by_id(flt_id, "cutoff", 0.35));
2309 assert!(original.set_param_by_id(osc_id, "voct", 1.0));
2310 assert!(original.set_param_by_id(dist_id, "oversample", 1.0));
2312 assert!(!original.set_param_by_id(flt_id, "no_such_param", 1.0));
2314
2315 let def = original.to_def("Round Trip");
2317 assert_eq!(def.output.as_deref(), Some("output"));
2318 let json = def.to_json().unwrap();
2319 let reloaded = PatchDef::from_json(&json).unwrap();
2320 let registry = ModuleRegistry::new();
2321 let mut rebuilt = Patch::from_def(&reloaded, ®istry, 44100.0).unwrap();
2322
2323 let r_osc = rebuilt.get_node_id_by_name("osc").unwrap();
2325 let r_dist = rebuilt.get_node_id_by_name("dist").unwrap();
2326 let r_flt = rebuilt.get_node_id_by_name("flt").unwrap();
2327 assert!((rebuilt.get_param_by_id(r_flt, "cutoff").unwrap() - 0.35).abs() < 1e-9);
2328 assert!((rebuilt.get_param_by_id(r_osc, "voct").unwrap() - 1.0).abs() < 1e-9);
2329 assert!((rebuilt.get_param_by_id(r_dist, "oversample").unwrap() - 1.0).abs() < 1e-9);
2330 assert_eq!(rebuilt.output_node(), Some(r_out_of(&rebuilt)));
2332
2333 for i in 0..256 {
2335 let a = original.tick();
2336 let b = rebuilt.tick();
2337 assert!(
2338 (a.0 - b.0).abs() < 1e-9 && (a.1 - b.1).abs() < 1e-9,
2339 "sample {i} diverged: {a:?} vs {b:?}"
2340 );
2341 }
2342 }
2343
2344 fn r_out_of(p: &Patch) -> crate::graph::NodeId {
2346 p.get_node_id_by_name("output").unwrap()
2347 }
2348
2349 #[test]
2350 fn test_roundtrip_preserves_metadata() {
2351 use crate::graph::PatchMeta;
2353 use crate::modules::StereoOutput;
2354
2355 let mut patch = Patch::new(44100.0);
2356 let out = patch.add("output", StereoOutput::new());
2357 patch.set_output(out.id());
2358 patch.set_meta(PatchMeta {
2359 name: Some("My Patch".into()),
2360 author: Some("Ada".into()),
2361 description: Some("A lovely patch".into()),
2362 tags: vec!["demo".into(), "test".into()],
2363 });
2364
2365 let def = patch.to_def("My Patch");
2366 assert_eq!(def.author.as_deref(), Some("Ada"));
2367 assert_eq!(def.description.as_deref(), Some("A lovely patch"));
2368 assert_eq!(def.tags, vec!["demo".to_string(), "test".to_string()]);
2369
2370 let json = def.to_json().unwrap();
2371 let reloaded = PatchDef::from_json(&json).unwrap();
2372 let registry = ModuleRegistry::new();
2373 let rebuilt = Patch::from_def(&reloaded, ®istry, 44100.0).unwrap();
2374 let meta = rebuilt.meta();
2375 assert_eq!(meta.author.as_deref(), Some("Ada"));
2376 assert_eq!(meta.description.as_deref(), Some("A lovely patch"));
2377 assert_eq!(meta.tags, vec!["demo".to_string(), "test".to_string()]);
2378 }
2379
2380 #[test]
2381 fn test_old_json_without_output_field_still_loads() {
2382 let json = r#"{
2385 "version": 1,
2386 "name": "Legacy",
2387 "author": null,
2388 "description": null,
2389 "tags": [],
2390 "modules": [
2391 {"name": "vco", "module_type": "vco", "position": null, "state": null},
2392 {"name": "output", "module_type": "stereo_output", "position": null, "state": null}
2393 ],
2394 "cables": [
2395 {"from": "vco.saw", "to": "output.left", "attenuation": null, "offset": null}
2396 ],
2397 "parameters": {}
2398 }"#;
2399 let def = PatchDef::from_json(json).unwrap();
2400 assert!(def.output.is_none());
2401 let registry = ModuleRegistry::new();
2402 let patch = Patch::from_def(&def, ®istry, 44100.0).unwrap();
2403 assert_eq!(patch.output_node(), patch.get_node_id_by_name("output"));
2405 }
2406
2407 #[test]
2412 fn test_schema_enum_matches_registry() {
2413 const SCHEMA: &str = include_str!("../schemas/patch.schema.json");
2414 let schema: serde_json::Value =
2415 serde_json::from_str(SCHEMA).expect("patch.schema.json must be valid JSON");
2416 let enum_vals = schema["$defs"]["ModuleDef"]["properties"]["module_type"]["enum"]
2417 .as_array()
2418 .expect("schema module_type must define an enum array");
2419 let enum_ids: Vec<&str> = enum_vals
2420 .iter()
2421 .map(|v| v.as_str().expect("enum entries must be strings"))
2422 .collect();
2423
2424 let registry = ModuleRegistry::new();
2425 let registry_ids: Vec<String> =
2426 registry.list_modules().map(|m| m.type_id.clone()).collect();
2427
2428 for id in ®istry_ids {
2430 assert!(
2431 enum_ids.contains(&id.as_str()),
2432 "registry type '{id}' is missing from the schema module_type enum"
2433 );
2434 }
2435 for id in &enum_ids {
2437 assert!(
2438 registry_ids.iter().any(|r| r == id),
2439 "schema module_type enum lists '{id}', which the registry does not register"
2440 );
2441 }
2442 assert_eq!(
2444 enum_ids.len(),
2445 registry_ids.len(),
2446 "schema enum ({}) and registry ({}) type counts diverge",
2447 enum_ids.len(),
2448 registry_ids.len()
2449 );
2450 }
2451
2452 #[test]
2455 fn test_roundtrip_modulated_cable_and_mult_behavioral_equality() {
2456 use crate::modules::{Lfo, StereoOutput, Svf, Vco};
2462
2463 let build = || -> Patch {
2464 let mut patch = Patch::new(44100.0);
2465 let lfo = patch.add("lfo", Lfo::new(44100.0));
2466 let osc = patch.add("osc", Vco::new(44100.0));
2467 let flt = patch.add("flt", Svf::new(44100.0));
2468 let out = patch.add("output", StereoOutput::new());
2469 patch.connect(osc.out("saw"), flt.in_("in")).unwrap();
2471 patch
2474 .connect_modulated(lfo.out("sin"), flt.in_("fm"), 0.5, 0.1)
2475 .unwrap();
2476 patch.connect(flt.out("lp"), out.in_("left")).unwrap();
2478 patch.connect(flt.out("lp"), out.in_("right")).unwrap();
2479 patch.set_output(out.id());
2480 let lfo_id = lfo.id();
2482 assert!(patch.set_param_by_id(lfo_id, "rate", 0.6));
2483 patch
2484 };
2485
2486 let mut original = build();
2487
2488 let def = original.to_def("Modulated Round Trip");
2490 assert_eq!(def.cables.len(), 4, "all four cables must serialize");
2491 let modulated: Vec<_> = def
2493 .cables
2494 .iter()
2495 .filter(|c| c.attenuation.is_some() && c.offset.is_some())
2496 .collect();
2497 assert_eq!(modulated.len(), 1, "the modulated cable must be preserved");
2498 assert!((modulated[0].attenuation.unwrap() - 0.5).abs() < 1e-9);
2499 assert!((modulated[0].offset.unwrap() - 0.1).abs() < 1e-9);
2500 let from_lp = def.cables.iter().filter(|c| c.from == "flt.lp").count();
2502 assert_eq!(
2503 from_lp, 2,
2504 "the mult must serialize as two cables from flt.lp"
2505 );
2506
2507 let json = def.to_json().unwrap();
2508 let reloaded = PatchDef::from_json(&json).unwrap();
2509 let registry = ModuleRegistry::new();
2510 let mut rebuilt = Patch::from_def(&reloaded, ®istry, 44100.0).unwrap();
2511
2512 assert_eq!(rebuilt.cable_count(), original.cable_count());
2513
2514 for i in 0..512 {
2516 let a = original.tick();
2517 let b = rebuilt.tick();
2518 assert!(
2519 (a.0 - b.0).abs() < 1e-9 && (a.1 - b.1).abs() < 1e-9,
2520 "sample {i} diverged after round-trip: {a:?} vs {b:?}"
2521 );
2522 }
2523 }
2524
2525 #[test]
2528 fn test_minimal_json_without_tags_or_parameters_loads() {
2529 let json = r#"{
2533 "version": 1,
2534 "name": "Minimal",
2535 "modules": [
2536 {"name": "output", "module_type": "stereo_output", "position": null, "state": null}
2537 ],
2538 "cables": []
2539 }"#;
2540 let def = PatchDef::from_json(json).expect("minimal schema-valid JSON must deserialize");
2541 assert!(def.tags.is_empty());
2542 assert!(def.parameters.is_empty());
2543 assert!(def.author.is_none());
2544
2545 let registry = ModuleRegistry::new();
2546 let patch = Patch::from_def(&def, ®istry, 44100.0).expect("minimal patch must load");
2547 assert_eq!(patch.output_node(), patch.get_node_id_by_name("output"));
2548 }
2549
2550 #[test]
2553 fn test_step_sequencer_gate_off_survives_roundtrip() {
2554 use crate::modules::{StepSequencer, StereoOutput};
2555
2556 let mut patch = Patch::new(44100.0);
2557 let seq = patch.add("seq", StepSequencer::new());
2558 let out = patch.add("output", StereoOutput::new());
2559 patch.set_output(out.id());
2560 let seq_id = seq.id();
2561
2562 assert!(patch.set_param_by_id(seq_id, "step_3_cv", 2.0));
2565 assert!(patch.set_param_by_id(seq_id, "step_3_gate", 0.0));
2566 assert!(patch.set_param_by_id(seq_id, "step_5_gate", 0.0));
2567
2568 let def = patch.to_def("Seq");
2571 assert_eq!(def.parameters.get("seq.step_3_gate"), Some(&0.0));
2572 assert_eq!(def.parameters.get("seq.step_3_cv"), Some(&2.0));
2573 assert_eq!(def.parameters.get("seq.step_5_gate"), Some(&0.0));
2574 assert!(!def.parameters.contains_key("seq.step_0_gate"));
2576
2577 let json = def.to_json().unwrap();
2578 let reloaded = PatchDef::from_json(&json).unwrap();
2579 let registry = ModuleRegistry::new();
2580 let rebuilt = Patch::from_def(&reloaded, ®istry, 44100.0).unwrap();
2581 let rid = rebuilt.get_node_id_by_name("seq").unwrap();
2582
2583 assert_eq!(rebuilt.get_param_by_id(rid, "step_3_gate"), Some(0.0));
2585 assert_eq!(rebuilt.get_param_by_id(rid, "step_3_cv"), Some(2.0));
2586 assert_eq!(rebuilt.get_param_by_id(rid, "step_5_gate"), Some(0.0));
2587 assert_eq!(rebuilt.get_param_by_id(rid, "step_0_gate"), Some(1.0));
2588 }
2589
2590 #[test]
2593 fn test_ducker_knobs_reachable_and_survive_roundtrip() {
2594 use crate::modules::{Ducker, StereoOutput};
2595
2596 let mut patch = Patch::new(44100.0);
2597 let d = patch.add("duck", Ducker::default());
2598 let out = patch.add("output", StereoOutput::new());
2599 patch.connect(d.out("out"), out.in_("left")).unwrap();
2600 patch.set_output(out.id());
2601 let did = d.id();
2602
2603 let ids: Vec<String> = patch.param_infos(did).into_iter().map(|p| p.id).collect();
2606 assert!(
2607 ids.iter().any(|i| i == "depth"),
2608 "depth knob missing: {ids:?}"
2609 );
2610 assert!(
2611 ids.iter().any(|i| i == "thresh"),
2612 "thresh knob missing: {ids:?}"
2613 );
2614 assert!(
2615 ids.iter().any(|i| i == "amount"),
2616 "amount CV port missing: {ids:?}"
2617 );
2618 assert!(
2619 ids.iter().any(|i| i == "threshold"),
2620 "threshold CV port missing: {ids:?}"
2621 );
2622
2623 assert!(patch.set_param_by_id(did, "depth", 0.4));
2625 assert!(patch.set_param_by_id(did, "thresh", 0.7));
2626 assert_eq!(patch.get_param_by_id(did, "depth"), Some(0.4));
2627 assert_eq!(patch.get_param_by_id(did, "thresh"), Some(0.7));
2628
2629 let def = patch.to_def("Duck");
2630 assert_eq!(def.parameters.get("duck.depth"), Some(&0.4));
2631 assert_eq!(def.parameters.get("duck.thresh"), Some(&0.7));
2632
2633 let json = def.to_json().unwrap();
2634 let reloaded = PatchDef::from_json(&json).unwrap();
2635 let registry = ModuleRegistry::new();
2636 let rebuilt = Patch::from_def(&reloaded, ®istry, 44100.0).unwrap();
2637 let rid = rebuilt.get_node_id_by_name("duck").unwrap();
2638 assert_eq!(rebuilt.get_param_by_id(rid, "depth"), Some(0.4));
2639 assert_eq!(rebuilt.get_param_by_id(rid, "thresh"), Some(0.7));
2640 }
2641
2642 #[test]
2645 fn test_scale_quantizer_custom_scale_survives_roundtrip() {
2646 use crate::modules::{ScaleQuantizer, StereoOutput};
2647
2648 let whole_tone = [0.0, 200.0, 400.0, 600.0, 800.0, 1000.0];
2650 let test_voct = 0.1; let build_custom = || -> Patch {
2655 let mut patch = Patch::new(44100.0);
2656 let mut q = ScaleQuantizer::new(44100.0);
2657 q.set_custom_scale(&whole_tone);
2658 let qh = patch.add("q", q);
2659 let out = patch.add("output", StereoOutput::new());
2660 patch.connect(qh.out("out"), out.in_("left")).unwrap();
2661 patch.set_output(out.id());
2662 assert!(patch.set_param_by_id(qh.id(), "in", test_voct));
2663 patch.compile().unwrap();
2664 patch
2665 };
2666
2667 let mut original = build_custom();
2668 let qid_o = original.get_node_id_by_name("q").unwrap();
2669
2670 let def = original.to_def("Tuned");
2672 let q_state = &def.modules.iter().find(|m| m.name == "q").unwrap().state;
2673 assert!(
2674 q_state.is_some(),
2675 "custom scale must serialize into ModuleDef.state"
2676 );
2677
2678 let json = def.to_json().unwrap();
2679 let reloaded = PatchDef::from_json(&json).unwrap();
2680 let registry = ModuleRegistry::new();
2681 let mut rebuilt = Patch::from_def(&reloaded, ®istry, 44100.0).unwrap();
2682 let qid_r = rebuilt.get_node_id_by_name("q").unwrap();
2683
2684 let mut plain = {
2686 let mut patch = Patch::new(44100.0);
2687 let qh = patch.add("q", ScaleQuantizer::new(44100.0));
2688 let out = patch.add("output", StereoOutput::new());
2689 patch.connect(qh.out("out"), out.in_("left")).unwrap();
2690 patch.set_output(out.id());
2691 assert!(patch.set_param_by_id(qh.id(), "in", test_voct));
2692 patch.compile().unwrap();
2693 patch
2694 };
2695 let qid_p = plain.get_node_id_by_name("q").unwrap();
2696
2697 original.tick();
2698 rebuilt.tick();
2699 plain.tick();
2700 let o = original.get_output_value(qid_o, 10).unwrap();
2702 let r = rebuilt.get_output_value(qid_r, 10).unwrap();
2703 let p = plain.get_output_value(qid_p, 10).unwrap();
2704
2705 assert!(
2706 (o - r).abs() < 1e-9,
2707 "custom scale must survive round-trip: {o} vs {r}"
2708 );
2709 assert!(
2710 (o - p).abs() > 1e-6,
2711 "custom (whole-tone) quantization must differ from default 12-TET: {o} vs {p}"
2712 );
2713 }
2714}