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 "chorus",
497 "Chorus",
498 "Effects",
499 "Classic chorus effect with modulated delay lines",
500 &[
501 "chorus",
502 "modulation",
503 "detune",
504 "ensemble",
505 "effect",
506 "stereo",
507 ],
508 &[],
509 |sr| Box::new(Chorus::new(sr)),
510 );
511
512 self.register_factory_with_keywords(
513 "flanger",
514 "Flanger",
515 "Effects",
516 "Classic flanging effect with modulated delay",
517 &["flanger", "modulation", "sweep", "jet", "effect"],
518 &[],
519 |sr| Box::new(Flanger::new(sr)),
520 );
521
522 self.register_factory_with_keywords(
523 "phaser",
524 "Phaser",
525 "Effects",
526 "Classic phaser effect with all-pass filters",
527 &["phaser", "modulation", "sweep", "effect", "allpass"],
528 &[],
529 |sr| Box::new(Phaser::new(sr)),
530 );
531
532 self.register_factory_with_keywords(
533 "limiter",
534 "Limiter",
535 "Dynamics",
536 "Prevents signals from exceeding threshold",
537 &["limiter", "dynamics", "ceiling", "clip", "loudness"],
538 &[],
539 |sr| Box::new(Limiter::new(sr)),
540 );
541
542 self.register_factory_with_keywords(
543 "noise_gate",
544 "Noise Gate",
545 "Dynamics",
546 "Attenuates signals below threshold",
547 &["gate", "dynamics", "noise", "threshold", "mute"],
548 &[],
549 |sr| Box::new(NoiseGate::new(sr)),
550 );
551
552 self.register_factory_with_keywords(
553 "compressor",
554 "Compressor",
555 "Dynamics",
556 "Dynamic range compression with sidechain",
557 &["compressor", "dynamics", "squeeze", "punch", "sidechain"],
558 &[],
559 |sr| Box::new(Compressor::new(sr)),
560 );
561
562 self.register_factory_with_keywords(
563 "envelope_follower",
564 "Envelope Follower",
565 "Utilities",
566 "Extracts amplitude envelope from audio",
567 &["envelope", "follower", "detector", "cv", "ducking"],
568 &[],
569 |sr| Box::new(EnvelopeFollower::new(sr)),
570 );
571
572 self.register_factory_with_keywords(
573 "ducker",
574 "Ducker",
575 "Dynamics",
576 "Sidechain ducking driven by a key input",
577 &["ducker", "duck", "sidechain", "key", "dynamics", "pump"],
578 &[],
579 |sr| Box::new(Ducker::new(sr)),
580 );
581
582 self.register_factory_with_keywords(
583 "sample_player",
584 "Sample Player",
585 "Oscillators",
586 "Mono sample playback with V/Oct pitch and looping",
587 &["sample", "player", "playback", "sampler", "wav", "loop"],
588 &[],
589 |sr| Box::new(SamplePlayer::empty(sr)),
592 );
593
594 self.register_factory_with_keywords(
595 "mid_side_encode",
596 "Mid/Side Encode",
597 "Utilities",
598 "Encode left/right stereo to mid/side",
599 &["mid", "side", "ms", "stereo", "encode", "matrix"],
600 &[],
601 |_| Box::new(MidSideEncode::new()),
602 );
603
604 self.register_factory_with_keywords(
605 "mid_side_decode",
606 "Mid/Side Decode",
607 "Utilities",
608 "Decode mid/side to left/right with width control",
609 &["mid", "side", "ms", "stereo", "decode", "width"],
610 &[],
611 |_| Box::new(MidSideDecode::new()),
612 );
613
614 self.register_factory_with_keywords(
615 "bitcrusher",
616 "Bitcrusher",
617 "Effects",
618 "Lo-fi bit depth and sample rate reduction",
619 &["bitcrusher", "lofi", "distortion", "digital", "retro"],
620 &[],
621 |_| Box::new(Bitcrusher::new()),
622 );
623
624 self.register_factory_with_keywords(
626 "tremolo",
627 "Tremolo",
628 "Effects",
629 "Amplitude modulation effect with rate and depth control",
630 &["tremolo", "amplitude", "modulation", "wobble", "lfo"],
631 &[],
632 |sr| Box::new(Tremolo::new(sr)),
633 );
634
635 self.register_factory_with_keywords(
636 "vibrato",
637 "Vibrato",
638 "Effects",
639 "Pitch modulation effect using modulated delay",
640 &["vibrato", "pitch", "modulation", "wobble", "lfo"],
641 &[],
642 |sr| Box::new(Vibrato::new(sr)),
643 );
644
645 self.register_factory_with_keywords(
646 "distortion",
647 "Distortion",
648 "Effects",
649 "Waveshaping distortion with multiple modes",
650 &["distortion", "overdrive", "fuzz", "saturation", "clip"],
651 &[],
652 |sr| Box::new(Distortion::new(sr)),
653 );
654
655 self.register_factory_with_keywords(
657 "supersaw",
658 "Supersaw",
659 "Oscillators",
660 "JP-8000 style 7-voice detuned supersaw oscillator",
661 &["supersaw", "trance", "unison", "detune", "thick"],
662 &[],
663 |sr| Box::new(Supersaw::new(sr)),
664 );
665
666 self.register_factory_with_keywords(
667 "karplus_strong",
668 "Karplus-Strong",
669 "Oscillators",
670 "Physical modeling plucked string synthesis",
671 &["karplus", "string", "pluck", "physical", "modeling"],
672 &[],
673 |sr| Box::new(KarplusStrong::new(sr)),
674 );
675
676 self.register_factory_with_keywords(
678 "scale_quantizer",
679 "Scale Quantizer",
680 "Utilities",
681 "Quantize CV to musical scale notes",
682 &["quantizer", "scale", "music", "notes", "pitch"],
683 &[],
684 |sr| Box::new(ScaleQuantizer::new(sr)),
685 );
686
687 self.register_factory_with_keywords(
688 "euclidean",
689 "Euclidean Rhythm",
690 "Sequencers",
691 "Euclidean rhythm generator for evenly distributed pulses",
692 &["euclidean", "rhythm", "pattern", "trigger", "clock"],
693 &[],
694 |sr| Box::new(Euclidean::new(sr)),
695 );
696
697 self.register_factory_with_keywords(
698 "attenuverter",
699 "Attenuverter",
700 "Utilities",
701 "Attenuate, invert, and offset signals",
702 &["attenuator", "invert", "scale", "offset", "gain"],
703 &["essential"],
704 |_| Box::new(Attenuverter::new()),
705 );
706
707 self.register_factory_with_keywords(
708 "multiple",
709 "Multiple",
710 "Utilities",
711 "Signal splitter (1 input to 4 outputs)",
712 &["split", "copy", "mult", "buffer", "distribute"],
713 &["essential"],
714 |_| Box::new(Multiple::new()),
715 );
716
717 self.register_factory_with_keywords(
718 "crossfader",
719 "Crossfader/Panner",
720 "Utilities",
721 "Crossfade between inputs or pan stereo",
722 &["crossfade", "pan", "stereo", "balance", "mix"],
723 &[],
724 |_| Box::new(Crossfader::new()),
725 );
726
727 self.register_factory_with_keywords(
728 "precision_adder",
729 "Precision Adder",
730 "Utilities",
731 "High-precision CV adder for V/Oct signals",
732 &["add", "sum", "transpose", "octave", "voct", "pitch"],
733 &[],
734 |_| Box::new(PrecisionAdder::new()),
735 );
736
737 self.register_factory_with_keywords(
738 "vc_switch",
739 "VC Switch",
740 "Utilities",
741 "Voltage-controlled signal router",
742 &["switch", "router", "selector", "mux", "demux"],
743 &[],
744 |_| Box::new(VcSwitch::new()),
745 );
746
747 self.register_factory_with_keywords(
748 "min",
749 "Min",
750 "Utilities",
751 "Output minimum of two signals",
752 &["minimum", "compare", "math", "lowest"],
753 &[],
754 |_| Box::new(Min::new()),
755 );
756
757 self.register_factory_with_keywords(
758 "max",
759 "Max",
760 "Utilities",
761 "Output maximum of two signals",
762 &["maximum", "compare", "math", "highest"],
763 &[],
764 |_| Box::new(Max::new()),
765 );
766
767 self.register_factory_with_keywords(
768 "sample_and_hold",
769 "Sample & Hold",
770 "Utilities",
771 "Sample input value on trigger",
772 &["sample", "hold", "trigger", "freeze", "snapshot"],
773 &[],
774 |_| Box::new(SampleAndHold::new()),
775 );
776
777 self.register_factory_with_keywords(
778 "slew_limiter",
779 "Slew Limiter",
780 "Utilities",
781 "Limits rate of change (portamento/glide)",
782 &["slew", "portamento", "glide", "lag", "smooth"],
783 &[],
784 |sr| Box::new(SlewLimiter::new(sr)),
785 );
786
787 self.register_factory_with_keywords(
788 "quantizer",
789 "Quantizer",
790 "Utilities",
791 "Quantize V/Oct to musical scales",
792 &["quantize", "scale", "pitch", "chromatic", "note", "tune"],
793 &[],
794 |_| Box::new(Quantizer::new(Scale::Chromatic)),
795 );
796
797 self.register_factory_with_keywords(
801 "noise",
802 "Noise",
803 "Sources",
804 "White and pink noise generator",
805 &["noise", "white", "pink", "random", "hiss"],
806 &["essential"],
807 |_| Box::new(NoiseGenerator::new()),
808 );
809
810 self.register_factory_with_keywords(
814 "step_sequencer",
815 "Step Sequencer",
816 "Sequencing",
817 "8-step CV/gate sequencer",
818 &["sequencer", "step", "pattern", "melody", "cv", "gate"],
819 &["essential"],
820 |_| Box::new(StepSequencer::new()),
821 );
822
823 self.register_factory_with_keywords(
824 "clock",
825 "Clock",
826 "Sequencing",
827 "Master clock with tempo control",
828 &["clock", "tempo", "bpm", "trigger", "pulse", "sync"],
829 &["essential"],
830 |sr| Box::new(Clock::new(sr)),
831 );
832
833 self.register_factory_with_keywords(
837 "stereo_output",
838 "Stereo Output",
839 "I/O",
840 "Final stereo audio output",
841 &["output", "stereo", "main", "master", "speaker", "audio"],
842 &["essential"],
843 |_| Box::new(StereoOutput::new()),
844 );
845
846 self.register_factory_with_keywords(
850 "saturator",
851 "Saturator",
852 "Effects",
853 "Soft saturation / overdrive",
854 &[
855 "saturation",
856 "overdrive",
857 "distortion",
858 "warm",
859 "tube",
860 "tape",
861 ],
862 &["analog"],
863 |_| Box::new(Saturator::default()),
864 );
865
866 self.register_factory_with_keywords(
867 "wavefolder",
868 "Wavefolder",
869 "Effects",
870 "Wavefolder for complex harmonics",
871 &["wavefolder", "fold", "harmonics", "timbre", "west coast"],
872 &[],
873 |_| Box::new(Wavefolder::default()),
874 );
875
876 self.register_factory_with_keywords(
877 "ring_mod",
878 "Ring Modulator",
879 "Effects",
880 "Multiplies two signals for metallic/bell sounds",
881 &["ring", "modulator", "multiply", "bell", "metallic", "am"],
882 &[],
883 |_| Box::new(RingModulator::new()),
884 );
885
886 self.register_factory_with_keywords(
887 "rectifier",
888 "Rectifier",
889 "Effects",
890 "Full-wave and half-wave rectification",
891 &["rectify", "absolute", "waveshape", "fold"],
892 &[],
893 |_| Box::new(Rectifier::new()),
894 );
895
896 self.register_factory_with_keywords(
900 "logic_and",
901 "Logic AND",
902 "Logic",
903 "Output high when both inputs are high",
904 &["and", "gate", "boolean", "logic", "digital"],
905 &[],
906 |_| Box::new(LogicAnd::new()),
907 );
908
909 self.register_factory_with_keywords(
910 "logic_or",
911 "Logic OR",
912 "Logic",
913 "Output high when either input is high",
914 &["or", "gate", "boolean", "logic", "digital"],
915 &[],
916 |_| Box::new(LogicOr::new()),
917 );
918
919 self.register_factory_with_keywords(
920 "logic_xor",
921 "Logic XOR",
922 "Logic",
923 "Output high when exactly one input is high",
924 &["xor", "exclusive", "gate", "boolean", "logic", "digital"],
925 &[],
926 |_| Box::new(LogicXor::new()),
927 );
928
929 self.register_factory_with_keywords(
930 "logic_not",
931 "Logic NOT",
932 "Logic",
933 "Invert gate signal",
934 &["not", "invert", "gate", "boolean", "logic", "digital"],
935 &[],
936 |_| Box::new(LogicNot::new()),
937 );
938
939 self.register_factory_with_keywords(
940 "comparator",
941 "Comparator",
942 "Logic",
943 "Compare two CVs, output gates for greater/less/equal",
944 &["compare", "greater", "less", "equal", "threshold", "cv"],
945 &[],
946 |_| Box::new(Comparator::new()),
947 );
948
949 self.register_factory_with_keywords(
953 "bernoulli_gate",
954 "Bernoulli Gate",
955 "Random",
956 "Probabilistic trigger router",
957 &[
958 "random",
959 "probability",
960 "chance",
961 "coin",
962 "trigger",
963 "router",
964 ],
965 &[],
966 |_| Box::new(BernoulliGate::new()),
967 );
968
969 self.register_factory_with_keywords(
973 "crosstalk",
974 "Crosstalk",
975 "Analog Modeling",
976 "Channel crosstalk simulation",
977 &["crosstalk", "bleed", "stereo", "channel", "analog"],
978 &["analog"],
979 |sr| Box::new(Crosstalk::new(sr)),
980 );
981
982 self.register_factory_with_keywords(
983 "ground_loop",
984 "Ground Loop",
985 "Analog Modeling",
986 "Ground loop hum simulation (50/60 Hz)",
987 &["ground", "hum", "buzz", "50hz", "60hz", "mains", "analog"],
988 &["analog"],
989 |sr| Box::new(GroundLoop::new(sr)),
990 );
991
992 self.register_factory_with_keywords(
998 "wavetable",
999 "Wavetable",
1000 "Oscillators",
1001 "Wavetable oscillator with 8 tables and morphing",
1002 &["wavetable", "oscillator", "morph", "digital", "synthesis"],
1003 &[],
1004 |sr| Box::new(Wavetable::new(sr)),
1005 );
1006
1007 self.register_factory_with_keywords(
1008 "formant_osc",
1009 "Formant Oscillator",
1010 "Oscillators",
1011 "Formant oscillator for vocal synthesis (a/e/i/o/u)",
1012 &["formant", "vocal", "vowel", "voice", "speech", "oscillator"],
1013 &[],
1014 |sr| Box::new(FormantOsc::new(sr)),
1015 );
1016
1017 self.register_factory_with_keywords(
1019 "reverb",
1020 "Reverb",
1021 "Effects",
1022 "Algorithmic reverb (Freeverb-style) with stereo output",
1023 &["reverb", "room", "hall", "space", "ambience", "freeverb"],
1024 &["essential"],
1025 |sr| Box::new(Reverb::new(sr)),
1026 );
1027
1028 self.register_factory_with_keywords(
1029 "parametric_eq",
1030 "Parametric EQ",
1031 "Effects",
1032 "3-band parametric equalizer (low shelf, mid peak, high shelf)",
1033 &["eq", "equalizer", "tone", "parametric", "shelf", "filter"],
1034 &[],
1035 |sr| Box::new(ParametricEq::new(sr)),
1036 );
1037
1038 self.register_factory_with_keywords(
1039 "vocoder",
1040 "Vocoder",
1041 "Effects",
1042 "16-band vocoder with carrier/modulator inputs",
1043 &["vocoder", "voice", "robot", "spectral", "filter", "bands"],
1044 &[],
1045 |sr| Box::new(Vocoder::new(sr)),
1046 );
1047
1048 self.register_factory_with_keywords(
1049 "pitch_shifter",
1050 "Pitch Shifter",
1051 "Effects",
1052 "Granular pitch shifter (±24 semitones)",
1053 &["pitch", "shift", "transpose", "semitone", "granular"],
1054 &[],
1055 |sr| Box::new(PitchShifter::new(sr)),
1056 );
1057
1058 self.register_factory_with_keywords(
1059 "granular",
1060 "Granular",
1061 "Effects",
1062 "Granular synthesis/processing with 16 concurrent grains",
1063 &[
1064 "granular", "grain", "texture", "freeze", "clouds", "ambient",
1065 ],
1066 &["advanced"],
1067 |sr| Box::new(Granular::new(sr)),
1068 );
1069
1070 self.register_factory_with_keywords(
1072 "chord_memory",
1073 "Chord Memory",
1074 "Utilities",
1075 "Generate chord voicings from root note (9 chord types)",
1076 &["chord", "harmony", "voicing", "major", "minor", "seventh"],
1077 &[],
1078 |_| Box::new(ChordMemory::new()),
1079 );
1080
1081 self.register_factory_with_keywords(
1082 "arpeggiator",
1083 "Arpeggiator",
1084 "Sequencers",
1085 "Pattern-based arpeggiator (up/down/up-down/random)",
1086 &[
1087 "arpeggiator",
1088 "arp",
1089 "pattern",
1090 "sequence",
1091 "melody",
1092 "clock",
1093 ],
1094 &[],
1095 |sr| Box::new(Arpeggiator::new(sr)),
1096 );
1097 }
1098
1099 pub fn register_factory<F>(
1101 &mut self,
1102 type_id: &str,
1103 name: &str,
1104 category: &str,
1105 description: &str,
1106 factory: F,
1107 ) where
1108 F: Fn(f64) -> Box<dyn GraphModule> + Send + Sync + 'static,
1109 {
1110 self.register_factory_with_keywords(
1111 type_id,
1112 name,
1113 category,
1114 description,
1115 &[],
1116 &[],
1117 factory,
1118 );
1119 }
1120
1121 #[allow(clippy::too_many_arguments)]
1123 pub fn register_factory_with_keywords<F>(
1124 &mut self,
1125 type_id: &str,
1126 name: &str,
1127 category: &str,
1128 description: &str,
1129 keywords: &[&str],
1130 tags: &[&str],
1131 factory: F,
1132 ) where
1133 F: Fn(f64) -> Box<dyn GraphModule> + Send + Sync + 'static,
1134 {
1135 let temp_instance = factory(44100.0);
1137 let port_spec = temp_instance.port_spec().clone();
1138
1139 self.factories
1140 .insert(type_id.to_string(), Box::new(factory));
1141
1142 self.metadata.insert(
1143 type_id.to_string(),
1144 ModuleMetadata {
1145 type_id: type_id.to_string(),
1146 name: name.to_string(),
1147 category: category.to_string(),
1148 description: description.to_string(),
1149 port_spec,
1150 keywords: keywords.iter().map(|s| s.to_string()).collect(),
1151 tags: tags.iter().map(|s| s.to_string()).collect(),
1152 },
1153 );
1154 }
1155
1156 pub fn instantiate(&self, type_id: &str, sample_rate: f64) -> Option<Box<dyn GraphModule>> {
1158 self.factories.get(type_id).map(|f| f(sample_rate))
1159 }
1160
1161 pub fn list_modules(&self) -> impl Iterator<Item = &ModuleMetadata> {
1163 self.metadata.values()
1164 }
1165
1166 pub fn get_metadata(&self, type_id: &str) -> Option<&ModuleMetadata> {
1168 self.metadata.get(type_id)
1169 }
1170
1171 pub fn list_by_category<'a>(
1173 &'a self,
1174 category: &'a str,
1175 ) -> impl Iterator<Item = &'a ModuleMetadata> {
1176 self.metadata
1177 .values()
1178 .filter(move |m| m.category == category)
1179 }
1180
1181 pub fn categories(&self) -> Vec<String> {
1183 let mut cats: Vec<_> = self.metadata.values().map(|m| m.category.clone()).collect();
1184 cats.sort();
1185 cats.dedup();
1186 cats
1187 }
1188
1189 pub fn catalog(&self) -> CatalogResponse {
1195 let mut modules: Vec<ModuleCatalogEntry> = self
1196 .metadata
1197 .values()
1198 .map(ModuleCatalogEntry::from_metadata)
1199 .collect();
1200
1201 modules.sort_by(|a, b| (&a.category, &a.name).cmp(&(&b.category, &b.name)));
1203
1204 CatalogResponse {
1205 modules,
1206 categories: self.categories(),
1207 }
1208 }
1209
1210 pub fn search(&self, query: &str) -> Vec<ModuleCatalogEntry> {
1214 let query_lower = query.to_lowercase();
1215
1216 let mut results: Vec<(ModuleCatalogEntry, u8)> = self
1217 .metadata
1218 .values()
1219 .filter_map(|m| {
1220 let mut score: u8 = 0;
1222
1223 if m.type_id.to_lowercase() == query_lower {
1225 score += 100;
1226 }
1227 else if m.name.to_lowercase() == query_lower {
1229 score += 90;
1230 }
1231 else if m.type_id.to_lowercase().contains(&query_lower) {
1233 score += 70;
1234 }
1235 else if m.name.to_lowercase().contains(&query_lower) {
1237 score += 60;
1238 }
1239 else if m.keywords.iter().any(|k| k.to_lowercase() == query_lower) {
1241 score += 50;
1242 }
1243 else if m
1245 .keywords
1246 .iter()
1247 .any(|k| k.to_lowercase().contains(&query_lower))
1248 {
1249 score += 40;
1250 }
1251 else if m.description.to_lowercase().contains(&query_lower) {
1253 score += 20;
1254 }
1255 else if m.category.to_lowercase().contains(&query_lower) {
1257 score += 10;
1258 }
1259
1260 if score > 0 {
1261 Some((ModuleCatalogEntry::from_metadata(m), score))
1262 } else {
1263 None
1264 }
1265 })
1266 .collect();
1267
1268 results.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.name.cmp(&b.0.name)));
1270
1271 results.into_iter().map(|(entry, _)| entry).collect()
1272 }
1273
1274 pub fn by_category(&self, category: &str) -> Vec<ModuleCatalogEntry> {
1276 let mut modules: Vec<ModuleCatalogEntry> = self
1277 .metadata
1278 .values()
1279 .filter(|m| m.category.eq_ignore_ascii_case(category))
1280 .map(ModuleCatalogEntry::from_metadata)
1281 .collect();
1282
1283 modules.sort_by(|a, b| a.name.cmp(&b.name));
1285 modules
1286 }
1287}
1288
1289impl Default for ModuleRegistry {
1290 fn default() -> Self {
1291 Self::new()
1292 }
1293}
1294
1295impl Patch {
1297 pub fn to_def(&self, name: &str) -> PatchDef {
1299 let modules: Vec<ModuleDef> = self
1300 .nodes()
1301 .map(|(node_id, node_name, module)| ModuleDef {
1302 name: node_name.to_string(),
1303 module_type: module.type_id().to_string(),
1304 position: self.get_position(node_id),
1305 state: module.serialize_state(),
1306 })
1307 .collect();
1308
1309 let cables: Vec<CableDef> = self
1310 .cables()
1311 .iter()
1312 .filter_map(|cable| {
1313 let from_name = self.get_name(cable.from.node)?;
1315 let to_name = self.get_name(cable.to.node)?;
1316
1317 let (_, _, from_module) = self.nodes().find(|(id, _, _)| *id == cable.from.node)?;
1319 let (_, _, to_module) = self.nodes().find(|(id, _, _)| *id == cable.to.node)?;
1320
1321 let from_port = from_module
1322 .port_spec()
1323 .outputs
1324 .iter()
1325 .find(|p| p.id == cable.from.port)
1326 .map(|p| p.name.as_str())?;
1327
1328 let to_port = to_module
1329 .port_spec()
1330 .inputs
1331 .iter()
1332 .find(|p| p.id == cable.to.port)
1333 .map(|p| p.name.as_str())?;
1334
1335 Some(CableDef {
1336 from: format!("{}.{}", from_name, from_port),
1337 to: format!("{}.{}", to_name, to_port),
1338 attenuation: cable.attenuation,
1339 offset: cable.offset,
1340 })
1341 })
1342 .collect();
1343
1344 let mut parameters: StdMap<String, f64> = StdMap::new();
1348 for (node_id, node_name, _) in self.nodes() {
1349 for p in self.param_infos(node_id) {
1350 if (p.value - p.default).abs() > f64::EPSILON {
1351 parameters.insert(format!("{}.{}", node_name, p.id), p.value);
1352 }
1353 }
1354 }
1355
1356 let output = self
1359 .output_node()
1360 .and_then(|id| self.get_name(id))
1361 .map(|n| n.to_string());
1362
1363 let meta = self.meta();
1364 PatchDef {
1365 version: CURRENT_PATCH_VERSION,
1366 name: name.to_string(),
1367 author: meta.author.clone(),
1368 description: meta.description.clone(),
1369 tags: meta.tags.clone(),
1370 output,
1371 modules,
1372 cables,
1373 parameters,
1374 }
1375 }
1376
1377 pub fn from_def(
1379 def: &PatchDef,
1380 registry: &ModuleRegistry,
1381 sample_rate: f64,
1382 ) -> Result<Self, PatchError> {
1383 if def.version > CURRENT_PATCH_VERSION {
1386 return Err(PatchError::CompilationFailed(format!(
1387 "Unsupported patch version {} (this build supports up to {})",
1388 def.version, CURRENT_PATCH_VERSION
1389 )));
1390 }
1391
1392 let mut patch = Patch::new(sample_rate);
1393 let mut name_to_handle: StdMap<String, NodeHandle> = StdMap::new();
1394
1395 for module_def in &def.modules {
1397 let module = registry
1398 .instantiate(&module_def.module_type, sample_rate)
1399 .ok_or_else(|| {
1400 PatchError::CompilationFailed(format!(
1401 "Unknown module type: {}",
1402 module_def.module_type
1403 ))
1404 })?;
1405
1406 let handle = patch.add_boxed(&module_def.name, module);
1407
1408 if let Some((x, y)) = module_def.position {
1410 patch.set_position(handle.id(), (x, y));
1411 }
1412
1413 if let Some(state) = &module_def.state {
1417 patch
1418 .deserialize_module_state(handle.id(), state)
1419 .map_err(PatchError::CompilationFailed)?;
1420 }
1421
1422 name_to_handle.insert(module_def.name.clone(), handle);
1423 }
1424
1425 for cable_def in &def.cables {
1427 let (from_module, from_port) = parse_port_ref(&cable_def.from)?;
1428 let (to_module, to_port) = parse_port_ref(&cable_def.to)?;
1429
1430 let from_handle = name_to_handle.get(from_module).ok_or_else(|| {
1431 PatchError::CompilationFailed(format!("Unknown module: {}", from_module))
1432 })?;
1433
1434 let to_handle = name_to_handle.get(to_module).ok_or_else(|| {
1435 PatchError::CompilationFailed(format!("Unknown module: {}", to_module))
1436 })?;
1437
1438 let from_ref = from_handle.output(from_port).map_err(|_| {
1443 PatchError::CompilationFailed(format!(
1444 "Unknown output port '{}' on module '{}' (available: {})",
1445 from_port,
1446 from_module,
1447 from_handle.output_names().join(", ")
1448 ))
1449 })?;
1450 let to_ref = to_handle.input(to_port).map_err(|_| {
1451 PatchError::CompilationFailed(format!(
1452 "Unknown input port '{}' on module '{}' (available: {})",
1453 to_port,
1454 to_module,
1455 to_handle.input_names().join(", ")
1456 ))
1457 })?;
1458
1459 match (cable_def.attenuation, cable_def.offset) {
1460 (Some(attenuation), Some(offset)) => {
1461 patch.connect_modulated(from_ref, to_ref, attenuation, offset)?;
1462 }
1463 (Some(attenuation), None) => {
1464 patch.connect_attenuated(from_ref, to_ref, attenuation)?;
1465 }
1466 (None, Some(offset)) => {
1467 patch.connect_modulated(
1468 from_ref, to_ref, 1.0, offset,
1470 )?;
1471 }
1472 (None, None) => {
1473 patch.connect(from_ref, to_ref)?;
1474 }
1475 }
1476 }
1477
1478 for (key, &value) in &def.parameters {
1483 if let Some((module_name, param_id)) = key.split_once('.') {
1484 if let Some(handle) = name_to_handle.get(module_name) {
1485 patch.set_param_by_id(handle.id(), param_id, value);
1486 }
1487 }
1488 }
1489
1490 let output_set = def
1493 .output
1494 .as_ref()
1495 .and_then(|name| name_to_handle.get(name))
1496 .map(|handle| patch.set_output(handle.id()))
1497 .is_some();
1498 if !output_set {
1499 if let Some(handle) = name_to_handle.get("output") {
1500 patch.set_output(handle.id());
1501 } else if let Some(handle) = name_to_handle.values().find(|h| {
1502 h.spec()
1503 .outputs
1504 .iter()
1505 .any(|p| p.name == "left" || p.name == "right")
1506 }) {
1507 patch.set_output(handle.id());
1508 }
1509 }
1510
1511 patch.set_meta(crate::graph::PatchMeta {
1513 name: Some(def.name.clone()),
1514 author: def.author.clone(),
1515 description: def.description.clone(),
1516 tags: def.tags.clone(),
1517 });
1518
1519 patch.compile()?;
1520 Ok(patch)
1521 }
1522}
1523
1524fn parse_port_ref(s: &str) -> Result<(&str, &str), PatchError> {
1525 let parts: Vec<&str> = s.splitn(2, '.').collect();
1526 if parts.len() != 2 {
1527 return Err(PatchError::CompilationFailed(format!(
1528 "Invalid port reference: {}",
1529 s
1530 )));
1531 }
1532 Ok((parts[0], parts[1]))
1533}
1534
1535#[derive(Debug, Clone, Serialize, Deserialize)]
1541#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
1542pub struct ValidationError {
1543 pub path: String,
1545 pub message: String,
1547}
1548
1549impl ValidationError {
1550 pub fn new(path: impl Into<String>, message: impl Into<String>) -> Self {
1551 Self {
1552 path: path.into(),
1553 message: message.into(),
1554 }
1555 }
1556}
1557
1558impl core::fmt::Display for ValidationError {
1559 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1560 write!(f, "{}: {}", self.path, self.message)
1561 }
1562}
1563
1564#[derive(Debug, Clone, Serialize, Deserialize)]
1566#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
1567#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1568pub struct ValidationResult {
1569 pub valid: bool,
1571 pub errors: Vec<ValidationError>,
1573}
1574
1575impl ValidationResult {
1576 pub fn ok() -> Self {
1577 Self {
1578 valid: true,
1579 errors: Vec::new(),
1580 }
1581 }
1582
1583 pub fn with_errors(errors: Vec<ValidationError>) -> Self {
1584 Self {
1585 valid: errors.is_empty(),
1586 errors,
1587 }
1588 }
1589}
1590
1591impl PatchDef {
1592 pub fn validate(&self) -> ValidationResult {
1598 let mut errors = Vec::new();
1599
1600 if self.version < 1 {
1602 errors.push(ValidationError::new(
1603 "version",
1604 "Version must be a positive integer",
1605 ));
1606 }
1607
1608 if self.name.is_empty() {
1610 errors.push(ValidationError::new(
1611 "name",
1612 "Name must be a non-empty string",
1613 ));
1614 }
1615
1616 let mut module_names = alloc::collections::BTreeSet::new();
1618
1619 for (i, module) in self.modules.iter().enumerate() {
1621 let path = format!("modules[{}]", i);
1622
1623 if module.name.is_empty() {
1624 errors.push(ValidationError::new(
1625 format!("{}.name", path),
1626 "Module name must be a non-empty string",
1627 ));
1628 } else if !module_names.insert(&module.name) {
1629 errors.push(ValidationError::new(
1630 format!("{}.name", path),
1631 format!("Duplicate module name: {}", module.name),
1632 ));
1633 }
1634
1635 if module.module_type.is_empty() {
1636 errors.push(ValidationError::new(
1637 format!("{}.module_type", path),
1638 "Module type must be a non-empty string",
1639 ));
1640 }
1641 }
1642
1643 for (i, cable) in self.cables.iter().enumerate() {
1645 let path = format!("cables[{}]", i);
1646
1647 if !is_valid_port_ref(&cable.from) {
1649 errors.push(ValidationError::new(
1650 format!("{}.from", path),
1651 "From must be a port reference in format 'module_name.port_name'",
1652 ));
1653 }
1654
1655 if !is_valid_port_ref(&cable.to) {
1656 errors.push(ValidationError::new(
1657 format!("{}.to", path),
1658 "To must be a port reference in format 'module_name.port_name'",
1659 ));
1660 }
1661
1662 if let Some(attenuation) = cable.attenuation {
1664 if !(-2.0..=2.0).contains(&attenuation) {
1665 errors.push(ValidationError::new(
1666 format!("{}.attenuation", path),
1667 "Attenuation must be between -2.0 and 2.0",
1668 ));
1669 }
1670 }
1671
1672 if let Some(offset) = cable.offset {
1674 if !(-10.0..=10.0).contains(&offset) {
1675 errors.push(ValidationError::new(
1676 format!("{}.offset", path),
1677 "Offset must be between -10.0 and 10.0",
1678 ));
1679 }
1680 }
1681 }
1682
1683 ValidationResult::with_errors(errors)
1684 }
1685
1686 pub fn validate_with_registry(&self, registry: &ModuleRegistry) -> ValidationResult {
1693 let mut result = self.validate();
1695 if !result.valid {
1696 return result;
1697 }
1698
1699 let mut errors = Vec::new();
1700
1701 let module_names: alloc::collections::BTreeSet<_> =
1703 self.modules.iter().map(|m| m.name.as_str()).collect();
1704
1705 for (i, module) in self.modules.iter().enumerate() {
1707 if registry.get_metadata(&module.module_type).is_none() {
1708 errors.push(ValidationError::new(
1709 format!("modules[{}].module_type", i),
1710 format!("Unknown module type: {}", module.module_type),
1711 ));
1712 }
1713 }
1714
1715 for (i, cable) in self.cables.iter().enumerate() {
1717 let path = format!("cables[{}]", i);
1718
1719 if let Ok((from_module, from_port)) = parse_port_ref(&cable.from) {
1721 if !module_names.contains(from_module) {
1722 errors.push(ValidationError::new(
1723 format!("{}.from", path),
1724 format!("Unknown module: {}", from_module),
1725 ));
1726 } else {
1727 if let Some(module_def) = self.modules.iter().find(|m| m.name == from_module) {
1729 if let Some(metadata) = registry.get_metadata(&module_def.module_type) {
1730 if metadata.port_spec.output_by_name(from_port).is_none() {
1731 errors.push(ValidationError::new(
1732 format!("{}.from", path),
1733 format!(
1734 "Unknown output port '{}' on module '{}'",
1735 from_port, from_module
1736 ),
1737 ));
1738 }
1739 }
1740 }
1741 }
1742 }
1743
1744 if let Ok((to_module, to_port)) = parse_port_ref(&cable.to) {
1746 if !module_names.contains(to_module) {
1747 errors.push(ValidationError::new(
1748 format!("{}.to", path),
1749 format!("Unknown module: {}", to_module),
1750 ));
1751 } else {
1752 if let Some(module_def) = self.modules.iter().find(|m| m.name == to_module) {
1754 if let Some(metadata) = registry.get_metadata(&module_def.module_type) {
1755 if metadata.port_spec.input_by_name(to_port).is_none() {
1756 errors.push(ValidationError::new(
1757 format!("{}.to", path),
1758 format!(
1759 "Unknown input port '{}' on module '{}'",
1760 to_port, to_module
1761 ),
1762 ));
1763 }
1764 }
1765 }
1766 }
1767 }
1768 }
1769
1770 if errors.is_empty() {
1771 result
1772 } else {
1773 result.valid = false;
1774 result.errors.extend(errors);
1775 result
1776 }
1777 }
1778}
1779
1780fn is_valid_port_ref(s: &str) -> bool {
1782 let parts: Vec<&str> = s.splitn(2, '.').collect();
1783 if parts.len() != 2 {
1784 return false;
1785 }
1786
1787 let valid_chars = |s: &str| {
1789 !s.is_empty()
1790 && s.chars()
1791 .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
1792 };
1793
1794 valid_chars(parts[0]) && valid_chars(parts[1])
1795}
1796
1797#[cfg(test)]
1798mod tests {
1799 use super::*;
1800
1801 #[test]
1802 fn test_patch_def_serialization() {
1803 let def = PatchDef::new("Test Patch")
1804 .with_author("Test Author")
1805 .with_description("A test patch")
1806 .with_tag("test");
1807
1808 let json = def.to_json().unwrap();
1809 let loaded = PatchDef::from_json(&json).unwrap();
1810
1811 assert_eq!(loaded.name, "Test Patch");
1812 assert_eq!(loaded.author, Some("Test Author".to_string()));
1813 }
1814
1815 #[test]
1816 fn test_cable_def() {
1817 let cable = CableDef::new("vco.saw", "vcf.in").with_attenuation(0.5);
1818 assert_eq!(cable.from, "vco.saw");
1819 assert_eq!(cable.to, "vcf.in");
1820 assert_eq!(cable.attenuation, Some(0.5));
1821 }
1822
1823 #[test]
1824 fn test_patch_def_default() {
1825 let def = PatchDef::default();
1826 assert_eq!(def.name, "Untitled");
1827 }
1828
1829 #[test]
1830 fn test_module_def_with_position() {
1831 let def = ModuleDef::new("vco1", "vco").with_position(100.0, 200.0);
1832 assert_eq!(def.position, Some((100.0, 200.0)));
1833 }
1834
1835 #[test]
1836 fn test_cable_def_with_offset() {
1837 let cable = CableDef::new("a.out", "b.in").with_offset(2.5);
1838 assert_eq!(cable.offset, Some(2.5));
1839 }
1840
1841 #[test]
1842 fn test_cable_def_with_modulation() {
1843 let cable = CableDef::new("a.out", "b.in").with_modulation(0.5, 1.0);
1844 assert_eq!(cable.attenuation, Some(0.5));
1845 assert_eq!(cable.offset, Some(1.0));
1846 }
1847
1848 #[test]
1853 fn test_valid_patch_validation() {
1854 let mut def = PatchDef::new("Test Patch");
1855 def.modules.push(ModuleDef::new("vco1", "vco"));
1856 def.modules.push(ModuleDef::new("output", "stereo_output"));
1857 def.cables.push(CableDef::new("vco1.saw", "output.left"));
1858
1859 let result = def.validate();
1860 assert!(
1861 result.valid,
1862 "Expected valid patch, got errors: {:?}",
1863 result.errors
1864 );
1865 assert!(result.errors.is_empty());
1866 }
1867
1868 #[test]
1869 fn test_empty_name_validation() {
1870 let mut def = PatchDef::new("");
1871 def.modules.push(ModuleDef::new("vco1", "vco"));
1872
1873 let result = def.validate();
1874 assert!(!result.valid);
1875 assert!(result.errors.iter().any(|e| e.path == "name"));
1876 }
1877
1878 #[test]
1879 fn test_duplicate_module_name_validation() {
1880 let mut def = PatchDef::new("Test");
1881 def.modules.push(ModuleDef::new("vco1", "vco"));
1882 def.modules.push(ModuleDef::new("vco1", "vco")); let result = def.validate();
1885 assert!(!result.valid);
1886 assert!(result
1887 .errors
1888 .iter()
1889 .any(|e| e.message.contains("Duplicate")));
1890 }
1891
1892 #[test]
1893 fn test_invalid_port_reference_validation() {
1894 let mut def = PatchDef::new("Test");
1895 def.modules.push(ModuleDef::new("vco1", "vco"));
1896 def.cables.push(CableDef::new("invalid", "also_invalid")); let result = def.validate();
1899 assert!(!result.valid);
1900 assert!(result.errors.len() >= 2);
1901 }
1902
1903 #[test]
1904 fn test_attenuation_range_validation() {
1905 let mut def = PatchDef::new("Test");
1906 def.modules.push(ModuleDef::new("vco1", "vco"));
1907 def.cables
1908 .push(CableDef::new("a.out", "b.in").with_attenuation(5.0)); let result = def.validate();
1911 assert!(!result.valid);
1912 assert!(result.errors.iter().any(|e| e.path.contains("attenuation")));
1913 }
1914
1915 #[test]
1916 fn test_offset_range_validation() {
1917 let mut def = PatchDef::new("Test");
1918 def.modules.push(ModuleDef::new("vco1", "vco"));
1919 def.cables
1920 .push(CableDef::new("a.out", "b.in").with_offset(15.0)); let result = def.validate();
1923 assert!(!result.valid);
1924 assert!(result.errors.iter().any(|e| e.path.contains("offset")));
1925 }
1926
1927 #[test]
1928 fn test_validate_with_registry_unknown_module_type() {
1929 let registry = ModuleRegistry::new();
1930
1931 let mut def = PatchDef::new("Test");
1932 def.modules.push(ModuleDef::new("foo", "nonexistent_type"));
1933
1934 let result = def.validate_with_registry(®istry);
1935 assert!(!result.valid);
1936 assert!(result
1937 .errors
1938 .iter()
1939 .any(|e| e.message.contains("Unknown module type")));
1940 }
1941
1942 #[test]
1943 fn test_validate_with_registry_unknown_module_reference() {
1944 let registry = ModuleRegistry::new();
1945
1946 let mut def = PatchDef::new("Test");
1947 def.modules.push(ModuleDef::new("vco1", "vco"));
1948 def.cables
1949 .push(CableDef::new("nonexistent.out", "vco1.voct"));
1950
1951 let result = def.validate_with_registry(®istry);
1952 assert!(!result.valid);
1953 assert!(result
1954 .errors
1955 .iter()
1956 .any(|e| e.message.contains("Unknown module")));
1957 }
1958
1959 #[test]
1960 fn test_validate_with_registry_unknown_port() {
1961 let registry = ModuleRegistry::new();
1962
1963 let mut def = PatchDef::new("Test");
1964 def.modules.push(ModuleDef::new("vco1", "vco"));
1965 def.modules.push(ModuleDef::new("output", "stereo_output"));
1966 def.cables
1967 .push(CableDef::new("vco1.nonexistent_port", "output.left"));
1968
1969 let result = def.validate_with_registry(®istry);
1970 assert!(!result.valid);
1971 assert!(result
1972 .errors
1973 .iter()
1974 .any(|e| e.message.contains("Unknown output port")));
1975 }
1976
1977 #[test]
1978 fn test_validate_with_registry_valid_patch() {
1979 let registry = ModuleRegistry::new();
1980
1981 let mut def = PatchDef::new("Valid Patch");
1982 def.modules.push(ModuleDef::new("vco1", "vco"));
1983 def.modules.push(ModuleDef::new("output", "stereo_output"));
1984 def.cables.push(CableDef::new("vco1.saw", "output.left"));
1985 def.cables.push(CableDef::new("vco1.sin", "output.right"));
1986
1987 let result = def.validate_with_registry(®istry);
1988 assert!(
1989 result.valid,
1990 "Expected valid patch, got errors: {:?}",
1991 result.errors
1992 );
1993 }
1994
1995 #[test]
1996 fn test_is_valid_port_ref() {
1997 assert!(is_valid_port_ref("vco1.out"));
1998 assert!(is_valid_port_ref("module_name.port_name"));
1999 assert!(is_valid_port_ref("a.b"));
2000 assert!(is_valid_port_ref("my-module.my-port"));
2001
2002 assert!(!is_valid_port_ref("nodot"));
2003 assert!(!is_valid_port_ref(".startswithdot"));
2004 assert!(!is_valid_port_ref("endswithdot."));
2005 assert!(!is_valid_port_ref(""));
2006 assert!(!is_valid_port_ref("has spaces.port"));
2007 }
2008
2009 #[test]
2010 fn test_validation_error_display() {
2011 let error = ValidationError::new("modules[0].name", "Name is empty");
2012 let display = format!("{}", error);
2013 assert_eq!(display, "modules[0].name: Name is empty");
2014 }
2015
2016 #[test]
2021 fn test_catalog_returns_all_modules() {
2022 let registry = ModuleRegistry::new();
2023 let catalog = registry.catalog();
2024
2025 assert!(catalog.modules.len() >= 36, "Expected at least 36 modules");
2027
2028 assert!(catalog.modules.iter().any(|m| m.type_id == "vco"));
2030 assert!(catalog.modules.iter().any(|m| m.type_id == "svf"));
2031 assert!(catalog.modules.iter().any(|m| m.type_id == "adsr"));
2032 }
2033
2034 #[test]
2035 fn test_remediation_modules_registered_and_instantiate() {
2036 let registry = ModuleRegistry::new();
2037 for id in [
2040 "sample_player",
2041 "ducker",
2042 "mid_side_encode",
2043 "mid_side_decode",
2044 ] {
2045 let module = registry
2046 .instantiate(id, 44100.0)
2047 .unwrap_or_else(|| panic!("module '{id}' not registered"));
2048 assert_eq!(module.type_id(), id, "type_id mismatch for '{id}'");
2049 }
2050 }
2051
2052 #[test]
2053 fn test_catalog_categories() {
2054 let registry = ModuleRegistry::new();
2055 let catalog = registry.catalog();
2056
2057 assert!(catalog.categories.contains(&"Oscillators".to_string()));
2059 assert!(catalog.categories.contains(&"Filters".to_string()));
2060 assert!(catalog.categories.contains(&"Utilities".to_string()));
2061 assert!(catalog.categories.contains(&"Effects".to_string()));
2062
2063 let mut sorted_cats = catalog.categories.clone();
2065 sorted_cats.sort();
2066 assert_eq!(catalog.categories, sorted_cats);
2067 }
2068
2069 #[test]
2070 fn test_catalog_entry_has_port_summary() {
2071 let registry = ModuleRegistry::new();
2072 let catalog = registry.catalog();
2073
2074 let vco = catalog.modules.iter().find(|m| m.type_id == "vco").unwrap();
2075 assert!(vco.ports.outputs > 0, "VCO should have outputs");
2076 assert!(vco.ports.has_audio_out, "VCO should have audio output");
2077
2078 let stereo_out = catalog
2079 .modules
2080 .iter()
2081 .find(|m| m.type_id == "stereo_output")
2082 .unwrap();
2083 assert!(
2084 stereo_out.ports.inputs > 0,
2085 "Stereo output should have inputs"
2086 );
2087 assert!(
2088 stereo_out.ports.has_audio_in,
2089 "Stereo output should have audio input"
2090 );
2091 }
2092
2093 #[test]
2094 fn test_search_exact_type_id_match() {
2095 let registry = ModuleRegistry::new();
2096 let results = registry.search("vco");
2097
2098 assert!(!results.is_empty());
2099 assert_eq!(results[0].type_id, "vco");
2101 }
2102
2103 #[test]
2104 fn test_search_by_keyword() {
2105 let registry = ModuleRegistry::new();
2106 let results = registry.search("oscillator");
2107
2108 assert!(results.len() >= 3);
2110 assert!(results.iter().any(|m| m.type_id == "vco"));
2111 assert!(results.iter().any(|m| m.type_id == "analog_vco"));
2112 assert!(results.iter().any(|m| m.type_id == "lfo"));
2113 }
2114
2115 #[test]
2116 fn test_search_case_insensitive() {
2117 let registry = ModuleRegistry::new();
2118 let results_lower = registry.search("filter");
2119 let results_upper = registry.search("FILTER");
2120 let results_mixed = registry.search("FiLtEr");
2121
2122 assert_eq!(results_lower.len(), results_upper.len());
2123 assert_eq!(results_lower.len(), results_mixed.len());
2124 }
2125
2126 #[test]
2127 fn test_search_by_description() {
2128 let registry = ModuleRegistry::new();
2129 let results = registry.search("saturation");
2130
2131 assert!(!results.is_empty());
2133 assert!(results.iter().any(|m| m.type_id == "saturator"));
2134 }
2135
2136 #[test]
2137 fn test_search_no_results() {
2138 let registry = ModuleRegistry::new();
2139 let results = registry.search("nonexistent_xyz_123");
2140
2141 assert!(results.is_empty());
2142 }
2143
2144 #[test]
2145 fn test_by_category() {
2146 let registry = ModuleRegistry::new();
2147 let oscillators = registry.by_category("Oscillators");
2148
2149 assert!(oscillators.len() >= 2);
2150 assert!(oscillators.iter().all(|m| m.category == "Oscillators"));
2151 assert!(oscillators.iter().any(|m| m.type_id == "vco"));
2152 assert!(oscillators.iter().any(|m| m.type_id == "analog_vco"));
2153 }
2154
2155 #[test]
2156 fn test_by_category_case_insensitive() {
2157 let registry = ModuleRegistry::new();
2158 let filters1 = registry.by_category("Filters");
2159 let filters2 = registry.by_category("filters");
2160 let filters3 = registry.by_category("FILTERS");
2161
2162 assert_eq!(filters1.len(), filters2.len());
2163 assert_eq!(filters1.len(), filters3.len());
2164 }
2165
2166 #[test]
2167 fn test_by_category_sorted_by_name() {
2168 let registry = ModuleRegistry::new();
2169 let utilities = registry.by_category("Utilities");
2170
2171 let names: Vec<_> = utilities.iter().map(|m| &m.name).collect();
2173 let mut sorted_names = names.clone();
2174 sorted_names.sort();
2175 assert_eq!(names, sorted_names);
2176 }
2177
2178 #[test]
2179 fn test_catalog_entry_serialization() {
2180 let registry = ModuleRegistry::new();
2181 let catalog = registry.catalog();
2182
2183 let json = serde_json::to_string(&catalog).unwrap();
2185 assert!(json.contains("\"type_id\""));
2186 assert!(json.contains("\"category\""));
2187 assert!(json.contains("\"keywords\""));
2188
2189 let deserialized: CatalogResponse = serde_json::from_str(&json).unwrap();
2191 assert_eq!(deserialized.modules.len(), catalog.modules.len());
2192 }
2193
2194 #[test]
2195 fn test_module_has_keywords_and_tags() {
2196 let registry = ModuleRegistry::new();
2197 let metadata = registry.get_metadata("vco").unwrap();
2198
2199 assert!(!metadata.keywords.is_empty());
2201 assert!(metadata.keywords.contains(&"oscillator".to_string()));
2202
2203 assert!(metadata.tags.contains(&"essential".to_string()));
2205 }
2206
2207 #[test]
2208 fn test_from_def_mistyped_cable_port_returns_err() {
2209 let registry = ModuleRegistry::new();
2212 let mut def = PatchDef::new("Bad Ports");
2213 def.modules.push(ModuleDef::new("vco", "vco"));
2214 def.modules.push(ModuleDef::new("output", "stereo_output"));
2215 def.cables
2217 .push(CableDef::new("vco.definitely_not_a_port", "output.left"));
2218
2219 let result = Patch::from_def(&def, ®istry, 44100.0);
2220 assert!(result.is_err(), "expected Err for mistyped cable port");
2221 match result {
2222 Err(PatchError::CompilationFailed(msg)) => {
2223 assert!(
2224 msg.contains("definitely_not_a_port") && msg.contains("vco"),
2225 "error should name the bad port and module: {}",
2226 msg
2227 );
2228 }
2229 other => panic!("expected CompilationFailed, got {:?}", other),
2230 }
2231 }
2232
2233 #[test]
2234 fn test_from_def_unknown_module_type_returns_err() {
2235 let registry = ModuleRegistry::new();
2237 let mut def = PatchDef::new("Bad Type");
2238 def.modules.push(ModuleDef::new("x", "not_a_real_module"));
2239 match Patch::from_def(&def, ®istry, 44100.0) {
2240 Err(PatchError::CompilationFailed(msg)) => {
2241 assert!(msg.contains("not_a_real_module"), "msg: {msg}");
2242 }
2243 other => panic!("expected CompilationFailed, got {other:?}"),
2244 }
2245 }
2246
2247 #[test]
2248 fn test_from_def_malformed_cable_ref_returns_err() {
2249 let registry = ModuleRegistry::new();
2251 let mut def = PatchDef::new("Malformed");
2252 def.modules.push(ModuleDef::new("vco", "vco"));
2253 def.modules.push(ModuleDef::new("output", "stereo_output"));
2254 def.cables.push(CableDef::new("no_dot_here", "output.left"));
2255 assert!(Patch::from_def(&def, ®istry, 44100.0).is_err());
2256 }
2257
2258 #[test]
2259 fn test_from_def_rejects_newer_version() {
2260 let registry = ModuleRegistry::new();
2262 let mut def = PatchDef::new("Future");
2263 def.version = CURRENT_PATCH_VERSION + 1;
2264 def.modules.push(ModuleDef::new("output", "stereo_output"));
2265 match Patch::from_def(&def, ®istry, 44100.0) {
2266 Err(PatchError::CompilationFailed(msg)) => {
2267 assert!(msg.contains("version"), "msg: {msg}");
2268 }
2269 other => panic!("expected version rejection, got {other:?}"),
2270 }
2271 def.version = CURRENT_PATCH_VERSION;
2273 assert!(Patch::from_def(&def, ®istry, 44100.0).is_ok());
2274 }
2275
2276 #[test]
2277 fn test_roundtrip_preserves_params_and_output() {
2278 use crate::modules::{Distortion, StereoOutput, Svf, Vco};
2281
2282 let build = || -> (Patch, crate::graph::NodeId, crate::graph::NodeId, crate::graph::NodeId) {
2283 let mut patch = Patch::new(44100.0);
2284 let osc = patch.add("osc", Vco::new(44100.0));
2285 let dist = patch.add("dist", Distortion::new(44100.0));
2286 let flt = patch.add("flt", Svf::new(44100.0));
2287 let out = patch.add("output", StereoOutput::new());
2288 patch.connect(osc.out("saw"), dist.in_("in")).unwrap();
2289 patch.connect(dist.out("out"), flt.in_("in")).unwrap();
2290 patch.connect(flt.out("lp"), out.in_("left")).unwrap();
2291 patch.connect(flt.out("lp"), out.in_("right")).unwrap();
2292 patch.set_output(out.id());
2293 (patch, osc.id(), dist.id(), flt.id())
2294 };
2295
2296 let (mut original, osc_id, dist_id, flt_id) = build();
2297 assert!(original.set_param_by_id(flt_id, "cutoff", 0.35));
2299 assert!(original.set_param_by_id(osc_id, "voct", 1.0));
2300 assert!(original.set_param_by_id(dist_id, "oversample", 1.0));
2302 assert!(!original.set_param_by_id(flt_id, "no_such_param", 1.0));
2304
2305 let def = original.to_def("Round Trip");
2307 assert_eq!(def.output.as_deref(), Some("output"));
2308 let json = def.to_json().unwrap();
2309 let reloaded = PatchDef::from_json(&json).unwrap();
2310 let registry = ModuleRegistry::new();
2311 let mut rebuilt = Patch::from_def(&reloaded, ®istry, 44100.0).unwrap();
2312
2313 let r_osc = rebuilt.get_node_id_by_name("osc").unwrap();
2315 let r_dist = rebuilt.get_node_id_by_name("dist").unwrap();
2316 let r_flt = rebuilt.get_node_id_by_name("flt").unwrap();
2317 assert!((rebuilt.get_param_by_id(r_flt, "cutoff").unwrap() - 0.35).abs() < 1e-9);
2318 assert!((rebuilt.get_param_by_id(r_osc, "voct").unwrap() - 1.0).abs() < 1e-9);
2319 assert!((rebuilt.get_param_by_id(r_dist, "oversample").unwrap() - 1.0).abs() < 1e-9);
2320 assert_eq!(rebuilt.output_node(), Some(r_out_of(&rebuilt)));
2322
2323 for i in 0..256 {
2325 let a = original.tick();
2326 let b = rebuilt.tick();
2327 assert!(
2328 (a.0 - b.0).abs() < 1e-9 && (a.1 - b.1).abs() < 1e-9,
2329 "sample {i} diverged: {a:?} vs {b:?}"
2330 );
2331 }
2332 }
2333
2334 fn r_out_of(p: &Patch) -> crate::graph::NodeId {
2336 p.get_node_id_by_name("output").unwrap()
2337 }
2338
2339 #[test]
2340 fn test_roundtrip_preserves_metadata() {
2341 use crate::graph::PatchMeta;
2343 use crate::modules::StereoOutput;
2344
2345 let mut patch = Patch::new(44100.0);
2346 let out = patch.add("output", StereoOutput::new());
2347 patch.set_output(out.id());
2348 patch.set_meta(PatchMeta {
2349 name: Some("My Patch".into()),
2350 author: Some("Ada".into()),
2351 description: Some("A lovely patch".into()),
2352 tags: vec!["demo".into(), "test".into()],
2353 });
2354
2355 let def = patch.to_def("My Patch");
2356 assert_eq!(def.author.as_deref(), Some("Ada"));
2357 assert_eq!(def.description.as_deref(), Some("A lovely patch"));
2358 assert_eq!(def.tags, vec!["demo".to_string(), "test".to_string()]);
2359
2360 let json = def.to_json().unwrap();
2361 let reloaded = PatchDef::from_json(&json).unwrap();
2362 let registry = ModuleRegistry::new();
2363 let rebuilt = Patch::from_def(&reloaded, ®istry, 44100.0).unwrap();
2364 let meta = rebuilt.meta();
2365 assert_eq!(meta.author.as_deref(), Some("Ada"));
2366 assert_eq!(meta.description.as_deref(), Some("A lovely patch"));
2367 assert_eq!(meta.tags, vec!["demo".to_string(), "test".to_string()]);
2368 }
2369
2370 #[test]
2371 fn test_old_json_without_output_field_still_loads() {
2372 let json = r#"{
2375 "version": 1,
2376 "name": "Legacy",
2377 "author": null,
2378 "description": null,
2379 "tags": [],
2380 "modules": [
2381 {"name": "vco", "module_type": "vco", "position": null, "state": null},
2382 {"name": "output", "module_type": "stereo_output", "position": null, "state": null}
2383 ],
2384 "cables": [
2385 {"from": "vco.saw", "to": "output.left", "attenuation": null, "offset": null}
2386 ],
2387 "parameters": {}
2388 }"#;
2389 let def = PatchDef::from_json(json).unwrap();
2390 assert!(def.output.is_none());
2391 let registry = ModuleRegistry::new();
2392 let patch = Patch::from_def(&def, ®istry, 44100.0).unwrap();
2393 assert_eq!(patch.output_node(), patch.get_node_id_by_name("output"));
2395 }
2396
2397 #[test]
2402 fn test_schema_enum_matches_registry() {
2403 const SCHEMA: &str = include_str!("../schemas/patch.schema.json");
2404 let schema: serde_json::Value =
2405 serde_json::from_str(SCHEMA).expect("patch.schema.json must be valid JSON");
2406 let enum_vals = schema["$defs"]["ModuleDef"]["properties"]["module_type"]["enum"]
2407 .as_array()
2408 .expect("schema module_type must define an enum array");
2409 let enum_ids: Vec<&str> = enum_vals
2410 .iter()
2411 .map(|v| v.as_str().expect("enum entries must be strings"))
2412 .collect();
2413
2414 let registry = ModuleRegistry::new();
2415 let registry_ids: Vec<String> =
2416 registry.list_modules().map(|m| m.type_id.clone()).collect();
2417
2418 for id in ®istry_ids {
2420 assert!(
2421 enum_ids.contains(&id.as_str()),
2422 "registry type '{id}' is missing from the schema module_type enum"
2423 );
2424 }
2425 for id in &enum_ids {
2427 assert!(
2428 registry_ids.iter().any(|r| r == id),
2429 "schema module_type enum lists '{id}', which the registry does not register"
2430 );
2431 }
2432 assert_eq!(
2434 enum_ids.len(),
2435 registry_ids.len(),
2436 "schema enum ({}) and registry ({}) type counts diverge",
2437 enum_ids.len(),
2438 registry_ids.len()
2439 );
2440 }
2441
2442 #[test]
2445 fn test_roundtrip_modulated_cable_and_mult_behavioral_equality() {
2446 use crate::modules::{Lfo, StereoOutput, Svf, Vco};
2452
2453 let build = || -> Patch {
2454 let mut patch = Patch::new(44100.0);
2455 let lfo = patch.add("lfo", Lfo::new(44100.0));
2456 let osc = patch.add("osc", Vco::new(44100.0));
2457 let flt = patch.add("flt", Svf::new(44100.0));
2458 let out = patch.add("output", StereoOutput::new());
2459 patch.connect(osc.out("saw"), flt.in_("in")).unwrap();
2461 patch
2464 .connect_modulated(lfo.out("sin"), flt.in_("fm"), 0.5, 0.1)
2465 .unwrap();
2466 patch.connect(flt.out("lp"), out.in_("left")).unwrap();
2468 patch.connect(flt.out("lp"), out.in_("right")).unwrap();
2469 patch.set_output(out.id());
2470 let lfo_id = lfo.id();
2472 assert!(patch.set_param_by_id(lfo_id, "rate", 0.6));
2473 patch
2474 };
2475
2476 let mut original = build();
2477
2478 let def = original.to_def("Modulated Round Trip");
2480 assert_eq!(def.cables.len(), 4, "all four cables must serialize");
2481 let modulated: Vec<_> = def
2483 .cables
2484 .iter()
2485 .filter(|c| c.attenuation.is_some() && c.offset.is_some())
2486 .collect();
2487 assert_eq!(modulated.len(), 1, "the modulated cable must be preserved");
2488 assert!((modulated[0].attenuation.unwrap() - 0.5).abs() < 1e-9);
2489 assert!((modulated[0].offset.unwrap() - 0.1).abs() < 1e-9);
2490 let from_lp = def.cables.iter().filter(|c| c.from == "flt.lp").count();
2492 assert_eq!(
2493 from_lp, 2,
2494 "the mult must serialize as two cables from flt.lp"
2495 );
2496
2497 let json = def.to_json().unwrap();
2498 let reloaded = PatchDef::from_json(&json).unwrap();
2499 let registry = ModuleRegistry::new();
2500 let mut rebuilt = Patch::from_def(&reloaded, ®istry, 44100.0).unwrap();
2501
2502 assert_eq!(rebuilt.cable_count(), original.cable_count());
2503
2504 for i in 0..512 {
2506 let a = original.tick();
2507 let b = rebuilt.tick();
2508 assert!(
2509 (a.0 - b.0).abs() < 1e-9 && (a.1 - b.1).abs() < 1e-9,
2510 "sample {i} diverged after round-trip: {a:?} vs {b:?}"
2511 );
2512 }
2513 }
2514
2515 #[test]
2518 fn test_minimal_json_without_tags_or_parameters_loads() {
2519 let json = r#"{
2523 "version": 1,
2524 "name": "Minimal",
2525 "modules": [
2526 {"name": "output", "module_type": "stereo_output", "position": null, "state": null}
2527 ],
2528 "cables": []
2529 }"#;
2530 let def = PatchDef::from_json(json).expect("minimal schema-valid JSON must deserialize");
2531 assert!(def.tags.is_empty());
2532 assert!(def.parameters.is_empty());
2533 assert!(def.author.is_none());
2534
2535 let registry = ModuleRegistry::new();
2536 let patch = Patch::from_def(&def, ®istry, 44100.0).expect("minimal patch must load");
2537 assert_eq!(patch.output_node(), patch.get_node_id_by_name("output"));
2538 }
2539
2540 #[test]
2543 fn test_step_sequencer_gate_off_survives_roundtrip() {
2544 use crate::modules::{StepSequencer, StereoOutput};
2545
2546 let mut patch = Patch::new(44100.0);
2547 let seq = patch.add("seq", StepSequencer::new());
2548 let out = patch.add("output", StereoOutput::new());
2549 patch.set_output(out.id());
2550 let seq_id = seq.id();
2551
2552 assert!(patch.set_param_by_id(seq_id, "step_3_cv", 2.0));
2555 assert!(patch.set_param_by_id(seq_id, "step_3_gate", 0.0));
2556 assert!(patch.set_param_by_id(seq_id, "step_5_gate", 0.0));
2557
2558 let def = patch.to_def("Seq");
2561 assert_eq!(def.parameters.get("seq.step_3_gate"), Some(&0.0));
2562 assert_eq!(def.parameters.get("seq.step_3_cv"), Some(&2.0));
2563 assert_eq!(def.parameters.get("seq.step_5_gate"), Some(&0.0));
2564 assert!(!def.parameters.contains_key("seq.step_0_gate"));
2566
2567 let json = def.to_json().unwrap();
2568 let reloaded = PatchDef::from_json(&json).unwrap();
2569 let registry = ModuleRegistry::new();
2570 let rebuilt = Patch::from_def(&reloaded, ®istry, 44100.0).unwrap();
2571 let rid = rebuilt.get_node_id_by_name("seq").unwrap();
2572
2573 assert_eq!(rebuilt.get_param_by_id(rid, "step_3_gate"), Some(0.0));
2575 assert_eq!(rebuilt.get_param_by_id(rid, "step_3_cv"), Some(2.0));
2576 assert_eq!(rebuilt.get_param_by_id(rid, "step_5_gate"), Some(0.0));
2577 assert_eq!(rebuilt.get_param_by_id(rid, "step_0_gate"), Some(1.0));
2578 }
2579
2580 #[test]
2583 fn test_ducker_knobs_reachable_and_survive_roundtrip() {
2584 use crate::modules::{Ducker, StereoOutput};
2585
2586 let mut patch = Patch::new(44100.0);
2587 let d = patch.add("duck", Ducker::default());
2588 let out = patch.add("output", StereoOutput::new());
2589 patch.connect(d.out("out"), out.in_("left")).unwrap();
2590 patch.set_output(out.id());
2591 let did = d.id();
2592
2593 let ids: Vec<String> = patch.param_infos(did).into_iter().map(|p| p.id).collect();
2596 assert!(
2597 ids.iter().any(|i| i == "depth"),
2598 "depth knob missing: {ids:?}"
2599 );
2600 assert!(
2601 ids.iter().any(|i| i == "thresh"),
2602 "thresh knob missing: {ids:?}"
2603 );
2604 assert!(
2605 ids.iter().any(|i| i == "amount"),
2606 "amount CV port missing: {ids:?}"
2607 );
2608 assert!(
2609 ids.iter().any(|i| i == "threshold"),
2610 "threshold CV port missing: {ids:?}"
2611 );
2612
2613 assert!(patch.set_param_by_id(did, "depth", 0.4));
2615 assert!(patch.set_param_by_id(did, "thresh", 0.7));
2616 assert_eq!(patch.get_param_by_id(did, "depth"), Some(0.4));
2617 assert_eq!(patch.get_param_by_id(did, "thresh"), Some(0.7));
2618
2619 let def = patch.to_def("Duck");
2620 assert_eq!(def.parameters.get("duck.depth"), Some(&0.4));
2621 assert_eq!(def.parameters.get("duck.thresh"), Some(&0.7));
2622
2623 let json = def.to_json().unwrap();
2624 let reloaded = PatchDef::from_json(&json).unwrap();
2625 let registry = ModuleRegistry::new();
2626 let rebuilt = Patch::from_def(&reloaded, ®istry, 44100.0).unwrap();
2627 let rid = rebuilt.get_node_id_by_name("duck").unwrap();
2628 assert_eq!(rebuilt.get_param_by_id(rid, "depth"), Some(0.4));
2629 assert_eq!(rebuilt.get_param_by_id(rid, "thresh"), Some(0.7));
2630 }
2631
2632 #[test]
2635 fn test_scale_quantizer_custom_scale_survives_roundtrip() {
2636 use crate::modules::{ScaleQuantizer, StereoOutput};
2637
2638 let whole_tone = [0.0, 200.0, 400.0, 600.0, 800.0, 1000.0];
2640 let test_voct = 0.1; let build_custom = || -> Patch {
2645 let mut patch = Patch::new(44100.0);
2646 let mut q = ScaleQuantizer::new(44100.0);
2647 q.set_custom_scale(&whole_tone);
2648 let qh = patch.add("q", q);
2649 let out = patch.add("output", StereoOutput::new());
2650 patch.connect(qh.out("out"), out.in_("left")).unwrap();
2651 patch.set_output(out.id());
2652 assert!(patch.set_param_by_id(qh.id(), "in", test_voct));
2653 patch.compile().unwrap();
2654 patch
2655 };
2656
2657 let mut original = build_custom();
2658 let qid_o = original.get_node_id_by_name("q").unwrap();
2659
2660 let def = original.to_def("Tuned");
2662 let q_state = &def.modules.iter().find(|m| m.name == "q").unwrap().state;
2663 assert!(
2664 q_state.is_some(),
2665 "custom scale must serialize into ModuleDef.state"
2666 );
2667
2668 let json = def.to_json().unwrap();
2669 let reloaded = PatchDef::from_json(&json).unwrap();
2670 let registry = ModuleRegistry::new();
2671 let mut rebuilt = Patch::from_def(&reloaded, ®istry, 44100.0).unwrap();
2672 let qid_r = rebuilt.get_node_id_by_name("q").unwrap();
2673
2674 let mut plain = {
2676 let mut patch = Patch::new(44100.0);
2677 let qh = patch.add("q", ScaleQuantizer::new(44100.0));
2678 let out = patch.add("output", StereoOutput::new());
2679 patch.connect(qh.out("out"), out.in_("left")).unwrap();
2680 patch.set_output(out.id());
2681 assert!(patch.set_param_by_id(qh.id(), "in", test_voct));
2682 patch.compile().unwrap();
2683 patch
2684 };
2685 let qid_p = plain.get_node_id_by_name("q").unwrap();
2686
2687 original.tick();
2688 rebuilt.tick();
2689 plain.tick();
2690 let o = original.get_output_value(qid_o, 10).unwrap();
2692 let r = rebuilt.get_output_value(qid_r, 10).unwrap();
2693 let p = plain.get_output_value(qid_p, 10).unwrap();
2694
2695 assert!(
2696 (o - r).abs() < 1e-9,
2697 "custom scale must survive round-trip: {o} vs {r}"
2698 );
2699 assert!(
2700 (o - p).abs() > 1e-6,
2701 "custom (whole-tone) quantization must differ from default 12-TET: {o} vs {p}"
2702 );
2703 }
2704}