1use crate::modules::common::{flush_denorm, sanitize_audio};
8use crate::port::{GraphModule, ParamId, PortId, PortSpec, PortValues, SignalKind};
9use crate::StdMap;
10use alloc::boxed::Box;
11use alloc::collections::VecDeque;
12use alloc::format;
13use alloc::string::{String, ToString};
14use alloc::vec::Vec;
15use serde::{Deserialize, Serialize};
16use slotmap::{DefaultKey, SlotMap};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum ValidationMode {
21 None,
23 #[default]
28 Warn,
29 Strict,
31}
32
33#[derive(Debug, Clone)]
35pub struct CompatibilityResult {
36 pub compatible: bool,
37 pub warning: Option<String>,
38}
39
40impl SignalKind {
41 pub fn is_compatible_with(&self, other: &SignalKind) -> CompatibilityResult {
44 use SignalKind::*;
45
46 if self == other {
48 return CompatibilityResult {
49 compatible: true,
50 warning: None,
51 };
52 }
53
54 match (self, other) {
56 (Audio, CvBipolar) | (CvBipolar, Audio) => CompatibilityResult {
58 compatible: true,
59 warning: Some("Audio/CV connection - ensure this is intentional".to_string()),
60 },
61
62 (CvBipolar, CvUnipolar) | (CvUnipolar, CvBipolar) => CompatibilityResult {
64 compatible: true,
65 warning: Some(
66 "Bipolar/Unipolar CV mismatch - signal may be clipped or offset".to_string(),
67 ),
68 },
69
70 (CvBipolar, VoltPerOctave) => CompatibilityResult {
72 compatible: true,
73 warning: None,
74 },
75
76 (VoltPerOctave, CvBipolar) => CompatibilityResult {
78 compatible: true,
79 warning: None,
80 },
81
82 (Gate, Trigger) | (Trigger, Gate) => CompatibilityResult {
84 compatible: true,
85 warning: Some("Gate/Trigger connection - timing behavior may differ".to_string()),
86 },
87
88 (Clock, Trigger) | (Trigger, Clock) => CompatibilityResult {
89 compatible: true,
90 warning: None,
91 },
92
93 (Clock, Gate) | (Gate, Clock) => CompatibilityResult {
94 compatible: true,
95 warning: Some("Clock/Gate connection - duty cycle may affect behavior".to_string()),
96 },
97
98 (Audio, VoltPerOctave) => CompatibilityResult {
100 compatible: true,
101 warning: Some(
102 "Audio-rate pitch modulation - ensure this is intentional".to_string(),
103 ),
104 },
105
106 (CvUnipolar, VoltPerOctave) => CompatibilityResult {
108 compatible: true,
109 warning: Some("Unipolar CV to V/Oct - may need offset adjustment".to_string()),
110 },
111
112 (VoltPerOctave, CvUnipolar) => CompatibilityResult {
114 compatible: true,
115 warning: Some("V/Oct to Unipolar - negative voltages will be clipped".to_string()),
116 },
117
118 (Audio, Gate) | (Audio, Trigger) => CompatibilityResult {
120 compatible: true,
121 warning: Some("Audio to Gate/Trigger - signal will be thresholded".to_string()),
122 },
123
124 _ => CompatibilityResult {
126 compatible: true,
127 warning: Some(format!("Unusual connection: {:?} -> {:?}", self, other)),
128 },
129 }
130 }
131}
132
133pub type NodeId = DefaultKey;
135
136pub type CableId = usize;
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
146pub struct PortRef {
147 pub node: NodeId,
148 pub port: PortId,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct Cable {
154 #[serde(default)]
156 pub id: CableId,
157 pub from: PortRef,
158 pub to: PortRef,
159 pub attenuation: Option<f64>,
162 pub offset: Option<f64>,
164}
165
166struct Node {
173 module_slot: usize,
174 name: String,
175 position: Option<(f32, f32)>,
176 param_overrides: StdMap<String, f64>,
181}
182
183#[derive(Debug, Clone, Default)]
188pub struct PatchMeta {
189 pub name: Option<String>,
191 pub author: Option<String>,
193 pub description: Option<String>,
195 pub tags: Vec<String>,
197}
198
199#[derive(Debug, Clone)]
204#[non_exhaustive]
205pub enum PatchError {
206 InvalidNode {
208 node: NodeId,
209 },
210 InvalidPort {
216 node: NodeId,
217 name: Option<String>,
218 port: Option<PortId>,
219 available: Vec<String>,
220 },
221 InvalidCable,
222 CycleDetected {
228 nodes: Vec<NodeId>,
229 names: Vec<String>,
230 },
231 CompilationFailed(String),
232 SignalMismatch {
234 from_kind: SignalKind,
235 to_kind: SignalKind,
236 message: String,
237 },
238}
239
240impl core::fmt::Display for PatchError {
241 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
242 match self {
243 PatchError::InvalidNode { node } => write!(f, "Invalid node: {:?}", node),
244 PatchError::InvalidPort {
245 node,
246 name,
247 port,
248 available,
249 } => {
250 write!(f, "Invalid port")?;
251 match (name, port) {
252 (Some(n), _) => write!(f, " '{}'", n)?,
253 (None, Some(p)) => write!(f, " #{}", p)?,
254 (None, None) => {}
255 }
256 write!(f, " on node {:?}", node)?;
257 if available.is_empty() {
258 write!(f, " (module exposes no matching ports)")
259 } else {
260 write!(f, " (available ports: {})", available.join(", "))
261 }
262 }
263 PatchError::InvalidCable => write!(f, "Invalid cable"),
264 PatchError::CycleDetected { nodes, names } => {
265 if names.is_empty() {
266 write!(f, "Cycle detected involving {} nodes", nodes.len())
267 } else {
268 write!(f, "Cycle detected: {}", names.join(" -> "))
269 }
270 }
271 PatchError::CompilationFailed(msg) => write!(f, "Compilation failed: {}", msg),
272 PatchError::SignalMismatch {
273 from_kind,
274 to_kind,
275 message,
276 } => write!(
277 f,
278 "Signal mismatch: {:?} -> {:?}: {}",
279 from_kind, to_kind, message
280 ),
281 }
282 }
283}
284
285#[cfg(feature = "std")]
286impl std::error::Error for PatchError {}
287
288#[derive(Clone)]
290pub struct NodeHandle {
291 id: NodeId,
292 spec: PortSpec,
293}
294
295impl NodeHandle {
296 pub fn id(&self) -> NodeId {
297 self.id
298 }
299
300 pub fn from_module(id: NodeId, module: &dyn GraphModule) -> Self {
302 Self {
303 id,
304 spec: module.port_spec().clone(),
305 }
306 }
307
308 pub fn output(&self, name: &str) -> Result<PortRef, PatchError> {
314 match self.spec.output_by_name(name) {
315 Some(port) => Ok(PortRef {
316 node: self.id,
317 port: port.id,
318 }),
319 None => Err(PatchError::InvalidPort {
320 node: self.id,
321 name: Some(name.to_string()),
322 port: None,
323 available: self.output_names().iter().map(|s| s.to_string()).collect(),
324 }),
325 }
326 }
327
328 pub fn input(&self, name: &str) -> Result<PortRef, PatchError> {
332 match self.spec.input_by_name(name) {
333 Some(port) => Ok(PortRef {
334 node: self.id,
335 port: port.id,
336 }),
337 None => Err(PatchError::InvalidPort {
338 node: self.id,
339 name: Some(name.to_string()),
340 port: None,
341 available: self.input_names().iter().map(|s| s.to_string()).collect(),
342 }),
343 }
344 }
345
346 pub fn out(&self, name: &str) -> PortRef {
351 self.output(name).unwrap_or_else(|_| {
352 panic!(
353 "Unknown output port: '{}'. Valid output ports: [{}]",
354 name,
355 self.output_names().join(", ")
356 )
357 })
358 }
359
360 pub fn in_(&self, name: &str) -> PortRef {
365 self.input(name).unwrap_or_else(|_| {
366 panic!(
367 "Unknown input port: '{}'. Valid input ports: [{}]",
368 name,
369 self.input_names().join(", ")
370 )
371 })
372 }
373
374 pub fn input_names(&self) -> Vec<&str> {
376 self.spec.inputs.iter().map(|p| p.name.as_str()).collect()
377 }
378
379 pub fn output_names(&self) -> Vec<&str> {
381 self.spec.outputs.iter().map(|p| p.name.as_str()).collect()
382 }
383
384 pub fn spec(&self) -> &PortSpec {
386 &self.spec
387 }
388}
389
390struct InEdge {
396 src_slot: usize,
398 attenuation: f64,
401 offset: f64,
404}
405
406struct InputPlan {
408 port_id: PortId,
410 default: f64,
412 normalled_to: Option<PortId>,
414 has_connection: bool,
418 edges: Vec<InEdge>,
420}
421
422struct NormalledPlan {
428 port_id: PortId,
430 source: PortId,
433 default: f64,
435}
436
437struct NodeExec {
439 out_base: usize,
442 out_ids: Vec<PortId>,
444 inputs: Vec<InputPlan>,
446 normalled_pending: Vec<NormalledPlan>,
449 wanted_outputs: u32,
453}
454
455impl NodeExec {
456 fn gather(&self, out_buf: &[f64], dst: &mut PortValues) {
461 dst.clear();
462 for plan in &self.inputs {
464 if plan.has_connection {
465 let mut sum = 0.0;
466 for e in &plan.edges {
467 let attenuated = out_buf[e.src_slot] * e.attenuation;
473 sum += attenuated + e.offset;
474 }
475 dst.set(plan.port_id, sum);
476 } else if plan.normalled_to.is_none() {
477 dst.set(plan.port_id, plan.default);
478 }
479 }
481 for plan in &self.normalled_pending {
489 let value = dst.get(plan.source).unwrap_or(plan.default);
490 dst.set(plan.port_id, value);
491 }
492 }
493
494 fn scatter(&self, src: &PortValues, out_buf: &mut [f64]) {
512 for (k, &port_id) in self.out_ids.iter().enumerate() {
513 if let Some(value) = src.get_at(k, port_id) {
514 out_buf[self.out_base + k] = flush_denorm(sanitize_audio(value));
515 }
516 }
517 }
518}
519
520fn resolve_normalled_chains(inputs: &mut [InputPlan]) {
538 let n = inputs.len();
539 let original: Vec<Option<PortId>> = inputs.iter().map(|p| p.normalled_to).collect();
544 for i in 0..n {
545 if inputs[i].has_connection || original[i].is_none() {
547 continue;
548 }
549 let mut terminal = None;
550 let mut cursor = original[i];
551 for _ in 0..n {
553 let Some(pid) = cursor else { break };
554 match inputs.iter().position(|p| p.port_id == pid) {
555 None => break,
557 Some(idx) => {
558 if inputs[idx].has_connection || original[idx].is_none() {
559 terminal = Some(pid);
561 break;
562 }
563 cursor = original[idx];
565 }
566 }
567 }
568 inputs[i].normalled_to = terminal;
569 }
570}
571
572fn collect_normalled_pending(inputs: &[InputPlan]) -> Vec<NormalledPlan> {
584 let mut claimed: Vec<PortId> = inputs
585 .iter()
586 .filter(|p| p.has_connection || p.normalled_to.is_none())
587 .map(|p| p.port_id)
588 .collect();
589 let mut pending = Vec::new();
590 for plan in inputs {
591 if plan.has_connection {
592 continue;
593 }
594 let Some(source) = plan.normalled_to else {
595 continue;
596 };
597 if claimed.contains(&plan.port_id) {
598 continue;
599 }
600 claimed.push(plan.port_id);
601 pending.push(NormalledPlan {
602 port_id: plan.port_id,
603 source,
604 default: plan.default,
605 });
606 }
607 pending
608}
609
610#[derive(Default)]
617struct Routing {
618 nodes: Vec<NodeExec>,
620 out_buf: Vec<f64>,
622 out_slot_index: StdMap<PortRef, usize>,
625 output_slots: Option<(usize, usize)>,
627 scratch_in: Vec<PortValues>,
629 scratch_out: Vec<PortValues>,
631}
632
633impl Routing {
634 fn read_output(&self) -> (f64, f64) {
637 match self.output_slots {
638 Some((left, right)) => (self.out_buf[left], self.out_buf[right]),
639 None => (0.0, 0.0),
640 }
641 }
642}
643
644pub struct Patch {
646 nodes: SlotMap<NodeId, Node>,
647 modules: Vec<Box<dyn GraphModule>>,
652 cables: Vec<Cable>,
653
654 kept_live_outputs: Vec<PortRef>,
660
661 next_cable_id: CableId,
663
664 execution_order: Vec<NodeId>,
666 routing: Routing,
668
669 dirty: bool,
672 last_compile_error: Option<PatchError>,
674
675 sample_rate: f64,
677
678 output_node: Option<NodeId>,
680
681 validation_mode: ValidationMode,
683 warnings: Vec<String>,
684
685 meta: PatchMeta,
687}
688
689impl Patch {
690 pub fn new(sample_rate: f64) -> Self {
692 Self {
693 nodes: SlotMap::new(),
694 modules: Vec::new(),
695 cables: Vec::new(),
696 kept_live_outputs: Vec::new(),
697 next_cable_id: 0,
698 execution_order: Vec::new(),
699 routing: Routing::default(),
700 dirty: true,
703 last_compile_error: None,
704 sample_rate,
705 output_node: None,
706 validation_mode: ValidationMode::Warn,
709 warnings: Vec::new(),
710 meta: PatchMeta::default(),
711 }
712 }
713
714 pub fn meta(&self) -> &PatchMeta {
716 &self.meta
717 }
718
719 pub fn meta_mut(&mut self) -> &mut PatchMeta {
721 &mut self.meta
722 }
723
724 pub fn set_meta(&mut self, meta: PatchMeta) {
726 self.meta = meta;
727 }
728
729 pub fn set_validation_mode(&mut self, mode: ValidationMode) {
731 self.validation_mode = mode;
732 }
733
734 pub fn validation_mode(&self) -> ValidationMode {
736 self.validation_mode
737 }
738
739 pub fn warnings(&self) -> &[String] {
741 &self.warnings
742 }
743
744 pub fn clear_warnings(&mut self) {
746 self.warnings.clear();
747 }
748
749 pub fn sample_rate(&self) -> f64 {
751 self.sample_rate
752 }
753
754 pub fn add<M: GraphModule + 'static>(
756 &mut self,
757 name: impl Into<String>,
758 module: M,
759 ) -> NodeHandle {
760 self.add_boxed(name, Box::new(module))
761 }
762
763 pub fn add_boxed(
765 &mut self,
766 name: impl Into<String>,
767 mut module: Box<dyn GraphModule>,
768 ) -> NodeHandle {
769 module.set_sample_rate(self.sample_rate);
770 let spec = module.port_spec().clone();
771 let module_slot = self.modules.len();
772 self.modules.push(module);
773 let id = self.nodes.insert(Node {
774 module_slot,
775 name: name.into(),
776 position: None,
777 param_overrides: StdMap::new(),
778 });
779 self.invalidate();
780 NodeHandle { id, spec }
781 }
782
783 fn module_of(&self, node: NodeId) -> Option<&dyn GraphModule> {
785 let slot = self.nodes.get(node)?.module_slot;
786 self.modules.get(slot).map(|m| m.as_ref())
787 }
788
789 fn module_of_mut(&mut self, node: NodeId) -> Option<&mut (dyn GraphModule + '_)> {
791 let slot = self.nodes.get(node)?.module_slot;
792 Some(self.modules.get_mut(slot)?.as_mut())
793 }
794
795 pub fn remove(&mut self, node: NodeId) -> Result<(), PatchError> {
797 let Some(removed) = self.nodes.remove(node) else {
798 return Err(PatchError::InvalidNode { node });
799 };
800
801 self.modules.remove(removed.module_slot);
803 for (_, other) in &mut self.nodes {
804 if other.module_slot > removed.module_slot {
805 other.module_slot -= 1;
806 }
807 }
808
809 self.cables
811 .retain(|cable| cable.from.node != node && cable.to.node != node);
812
813 self.kept_live_outputs.retain(|port| port.node != node);
817
818 if self.output_node == Some(node) {
819 self.output_node = None;
820 }
821
822 self.invalidate();
823 Ok(())
824 }
825
826 pub fn keep_output_live(&mut self, node: NodeId, port: PortId) -> bool {
875 let port_ref = PortRef { node, port };
876 if self.kept_live_outputs.contains(&port_ref) {
877 return false;
878 }
879 self.kept_live_outputs.push(port_ref);
880 self.dirty = true;
883 true
884 }
885
886 pub fn release_output_live(&mut self, node: NodeId, port: PortId) -> bool {
889 let port_ref = PortRef { node, port };
890 let before = self.kept_live_outputs.len();
891 self.kept_live_outputs.retain(|kept| *kept != port_ref);
892 let removed = self.kept_live_outputs.len() != before;
893 if removed {
894 self.dirty = true;
895 }
896 removed
897 }
898
899 pub fn clear_kept_live_outputs(&mut self) -> bool {
902 if self.kept_live_outputs.is_empty() {
903 return false;
904 }
905 self.kept_live_outputs.clear();
906 self.dirty = true;
907 true
908 }
909
910 pub fn kept_live_outputs(&self) -> &[PortRef] {
913 &self.kept_live_outputs
914 }
915
916 fn alloc_cable_id(&mut self) -> CableId {
918 let id = self.next_cable_id;
919 self.next_cable_id += 1;
920 id
921 }
922
923 pub fn connect(&mut self, from: PortRef, to: PortRef) -> Result<CableId, PatchError> {
928 self.validate_output_port(from)?;
929 self.validate_input_port(to)?;
930 self.validate_signal_compatibility(from, to)?;
931
932 let id = self.alloc_cable_id();
933 self.cables.push(Cable {
934 id,
935 from,
936 to,
937 attenuation: None,
938 offset: None,
939 });
940 self.invalidate();
941 Ok(id)
942 }
943
944 pub fn connect_attenuated(
946 &mut self,
947 from: PortRef,
948 to: PortRef,
949 attenuation: f64,
950 ) -> Result<CableId, PatchError> {
951 self.validate_output_port(from)?;
952 self.validate_input_port(to)?;
953 self.validate_signal_compatibility(from, to)?;
954
955 let id = self.alloc_cable_id();
956 self.cables.push(Cable {
957 id,
958 from,
959 to,
960 attenuation: Some(attenuation.clamp(0.0, 1.0)),
961 offset: None,
962 });
963 self.invalidate();
964 Ok(id)
965 }
966
967 pub fn connect_modulated(
971 &mut self,
972 from: PortRef,
973 to: PortRef,
974 attenuation: f64,
975 offset: f64,
976 ) -> Result<CableId, PatchError> {
977 self.validate_output_port(from)?;
978 self.validate_input_port(to)?;
979 self.validate_signal_compatibility(from, to)?;
980
981 let id = self.alloc_cable_id();
982 self.cables.push(Cable {
983 id,
984 from,
985 to,
986 attenuation: Some(attenuation.clamp(-2.0, 2.0)),
987 offset: Some(offset.clamp(-10.0, 10.0)),
988 });
989 self.invalidate();
990 Ok(id)
991 }
992
993 fn validate_signal_compatibility(
995 &mut self,
996 from: PortRef,
997 to: PortRef,
998 ) -> Result<(), PatchError> {
999 if self.validation_mode == ValidationMode::None {
1000 return Ok(());
1001 }
1002
1003 let from_kind = self.get_output_port_kind(from);
1005 let to_kind = self.get_input_port_kind(to);
1006
1007 if let (Some(from_kind), Some(to_kind)) = (from_kind, to_kind) {
1008 let result = from_kind.is_compatible_with(&to_kind);
1009
1010 if let Some(warning) = result.warning {
1011 let from_name = self.get_name(from.node).unwrap_or("unknown");
1012 let to_name = self.get_name(to.node).unwrap_or("unknown");
1013 let full_warning = format!(
1014 "{}.{} -> {}.{}: {}",
1015 from_name, from.port, to_name, to.port, warning
1016 );
1017
1018 match self.validation_mode {
1019 ValidationMode::Warn => {
1020 self.warnings.push(full_warning);
1021 }
1022 ValidationMode::Strict => {
1023 return Err(PatchError::SignalMismatch {
1024 from_kind,
1025 to_kind,
1026 message: warning,
1027 });
1028 }
1029 ValidationMode::None => {}
1030 }
1031 }
1032 }
1033
1034 Ok(())
1035 }
1036
1037 fn get_output_port_kind(&self, port_ref: PortRef) -> Option<SignalKind> {
1039 self.module_of(port_ref.node)?
1040 .port_spec()
1041 .outputs
1042 .iter()
1043 .find(|p| p.id == port_ref.port)
1044 .map(|p| p.kind)
1045 }
1046
1047 fn get_input_port_kind(&self, port_ref: PortRef) -> Option<SignalKind> {
1049 self.module_of(port_ref.node)?
1050 .port_spec()
1051 .inputs
1052 .iter()
1053 .find(|p| p.id == port_ref.port)
1054 .map(|p| p.kind)
1055 }
1056
1057 pub fn mult(&mut self, from: PortRef, to: &[PortRef]) -> Result<Vec<CableId>, PatchError> {
1059 to.iter().map(|&dest| self.connect(from, dest)).collect()
1060 }
1061
1062 pub fn disconnect(&mut self, cable_id: CableId) -> Result<(), PatchError> {
1067 let idx = self
1068 .cables
1069 .iter()
1070 .position(|c| c.id == cable_id)
1071 .ok_or(PatchError::InvalidCable)?;
1072 self.cables.remove(idx);
1073 self.invalidate();
1074 Ok(())
1075 }
1076
1077 pub fn set_output(&mut self, node: NodeId) {
1084 self.output_node = Some(node);
1085 self.dirty = true;
1086 }
1087
1088 pub fn output_node(&self) -> Option<NodeId> {
1090 self.output_node
1091 }
1092
1093 pub fn try_set_output(&mut self, node: NodeId) -> Result<(), PatchError> {
1098 let module = self
1099 .module_of(node)
1100 .ok_or(PatchError::InvalidNode { node })?;
1101 if module.port_spec().outputs.is_empty() {
1102 return Err(PatchError::InvalidPort {
1103 node,
1104 name: None,
1105 port: None,
1106 available: Vec::new(),
1107 });
1108 }
1109 self.output_node = Some(node);
1110 self.dirty = true;
1111 Ok(())
1112 }
1113
1114 pub fn set_param(&mut self, node: NodeId, param: ParamId, value: f64) {
1116 if let Some(module) = self.module_of_mut(node) {
1117 module.set_param(param, value);
1118 }
1119 }
1120
1121 pub fn get_param(&self, node: NodeId, param: ParamId) -> Option<f64> {
1123 self.module_of(node).and_then(|m| m.get_param(param))
1124 }
1125
1126 pub fn set_position(&mut self, node: NodeId, position: (f32, f32)) {
1128 if let Some(n) = self.nodes.get_mut(node) {
1129 n.position = Some(position);
1130 }
1131 }
1132
1133 pub fn get_position(&self, node: NodeId) -> Option<(f32, f32)> {
1135 self.nodes.get(node).and_then(|n| n.position)
1136 }
1137
1138 pub fn get_name(&self, node: NodeId) -> Option<&str> {
1140 self.nodes.get(node).map(|n| n.name.as_str())
1141 }
1142
1143 pub fn node_count(&self) -> usize {
1145 self.nodes.len()
1146 }
1147
1148 pub fn cable_count(&self) -> usize {
1150 self.cables.len()
1151 }
1152
1153 pub fn cables(&self) -> &[Cable] {
1155 &self.cables
1156 }
1157
1158 pub fn execution_order(&self) -> &[NodeId] {
1160 &self.execution_order
1161 }
1162
1163 fn invalidate(&mut self) {
1169 self.execution_order.clear();
1170 self.routing = Routing::default();
1171 self.dirty = true;
1172 }
1173
1174 fn validate_output_port(&self, port_ref: PortRef) -> Result<(), PatchError> {
1175 let spec = self
1176 .module_of(port_ref.node)
1177 .ok_or(PatchError::InvalidNode {
1178 node: port_ref.node,
1179 })?
1180 .port_spec();
1181 if spec.outputs.iter().any(|p| p.id == port_ref.port) {
1182 Ok(())
1183 } else {
1184 Err(PatchError::InvalidPort {
1185 node: port_ref.node,
1186 name: None,
1187 port: Some(port_ref.port),
1188 available: spec.outputs.iter().map(|p| p.name.clone()).collect(),
1189 })
1190 }
1191 }
1192
1193 fn validate_input_port(&self, port_ref: PortRef) -> Result<(), PatchError> {
1194 let spec = self
1195 .module_of(port_ref.node)
1196 .ok_or(PatchError::InvalidNode {
1197 node: port_ref.node,
1198 })?
1199 .port_spec();
1200 if spec.inputs.iter().any(|p| p.id == port_ref.port) {
1201 Ok(())
1202 } else {
1203 Err(PatchError::InvalidPort {
1204 node: port_ref.node,
1205 name: None,
1206 port: Some(port_ref.port),
1207 available: spec.inputs.iter().map(|p| p.name.clone()).collect(),
1208 })
1209 }
1210 }
1211
1212 pub fn compile(&mut self) -> Result<(), PatchError> {
1220 let order = match self.topological_sort() {
1221 Ok(order) => order,
1222 Err(e) => {
1223 self.execution_order.clear();
1224 self.routing = Routing::default();
1225 self.dirty = false;
1228 self.last_compile_error = Some(e.clone());
1229 return Err(e);
1230 }
1231 };
1232 self.execution_order = order;
1233
1234 self.reorder_modules_for_execution();
1238
1239 self.build_routing();
1241
1242 self.dirty = false;
1243 self.last_compile_error = None;
1244 Ok(())
1245 }
1246
1247 fn consumed_output_mask(&self, node: NodeId, out_ids: &[PortId]) -> u32 {
1265 if out_ids.len() > u32::BITS as usize || self.output_node == Some(node) {
1266 return u32::MAX;
1267 }
1268 let mut wanted = 0u32;
1269 for (k, &port) in out_ids.iter().enumerate() {
1270 let port_ref = PortRef { node, port };
1271 let consumed = self.cables.iter().any(|cable| cable.from == port_ref)
1272 || self.kept_live_outputs.contains(&port_ref);
1273 if consumed {
1274 wanted |= 1 << k;
1275 }
1276 }
1277 wanted
1278 }
1279
1280 fn reorder_modules_for_execution(&mut self) {
1288 let mut taken: Vec<Option<Box<dyn GraphModule>>> = core::mem::take(&mut self.modules)
1289 .into_iter()
1290 .map(Some)
1291 .collect();
1292 let mut old_to_new: Vec<Option<usize>> = taken.iter().map(|_| None).collect();
1293 let mut ordered: Vec<Box<dyn GraphModule>> = Vec::with_capacity(taken.len());
1294
1295 for &node_id in &self.execution_order {
1296 let Some(node) = self.nodes.get(node_id) else {
1297 continue;
1298 };
1299 let old = node.module_slot;
1300 let Some(module) = taken.get_mut(old).and_then(Option::take) else {
1301 continue;
1302 };
1303 old_to_new[old] = Some(ordered.len());
1304 ordered.push(module);
1305 }
1306 for (old, slot) in taken.iter_mut().enumerate() {
1309 if let Some(module) = slot.take() {
1310 old_to_new[old] = Some(ordered.len());
1311 ordered.push(module);
1312 }
1313 }
1314
1315 self.modules = ordered;
1316 for (_, node) in &mut self.nodes {
1318 if let Some(new) = old_to_new.get(node.module_slot).copied().flatten() {
1319 node.module_slot = new;
1320 }
1321 }
1322 }
1323
1324 fn build_routing(&mut self) {
1328 let mut routing = Routing::default();
1329
1330 let mut slot: usize = 0;
1334 for &node_id in &self.execution_order {
1335 let spec = self
1336 .module_of(node_id)
1337 .expect("execution_order only holds live nodes")
1338 .port_spec();
1339 let out_base = slot;
1340 let mut out_ids = Vec::with_capacity(spec.outputs.len());
1341 for output in &spec.outputs {
1342 routing.out_slot_index.insert(
1343 PortRef {
1344 node: node_id,
1345 port: output.id,
1346 },
1347 slot,
1348 );
1349 out_ids.push(output.id);
1350 slot += 1;
1351 }
1352 routing.nodes.push(NodeExec {
1353 out_base,
1354 out_ids,
1355 inputs: Vec::new(),
1356 normalled_pending: Vec::new(),
1357 wanted_outputs: u32::MAX,
1358 });
1359 }
1360 routing.out_buf.resize(slot, 0.0);
1361
1362 for (exec_idx, &node_id) in self.execution_order.iter().enumerate() {
1365 let node = self
1366 .nodes
1367 .get(node_id)
1368 .expect("execution_order only holds live nodes");
1369 let spec = self.modules[node.module_slot].port_spec();
1370
1371 let mut scratch_in = PortValues::new();
1372 let mut scratch_out = PortValues::new();
1373 let mut inputs = Vec::with_capacity(spec.inputs.len());
1374
1375 for input in &spec.inputs {
1376 let port_ref = PortRef {
1377 node: node_id,
1378 port: input.id,
1379 };
1380 let mut edges = Vec::new();
1381 let mut has_connection = false;
1382 for cable in &self.cables {
1383 if cable.to == port_ref {
1384 has_connection = true;
1385 if let Some(&src_slot) = routing.out_slot_index.get(&cable.from) {
1387 edges.push(InEdge {
1388 src_slot,
1389 attenuation: cable.attenuation.unwrap_or(1.0),
1390 offset: cable.offset.unwrap_or(0.0),
1391 });
1392 }
1393 }
1394 }
1395 let default = node
1399 .param_overrides
1400 .get(&input.name)
1401 .copied()
1402 .unwrap_or(input.default);
1403 inputs.push(InputPlan {
1404 port_id: input.id,
1405 default,
1406 normalled_to: input.normalled_to,
1407 has_connection,
1408 edges,
1409 });
1410 scratch_in.set(input.id, 0.0);
1411 }
1412 resolve_normalled_chains(&mut inputs);
1415 let normalled_pending = collect_normalled_pending(&inputs);
1416 for output in &spec.outputs {
1417 scratch_out.set(output.id, 0.0);
1418 }
1419
1420 routing.nodes[exec_idx].wanted_outputs =
1421 self.consumed_output_mask(node_id, &routing.nodes[exec_idx].out_ids);
1422 routing.nodes[exec_idx].normalled_pending = normalled_pending;
1423 routing.nodes[exec_idx].inputs = inputs;
1424 routing.scratch_in.push(scratch_in);
1425 routing.scratch_out.push(scratch_out);
1426 }
1427
1428 routing.output_slots = self.output_node.and_then(|out_node| {
1430 let outputs = &self.module_of(out_node)?.port_spec().outputs;
1431 let left_id = outputs.first()?.id;
1432 let left = *routing.out_slot_index.get(&PortRef {
1433 node: out_node,
1434 port: left_id,
1435 })?;
1436 let right = outputs
1437 .get(1)
1438 .and_then(|p| {
1439 routing
1440 .out_slot_index
1441 .get(&PortRef {
1442 node: out_node,
1443 port: p.id,
1444 })
1445 .copied()
1446 })
1447 .unwrap_or(left);
1448 Some((left, right))
1449 });
1450
1451 self.routing = routing;
1452 }
1453
1454 fn node_breaks_feedback(&self, node: NodeId) -> bool {
1456 self.module_of(node)
1457 .map(|m| m.breaks_feedback_cycle())
1458 .unwrap_or(false)
1459 }
1460
1461 fn topological_sort(&self) -> Result<Vec<NodeId>, PatchError> {
1462 let mut in_degree: StdMap<NodeId, usize> = self.nodes.keys().map(|k| (k, 0)).collect();
1463 let mut successors: StdMap<NodeId, Vec<NodeId>> =
1464 self.nodes.keys().map(|k| (k, Vec::new())).collect();
1465
1466 for cable in &self.cables {
1467 if self.node_breaks_feedback(cable.to.node) {
1473 continue;
1474 }
1475 if let Some(deg) = in_degree.get_mut(&cable.to.node) {
1476 *deg += 1;
1477 }
1478 if let Some(succ) = successors.get_mut(&cable.from.node) {
1479 succ.push(cable.to.node);
1480 }
1481 }
1482
1483 let mut queue: VecDeque<NodeId> = VecDeque::new();
1487 for id in self.nodes.keys() {
1488 if in_degree.get(&id).copied().unwrap_or(0) == 0 {
1489 queue.push_back(id);
1490 }
1491 }
1492
1493 let mut result = Vec::with_capacity(self.nodes.len());
1494
1495 while let Some(node) = queue.pop_front() {
1496 result.push(node);
1497 if let Some(succ) = successors.get(&node) {
1499 for &s in succ {
1500 if let Some(deg) = in_degree.get_mut(&s) {
1501 *deg -= 1;
1502 if *deg == 0 {
1503 queue.push_back(s);
1504 }
1505 }
1506 }
1507 }
1508 }
1509
1510 if result.len() != self.nodes.len() {
1511 let nodes: Vec<NodeId> = self
1513 .nodes
1514 .keys()
1515 .filter(|k| in_degree.get(k).copied().unwrap_or(0) > 0)
1516 .collect();
1517 let names = nodes
1518 .iter()
1519 .map(|&id| self.get_name(id).unwrap_or("<unknown>").to_string())
1520 .collect();
1521 return Err(PatchError::CycleDetected { nodes, names });
1522 }
1523
1524 Ok(result)
1525 }
1526
1527 pub fn last_compile_error(&self) -> Option<&PatchError> {
1533 self.last_compile_error.as_ref()
1534 }
1535
1536 pub fn tick(&mut self) -> (f64, f64) {
1552 if self.dirty {
1556 let _ = self.compile();
1557 }
1558 self.tick_step()
1559 }
1560
1561 pub fn tick_block(&mut self, out_left: &mut [f64], out_right: &mut [f64]) {
1571 if self.dirty {
1572 let _ = self.compile();
1573 }
1574 let frames = out_left.len().min(out_right.len());
1575 for frame in 0..frames {
1576 let (left, right) = self.tick_step();
1577 out_left[frame] = left;
1578 out_right[frame] = right;
1579 }
1580 }
1581
1582 fn tick_step(&mut self) -> (f64, f64) {
1590 let Routing {
1591 nodes,
1592 out_buf,
1593 scratch_in,
1594 scratch_out,
1595 ..
1596 } = &mut self.routing;
1597 let modules = &mut self.modules;
1598
1599 if nodes.len() != modules.len() {
1602 debug_assert!(nodes.is_empty(), "routing plan is out of step with modules");
1603 return (0.0, 0.0);
1604 }
1605
1606 for (((exec, module), inputs), outputs) in nodes
1607 .iter()
1608 .zip(modules.iter_mut())
1609 .zip(scratch_in.iter_mut())
1610 .zip(scratch_out.iter_mut())
1611 {
1612 exec.gather(out_buf, inputs);
1614
1615 outputs.clear();
1618 module.tick_masked(inputs, outputs, exec.wanted_outputs);
1619
1620 exec.scatter(outputs, out_buf);
1622 }
1623
1624 self.routing.read_output()
1625 }
1626
1627 pub fn reset(&mut self) {
1629 for module in &mut self.modules {
1630 module.reset();
1631 }
1632 for value in self.routing.out_buf.iter_mut() {
1633 *value = 0.0;
1634 }
1635 }
1636
1637 pub fn nodes(&self) -> impl Iterator<Item = (NodeId, &str, &dyn GraphModule)> {
1639 self.nodes.iter().map(|(id, node)| {
1640 (
1641 id,
1642 node.name.as_str(),
1643 self.modules[node.module_slot].as_ref(),
1644 )
1645 })
1646 }
1647
1648 pub fn get_node_id_by_name(&self, name: &str) -> Option<NodeId> {
1650 self.nodes
1651 .iter()
1652 .find(|(_, node)| node.name == name)
1653 .map(|(id, _)| id)
1654 }
1655
1656 pub fn get_handle_by_name(&self, name: &str) -> Option<NodeHandle> {
1658 self.nodes
1659 .iter()
1660 .find(|(_, node)| node.name == name)
1661 .map(|(id, node)| NodeHandle::from_module(id, self.modules[node.module_slot].as_ref()))
1662 }
1663
1664 pub fn disconnect_ports(&mut self, from: PortRef, to: PortRef) -> Result<(), PatchError> {
1666 let idx = self
1667 .cables
1668 .iter()
1669 .position(|c| c.from == from && c.to == to)
1670 .ok_or(PatchError::InvalidCable)?;
1671
1672 self.cables.remove(idx);
1673 self.invalidate();
1674 Ok(())
1675 }
1676
1677 pub fn module_names(&self) -> Vec<&str> {
1679 self.nodes
1680 .iter()
1681 .map(|(_, node)| node.name.as_str())
1682 .collect()
1683 }
1684
1685 pub fn get_output_value(&self, node: NodeId, port: PortId) -> Option<f64> {
1703 self.routing
1704 .out_slot_index
1705 .get(&PortRef { node, port })
1706 .map(|&slot| self.routing.out_buf[slot])
1707 }
1708
1709 pub fn get_output_signal_kind(&self, node: NodeId, port: PortId) -> Option<SignalKind> {
1711 self.module_of(node)?
1712 .port_spec()
1713 .outputs
1714 .iter()
1715 .find(|p| p.id == port)
1716 .map(|p| p.kind)
1717 }
1718}
1719
1720#[cfg(feature = "alloc")]
1723fn is_control_input(kind: SignalKind) -> bool {
1724 kind != SignalKind::Audio
1725}
1726
1727#[cfg(feature = "alloc")]
1730fn port_param_info(port: &crate::port::PortDef, value: f64) -> crate::introspection::ParamInfo {
1731 let (min, max) = port.kind.voltage_range();
1732 crate::introspection::ParamInfo::new(port.name.clone(), port.name.clone())
1733 .with_range(min, max)
1734 .with_default(port.default)
1735 .with_value(value)
1736}
1737
1738#[cfg(feature = "alloc")]
1750impl Patch {
1751 pub fn param_infos(&self, node: NodeId) -> Vec<crate::introspection::ParamInfo> {
1757 let Some(n) = self.nodes.get(node) else {
1758 return Vec::new();
1759 };
1760 let module = self.modules[n.module_slot].as_ref();
1761 let spec = module.port_spec();
1762 let mut infos: Vec<crate::introspection::ParamInfo> = module
1763 .introspect()
1764 .map(|i| i.param_infos())
1765 .unwrap_or_default()
1766 .into_iter()
1767 .filter(|p| spec.input_by_name(&p.id).is_none())
1769 .collect();
1770
1771 for input in &spec.inputs {
1772 if !is_control_input(input.kind) {
1773 continue;
1774 }
1775 let value = n
1776 .param_overrides
1777 .get(&input.name)
1778 .copied()
1779 .unwrap_or(input.default);
1780 infos.push(port_param_info(input, value));
1781 }
1782 infos
1783 }
1784
1785 pub fn get_param_by_id(&self, node: NodeId, id: &str) -> Option<f64> {
1787 let n = self.nodes.get(node)?;
1788 let module = self.modules[n.module_slot].as_ref();
1789 if let Some(port) = module.port_spec().input_by_name(id) {
1791 if is_control_input(port.kind) {
1792 return Some(n.param_overrides.get(id).copied().unwrap_or(port.default));
1793 }
1794 }
1795 module
1796 .introspect()
1797 .and_then(|i| i.get_param_info(id))
1798 .map(|p| p.value)
1799 }
1800
1801 pub fn set_param_by_id(&mut self, node: NodeId, id: &str, value: f64) -> bool {
1807 let is_port = self
1809 .module_of(node)
1810 .and_then(|m| m.port_spec().input_by_name(id))
1811 .map(|p| is_control_input(p.kind))
1812 .unwrap_or(false);
1813
1814 if is_port {
1815 if let Some(n) = self.nodes.get_mut(node) {
1816 n.param_overrides.insert(id.to_string(), value);
1817 }
1818 self.invalidate();
1820 return true;
1821 }
1822
1823 if let Some(intro) = self.module_of_mut(node).and_then(|m| m.introspect_mut()) {
1824 return intro.set_param_by_id(id, value);
1825 }
1826 false
1827 }
1828
1829 pub fn deserialize_module_state(
1838 &mut self,
1839 node: NodeId,
1840 state: &serde_json::Value,
1841 ) -> Result<(), String> {
1842 match self.module_of_mut(node) {
1843 Some(module) => module.deserialize_state(state),
1844 None => Ok(()),
1845 }
1846 }
1847}
1848
1849impl core::fmt::Debug for Patch {
1853 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1854 struct NodeDebug<'a> {
1856 id: NodeId,
1857 name: &'a str,
1858 type_id: &'a str,
1859 }
1860 impl core::fmt::Debug for NodeDebug<'_> {
1861 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1862 write!(f, "{:?}: {} ({})", self.id, self.name, self.type_id)
1863 }
1864 }
1865
1866 let nodes: Vec<NodeDebug> = self
1867 .nodes
1868 .iter()
1869 .map(|(id, n)| NodeDebug {
1870 id,
1871 name: n.name.as_str(),
1872 type_id: self.modules[n.module_slot].type_id(),
1873 })
1874 .collect();
1875
1876 f.debug_struct("Patch")
1877 .field("sample_rate", &self.sample_rate)
1878 .field("nodes", &nodes)
1879 .field("cables", &self.cables)
1880 .field("output_node", &self.output_node)
1881 .field("validation_mode", &self.validation_mode)
1882 .field("dirty", &self.dirty)
1883 .field("warnings", &self.warnings.len())
1884 .finish()
1885 }
1886}
1887
1888#[cfg(test)]
1889mod tests {
1890 use super::*;
1891 use crate::port::{PortDef, SignalKind};
1892 use alloc::vec;
1893
1894 struct Passthrough {
1896 spec: PortSpec,
1897 }
1898
1899 impl Passthrough {
1900 fn new() -> Self {
1901 Self {
1902 spec: PortSpec {
1903 inputs: vec![PortDef::new(0, "in", SignalKind::Audio)],
1904 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
1905 },
1906 }
1907 }
1908 }
1909
1910 impl GraphModule for Passthrough {
1911 fn port_spec(&self) -> &PortSpec {
1912 &self.spec
1913 }
1914
1915 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1916 let input = inputs.get_or(0, 0.0);
1917 outputs.set(10, input);
1918 }
1919
1920 fn reset(&mut self) {}
1921
1922 fn set_sample_rate(&mut self, _: f64) {}
1923 }
1924
1925 #[test]
1926 fn test_add_module() {
1927 let mut patch = Patch::new(44100.0);
1928 let handle = patch.add("test", Passthrough::new());
1929 assert_eq!(patch.node_count(), 1);
1930 assert!(patch.get_name(handle.id()).is_some());
1931 }
1932
1933 #[test]
1934 fn test_connect() {
1935 let mut patch = Patch::new(44100.0);
1936 let a = patch.add("a", Passthrough::new());
1937 let b = patch.add("b", Passthrough::new());
1938
1939 let result = patch.connect(a.out("out"), b.in_("in"));
1940 assert!(result.is_ok());
1941 assert_eq!(patch.cable_count(), 1);
1942 }
1943
1944 #[test]
1945 fn test_topological_sort() {
1946 let mut patch = Patch::new(44100.0);
1947 let a = patch.add("a", Passthrough::new());
1948 let b = patch.add("b", Passthrough::new());
1949 let c = patch.add("c", Passthrough::new());
1950
1951 patch.connect(a.out("out"), b.in_("in")).unwrap();
1953 patch.connect(b.out("out"), c.in_("in")).unwrap();
1954
1955 patch.compile().unwrap();
1956
1957 let order = patch.execution_order();
1958 let a_pos = order.iter().position(|&x| x == a.id()).unwrap();
1959 let b_pos = order.iter().position(|&x| x == b.id()).unwrap();
1960 let c_pos = order.iter().position(|&x| x == c.id()).unwrap();
1961
1962 assert!(a_pos < b_pos, "A should come before B");
1963 assert!(b_pos < c_pos, "B should come before C");
1964 }
1965
1966 #[test]
1967 fn test_cycle_detection() {
1968 let mut patch = Patch::new(44100.0);
1969 let a = patch.add("a", Passthrough::new());
1970 let b = patch.add("b", Passthrough::new());
1971
1972 patch.connect(a.out("out"), b.in_("in")).unwrap();
1974 patch.connect(b.out("out"), a.in_("in")).unwrap();
1975
1976 let result = patch.compile();
1977 assert!(matches!(result, Err(PatchError::CycleDetected { .. })));
1978 }
1979
1980 #[test]
1981 fn test_mult() {
1982 let mut patch = Patch::new(44100.0);
1983 let a = patch.add("a", Passthrough::new());
1984 let b = patch.add("b", Passthrough::new());
1985 let c = patch.add("c", Passthrough::new());
1986
1987 let result = patch.mult(a.out("out"), &[b.in_("in"), c.in_("in")]);
1988 assert!(result.is_ok());
1989 assert_eq!(patch.cable_count(), 2);
1990 }
1991
1992 #[test]
1993 fn test_disconnect() {
1994 let mut patch = Patch::new(44100.0);
1995 let a = patch.add("a", Passthrough::new());
1996 let b = patch.add("b", Passthrough::new());
1997
1998 let cable_id = patch.connect(a.out("out"), b.in_("in")).unwrap();
1999 assert_eq!(patch.cable_count(), 1);
2000
2001 patch.disconnect(cable_id).unwrap();
2002 assert_eq!(patch.cable_count(), 0);
2003 }
2004
2005 #[test]
2006 fn test_remove_module() {
2007 let mut patch = Patch::new(44100.0);
2008 let a = patch.add("a", Passthrough::new());
2009 let b = patch.add("b", Passthrough::new());
2010
2011 patch.connect(a.out("out"), b.in_("in")).unwrap();
2012 assert_eq!(patch.node_count(), 2);
2013 assert_eq!(patch.cable_count(), 1);
2014
2015 patch.remove(a.id()).unwrap();
2016 assert_eq!(patch.node_count(), 1);
2017 assert_eq!(patch.cable_count(), 0); }
2019
2020 #[test]
2024 fn test_module_identity_survives_remove_and_compile() {
2025 let mut patch = Patch::new(44100.0);
2026 let vco = patch.add("vco", crate::modules::Vco::new(44100.0));
2027 let doomed = patch.add("doomed", Passthrough::new());
2028 let vca = patch.add("vca", crate::modules::Vca::new());
2029 let out = patch.add("out", crate::modules::StereoOutput::new());
2030
2031 patch.connect(vco.out("saw"), vca.in_("in")).unwrap();
2032 patch.connect(vca.out("out"), out.in_("left")).unwrap();
2033 patch.set_output(out.id());
2034
2035 patch.remove(doomed.id()).unwrap();
2037 patch.compile().unwrap();
2039
2040 let by_id = |id| {
2041 patch
2042 .nodes()
2043 .find(|(nid, _, _)| *nid == id)
2044 .map(|(_, name, module)| (name, module.type_id()))
2045 };
2046 assert_eq!(by_id(vco.id()), Some(("vco", "vco")));
2047 assert_eq!(by_id(vca.id()), Some(("vca", "vca")));
2048 assert_eq!(by_id(out.id()), Some(("out", "stereo_output")));
2049 assert_eq!(patch.node_count(), 3);
2050 }
2051
2052 struct GateModule {
2058 spec: PortSpec,
2059 }
2060
2061 impl GateModule {
2062 fn new() -> Self {
2063 Self {
2064 spec: PortSpec {
2065 inputs: vec![PortDef::new(0, "in", SignalKind::Gate)],
2066 outputs: vec![PortDef::new(10, "out", SignalKind::Gate)],
2067 },
2068 }
2069 }
2070 }
2071
2072 impl GraphModule for GateModule {
2073 fn port_spec(&self) -> &PortSpec {
2074 &self.spec
2075 }
2076 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
2077 outputs.set(10, inputs.get_or(0, 0.0));
2078 }
2079 fn reset(&mut self) {}
2080 fn set_sample_rate(&mut self, _: f64) {}
2081 }
2082
2083 #[test]
2084 fn test_validation_mode_none() {
2085 let mut patch = Patch::new(44100.0);
2086 patch.set_validation_mode(ValidationMode::None);
2087
2088 let audio = patch.add("audio", Passthrough::new());
2089 let gate = patch.add("gate", GateModule::new());
2090
2091 let result = patch.connect(audio.out("out"), gate.in_("in"));
2093 assert!(result.is_ok());
2094 assert!(patch.warnings().is_empty());
2095 }
2096
2097 #[test]
2098 fn test_validation_mode_warn() {
2099 let mut patch = Patch::new(44100.0);
2100 patch.set_validation_mode(ValidationMode::Warn);
2101
2102 let audio = patch.add("audio", Passthrough::new());
2103 let gate = patch.add("gate", GateModule::new());
2104
2105 let result = patch.connect(audio.out("out"), gate.in_("in"));
2107 assert!(result.is_ok());
2108 assert!(!patch.warnings().is_empty());
2109 }
2110
2111 #[test]
2112 fn test_validation_mode_strict() {
2113 let mut patch = Patch::new(44100.0);
2114 patch.set_validation_mode(ValidationMode::Strict);
2115
2116 let audio = patch.add("audio", Passthrough::new());
2117 let gate = patch.add("gate", GateModule::new());
2118
2119 let result = patch.connect(audio.out("out"), gate.in_("in"));
2121 assert!(matches!(result, Err(PatchError::SignalMismatch { .. })));
2122 }
2123
2124 #[test]
2125 fn test_same_signal_type_no_warning() {
2126 let mut patch = Patch::new(44100.0);
2127 patch.set_validation_mode(ValidationMode::Warn);
2128
2129 let a = patch.add("a", Passthrough::new());
2130 let b = patch.add("b", Passthrough::new());
2131
2132 let result = patch.connect(a.out("out"), b.in_("in"));
2134 assert!(result.is_ok());
2135 assert!(patch.warnings().is_empty());
2136 }
2137
2138 #[test]
2139 fn test_connect_modulated() {
2140 let mut patch = Patch::new(44100.0);
2141 let a = patch.add("a", Passthrough::new());
2142 let b = patch.add("b", Passthrough::new());
2143
2144 let result = patch.connect_modulated(a.out("out"), b.in_("in"), 0.5, 1.0);
2146 assert!(result.is_ok());
2147
2148 let cables = patch.cables();
2149 assert_eq!(cables.len(), 1);
2150 assert_eq!(cables[0].attenuation, Some(0.5));
2151 assert_eq!(cables[0].offset, Some(1.0));
2152 }
2153
2154 #[test]
2155 fn test_modulated_signal_processing() {
2156 let mut patch = Patch::new(44100.0);
2157
2158 struct ConstModule {
2160 spec: PortSpec,
2161 value: f64,
2162 }
2163
2164 impl ConstModule {
2165 fn new(value: f64) -> Self {
2166 Self {
2167 value,
2168 spec: PortSpec {
2169 inputs: vec![],
2170 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
2171 },
2172 }
2173 }
2174 }
2175
2176 impl GraphModule for ConstModule {
2177 fn port_spec(&self) -> &PortSpec {
2178 &self.spec
2179 }
2180 fn tick(&mut self, _: &PortValues, outputs: &mut PortValues) {
2181 outputs.set(10, self.value);
2182 }
2183 fn reset(&mut self) {}
2184 fn set_sample_rate(&mut self, _: f64) {}
2185 }
2186
2187 struct RecordModule {
2188 spec: PortSpec,
2189 last_value: f64,
2190 }
2191
2192 impl RecordModule {
2193 fn new() -> Self {
2194 Self {
2195 spec: PortSpec {
2196 inputs: vec![PortDef::new(0, "in", SignalKind::Audio)],
2197 outputs: vec![],
2198 },
2199 last_value: 0.0,
2200 }
2201 }
2202 }
2203
2204 impl GraphModule for RecordModule {
2205 fn port_spec(&self) -> &PortSpec {
2206 &self.spec
2207 }
2208 fn tick(&mut self, inputs: &PortValues, _: &mut PortValues) {
2209 self.last_value = inputs.get_or(0, 0.0);
2210 }
2211 fn reset(&mut self) {}
2212 fn set_sample_rate(&mut self, _: f64) {}
2213 }
2214
2215 let source = patch.add("source", ConstModule::new(4.0));
2216 let sink = patch.add("sink", RecordModule::new());
2217
2218 patch
2220 .connect_modulated(source.out("out"), sink.in_("in"), 0.5, 2.0)
2221 .unwrap();
2222 patch.set_output(sink.id());
2223 patch.compile().unwrap();
2224 patch.tick();
2225
2226 }
2229
2230 #[test]
2231 fn test_signal_compatibility() {
2232 assert!(SignalKind::Audio
2234 .is_compatible_with(&SignalKind::Audio)
2235 .warning
2236 .is_none());
2237 assert!(SignalKind::Audio
2238 .is_compatible_with(&SignalKind::CvBipolar)
2239 .warning
2240 .is_some());
2241 assert!(SignalKind::Gate
2242 .is_compatible_with(&SignalKind::Trigger)
2243 .warning
2244 .is_some());
2245 assert!(SignalKind::Clock
2246 .is_compatible_with(&SignalKind::Trigger)
2247 .warning
2248 .is_none());
2249 }
2250
2251 #[test]
2252 fn test_patch_get_name() {
2253 let mut patch = Patch::new(44100.0);
2254 let a = patch.add("my_module", Passthrough::new());
2255
2256 let name = patch.get_name(a.id());
2257 assert_eq!(name, Some("my_module"));
2258
2259 use slotmap::DefaultKey;
2261 let fake_id: NodeId = DefaultKey::default();
2262 assert!(patch.get_name(fake_id).is_none());
2263 }
2264
2265 #[test]
2266 fn test_patch_set_position() {
2267 let mut patch = Patch::new(44100.0);
2268 let a = patch.add("a", Passthrough::new());
2269
2270 patch.set_position(a.id(), (100.0, 200.0));
2271 }
2273
2274 #[test]
2275 fn test_patch_clear_warnings() {
2276 let mut patch = Patch::new(44100.0);
2277 patch.set_validation_mode(ValidationMode::Warn);
2278
2279 let audio = patch.add("audio", Passthrough::new());
2280 let gate = patch.add("gate", GateModule::new());
2281
2282 patch.connect(audio.out("out"), gate.in_("in")).unwrap();
2283 assert!(!patch.warnings().is_empty());
2284
2285 patch.clear_warnings();
2286 assert!(patch.warnings().is_empty());
2287 }
2288
2289 #[test]
2290 fn test_patch_validation_mode_getter() {
2291 let mut patch = Patch::new(44100.0);
2292 patch.set_validation_mode(ValidationMode::Strict);
2293 assert_eq!(patch.validation_mode(), ValidationMode::Strict);
2294 }
2295
2296 #[test]
2297 fn test_patch_sample_rate() {
2298 let patch = Patch::new(48000.0);
2299 assert_eq!(patch.sample_rate(), 48000.0);
2300 }
2301
2302 #[test]
2303 fn test_patch_execution_order() {
2304 let mut patch = Patch::new(44100.0);
2305 let a = patch.add("a", Passthrough::new());
2306 let b = patch.add("b", Passthrough::new());
2307 patch.connect(a.out("out"), b.in_("in")).unwrap();
2308 patch.compile().unwrap();
2309
2310 let order = patch.execution_order();
2311 assert_eq!(order.len(), 2);
2312 }
2313
2314 #[test]
2315 fn test_patch_mult() {
2316 let mut patch = Patch::new(44100.0);
2317 let a = patch.add("a", Passthrough::new());
2318 let b = patch.add("b", Passthrough::new());
2319 let c = patch.add("c", Passthrough::new());
2320
2321 let result = patch.mult(a.out("out"), &[b.in_("in"), c.in_("in")]);
2323 assert!(result.is_ok());
2324 assert_eq!(patch.cable_count(), 2);
2325 }
2326
2327 #[test]
2328 fn test_patch_reset() {
2329 let mut patch = Patch::new(44100.0);
2330 let a = patch.add("a", Passthrough::new());
2331 patch.set_output(a.id());
2332 patch.compile().unwrap();
2333
2334 for _ in 0..100 {
2335 patch.tick();
2336 }
2337
2338 patch.reset();
2339 }
2341
2342 #[test]
2343 fn test_patch_set_param_get_param() {
2344 use crate::modules::Vco;
2345 let mut patch = Patch::new(44100.0);
2346 let vco = patch.add("vco", Vco::new(44100.0));
2347
2348 patch.set_param(vco.id(), 0, 0.5);
2350 let _ = patch.get_param(vco.id(), 0);
2351 }
2352
2353 #[test]
2354 fn test_node_handle_spec() {
2355 let mut patch = Patch::new(44100.0);
2356 let a = patch.add("a", Passthrough::new());
2357
2358 let spec = a.spec();
2359 assert!(!spec.inputs.is_empty());
2360 assert!(!spec.outputs.is_empty());
2361 }
2362
2363 #[test]
2364 fn test_patch_validation_mode() {
2365 let mut patch = Patch::new(44100.0);
2366
2367 patch.set_validation_mode(ValidationMode::Strict);
2368 assert_eq!(patch.validation_mode(), ValidationMode::Strict);
2369
2370 patch.set_validation_mode(ValidationMode::Warn);
2371 assert_eq!(patch.validation_mode(), ValidationMode::Warn);
2372 }
2373
2374 struct SumModule {
2380 spec: PortSpec,
2381 }
2382 impl SumModule {
2383 fn new() -> Self {
2384 Self {
2385 spec: PortSpec {
2386 inputs: vec![
2387 PortDef::new(0, "a", SignalKind::Audio),
2388 PortDef::new(1, "b", SignalKind::Audio),
2389 ],
2390 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
2391 },
2392 }
2393 }
2394 }
2395 impl GraphModule for SumModule {
2396 fn port_spec(&self) -> &PortSpec {
2397 &self.spec
2398 }
2399 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
2400 outputs.set(10, inputs.get_or(0, 0.0) + inputs.get_or(1, 0.0));
2401 }
2402 fn reset(&mut self) {}
2403 fn set_sample_rate(&mut self, _: f64) {}
2404 }
2405
2406 struct FeedbackDelay {
2408 spec: PortSpec,
2409 buffer: f64,
2410 }
2411 impl FeedbackDelay {
2412 fn new() -> Self {
2413 Self {
2414 spec: PortSpec {
2415 inputs: vec![PortDef::new(0, "in", SignalKind::Audio)],
2416 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
2417 },
2418 buffer: 0.0,
2419 }
2420 }
2421 }
2422 impl GraphModule for FeedbackDelay {
2423 fn port_spec(&self) -> &PortSpec {
2424 &self.spec
2425 }
2426 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
2427 outputs.set(10, self.buffer);
2428 self.buffer = inputs.get_or(0, 0.0);
2429 }
2430 fn reset(&mut self) {
2431 self.buffer = 0.0;
2432 }
2433 fn set_sample_rate(&mut self, _: f64) {}
2434 fn breaks_feedback_cycle(&self) -> bool {
2435 true
2436 }
2437 }
2438
2439 struct ConstSource {
2441 spec: PortSpec,
2442 value: f64,
2443 }
2444 impl ConstSource {
2445 fn new(value: f64) -> Self {
2446 Self {
2447 spec: PortSpec {
2448 inputs: vec![],
2449 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
2450 },
2451 value,
2452 }
2453 }
2454 }
2455 impl GraphModule for ConstSource {
2456 fn port_spec(&self) -> &PortSpec {
2457 &self.spec
2458 }
2459 fn tick(&mut self, _: &PortValues, outputs: &mut PortValues) {
2460 outputs.set(10, self.value);
2461 }
2462 fn reset(&mut self) {}
2463 fn set_sample_rate(&mut self, _: f64) {}
2464 }
2465
2466 #[test]
2468 fn test_cable_ids_are_stable_across_disconnect() {
2469 let mut patch = Patch::new(44100.0);
2470 let a = patch.add("a", Passthrough::new());
2471 let b = patch.add("b", SumModule::new());
2472 let c = patch.add("c", SumModule::new());
2473
2474 let c1 = patch.connect(a.out("out"), b.in_("a")).unwrap();
2475 let c2 = patch.connect(a.out("out"), b.in_("b")).unwrap();
2476 let c3 = patch.connect(a.out("out"), c.in_("a")).unwrap();
2477 assert_eq!(patch.cable_count(), 3);
2478
2479 patch.disconnect(c1).unwrap();
2481 assert_eq!(patch.cable_count(), 2);
2482
2483 patch.disconnect(c3).unwrap();
2486 assert_eq!(patch.cable_count(), 1);
2487 let remaining = &patch.cables()[0];
2488 assert_eq!(remaining.id, c2);
2489 assert_eq!(remaining.to, b.in_("b"));
2490
2491 assert!(matches!(
2493 patch.disconnect(c1),
2494 Err(PatchError::InvalidCable)
2495 ));
2496 }
2497
2498 #[test]
2500 fn test_mutation_after_compile_is_reflected_on_tick() {
2501 let mut patch = Patch::new(44100.0);
2502 let src = patch.add("src", ConstSource::new(1.0));
2503 let out = patch.add("out", Passthrough::new());
2504 patch.connect(src.out("out"), out.in_("in")).unwrap();
2505 patch.set_output(out.id());
2506 patch.compile().unwrap();
2507
2508 let (l0, _) = patch.tick();
2510 assert!((l0 - 1.0).abs() < 1e-9);
2511
2512 let src2 = patch.add("src2", ConstSource::new(2.0));
2514 let sum = patch.add("sum", SumModule::new());
2515 patch
2517 .disconnect_ports(src.out("out"), out.in_("in"))
2518 .unwrap();
2519 patch.connect(src.out("out"), sum.in_("a")).unwrap();
2520 patch.connect(src2.out("out"), sum.in_("b")).unwrap();
2521 patch.connect(sum.out("out"), out.in_("in")).unwrap();
2522
2523 let (l1, _) = patch.tick();
2526 assert!((l1 - 3.0).abs() < 1e-9, "expected 3.0, got {}", l1);
2527 }
2528
2529 #[test]
2531 fn test_cycle_mutation_surfaces_via_last_compile_error() {
2532 let mut patch = Patch::new(44100.0);
2533 let a = patch.add("a", Passthrough::new());
2534 let b = patch.add("b", Passthrough::new());
2535 patch.connect(a.out("out"), b.in_("in")).unwrap();
2536 patch.set_output(b.id());
2537 patch.compile().unwrap();
2538 assert!(patch.last_compile_error().is_none());
2539
2540 patch.connect(b.out("out"), a.in_("in")).unwrap();
2542
2543 let (l, r) = patch.tick();
2545 assert_eq!((l, r), (0.0, 0.0));
2546 match patch.last_compile_error() {
2547 Some(PatchError::CycleDetected { names, .. }) => {
2548 assert_eq!(names.len(), 2);
2549 }
2550 other => panic!("expected CycleDetected, got {:?}", other),
2551 }
2552 }
2553
2554 #[test]
2556 fn test_feedback_loop_with_delay_compiles_and_decays() {
2557 struct Impulse {
2560 spec: PortSpec,
2561 fired: bool,
2562 }
2563 impl GraphModule for Impulse {
2564 fn port_spec(&self) -> &PortSpec {
2565 &self.spec
2566 }
2567 fn tick(&mut self, _: &PortValues, outputs: &mut PortValues) {
2568 outputs.set(10, if self.fired { 0.0 } else { 1.0 });
2569 self.fired = true;
2570 }
2571 fn reset(&mut self) {
2572 self.fired = false;
2573 }
2574 fn set_sample_rate(&mut self, _: f64) {}
2575 }
2576
2577 let mut patch = Patch::new(44100.0);
2578 let impulse = patch.add(
2579 "impulse",
2580 Impulse {
2581 spec: PortSpec {
2582 inputs: vec![],
2583 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
2584 },
2585 fired: false,
2586 },
2587 );
2588 let sum = patch.add("sum", SumModule::new());
2589 let delay = patch.add("delay", FeedbackDelay::new());
2590
2591 patch.connect(impulse.out("out"), sum.in_("a")).unwrap();
2594 patch
2595 .connect_attenuated(delay.out("out"), sum.in_("b"), 0.5)
2596 .unwrap();
2597 patch.connect(sum.out("out"), delay.in_("in")).unwrap();
2598 patch.set_output(delay.id());
2599
2600 patch.compile().expect("feedback loop should compile");
2602 assert!(patch.last_compile_error().is_none());
2603
2604 let mut outs = Vec::new();
2605 for _ in 0..14 {
2606 outs.push(patch.tick().0);
2607 }
2608
2609 let nonzero: Vec<f64> = outs.iter().copied().filter(|v| v.abs() > 1e-9).collect();
2611 assert!(
2612 nonzero.len() >= 3,
2613 "expected multiple decaying echoes, got {:?}",
2614 outs
2615 );
2616 let peak_early = outs.iter().cloned().fold(0.0_f64, f64::max);
2619 let peak_late = outs[outs.len() - 3..]
2620 .iter()
2621 .cloned()
2622 .fold(0.0_f64, f64::max);
2623 assert!(
2624 peak_late < peak_early,
2625 "echo should decay: early peak {}, late peak {}",
2626 peak_early,
2627 peak_late
2628 );
2629 }
2630
2631 #[test]
2633 fn test_breakerless_cycle_still_errors() {
2634 let mut patch = Patch::new(44100.0);
2635 let a = patch.add("a", Passthrough::new());
2636 let b = patch.add("b", Passthrough::new());
2637 patch.connect(a.out("out"), b.in_("in")).unwrap();
2638 patch.connect(b.out("out"), a.in_("in")).unwrap();
2639 assert!(matches!(
2640 patch.compile(),
2641 Err(PatchError::CycleDetected { .. })
2642 ));
2643 }
2644
2645 #[test]
2648 fn test_normalled_input_uses_current_sibling_value() {
2649 use crate::modules::StereoOutput;
2650 let mut patch = Patch::new(44100.0);
2651 struct Ramp {
2653 spec: PortSpec,
2654 n: f64,
2655 }
2656 impl GraphModule for Ramp {
2657 fn port_spec(&self) -> &PortSpec {
2658 &self.spec
2659 }
2660 fn tick(&mut self, _: &PortValues, outputs: &mut PortValues) {
2661 self.n += 1.0;
2662 outputs.set(10, self.n);
2663 }
2664 fn reset(&mut self) {
2665 self.n = 0.0;
2666 }
2667 fn set_sample_rate(&mut self, _: f64) {}
2668 }
2669 let ramp = patch.add(
2670 "ramp",
2671 Ramp {
2672 spec: PortSpec {
2673 inputs: vec![],
2674 outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
2675 },
2676 n: 0.0,
2677 },
2678 );
2679 let out = patch.add("out", StereoOutput::new());
2680 patch.connect(ramp.out("out"), out.in_("left")).unwrap();
2682 patch.set_output(out.id());
2683 patch.compile().unwrap();
2684
2685 for _ in 0..5 {
2686 let (l, r) = patch.tick();
2687 assert!(l > 0.0);
2688 assert_eq!(l, r, "mono fallback must be current-sample, not delayed");
2689 }
2690 }
2691
2692 struct NormalChain {
2698 spec: PortSpec,
2699 }
2700 impl NormalChain {
2701 fn new(cycle: bool) -> Self {
2702 let (n0, n1) = if cycle { (1, 0) } else { (1, 2) };
2703 Self {
2704 spec: PortSpec {
2705 inputs: vec![
2706 PortDef::new(0, "a", SignalKind::Audio)
2707 .with_default(0.1)
2708 .normalled_to(n0),
2709 PortDef::new(1, "b", SignalKind::Audio)
2710 .with_default(0.2)
2711 .normalled_to(n1),
2712 PortDef::new(2, "c", SignalKind::Audio).with_default(0.3),
2713 ],
2714 outputs: vec![
2715 PortDef::new(10, "oa", SignalKind::Audio),
2716 PortDef::new(11, "ob", SignalKind::Audio),
2717 PortDef::new(12, "oc", SignalKind::Audio),
2718 ],
2719 },
2720 }
2721 }
2722 }
2723 impl GraphModule for NormalChain {
2724 fn port_spec(&self) -> &PortSpec {
2725 &self.spec
2726 }
2727 fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
2728 outputs.set(10, inputs.get_or(0, f64::NAN));
2729 outputs.set(11, inputs.get_or(1, f64::NAN));
2730 outputs.set(12, inputs.get_or(2, f64::NAN));
2731 }
2732 fn reset(&mut self) {}
2733 fn set_sample_rate(&mut self, _: f64) {}
2734 }
2735
2736 #[test]
2742 fn test_normalled_chain_resolves_transitively() {
2743 let mut patch = Patch::new(44100.0);
2744 let src = patch.add("src", ConstSource::new(0.75));
2745 let node = patch.add("chain", NormalChain::new(false));
2746 patch.connect(src.out("out"), node.in_("c")).unwrap();
2748 patch.set_output(node.id());
2749 patch.compile().unwrap();
2750
2751 let (oa, ob) = patch.tick();
2754 assert!(
2755 (oa - 0.75).abs() < 1e-9,
2756 "forward-normalled input 0 must resolve to patched source, got {oa}"
2757 );
2758 assert!(
2759 (ob - 0.75).abs() < 1e-9,
2760 "transitively-normalled input 1 must resolve to patched source, got {ob}"
2761 );
2762 }
2763
2764 #[test]
2767 fn test_normalled_cycle_falls_back_to_default() {
2768 let mut patch = Patch::new(44100.0);
2769 let node = patch.add("chain", NormalChain::new(true));
2770 patch.set_output(node.id());
2771 patch.compile().unwrap();
2772 let (oa, ob) = patch.tick();
2773 assert!(
2774 (oa - 0.1).abs() < 1e-9,
2775 "cycled input 0 -> own default, got {oa}"
2776 );
2777 assert!(
2778 (ob - 0.2).abs() < 1e-9,
2779 "cycled input 1 -> own default, got {ob}"
2780 );
2781 }
2782
2783 #[test]
2785 fn test_execution_order_is_deterministic() {
2786 fn build_order() -> Vec<usize> {
2787 let mut patch = Patch::new(44100.0);
2788 let s1 = patch.add("s1", ConstSource::new(1.0));
2790 let s2 = patch.add("s2", ConstSource::new(2.0));
2791 let s3 = patch.add("s3", ConstSource::new(3.0));
2792 let sum = patch.add("sum", SumModule::new());
2793 patch.connect(s1.out("out"), sum.in_("a")).unwrap();
2794 patch.connect(s2.out("out"), sum.in_("b")).unwrap();
2795 patch.connect(s3.out("out"), sum.in_("a")).unwrap();
2796 patch.compile().unwrap();
2797 let ids = [s1.id(), s2.id(), s3.id(), sum.id()];
2799 patch
2800 .execution_order()
2801 .iter()
2802 .map(|nid| ids.iter().position(|x| x == nid).unwrap())
2803 .collect()
2804 }
2805 assert_eq!(build_order(), build_order());
2806 }
2807
2808 #[test]
2811 fn test_read_output_uses_first_two_outputs_mono_duplicated() {
2812 let mut patch = Patch::new(44100.0);
2813 let src = patch.add("src", ConstSource::new(0.7));
2814 patch.set_output(src.id());
2815 patch.compile().unwrap();
2816 let (l, r) = patch.tick();
2817 assert!((l - 0.7).abs() < 1e-9);
2818 assert_eq!(l, r, "mono node must duplicate to both channels");
2819 }
2820
2821 #[test]
2823 fn test_try_set_output_validates() {
2824 let mut patch = Patch::new(44100.0);
2825 let src = patch.add("src", ConstSource::new(1.0));
2826 assert!(patch.try_set_output(src.id()).is_ok());
2827
2828 struct SinkNoOut {
2830 spec: PortSpec,
2831 }
2832 impl GraphModule for SinkNoOut {
2833 fn port_spec(&self) -> &PortSpec {
2834 &self.spec
2835 }
2836 fn tick(&mut self, _: &PortValues, _: &mut PortValues) {}
2837 fn reset(&mut self) {}
2838 fn set_sample_rate(&mut self, _: f64) {}
2839 }
2840 let sink = patch.add(
2841 "sink",
2842 SinkNoOut {
2843 spec: PortSpec {
2844 inputs: vec![PortDef::new(0, "in", SignalKind::Audio)],
2845 outputs: vec![],
2846 },
2847 },
2848 );
2849 assert!(matches!(
2850 patch.try_set_output(sink.id()),
2851 Err(PatchError::InvalidPort { .. })
2852 ));
2853 }
2854
2855 #[test]
2857 fn test_node_handle_fallible_ports_and_names() {
2858 let mut patch = Patch::new(44100.0);
2859 let a = patch.add("a", Passthrough::new());
2860
2861 assert!(a.output("out").is_ok());
2862 assert!(a.input("in").is_ok());
2863
2864 match a.output("nope") {
2866 Err(PatchError::InvalidPort { available, .. }) => {
2867 assert!(available.iter().any(|n| n == "out"));
2868 }
2869 other => panic!("expected InvalidPort, got {:?}", other),
2870 }
2871 assert!(a.input("nope").is_err());
2872
2873 assert_eq!(a.input_names(), vec!["in"]);
2874 assert_eq!(a.output_names(), vec!["out"]);
2875 }
2876
2877 #[test]
2879 fn test_invalid_port_display_lists_available() {
2880 let mut patch = Patch::new(44100.0);
2881 let a = patch.add("a", Passthrough::new());
2882 let b = patch.add("b", Passthrough::new());
2883 let bad = PortRef {
2885 node: b.id(),
2886 port: 999,
2887 };
2888 let err = patch.connect(a.out("out"), bad).unwrap_err();
2889 let msg = alloc::format!("{}", err);
2890 assert!(msg.contains("Invalid port"), "got: {}", msg);
2891 assert!(
2892 msg.contains("in"),
2893 "should list available port 'in': {}",
2894 msg
2895 );
2896 }
2897
2898 #[test]
2900 fn test_cycle_detected_display_names() {
2901 let mut patch = Patch::new(44100.0);
2902 let a = patch.add("osc", Passthrough::new());
2903 let b = patch.add("filt", Passthrough::new());
2904 patch.connect(a.out("out"), b.in_("in")).unwrap();
2905 patch.connect(b.out("out"), a.in_("in")).unwrap();
2906 let err = patch.compile().unwrap_err();
2907 let msg = alloc::format!("{}", err);
2908 assert!(msg.contains("Cycle detected"), "got: {}", msg);
2909 assert!(
2910 msg.contains("osc") && msg.contains("filt"),
2911 "cycle message should name modules: {}",
2912 msg
2913 );
2914 }
2915
2916 #[test]
2918 fn test_default_validation_mode_is_warn() {
2919 let patch = Patch::new(44100.0);
2920 assert_eq!(patch.validation_mode(), ValidationMode::Warn);
2921 assert_eq!(ValidationMode::default(), ValidationMode::Warn);
2922 }
2923
2924 #[test]
2926 fn test_patch_debug_impl() {
2927 let mut patch = Patch::new(44100.0);
2928 let a = patch.add("my_osc", Passthrough::new());
2929 let b = patch.add("my_out", Passthrough::new());
2930 patch.connect(a.out("out"), b.in_("in")).unwrap();
2931 patch.set_output(b.id());
2932 let s = alloc::format!("{:?}", patch);
2933 assert!(s.contains("Patch"));
2934 assert!(s.contains("my_osc"));
2935 assert!(s.contains("my_out"));
2936 assert!(s.contains("validation_mode"));
2937 }
2938
2939 #[test]
2946 fn test_tick_block_matches_per_sample_tick() {
2947 fn ramp_svf_patch() -> (Patch, NodeHandle) {
2948 let mut patch = Patch::new(44100.0);
2949 let src = patch.add("src", ConstSource::new(0.9));
2950 let pass = patch.add("pass", Passthrough::new());
2951 patch.connect(src.out("out"), pass.in_("in")).unwrap();
2952 patch.set_output(pass.id());
2953 patch.compile().unwrap();
2954 (patch, pass)
2955 }
2956
2957 let (mut a, _) = ramp_svf_patch();
2959 let mut reference = Vec::new();
2960 for _ in 0..8 {
2961 reference.push(a.tick());
2962 }
2963
2964 let (mut b, _) = ramp_svf_patch();
2966 let mut left = [0.0_f64; 8];
2967 let mut right = [0.0_f64; 8];
2968 b.tick_block(&mut left, &mut right);
2969
2970 for (i, &(l, r)) in reference.iter().enumerate() {
2971 assert!((left[i] - l).abs() < 1e-12, "left[{}] mismatch", i);
2972 assert!((right[i] - r).abs() < 1e-12, "right[{}] mismatch", i);
2973 }
2974 }
2975
2976 #[test]
2978 fn test_tick_block_uses_min_length() {
2979 let mut patch = Patch::new(44100.0);
2980 let src = patch.add("src", ConstSource::new(1.0));
2981 patch.set_output(src.id());
2982 patch.compile().unwrap();
2983
2984 let mut left = [0.0_f64; 4];
2985 let mut right = [0.0_f64; 2]; patch.tick_block(&mut left, &mut right);
2987
2988 assert_eq!(left[0], 1.0);
2989 assert_eq!(left[1], 1.0);
2990 assert_eq!(left[2], 0.0, "frame beyond min length must be untouched");
2991 assert_eq!(left[3], 0.0);
2992 assert_eq!(right[0], 1.0);
2993 assert_eq!(right[1], 1.0);
2994 }
2995
2996 #[test]
2999 fn test_denormal_is_flushed_at_scatter() {
3000 let mut patch = Patch::new(44100.0);
3001 let src = patch.add("src", ConstSource::new(1e-30));
3003 let pass = patch.add("pass", Passthrough::new());
3004 patch.connect(src.out("out"), pass.in_("in")).unwrap();
3005 patch.set_output(pass.id());
3006 patch.compile().unwrap();
3007
3008 let (l, r) = patch.tick();
3009 assert_eq!(l, 0.0, "subnormal must be flushed to zero");
3010 assert_eq!(r, 0.0);
3011 assert_eq!(patch.get_output_value(src.id(), 10), Some(0.0));
3013 }
3014
3015 #[test]
3018 fn test_non_finite_is_sanitized_at_scatter() {
3019 for &bad in &[f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
3020 let mut patch = Patch::new(44100.0);
3021 let src = patch.add("src", ConstSource::new(bad));
3022 let pass = patch.add("pass", Passthrough::new());
3023 patch.connect(src.out("out"), pass.in_("in")).unwrap();
3024 patch.set_output(pass.id());
3025 patch.compile().unwrap();
3026
3027 let (l, r) = patch.tick();
3028 assert_eq!(l, 0.0, "non-finite ({bad}) must be zeroed at scatter");
3029 assert_eq!(r, 0.0);
3030 assert_eq!(patch.get_output_value(src.id(), 10), Some(0.0));
3031 }
3032 }
3033
3034 #[test]
3037 fn test_precompiled_adjacency_sums_and_exposes_outputs() {
3038 let mut patch = Patch::new(44100.0);
3039 let s1 = patch.add("s1", ConstSource::new(2.0));
3040 let s2 = patch.add("s2", ConstSource::new(3.0));
3041 let sum = patch.add("sum", SumModule::new());
3042 patch.connect(s1.out("out"), sum.in_("a")).unwrap();
3044 patch
3045 .connect_modulated(s2.out("out"), sum.in_("a"), 0.5, 1.0)
3046 .unwrap();
3047 patch.set_output(sum.id());
3048 patch.compile().unwrap();
3049
3050 let (l, _) = patch.tick();
3051 assert!((l - 4.5).abs() < 1e-12, "expected 4.5, got {}", l);
3052 assert_eq!(patch.get_output_value(sum.id(), 10), Some(4.5));
3053 assert_eq!(patch.get_output_value(sum.id(), 999), None);
3055 }
3056}