use crate::modules::common::flush_denorm;
use crate::port::{GraphModule, ParamId, PortId, PortSpec, PortValues, SignalKind};
use crate::StdMap;
use alloc::boxed::Box;
use alloc::collections::VecDeque;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use slotmap::{DefaultKey, SlotMap};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ValidationMode {
None,
#[default]
Warn,
Strict,
}
#[derive(Debug, Clone)]
pub struct CompatibilityResult {
pub compatible: bool,
pub warning: Option<String>,
}
impl SignalKind {
pub fn is_compatible_with(&self, other: &SignalKind) -> CompatibilityResult {
use SignalKind::*;
if self == other {
return CompatibilityResult {
compatible: true,
warning: None,
};
}
match (self, other) {
(Audio, CvBipolar) | (CvBipolar, Audio) => CompatibilityResult {
compatible: true,
warning: Some("Audio/CV connection - ensure this is intentional".to_string()),
},
(CvBipolar, CvUnipolar) | (CvUnipolar, CvBipolar) => CompatibilityResult {
compatible: true,
warning: Some(
"Bipolar/Unipolar CV mismatch - signal may be clipped or offset".to_string(),
),
},
(CvBipolar, VoltPerOctave) => CompatibilityResult {
compatible: true,
warning: None,
},
(VoltPerOctave, CvBipolar) => CompatibilityResult {
compatible: true,
warning: None,
},
(Gate, Trigger) | (Trigger, Gate) => CompatibilityResult {
compatible: true,
warning: Some("Gate/Trigger connection - timing behavior may differ".to_string()),
},
(Clock, Trigger) | (Trigger, Clock) => CompatibilityResult {
compatible: true,
warning: None,
},
(Clock, Gate) | (Gate, Clock) => CompatibilityResult {
compatible: true,
warning: Some("Clock/Gate connection - duty cycle may affect behavior".to_string()),
},
(Audio, VoltPerOctave) => CompatibilityResult {
compatible: true,
warning: Some(
"Audio-rate pitch modulation - ensure this is intentional".to_string(),
),
},
(CvUnipolar, VoltPerOctave) => CompatibilityResult {
compatible: true,
warning: Some("Unipolar CV to V/Oct - may need offset adjustment".to_string()),
},
(VoltPerOctave, CvUnipolar) => CompatibilityResult {
compatible: true,
warning: Some("V/Oct to Unipolar - negative voltages will be clipped".to_string()),
},
(Audio, Gate) | (Audio, Trigger) => CompatibilityResult {
compatible: true,
warning: Some("Audio to Gate/Trigger - signal will be thresholded".to_string()),
},
_ => CompatibilityResult {
compatible: true,
warning: Some(format!("Unusual connection: {:?} -> {:?}", self, other)),
},
}
}
}
pub type NodeId = DefaultKey;
pub type CableId = usize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct PortRef {
pub node: NodeId,
pub port: PortId,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cable {
#[serde(default)]
pub id: CableId,
pub from: PortRef,
pub to: PortRef,
pub attenuation: Option<f64>,
pub offset: Option<f64>,
}
struct Node {
module: Box<dyn GraphModule>,
name: String,
position: Option<(f32, f32)>,
param_overrides: StdMap<String, f64>,
}
#[derive(Debug, Clone, Default)]
pub struct PatchMeta {
pub name: Option<String>,
pub author: Option<String>,
pub description: Option<String>,
pub tags: Vec<String>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum PatchError {
InvalidNode {
node: NodeId,
},
InvalidPort {
node: NodeId,
name: Option<String>,
port: Option<PortId>,
available: Vec<String>,
},
InvalidCable,
CycleDetected {
nodes: Vec<NodeId>,
names: Vec<String>,
},
CompilationFailed(String),
SignalMismatch {
from_kind: SignalKind,
to_kind: SignalKind,
message: String,
},
}
impl core::fmt::Display for PatchError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
PatchError::InvalidNode { node } => write!(f, "Invalid node: {:?}", node),
PatchError::InvalidPort {
node,
name,
port,
available,
} => {
write!(f, "Invalid port")?;
match (name, port) {
(Some(n), _) => write!(f, " '{}'", n)?,
(None, Some(p)) => write!(f, " #{}", p)?,
(None, None) => {}
}
write!(f, " on node {:?}", node)?;
if available.is_empty() {
write!(f, " (module exposes no matching ports)")
} else {
write!(f, " (available ports: {})", available.join(", "))
}
}
PatchError::InvalidCable => write!(f, "Invalid cable"),
PatchError::CycleDetected { nodes, names } => {
if names.is_empty() {
write!(f, "Cycle detected involving {} nodes", nodes.len())
} else {
write!(f, "Cycle detected: {}", names.join(" -> "))
}
}
PatchError::CompilationFailed(msg) => write!(f, "Compilation failed: {}", msg),
PatchError::SignalMismatch {
from_kind,
to_kind,
message,
} => write!(
f,
"Signal mismatch: {:?} -> {:?}: {}",
from_kind, to_kind, message
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for PatchError {}
#[derive(Clone)]
pub struct NodeHandle {
id: NodeId,
spec: PortSpec,
}
impl NodeHandle {
pub fn id(&self) -> NodeId {
self.id
}
pub fn from_module(id: NodeId, module: &dyn GraphModule) -> Self {
Self {
id,
spec: module.port_spec().clone(),
}
}
pub fn output(&self, name: &str) -> Result<PortRef, PatchError> {
match self.spec.output_by_name(name) {
Some(port) => Ok(PortRef {
node: self.id,
port: port.id,
}),
None => Err(PatchError::InvalidPort {
node: self.id,
name: Some(name.to_string()),
port: None,
available: self.output_names().iter().map(|s| s.to_string()).collect(),
}),
}
}
pub fn input(&self, name: &str) -> Result<PortRef, PatchError> {
match self.spec.input_by_name(name) {
Some(port) => Ok(PortRef {
node: self.id,
port: port.id,
}),
None => Err(PatchError::InvalidPort {
node: self.id,
name: Some(name.to_string()),
port: None,
available: self.input_names().iter().map(|s| s.to_string()).collect(),
}),
}
}
pub fn out(&self, name: &str) -> PortRef {
self.output(name).unwrap_or_else(|_| {
panic!(
"Unknown output port: '{}'. Valid output ports: [{}]",
name,
self.output_names().join(", ")
)
})
}
pub fn in_(&self, name: &str) -> PortRef {
self.input(name).unwrap_or_else(|_| {
panic!(
"Unknown input port: '{}'. Valid input ports: [{}]",
name,
self.input_names().join(", ")
)
})
}
pub fn input_names(&self) -> Vec<&str> {
self.spec.inputs.iter().map(|p| p.name.as_str()).collect()
}
pub fn output_names(&self) -> Vec<&str> {
self.spec.outputs.iter().map(|p| p.name.as_str()).collect()
}
pub fn spec(&self) -> &PortSpec {
&self.spec
}
}
struct InEdge {
src_slot: usize,
attenuation: Option<f64>,
offset: Option<f64>,
}
struct InputPlan {
port_id: PortId,
default: f64,
normalled_to: Option<PortId>,
has_connection: bool,
edges: Vec<InEdge>,
}
struct NodeExec {
node_id: NodeId,
out_base: usize,
out_ids: Vec<PortId>,
inputs: Vec<InputPlan>,
}
impl NodeExec {
fn gather(&self, out_buf: &[f64], dst: &mut PortValues) {
dst.clear();
for plan in &self.inputs {
if plan.has_connection {
let mut sum = 0.0;
for e in &plan.edges {
let value = out_buf[e.src_slot];
let attenuated = e.attenuation.map(|a| value * a).unwrap_or(value);
let with_offset = e.offset.map(|o| attenuated + o).unwrap_or(attenuated);
sum += with_offset;
}
dst.set(plan.port_id, sum);
} else if plan.normalled_to.is_none() {
dst.set(plan.port_id, plan.default);
}
}
for plan in &self.inputs {
if dst.has(plan.port_id) {
continue;
}
let value = match plan.normalled_to {
Some(norm) => dst.get(norm).unwrap_or(plan.default),
None => plan.default,
};
dst.set(plan.port_id, value);
}
}
fn scatter(&self, src: &PortValues, out_buf: &mut [f64]) {
for (k, &port_id) in self.out_ids.iter().enumerate() {
if let Some(value) = src.get(port_id) {
out_buf[self.out_base + k] = flush_denorm(value);
}
}
}
}
fn resolve_normalled_chains(inputs: &mut [InputPlan]) {
let n = inputs.len();
let original: Vec<Option<PortId>> = inputs.iter().map(|p| p.normalled_to).collect();
for i in 0..n {
if inputs[i].has_connection || original[i].is_none() {
continue;
}
let mut terminal = None;
let mut cursor = original[i];
for _ in 0..n {
let Some(pid) = cursor else { break };
match inputs.iter().position(|p| p.port_id == pid) {
None => break,
Some(idx) => {
if inputs[idx].has_connection || original[idx].is_none() {
terminal = Some(pid);
break;
}
cursor = original[idx];
}
}
}
inputs[i].normalled_to = terminal;
}
}
#[derive(Default)]
struct Routing {
nodes: Vec<NodeExec>,
out_buf: Vec<f64>,
out_slot_index: StdMap<PortRef, usize>,
output_slots: Option<(usize, usize)>,
scratch_in: Vec<PortValues>,
scratch_out: Vec<PortValues>,
}
impl Routing {
fn read_output(&self) -> (f64, f64) {
match self.output_slots {
Some((left, right)) => (self.out_buf[left], self.out_buf[right]),
None => (0.0, 0.0),
}
}
}
pub struct Patch {
nodes: SlotMap<NodeId, Node>,
cables: Vec<Cable>,
next_cable_id: CableId,
execution_order: Vec<NodeId>,
routing: Routing,
dirty: bool,
last_compile_error: Option<PatchError>,
sample_rate: f64,
output_node: Option<NodeId>,
validation_mode: ValidationMode,
warnings: Vec<String>,
meta: PatchMeta,
}
impl Patch {
pub fn new(sample_rate: f64) -> Self {
Self {
nodes: SlotMap::new(),
cables: Vec::new(),
next_cable_id: 0,
execution_order: Vec::new(),
routing: Routing::default(),
dirty: true,
last_compile_error: None,
sample_rate,
output_node: None,
validation_mode: ValidationMode::Warn,
warnings: Vec::new(),
meta: PatchMeta::default(),
}
}
pub fn meta(&self) -> &PatchMeta {
&self.meta
}
pub fn meta_mut(&mut self) -> &mut PatchMeta {
&mut self.meta
}
pub fn set_meta(&mut self, meta: PatchMeta) {
self.meta = meta;
}
pub fn set_validation_mode(&mut self, mode: ValidationMode) {
self.validation_mode = mode;
}
pub fn validation_mode(&self) -> ValidationMode {
self.validation_mode
}
pub fn warnings(&self) -> &[String] {
&self.warnings
}
pub fn clear_warnings(&mut self) {
self.warnings.clear();
}
pub fn sample_rate(&self) -> f64 {
self.sample_rate
}
pub fn add<M: GraphModule + 'static>(
&mut self,
name: impl Into<String>,
mut module: M,
) -> NodeHandle {
module.set_sample_rate(self.sample_rate);
let spec = module.port_spec().clone();
let id = self.nodes.insert(Node {
module: Box::new(module),
name: name.into(),
position: None,
param_overrides: StdMap::new(),
});
self.invalidate();
NodeHandle { id, spec }
}
pub fn add_boxed(
&mut self,
name: impl Into<String>,
mut module: Box<dyn GraphModule>,
) -> NodeHandle {
module.set_sample_rate(self.sample_rate);
let spec = module.port_spec().clone();
let id = self.nodes.insert(Node {
module,
name: name.into(),
position: None,
param_overrides: StdMap::new(),
});
self.invalidate();
NodeHandle { id, spec }
}
pub fn remove(&mut self, node: NodeId) -> Result<(), PatchError> {
if self.nodes.remove(node).is_none() {
return Err(PatchError::InvalidNode { node });
}
self.cables
.retain(|cable| cable.from.node != node && cable.to.node != node);
if self.output_node == Some(node) {
self.output_node = None;
}
self.invalidate();
Ok(())
}
fn alloc_cable_id(&mut self) -> CableId {
let id = self.next_cable_id;
self.next_cable_id += 1;
id
}
pub fn connect(&mut self, from: PortRef, to: PortRef) -> Result<CableId, PatchError> {
self.validate_output_port(from)?;
self.validate_input_port(to)?;
self.validate_signal_compatibility(from, to)?;
let id = self.alloc_cable_id();
self.cables.push(Cable {
id,
from,
to,
attenuation: None,
offset: None,
});
self.invalidate();
Ok(id)
}
pub fn connect_attenuated(
&mut self,
from: PortRef,
to: PortRef,
attenuation: f64,
) -> Result<CableId, PatchError> {
self.validate_output_port(from)?;
self.validate_input_port(to)?;
self.validate_signal_compatibility(from, to)?;
let id = self.alloc_cable_id();
self.cables.push(Cable {
id,
from,
to,
attenuation: Some(attenuation.clamp(0.0, 1.0)),
offset: None,
});
self.invalidate();
Ok(id)
}
pub fn connect_modulated(
&mut self,
from: PortRef,
to: PortRef,
attenuation: f64,
offset: f64,
) -> Result<CableId, PatchError> {
self.validate_output_port(from)?;
self.validate_input_port(to)?;
self.validate_signal_compatibility(from, to)?;
let id = self.alloc_cable_id();
self.cables.push(Cable {
id,
from,
to,
attenuation: Some(attenuation.clamp(-2.0, 2.0)),
offset: Some(offset.clamp(-10.0, 10.0)),
});
self.invalidate();
Ok(id)
}
fn validate_signal_compatibility(
&mut self,
from: PortRef,
to: PortRef,
) -> Result<(), PatchError> {
if self.validation_mode == ValidationMode::None {
return Ok(());
}
let from_kind = self.get_output_port_kind(from);
let to_kind = self.get_input_port_kind(to);
if let (Some(from_kind), Some(to_kind)) = (from_kind, to_kind) {
let result = from_kind.is_compatible_with(&to_kind);
if let Some(warning) = result.warning {
let from_name = self.get_name(from.node).unwrap_or("unknown");
let to_name = self.get_name(to.node).unwrap_or("unknown");
let full_warning = format!(
"{}.{} -> {}.{}: {}",
from_name, from.port, to_name, to.port, warning
);
match self.validation_mode {
ValidationMode::Warn => {
self.warnings.push(full_warning);
}
ValidationMode::Strict => {
return Err(PatchError::SignalMismatch {
from_kind,
to_kind,
message: warning,
});
}
ValidationMode::None => {}
}
}
}
Ok(())
}
fn get_output_port_kind(&self, port_ref: PortRef) -> Option<SignalKind> {
let node = self.nodes.get(port_ref.node)?;
node.module
.port_spec()
.outputs
.iter()
.find(|p| p.id == port_ref.port)
.map(|p| p.kind)
}
fn get_input_port_kind(&self, port_ref: PortRef) -> Option<SignalKind> {
let node = self.nodes.get(port_ref.node)?;
node.module
.port_spec()
.inputs
.iter()
.find(|p| p.id == port_ref.port)
.map(|p| p.kind)
}
pub fn mult(&mut self, from: PortRef, to: &[PortRef]) -> Result<Vec<CableId>, PatchError> {
to.iter().map(|&dest| self.connect(from, dest)).collect()
}
pub fn disconnect(&mut self, cable_id: CableId) -> Result<(), PatchError> {
let idx = self
.cables
.iter()
.position(|c| c.id == cable_id)
.ok_or(PatchError::InvalidCable)?;
self.cables.remove(idx);
self.invalidate();
Ok(())
}
pub fn set_output(&mut self, node: NodeId) {
self.output_node = Some(node);
self.dirty = true;
}
pub fn output_node(&self) -> Option<NodeId> {
self.output_node
}
pub fn try_set_output(&mut self, node: NodeId) -> Result<(), PatchError> {
let n = self
.nodes
.get(node)
.ok_or(PatchError::InvalidNode { node })?;
if n.module.port_spec().outputs.is_empty() {
return Err(PatchError::InvalidPort {
node,
name: None,
port: None,
available: Vec::new(),
});
}
self.output_node = Some(node);
self.dirty = true;
Ok(())
}
pub fn set_param(&mut self, node: NodeId, param: ParamId, value: f64) {
if let Some(n) = self.nodes.get_mut(node) {
n.module.set_param(param, value);
}
}
pub fn get_param(&self, node: NodeId, param: ParamId) -> Option<f64> {
self.nodes.get(node).and_then(|n| n.module.get_param(param))
}
pub fn set_position(&mut self, node: NodeId, position: (f32, f32)) {
if let Some(n) = self.nodes.get_mut(node) {
n.position = Some(position);
}
}
pub fn get_position(&self, node: NodeId) -> Option<(f32, f32)> {
self.nodes.get(node).and_then(|n| n.position)
}
pub fn get_name(&self, node: NodeId) -> Option<&str> {
self.nodes.get(node).map(|n| n.name.as_str())
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn cable_count(&self) -> usize {
self.cables.len()
}
pub fn cables(&self) -> &[Cable] {
&self.cables
}
pub fn execution_order(&self) -> &[NodeId] {
&self.execution_order
}
fn invalidate(&mut self) {
self.execution_order.clear();
self.routing = Routing::default();
self.dirty = true;
}
fn validate_output_port(&self, port_ref: PortRef) -> Result<(), PatchError> {
let node = self
.nodes
.get(port_ref.node)
.ok_or(PatchError::InvalidNode {
node: port_ref.node,
})?;
let spec = node.module.port_spec();
if spec.outputs.iter().any(|p| p.id == port_ref.port) {
Ok(())
} else {
Err(PatchError::InvalidPort {
node: port_ref.node,
name: None,
port: Some(port_ref.port),
available: spec.outputs.iter().map(|p| p.name.clone()).collect(),
})
}
}
fn validate_input_port(&self, port_ref: PortRef) -> Result<(), PatchError> {
let node = self
.nodes
.get(port_ref.node)
.ok_or(PatchError::InvalidNode {
node: port_ref.node,
})?;
let spec = node.module.port_spec();
if spec.inputs.iter().any(|p| p.id == port_ref.port) {
Ok(())
} else {
Err(PatchError::InvalidPort {
node: port_ref.node,
name: None,
port: Some(port_ref.port),
available: spec.inputs.iter().map(|p| p.name.clone()).collect(),
})
}
}
pub fn compile(&mut self) -> Result<(), PatchError> {
let order = match self.topological_sort() {
Ok(order) => order,
Err(e) => {
self.execution_order.clear();
self.routing = Routing::default();
self.dirty = false;
self.last_compile_error = Some(e.clone());
return Err(e);
}
};
self.execution_order = order;
self.build_routing();
self.dirty = false;
self.last_compile_error = None;
Ok(())
}
fn build_routing(&mut self) {
let mut routing = Routing::default();
let mut slot: usize = 0;
for &node_id in &self.execution_order {
let node = self
.nodes
.get(node_id)
.expect("execution_order only holds live nodes");
let spec = node.module.port_spec();
let out_base = slot;
let mut out_ids = Vec::with_capacity(spec.outputs.len());
for output in &spec.outputs {
routing.out_slot_index.insert(
PortRef {
node: node_id,
port: output.id,
},
slot,
);
out_ids.push(output.id);
slot += 1;
}
routing.nodes.push(NodeExec {
node_id,
out_base,
out_ids,
inputs: Vec::new(),
});
}
routing.out_buf.resize(slot, 0.0);
for (exec_idx, &node_id) in self.execution_order.iter().enumerate() {
let node = self
.nodes
.get(node_id)
.expect("execution_order only holds live nodes");
let spec = node.module.port_spec();
let mut scratch_in = PortValues::new();
let mut scratch_out = PortValues::new();
let mut inputs = Vec::with_capacity(spec.inputs.len());
for input in &spec.inputs {
let port_ref = PortRef {
node: node_id,
port: input.id,
};
let mut edges = Vec::new();
let mut has_connection = false;
for cable in &self.cables {
if cable.to == port_ref {
has_connection = true;
if let Some(&src_slot) = routing.out_slot_index.get(&cable.from) {
edges.push(InEdge {
src_slot,
attenuation: cable.attenuation,
offset: cable.offset,
});
}
}
}
let default = node
.param_overrides
.get(&input.name)
.copied()
.unwrap_or(input.default);
inputs.push(InputPlan {
port_id: input.id,
default,
normalled_to: input.normalled_to,
has_connection,
edges,
});
scratch_in.set(input.id, 0.0);
}
resolve_normalled_chains(&mut inputs);
for output in &spec.outputs {
scratch_out.set(output.id, 0.0);
}
routing.nodes[exec_idx].inputs = inputs;
routing.scratch_in.push(scratch_in);
routing.scratch_out.push(scratch_out);
}
routing.output_slots = self.output_node.and_then(|out_node| {
let node = self.nodes.get(out_node)?;
let outputs = &node.module.port_spec().outputs;
let left_id = outputs.first()?.id;
let left = *routing.out_slot_index.get(&PortRef {
node: out_node,
port: left_id,
})?;
let right = outputs
.get(1)
.and_then(|p| {
routing
.out_slot_index
.get(&PortRef {
node: out_node,
port: p.id,
})
.copied()
})
.unwrap_or(left);
Some((left, right))
});
self.routing = routing;
}
fn node_breaks_feedback(&self, node: NodeId) -> bool {
self.nodes
.get(node)
.map(|n| n.module.breaks_feedback_cycle())
.unwrap_or(false)
}
fn topological_sort(&self) -> Result<Vec<NodeId>, PatchError> {
let mut in_degree: StdMap<NodeId, usize> = self.nodes.keys().map(|k| (k, 0)).collect();
let mut successors: StdMap<NodeId, Vec<NodeId>> =
self.nodes.keys().map(|k| (k, Vec::new())).collect();
for cable in &self.cables {
if self.node_breaks_feedback(cable.to.node) {
continue;
}
if let Some(deg) = in_degree.get_mut(&cable.to.node) {
*deg += 1;
}
if let Some(succ) = successors.get_mut(&cable.from.node) {
succ.push(cable.to.node);
}
}
let mut queue: VecDeque<NodeId> = VecDeque::new();
for id in self.nodes.keys() {
if in_degree.get(&id).copied().unwrap_or(0) == 0 {
queue.push_back(id);
}
}
let mut result = Vec::with_capacity(self.nodes.len());
while let Some(node) = queue.pop_front() {
result.push(node);
if let Some(succ) = successors.get(&node) {
for &s in succ {
if let Some(deg) = in_degree.get_mut(&s) {
*deg -= 1;
if *deg == 0 {
queue.push_back(s);
}
}
}
}
}
if result.len() != self.nodes.len() {
let nodes: Vec<NodeId> = self
.nodes
.keys()
.filter(|k| in_degree.get(k).copied().unwrap_or(0) > 0)
.collect();
let names = nodes
.iter()
.map(|&id| self.get_name(id).unwrap_or("<unknown>").to_string())
.collect();
return Err(PatchError::CycleDetected { nodes, names });
}
Ok(result)
}
pub fn last_compile_error(&self) -> Option<&PatchError> {
self.last_compile_error.as_ref()
}
pub fn tick(&mut self) -> (f64, f64) {
if self.dirty {
let _ = self.compile();
}
self.tick_step()
}
pub fn tick_block(&mut self, out_left: &mut [f64], out_right: &mut [f64]) {
if self.dirty {
let _ = self.compile();
}
let frames = out_left.len().min(out_right.len());
for frame in 0..frames {
let (left, right) = self.tick_step();
out_left[frame] = left;
out_right[frame] = right;
}
}
fn tick_step(&mut self) -> (f64, f64) {
let routing = &mut self.routing;
let nodes = &mut self.nodes;
for i in 0..routing.nodes.len() {
routing.nodes[i].gather(&routing.out_buf, &mut routing.scratch_in[i]);
let node_id = routing.nodes[i].node_id;
routing.scratch_out[i].clear();
if let Some(node) = nodes.get_mut(node_id) {
node.module
.tick(&routing.scratch_in[i], &mut routing.scratch_out[i]);
}
routing.nodes[i].scatter(&routing.scratch_out[i], &mut routing.out_buf);
}
routing.read_output()
}
pub fn reset(&mut self) {
for (_, node) in &mut self.nodes {
node.module.reset();
}
for value in self.routing.out_buf.iter_mut() {
*value = 0.0;
}
}
pub fn nodes(&self) -> impl Iterator<Item = (NodeId, &str, &dyn GraphModule)> {
self.nodes
.iter()
.map(|(id, node)| (id, node.name.as_str(), node.module.as_ref()))
}
pub fn get_node_id_by_name(&self, name: &str) -> Option<NodeId> {
self.nodes
.iter()
.find(|(_, node)| node.name == name)
.map(|(id, _)| id)
}
pub fn get_handle_by_name(&self, name: &str) -> Option<NodeHandle> {
self.nodes
.iter()
.find(|(_, node)| node.name == name)
.map(|(id, node)| NodeHandle::from_module(id, node.module.as_ref()))
}
pub fn disconnect_ports(&mut self, from: PortRef, to: PortRef) -> Result<(), PatchError> {
let idx = self
.cables
.iter()
.position(|c| c.from == from && c.to == to)
.ok_or(PatchError::InvalidCable)?;
self.cables.remove(idx);
self.invalidate();
Ok(())
}
pub fn module_names(&self) -> Vec<&str> {
self.nodes
.iter()
.map(|(_, node)| node.name.as_str())
.collect()
}
pub fn get_output_value(&self, node: NodeId, port: PortId) -> Option<f64> {
self.routing
.out_slot_index
.get(&PortRef { node, port })
.map(|&slot| self.routing.out_buf[slot])
}
pub fn get_output_signal_kind(&self, node: NodeId, port: PortId) -> Option<SignalKind> {
let node_data = self.nodes.get(node)?;
node_data
.module
.port_spec()
.outputs
.iter()
.find(|p| p.id == port)
.map(|p| p.kind)
}
}
#[cfg(feature = "alloc")]
fn is_control_input(kind: SignalKind) -> bool {
kind != SignalKind::Audio
}
#[cfg(feature = "alloc")]
fn port_param_info(port: &crate::port::PortDef, value: f64) -> crate::introspection::ParamInfo {
let (min, max) = port.kind.voltage_range();
crate::introspection::ParamInfo::new(port.name.clone(), port.name.clone())
.with_range(min, max)
.with_default(port.default)
.with_value(value)
}
#[cfg(feature = "alloc")]
impl Patch {
pub fn param_infos(&self, node: NodeId) -> Vec<crate::introspection::ParamInfo> {
let Some(n) = self.nodes.get(node) else {
return Vec::new();
};
let spec = n.module.port_spec();
let mut infos: Vec<crate::introspection::ParamInfo> = n
.module
.introspect()
.map(|i| i.param_infos())
.unwrap_or_default()
.into_iter()
.filter(|p| spec.input_by_name(&p.id).is_none())
.collect();
for input in &spec.inputs {
if !is_control_input(input.kind) {
continue;
}
let value = n
.param_overrides
.get(&input.name)
.copied()
.unwrap_or(input.default);
infos.push(port_param_info(input, value));
}
infos
}
pub fn get_param_by_id(&self, node: NodeId, id: &str) -> Option<f64> {
let n = self.nodes.get(node)?;
if let Some(port) = n.module.port_spec().input_by_name(id) {
if is_control_input(port.kind) {
return Some(n.param_overrides.get(id).copied().unwrap_or(port.default));
}
}
n.module
.introspect()
.and_then(|i| i.get_param_info(id))
.map(|p| p.value)
}
pub fn set_param_by_id(&mut self, node: NodeId, id: &str, value: f64) -> bool {
let is_port = self
.nodes
.get(node)
.map(|n| {
n.module
.port_spec()
.input_by_name(id)
.map(|p| is_control_input(p.kind))
.unwrap_or(false)
})
.unwrap_or(false);
if is_port {
if let Some(n) = self.nodes.get_mut(node) {
n.param_overrides.insert(id.to_string(), value);
}
self.invalidate();
return true;
}
if let Some(n) = self.nodes.get_mut(node) {
if let Some(intro) = n.module.introspect_mut() {
return intro.set_param_by_id(id, value);
}
}
false
}
pub fn deserialize_module_state(
&mut self,
node: NodeId,
state: &serde_json::Value,
) -> Result<(), String> {
match self.nodes.get_mut(node) {
Some(n) => n.module.deserialize_state(state),
None => Ok(()),
}
}
}
impl core::fmt::Debug for Patch {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
struct NodeDebug<'a> {
id: NodeId,
name: &'a str,
type_id: &'a str,
}
impl core::fmt::Debug for NodeDebug<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{:?}: {} ({})", self.id, self.name, self.type_id)
}
}
let nodes: Vec<NodeDebug> = self
.nodes
.iter()
.map(|(id, n)| NodeDebug {
id,
name: n.name.as_str(),
type_id: n.module.type_id(),
})
.collect();
f.debug_struct("Patch")
.field("sample_rate", &self.sample_rate)
.field("nodes", &nodes)
.field("cables", &self.cables)
.field("output_node", &self.output_node)
.field("validation_mode", &self.validation_mode)
.field("dirty", &self.dirty)
.field("warnings", &self.warnings.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::port::{PortDef, SignalKind};
use alloc::vec;
struct Passthrough {
spec: PortSpec,
}
impl Passthrough {
fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![PortDef::new(0, "in", SignalKind::Audio)],
outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
},
}
}
}
impl GraphModule for Passthrough {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
let input = inputs.get_or(0, 0.0);
outputs.set(10, input);
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
}
#[test]
fn test_add_module() {
let mut patch = Patch::new(44100.0);
let handle = patch.add("test", Passthrough::new());
assert_eq!(patch.node_count(), 1);
assert!(patch.get_name(handle.id()).is_some());
}
#[test]
fn test_connect() {
let mut patch = Patch::new(44100.0);
let a = patch.add("a", Passthrough::new());
let b = patch.add("b", Passthrough::new());
let result = patch.connect(a.out("out"), b.in_("in"));
assert!(result.is_ok());
assert_eq!(patch.cable_count(), 1);
}
#[test]
fn test_topological_sort() {
let mut patch = Patch::new(44100.0);
let a = patch.add("a", Passthrough::new());
let b = patch.add("b", Passthrough::new());
let c = patch.add("c", Passthrough::new());
patch.connect(a.out("out"), b.in_("in")).unwrap();
patch.connect(b.out("out"), c.in_("in")).unwrap();
patch.compile().unwrap();
let order = patch.execution_order();
let a_pos = order.iter().position(|&x| x == a.id()).unwrap();
let b_pos = order.iter().position(|&x| x == b.id()).unwrap();
let c_pos = order.iter().position(|&x| x == c.id()).unwrap();
assert!(a_pos < b_pos, "A should come before B");
assert!(b_pos < c_pos, "B should come before C");
}
#[test]
fn test_cycle_detection() {
let mut patch = Patch::new(44100.0);
let a = patch.add("a", Passthrough::new());
let b = patch.add("b", Passthrough::new());
patch.connect(a.out("out"), b.in_("in")).unwrap();
patch.connect(b.out("out"), a.in_("in")).unwrap();
let result = patch.compile();
assert!(matches!(result, Err(PatchError::CycleDetected { .. })));
}
#[test]
fn test_mult() {
let mut patch = Patch::new(44100.0);
let a = patch.add("a", Passthrough::new());
let b = patch.add("b", Passthrough::new());
let c = patch.add("c", Passthrough::new());
let result = patch.mult(a.out("out"), &[b.in_("in"), c.in_("in")]);
assert!(result.is_ok());
assert_eq!(patch.cable_count(), 2);
}
#[test]
fn test_disconnect() {
let mut patch = Patch::new(44100.0);
let a = patch.add("a", Passthrough::new());
let b = patch.add("b", Passthrough::new());
let cable_id = patch.connect(a.out("out"), b.in_("in")).unwrap();
assert_eq!(patch.cable_count(), 1);
patch.disconnect(cable_id).unwrap();
assert_eq!(patch.cable_count(), 0);
}
#[test]
fn test_remove_module() {
let mut patch = Patch::new(44100.0);
let a = patch.add("a", Passthrough::new());
let b = patch.add("b", Passthrough::new());
patch.connect(a.out("out"), b.in_("in")).unwrap();
assert_eq!(patch.node_count(), 2);
assert_eq!(patch.cable_count(), 1);
patch.remove(a.id()).unwrap();
assert_eq!(patch.node_count(), 1);
assert_eq!(patch.cable_count(), 0); }
struct GateModule {
spec: PortSpec,
}
impl GateModule {
fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![PortDef::new(0, "in", SignalKind::Gate)],
outputs: vec![PortDef::new(10, "out", SignalKind::Gate)],
},
}
}
}
impl GraphModule for GateModule {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
outputs.set(10, inputs.get_or(0, 0.0));
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
}
#[test]
fn test_validation_mode_none() {
let mut patch = Patch::new(44100.0);
patch.set_validation_mode(ValidationMode::None);
let audio = patch.add("audio", Passthrough::new());
let gate = patch.add("gate", GateModule::new());
let result = patch.connect(audio.out("out"), gate.in_("in"));
assert!(result.is_ok());
assert!(patch.warnings().is_empty());
}
#[test]
fn test_validation_mode_warn() {
let mut patch = Patch::new(44100.0);
patch.set_validation_mode(ValidationMode::Warn);
let audio = patch.add("audio", Passthrough::new());
let gate = patch.add("gate", GateModule::new());
let result = patch.connect(audio.out("out"), gate.in_("in"));
assert!(result.is_ok());
assert!(!patch.warnings().is_empty());
}
#[test]
fn test_validation_mode_strict() {
let mut patch = Patch::new(44100.0);
patch.set_validation_mode(ValidationMode::Strict);
let audio = patch.add("audio", Passthrough::new());
let gate = patch.add("gate", GateModule::new());
let result = patch.connect(audio.out("out"), gate.in_("in"));
assert!(matches!(result, Err(PatchError::SignalMismatch { .. })));
}
#[test]
fn test_same_signal_type_no_warning() {
let mut patch = Patch::new(44100.0);
patch.set_validation_mode(ValidationMode::Warn);
let a = patch.add("a", Passthrough::new());
let b = patch.add("b", Passthrough::new());
let result = patch.connect(a.out("out"), b.in_("in"));
assert!(result.is_ok());
assert!(patch.warnings().is_empty());
}
#[test]
fn test_connect_modulated() {
let mut patch = Patch::new(44100.0);
let a = patch.add("a", Passthrough::new());
let b = patch.add("b", Passthrough::new());
let result = patch.connect_modulated(a.out("out"), b.in_("in"), 0.5, 1.0);
assert!(result.is_ok());
let cables = patch.cables();
assert_eq!(cables.len(), 1);
assert_eq!(cables[0].attenuation, Some(0.5));
assert_eq!(cables[0].offset, Some(1.0));
}
#[test]
fn test_modulated_signal_processing() {
let mut patch = Patch::new(44100.0);
struct ConstModule {
spec: PortSpec,
value: f64,
}
impl ConstModule {
fn new(value: f64) -> Self {
Self {
value,
spec: PortSpec {
inputs: vec![],
outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
},
}
}
}
impl GraphModule for ConstModule {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, _: &PortValues, outputs: &mut PortValues) {
outputs.set(10, self.value);
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
}
struct RecordModule {
spec: PortSpec,
last_value: f64,
}
impl RecordModule {
fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![PortDef::new(0, "in", SignalKind::Audio)],
outputs: vec![],
},
last_value: 0.0,
}
}
}
impl GraphModule for RecordModule {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, _: &mut PortValues) {
self.last_value = inputs.get_or(0, 0.0);
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
}
let source = patch.add("source", ConstModule::new(4.0));
let sink = patch.add("sink", RecordModule::new());
patch
.connect_modulated(source.out("out"), sink.in_("in"), 0.5, 2.0)
.unwrap();
patch.set_output(sink.id());
patch.compile().unwrap();
patch.tick();
}
#[test]
fn test_signal_compatibility() {
assert!(SignalKind::Audio
.is_compatible_with(&SignalKind::Audio)
.warning
.is_none());
assert!(SignalKind::Audio
.is_compatible_with(&SignalKind::CvBipolar)
.warning
.is_some());
assert!(SignalKind::Gate
.is_compatible_with(&SignalKind::Trigger)
.warning
.is_some());
assert!(SignalKind::Clock
.is_compatible_with(&SignalKind::Trigger)
.warning
.is_none());
}
#[test]
fn test_patch_get_name() {
let mut patch = Patch::new(44100.0);
let a = patch.add("my_module", Passthrough::new());
let name = patch.get_name(a.id());
assert_eq!(name, Some("my_module"));
use slotmap::DefaultKey;
let fake_id: NodeId = DefaultKey::default();
assert!(patch.get_name(fake_id).is_none());
}
#[test]
fn test_patch_set_position() {
let mut patch = Patch::new(44100.0);
let a = patch.add("a", Passthrough::new());
patch.set_position(a.id(), (100.0, 200.0));
}
#[test]
fn test_patch_clear_warnings() {
let mut patch = Patch::new(44100.0);
patch.set_validation_mode(ValidationMode::Warn);
let audio = patch.add("audio", Passthrough::new());
let gate = patch.add("gate", GateModule::new());
patch.connect(audio.out("out"), gate.in_("in")).unwrap();
assert!(!patch.warnings().is_empty());
patch.clear_warnings();
assert!(patch.warnings().is_empty());
}
#[test]
fn test_patch_validation_mode_getter() {
let mut patch = Patch::new(44100.0);
patch.set_validation_mode(ValidationMode::Strict);
assert_eq!(patch.validation_mode(), ValidationMode::Strict);
}
#[test]
fn test_patch_sample_rate() {
let patch = Patch::new(48000.0);
assert_eq!(patch.sample_rate(), 48000.0);
}
#[test]
fn test_patch_execution_order() {
let mut patch = Patch::new(44100.0);
let a = patch.add("a", Passthrough::new());
let b = patch.add("b", Passthrough::new());
patch.connect(a.out("out"), b.in_("in")).unwrap();
patch.compile().unwrap();
let order = patch.execution_order();
assert_eq!(order.len(), 2);
}
#[test]
fn test_patch_mult() {
let mut patch = Patch::new(44100.0);
let a = patch.add("a", Passthrough::new());
let b = patch.add("b", Passthrough::new());
let c = patch.add("c", Passthrough::new());
let result = patch.mult(a.out("out"), &[b.in_("in"), c.in_("in")]);
assert!(result.is_ok());
assert_eq!(patch.cable_count(), 2);
}
#[test]
fn test_patch_reset() {
let mut patch = Patch::new(44100.0);
let a = patch.add("a", Passthrough::new());
patch.set_output(a.id());
patch.compile().unwrap();
for _ in 0..100 {
patch.tick();
}
patch.reset();
}
#[test]
fn test_patch_set_param_get_param() {
use crate::modules::Vco;
let mut patch = Patch::new(44100.0);
let vco = patch.add("vco", Vco::new(44100.0));
patch.set_param(vco.id(), 0, 0.5);
let _ = patch.get_param(vco.id(), 0);
}
#[test]
fn test_node_handle_spec() {
let mut patch = Patch::new(44100.0);
let a = patch.add("a", Passthrough::new());
let spec = a.spec();
assert!(!spec.inputs.is_empty());
assert!(!spec.outputs.is_empty());
}
#[test]
fn test_patch_validation_mode() {
let mut patch = Patch::new(44100.0);
patch.set_validation_mode(ValidationMode::Strict);
assert_eq!(patch.validation_mode(), ValidationMode::Strict);
patch.set_validation_mode(ValidationMode::Warn);
assert_eq!(patch.validation_mode(), ValidationMode::Warn);
}
struct SumModule {
spec: PortSpec,
}
impl SumModule {
fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![
PortDef::new(0, "a", SignalKind::Audio),
PortDef::new(1, "b", SignalKind::Audio),
],
outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
},
}
}
}
impl GraphModule for SumModule {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
outputs.set(10, inputs.get_or(0, 0.0) + inputs.get_or(1, 0.0));
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
}
struct FeedbackDelay {
spec: PortSpec,
buffer: f64,
}
impl FeedbackDelay {
fn new() -> Self {
Self {
spec: PortSpec {
inputs: vec![PortDef::new(0, "in", SignalKind::Audio)],
outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
},
buffer: 0.0,
}
}
}
impl GraphModule for FeedbackDelay {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
outputs.set(10, self.buffer);
self.buffer = inputs.get_or(0, 0.0);
}
fn reset(&mut self) {
self.buffer = 0.0;
}
fn set_sample_rate(&mut self, _: f64) {}
fn breaks_feedback_cycle(&self) -> bool {
true
}
}
struct ConstSource {
spec: PortSpec,
value: f64,
}
impl ConstSource {
fn new(value: f64) -> Self {
Self {
spec: PortSpec {
inputs: vec![],
outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
},
value,
}
}
}
impl GraphModule for ConstSource {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, _: &PortValues, outputs: &mut PortValues) {
outputs.set(10, self.value);
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
}
#[test]
fn test_cable_ids_are_stable_across_disconnect() {
let mut patch = Patch::new(44100.0);
let a = patch.add("a", Passthrough::new());
let b = patch.add("b", SumModule::new());
let c = patch.add("c", SumModule::new());
let c1 = patch.connect(a.out("out"), b.in_("a")).unwrap();
let c2 = patch.connect(a.out("out"), b.in_("b")).unwrap();
let c3 = patch.connect(a.out("out"), c.in_("a")).unwrap();
assert_eq!(patch.cable_count(), 3);
patch.disconnect(c1).unwrap();
assert_eq!(patch.cable_count(), 2);
patch.disconnect(c3).unwrap();
assert_eq!(patch.cable_count(), 1);
let remaining = &patch.cables()[0];
assert_eq!(remaining.id, c2);
assert_eq!(remaining.to, b.in_("b"));
assert!(matches!(
patch.disconnect(c1),
Err(PatchError::InvalidCable)
));
}
#[test]
fn test_mutation_after_compile_is_reflected_on_tick() {
let mut patch = Patch::new(44100.0);
let src = patch.add("src", ConstSource::new(1.0));
let out = patch.add("out", Passthrough::new());
patch.connect(src.out("out"), out.in_("in")).unwrap();
patch.set_output(out.id());
patch.compile().unwrap();
let (l0, _) = patch.tick();
assert!((l0 - 1.0).abs() < 1e-9);
let src2 = patch.add("src2", ConstSource::new(2.0));
let sum = patch.add("sum", SumModule::new());
patch
.disconnect_ports(src.out("out"), out.in_("in"))
.unwrap();
patch.connect(src.out("out"), sum.in_("a")).unwrap();
patch.connect(src2.out("out"), sum.in_("b")).unwrap();
patch.connect(sum.out("out"), out.in_("in")).unwrap();
let (l1, _) = patch.tick();
assert!((l1 - 3.0).abs() < 1e-9, "expected 3.0, got {}", l1);
}
#[test]
fn test_cycle_mutation_surfaces_via_last_compile_error() {
let mut patch = Patch::new(44100.0);
let a = patch.add("a", Passthrough::new());
let b = patch.add("b", Passthrough::new());
patch.connect(a.out("out"), b.in_("in")).unwrap();
patch.set_output(b.id());
patch.compile().unwrap();
assert!(patch.last_compile_error().is_none());
patch.connect(b.out("out"), a.in_("in")).unwrap();
let (l, r) = patch.tick();
assert_eq!((l, r), (0.0, 0.0));
match patch.last_compile_error() {
Some(PatchError::CycleDetected { names, .. }) => {
assert_eq!(names.len(), 2);
}
other => panic!("expected CycleDetected, got {:?}", other),
}
}
#[test]
fn test_feedback_loop_with_delay_compiles_and_decays() {
struct Impulse {
spec: PortSpec,
fired: bool,
}
impl GraphModule for Impulse {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, _: &PortValues, outputs: &mut PortValues) {
outputs.set(10, if self.fired { 0.0 } else { 1.0 });
self.fired = true;
}
fn reset(&mut self) {
self.fired = false;
}
fn set_sample_rate(&mut self, _: f64) {}
}
let mut patch = Patch::new(44100.0);
let impulse = patch.add(
"impulse",
Impulse {
spec: PortSpec {
inputs: vec![],
outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
},
fired: false,
},
);
let sum = patch.add("sum", SumModule::new());
let delay = patch.add("delay", FeedbackDelay::new());
patch.connect(impulse.out("out"), sum.in_("a")).unwrap();
patch
.connect_attenuated(delay.out("out"), sum.in_("b"), 0.5)
.unwrap();
patch.connect(sum.out("out"), delay.in_("in")).unwrap();
patch.set_output(delay.id());
patch.compile().expect("feedback loop should compile");
assert!(patch.last_compile_error().is_none());
let mut outs = Vec::new();
for _ in 0..14 {
outs.push(patch.tick().0);
}
let nonzero: Vec<f64> = outs.iter().copied().filter(|v| v.abs() > 1e-9).collect();
assert!(
nonzero.len() >= 3,
"expected multiple decaying echoes, got {:?}",
outs
);
let peak_early = outs.iter().cloned().fold(0.0_f64, f64::max);
let peak_late = outs[outs.len() - 3..]
.iter()
.cloned()
.fold(0.0_f64, f64::max);
assert!(
peak_late < peak_early,
"echo should decay: early peak {}, late peak {}",
peak_early,
peak_late
);
}
#[test]
fn test_breakerless_cycle_still_errors() {
let mut patch = Patch::new(44100.0);
let a = patch.add("a", Passthrough::new());
let b = patch.add("b", Passthrough::new());
patch.connect(a.out("out"), b.in_("in")).unwrap();
patch.connect(b.out("out"), a.in_("in")).unwrap();
assert!(matches!(
patch.compile(),
Err(PatchError::CycleDetected { .. })
));
}
#[test]
fn test_normalled_input_uses_current_sibling_value() {
use crate::modules::StereoOutput;
let mut patch = Patch::new(44100.0);
struct Ramp {
spec: PortSpec,
n: f64,
}
impl GraphModule for Ramp {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, _: &PortValues, outputs: &mut PortValues) {
self.n += 1.0;
outputs.set(10, self.n);
}
fn reset(&mut self) {
self.n = 0.0;
}
fn set_sample_rate(&mut self, _: f64) {}
}
let ramp = patch.add(
"ramp",
Ramp {
spec: PortSpec {
inputs: vec![],
outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
},
n: 0.0,
},
);
let out = patch.add("out", StereoOutput::new());
patch.connect(ramp.out("out"), out.in_("left")).unwrap();
patch.set_output(out.id());
patch.compile().unwrap();
for _ in 0..5 {
let (l, r) = patch.tick();
assert!(l > 0.0);
assert_eq!(l, r, "mono fallback must be current-sample, not delayed");
}
}
struct NormalChain {
spec: PortSpec,
}
impl NormalChain {
fn new(cycle: bool) -> Self {
let (n0, n1) = if cycle { (1, 0) } else { (1, 2) };
Self {
spec: PortSpec {
inputs: vec![
PortDef::new(0, "a", SignalKind::Audio)
.with_default(0.1)
.normalled_to(n0),
PortDef::new(1, "b", SignalKind::Audio)
.with_default(0.2)
.normalled_to(n1),
PortDef::new(2, "c", SignalKind::Audio).with_default(0.3),
],
outputs: vec![
PortDef::new(10, "oa", SignalKind::Audio),
PortDef::new(11, "ob", SignalKind::Audio),
PortDef::new(12, "oc", SignalKind::Audio),
],
},
}
}
}
impl GraphModule for NormalChain {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
outputs.set(10, inputs.get_or(0, f64::NAN));
outputs.set(11, inputs.get_or(1, f64::NAN));
outputs.set(12, inputs.get_or(2, f64::NAN));
}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
}
#[test]
fn test_normalled_chain_resolves_transitively() {
let mut patch = Patch::new(44100.0);
let src = patch.add("src", ConstSource::new(0.75));
let node = patch.add("chain", NormalChain::new(false));
patch.connect(src.out("out"), node.in_("c")).unwrap();
patch.set_output(node.id());
patch.compile().unwrap();
let (oa, ob) = patch.tick();
assert!(
(oa - 0.75).abs() < 1e-9,
"forward-normalled input 0 must resolve to patched source, got {oa}"
);
assert!(
(ob - 0.75).abs() < 1e-9,
"transitively-normalled input 1 must resolve to patched source, got {ob}"
);
}
#[test]
fn test_normalled_cycle_falls_back_to_default() {
let mut patch = Patch::new(44100.0);
let node = patch.add("chain", NormalChain::new(true));
patch.set_output(node.id());
patch.compile().unwrap();
let (oa, ob) = patch.tick();
assert!(
(oa - 0.1).abs() < 1e-9,
"cycled input 0 -> own default, got {oa}"
);
assert!(
(ob - 0.2).abs() < 1e-9,
"cycled input 1 -> own default, got {ob}"
);
}
#[test]
fn test_execution_order_is_deterministic() {
fn build_order() -> Vec<usize> {
let mut patch = Patch::new(44100.0);
let s1 = patch.add("s1", ConstSource::new(1.0));
let s2 = patch.add("s2", ConstSource::new(2.0));
let s3 = patch.add("s3", ConstSource::new(3.0));
let sum = patch.add("sum", SumModule::new());
patch.connect(s1.out("out"), sum.in_("a")).unwrap();
patch.connect(s2.out("out"), sum.in_("b")).unwrap();
patch.connect(s3.out("out"), sum.in_("a")).unwrap();
patch.compile().unwrap();
let ids = [s1.id(), s2.id(), s3.id(), sum.id()];
patch
.execution_order()
.iter()
.map(|nid| ids.iter().position(|x| x == nid).unwrap())
.collect()
}
assert_eq!(build_order(), build_order());
}
#[test]
fn test_read_output_uses_first_two_outputs_mono_duplicated() {
let mut patch = Patch::new(44100.0);
let src = patch.add("src", ConstSource::new(0.7));
patch.set_output(src.id());
patch.compile().unwrap();
let (l, r) = patch.tick();
assert!((l - 0.7).abs() < 1e-9);
assert_eq!(l, r, "mono node must duplicate to both channels");
}
#[test]
fn test_try_set_output_validates() {
let mut patch = Patch::new(44100.0);
let src = patch.add("src", ConstSource::new(1.0));
assert!(patch.try_set_output(src.id()).is_ok());
struct SinkNoOut {
spec: PortSpec,
}
impl GraphModule for SinkNoOut {
fn port_spec(&self) -> &PortSpec {
&self.spec
}
fn tick(&mut self, _: &PortValues, _: &mut PortValues) {}
fn reset(&mut self) {}
fn set_sample_rate(&mut self, _: f64) {}
}
let sink = patch.add(
"sink",
SinkNoOut {
spec: PortSpec {
inputs: vec![PortDef::new(0, "in", SignalKind::Audio)],
outputs: vec![],
},
},
);
assert!(matches!(
patch.try_set_output(sink.id()),
Err(PatchError::InvalidPort { .. })
));
}
#[test]
fn test_node_handle_fallible_ports_and_names() {
let mut patch = Patch::new(44100.0);
let a = patch.add("a", Passthrough::new());
assert!(a.output("out").is_ok());
assert!(a.input("in").is_ok());
match a.output("nope") {
Err(PatchError::InvalidPort { available, .. }) => {
assert!(available.iter().any(|n| n == "out"));
}
other => panic!("expected InvalidPort, got {:?}", other),
}
assert!(a.input("nope").is_err());
assert_eq!(a.input_names(), vec!["in"]);
assert_eq!(a.output_names(), vec!["out"]);
}
#[test]
fn test_invalid_port_display_lists_available() {
let mut patch = Patch::new(44100.0);
let a = patch.add("a", Passthrough::new());
let b = patch.add("b", Passthrough::new());
let bad = PortRef {
node: b.id(),
port: 999,
};
let err = patch.connect(a.out("out"), bad).unwrap_err();
let msg = alloc::format!("{}", err);
assert!(msg.contains("Invalid port"), "got: {}", msg);
assert!(
msg.contains("in"),
"should list available port 'in': {}",
msg
);
}
#[test]
fn test_cycle_detected_display_names() {
let mut patch = Patch::new(44100.0);
let a = patch.add("osc", Passthrough::new());
let b = patch.add("filt", Passthrough::new());
patch.connect(a.out("out"), b.in_("in")).unwrap();
patch.connect(b.out("out"), a.in_("in")).unwrap();
let err = patch.compile().unwrap_err();
let msg = alloc::format!("{}", err);
assert!(msg.contains("Cycle detected"), "got: {}", msg);
assert!(
msg.contains("osc") && msg.contains("filt"),
"cycle message should name modules: {}",
msg
);
}
#[test]
fn test_default_validation_mode_is_warn() {
let patch = Patch::new(44100.0);
assert_eq!(patch.validation_mode(), ValidationMode::Warn);
assert_eq!(ValidationMode::default(), ValidationMode::Warn);
}
#[test]
fn test_patch_debug_impl() {
let mut patch = Patch::new(44100.0);
let a = patch.add("my_osc", Passthrough::new());
let b = patch.add("my_out", Passthrough::new());
patch.connect(a.out("out"), b.in_("in")).unwrap();
patch.set_output(b.id());
let s = alloc::format!("{:?}", patch);
assert!(s.contains("Patch"));
assert!(s.contains("my_osc"));
assert!(s.contains("my_out"));
assert!(s.contains("validation_mode"));
}
#[test]
fn test_tick_block_matches_per_sample_tick() {
fn ramp_svf_patch() -> (Patch, NodeHandle) {
let mut patch = Patch::new(44100.0);
let src = patch.add("src", ConstSource::new(0.9));
let pass = patch.add("pass", Passthrough::new());
patch.connect(src.out("out"), pass.in_("in")).unwrap();
patch.set_output(pass.id());
patch.compile().unwrap();
(patch, pass)
}
let (mut a, _) = ramp_svf_patch();
let mut reference = Vec::new();
for _ in 0..8 {
reference.push(a.tick());
}
let (mut b, _) = ramp_svf_patch();
let mut left = [0.0_f64; 8];
let mut right = [0.0_f64; 8];
b.tick_block(&mut left, &mut right);
for (i, &(l, r)) in reference.iter().enumerate() {
assert!((left[i] - l).abs() < 1e-12, "left[{}] mismatch", i);
assert!((right[i] - r).abs() < 1e-12, "right[{}] mismatch", i);
}
}
#[test]
fn test_tick_block_uses_min_length() {
let mut patch = Patch::new(44100.0);
let src = patch.add("src", ConstSource::new(1.0));
patch.set_output(src.id());
patch.compile().unwrap();
let mut left = [0.0_f64; 4];
let mut right = [0.0_f64; 2]; patch.tick_block(&mut left, &mut right);
assert_eq!(left[0], 1.0);
assert_eq!(left[1], 1.0);
assert_eq!(left[2], 0.0, "frame beyond min length must be untouched");
assert_eq!(left[3], 0.0);
assert_eq!(right[0], 1.0);
assert_eq!(right[1], 1.0);
}
#[test]
fn test_denormal_is_flushed_at_scatter() {
let mut patch = Patch::new(44100.0);
let src = patch.add("src", ConstSource::new(1e-30));
let pass = patch.add("pass", Passthrough::new());
patch.connect(src.out("out"), pass.in_("in")).unwrap();
patch.set_output(pass.id());
patch.compile().unwrap();
let (l, r) = patch.tick();
assert_eq!(l, 0.0, "subnormal must be flushed to zero");
assert_eq!(r, 0.0);
assert_eq!(patch.get_output_value(src.id(), 10), Some(0.0));
}
#[test]
fn test_precompiled_adjacency_sums_and_exposes_outputs() {
let mut patch = Patch::new(44100.0);
let s1 = patch.add("s1", ConstSource::new(2.0));
let s2 = patch.add("s2", ConstSource::new(3.0));
let sum = patch.add("sum", SumModule::new());
patch.connect(s1.out("out"), sum.in_("a")).unwrap();
patch
.connect_modulated(s2.out("out"), sum.in_("a"), 0.5, 1.0)
.unwrap();
patch.set_output(sum.id());
patch.compile().unwrap();
let (l, _) = patch.tick();
assert!((l - 4.5).abs() < 1e-12, "expected 4.5, got {}", l);
assert_eq!(patch.get_output_value(sum.id(), 10), Some(4.5));
assert_eq!(patch.get_output_value(sum.id(), 999), None);
}
}