use std::collections::{HashMap, HashSet};
use rill_core::math::Transcendental;
use rill_core::traits::{Node, NodeId, NodeVariant, ParamValue, Params};
use rill_core::ParameterId;
#[cfg(test)]
use crate::factory::NodeFactory;
use crate::factory::RegistryError;
use crate::graph::GraphBuilder;
use serde::de;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceDef {
pub name: String,
pub kind: String,
pub capacity: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphDef {
pub format_version: String,
pub sample_rate: f32,
pub block_size: usize,
#[serde(default)]
pub resources: Vec<ResourceDef>,
pub nodes: Vec<NodeDef>,
pub connections: Vec<ConnectionDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NodeDef {
Source(SourceDef),
Processor(ProcessorDef),
Router(RouterDef),
Sink(SinkDef),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceDef {
pub id: u32,
pub type_name: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub backend: Option<String>,
#[serde(
deserialize_with = "deserialize_params",
serialize_with = "serialize_params"
)]
pub parameters: HashMap<String, ParamValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessorDef {
pub id: u32,
pub type_name: String,
pub name: String,
#[serde(
deserialize_with = "deserialize_params",
serialize_with = "serialize_params"
)]
pub parameters: HashMap<String, ParamValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RouterDef {
pub id: u32,
pub type_name: String,
pub name: String,
#[serde(
deserialize_with = "deserialize_params",
serialize_with = "serialize_params"
)]
pub parameters: HashMap<String, ParamValue>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub routing_matrix: Vec<RoutingEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoutingEntry {
pub from: usize,
pub to: usize,
pub gain: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SinkDef {
pub id: u32,
pub type_name: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub backend: Option<String>,
#[serde(
deserialize_with = "deserialize_params",
serialize_with = "serialize_params"
)]
pub parameters: HashMap<String, ParamValue>,
}
impl NodeDef {
pub fn id(&self) -> u32 {
match self {
NodeDef::Source(s) => s.id,
NodeDef::Processor(p) => p.id,
NodeDef::Router(r) => r.id,
NodeDef::Sink(s) => s.id,
}
}
pub fn type_name(&self) -> &str {
match self {
NodeDef::Source(s) => &s.type_name,
NodeDef::Processor(p) => &p.type_name,
NodeDef::Router(r) => &r.type_name,
NodeDef::Sink(s) => &s.type_name,
}
}
pub fn name(&self) -> &str {
match self {
NodeDef::Source(s) => &s.name,
NodeDef::Processor(p) => &p.name,
NodeDef::Router(r) => &r.name,
NodeDef::Sink(s) => &s.name,
}
}
pub fn parameters(&self) -> &HashMap<String, ParamValue> {
match self {
NodeDef::Source(s) => &s.parameters,
NodeDef::Processor(p) => &p.parameters,
NodeDef::Router(r) => &r.parameters,
NodeDef::Sink(s) => &s.parameters,
}
}
pub fn parameters_mut(&mut self) -> &mut HashMap<String, ParamValue> {
match self {
NodeDef::Source(s) => &mut s.parameters,
NodeDef::Processor(p) => &mut p.parameters,
NodeDef::Router(r) => &mut r.parameters,
NodeDef::Sink(s) => &mut s.parameters,
}
}
pub fn backend(&self) -> Option<&str> {
match self {
NodeDef::Source(s) => s.backend.as_deref(),
NodeDef::Processor(_) => None,
NodeDef::Router(_) => None,
NodeDef::Sink(s) => s.backend.as_deref(),
}
}
pub fn kind_str(&self) -> &'static str {
match self {
NodeDef::Source(_) => "Source",
NodeDef::Processor(_) => "Processor",
NodeDef::Router(_) => "Router",
NodeDef::Sink(_) => "Sink",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectionDef {
pub kind: SignalKind,
pub from_node: u32,
pub from_port: usize,
pub to_node: u32,
pub to_port: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SignalKind {
Signal,
Control,
Clock,
Feedback,
}
#[derive(Debug, Clone)]
pub enum SerializationError {
UnknownType(String),
DuplicateNodeId(u32),
InvalidFormat(String),
Registry(RegistryError),
}
impl From<RegistryError> for SerializationError {
fn from(e: RegistryError) -> Self {
Self::Registry(e)
}
}
impl std::fmt::Display for SerializationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownType(t) => write!(f, "unknown node type: {t}"),
Self::DuplicateNodeId(id) => write!(f, "duplicate NodeId: {id}"),
Self::InvalidFormat(d) => write!(f, "invalid format: {d}"),
Self::Registry(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for SerializationError {}
impl GraphDef {
pub fn new(sample_rate: f32, block_size: usize) -> Self {
Self {
format_version: "rill/1".to_string(),
sample_rate,
block_size,
resources: Vec::new(),
nodes: Vec::new(),
connections: Vec::new(),
description: None,
}
}
pub fn add_node(&mut self, def: NodeDef) -> Result<(), SerializationError> {
let id = def.id();
if self.nodes.iter().any(|n| n.id() == id) {
return Err(SerializationError::DuplicateNodeId(id));
}
self.nodes.push(def);
Ok(())
}
pub fn add_connection(&mut self, conn: ConnectionDef) {
self.connections.push(conn);
}
pub fn set_node_param(&mut self, node_id: u32, key: &str, value: ParamValue) {
if let Some(nd) = self.nodes.iter_mut().find(|n| n.id() == node_id) {
nd.parameters_mut().insert(key.to_string(), value);
}
}
pub fn clear(&mut self) {
self.nodes.clear();
self.connections.clear();
}
}
impl GraphDef {
pub fn from_graph<T: Transcendental, const B: usize>(graph: &super::Graph<T, B>) -> Self {
let nodes_slice = graph.nodes();
let sample_rate = graph.sample_rate();
let nodes: Vec<NodeDef> = nodes_slice.iter().map(node_to_def).collect();
let connections = extract_connections(nodes_slice);
let resources = graph
.resources()
.iter()
.map(|r| ResourceDef {
name: r.name.clone(),
kind: r.kind.clone(),
capacity: r.capacity,
})
.collect();
Self {
format_version: "rill/1".to_string(),
sample_rate,
block_size: B,
resources,
nodes,
connections,
description: None,
}
}
}
fn node_to_def<T: Transcendental, const B: usize>(variant: &NodeVariant<T, B>) -> NodeDef {
let meta = variant.metadata();
let type_name = meta.type_name.clone().unwrap_or_else(|| meta.name.clone());
let id = variant.id().inner();
let name = meta.name.clone();
let mut parameters = HashMap::new();
for pm in &meta.parameters {
let pid = match ParameterId::new(&pm.name) {
Ok(id) => id,
Err(_) => continue,
};
if let Some(val) = variant.get_parameter(&pid) {
parameters.insert(pm.name.clone(), val);
}
}
match variant {
NodeVariant::Source(_) => NodeDef::Source(SourceDef {
id,
type_name,
name,
backend: None,
parameters,
}),
NodeVariant::Processor(_) => NodeDef::Processor(ProcessorDef {
id,
type_name,
name,
parameters,
}),
NodeVariant::Router(router) => {
let routing_matrix = router
.routing_matrix()
.into_iter()
.enumerate()
.flat_map(|(to, entries)| {
entries.into_iter().map(move |(from, gain)| RoutingEntry {
from,
to,
gain: gain.to_f32(),
})
})
.collect();
NodeDef::Router(RouterDef {
id,
type_name,
name,
parameters,
routing_matrix,
})
}
NodeVariant::Sink(_) => NodeDef::Sink(SinkDef {
id,
type_name,
name,
backend: None,
parameters,
}),
}
}
fn extract_connections<T: Transcendental, const B: usize>(
nodes: &[NodeVariant<T, B>],
) -> Vec<ConnectionDef> {
let mut conns = Vec::new();
for (from_idx, variant) in nodes.iter().enumerate() {
let _ = from_idx;
let from_id = variant.id().inner();
let signal_outs = variant.metadata().signal_outputs;
for from_port in 0..signal_outs {
if let Some(port) = variant.output_port(from_port) {
for &(to_idx, to_port) in &port.downstream {
let to_id = nodes[to_idx].id().inner();
conns.push(ConnectionDef {
kind: SignalKind::Signal,
from_node: from_id,
from_port,
to_node: to_id,
to_port,
});
}
for &(to_idx, to_port) in &port.feedback_downstream {
let to_id = nodes[to_idx].id().inner();
conns.push(ConnectionDef {
kind: SignalKind::Feedback,
from_node: from_id,
from_port,
to_node: to_id,
to_port,
});
}
}
}
}
conns
}
impl GraphDef {
pub fn populate<T: Transcendental, const B: usize>(
&self,
builder: &mut GraphBuilder<T, B>,
) -> Result<(), SerializationError> {
builder.set_sample_rate(self.sample_rate);
let mut seen = HashSet::new();
for nd in &self.nodes {
if !seen.insert(nd.id()) {
return Err(SerializationError::DuplicateNodeId(nd.id()));
}
}
if self.block_size != B {
return Err(SerializationError::InvalidFormat(format!(
"expected block_size={B}, document has block_size={}",
self.block_size
)));
}
for rd in &self.resources {
builder.add_resource(crate::graph::GraphResource {
name: rd.name.clone(),
kind: rd.kind.clone(),
capacity: rd.capacity,
});
}
for nd in &self.nodes {
let mut p = Params::new(self.sample_rate);
for (k, v) in nd.parameters() {
p = p.with(k.clone(), v.clone());
}
let idx = builder.add_node_with_id(nd.type_name(), &p, NodeId(nd.id()));
if let Some(name) = nd.backend() {
builder.set_node_backend(idx, name.to_string());
} else if let Some(ref name) = builder.default_backend_name() {
builder.set_node_backend(idx, name.to_string());
}
if let NodeDef::Router(ref r) = nd {
for entry in &r.routing_matrix {
builder.add_routing_entry(idx, entry.from, entry.to, entry.gain);
}
}
}
let id_to_idx: HashMap<u32, usize> = self
.nodes
.iter()
.enumerate()
.map(|(i, n)| (n.id(), i))
.collect();
for conn in &self.connections {
let from = *id_to_idx.get(&conn.from_node).ok_or_else(|| {
SerializationError::InvalidFormat(format!(
"connection references unknown from_node {}",
conn.from_node
))
})?;
let to = *id_to_idx.get(&conn.to_node).ok_or_else(|| {
SerializationError::InvalidFormat(format!(
"connection references unknown to_node {}",
conn.to_node
))
})?;
match conn.kind {
SignalKind::Signal => {
builder.connect_signal(from, conn.from_port, to, conn.to_port);
}
SignalKind::Control => {
builder.connect_control(from, conn.from_port, to, conn.to_port);
}
SignalKind::Clock => {
builder.connect_clock(from, conn.from_port, to, conn.to_port);
}
SignalKind::Feedback => {
builder.connect_feedback(from, conn.from_port, to, conn.to_port);
}
}
}
Ok(())
}
}
fn deserialize_params<'de, D>(deserializer: D) -> Result<HashMap<String, ParamValue>, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw: HashMap<String, serde_json::Value> = HashMap::deserialize(deserializer)?;
raw.into_iter()
.map(|(k, v)| {
json_to_param_value(v)
.map(|pv| (k, pv))
.map_err(de::Error::custom)
})
.collect()
}
fn json_to_param_value(v: serde_json::Value) -> Result<ParamValue, String> {
match v {
serde_json::Value::Number(n) => n
.as_f64()
.map(|f| ParamValue::Float(f as f32))
.ok_or_else(|| "invalid number".to_string()),
serde_json::Value::String(s) => Ok(ParamValue::String(s)),
serde_json::Value::Bool(b) => Ok(ParamValue::Bool(b)),
serde_json::Value::Object(obj) => {
if let Some(val) = obj.get("Float").and_then(|v| v.as_f64()) {
return Ok(ParamValue::Float(val as f32));
}
if let Some(val) = obj.get("Int").and_then(|v| v.as_i64()) {
return Ok(ParamValue::Int(val as i32));
}
if let Some(val) = obj.get("Bool").and_then(|v| v.as_bool()) {
return Ok(ParamValue::Bool(val));
}
if let Some(val) = obj.get("String").and_then(|v| v.as_str()) {
return Ok(ParamValue::String(val.to_string()));
}
if let Some(val) = obj.get("Choice").and_then(|v| v.as_str()) {
return Ok(ParamValue::Choice(val.to_string()));
}
if let Some(arr) = obj.get("Bytes").and_then(|v| v.as_array()) {
let bytes: Vec<u8> = arr
.iter()
.filter_map(|v| v.as_u64().map(|n| n as u8))
.collect();
return Ok(ParamValue::Bytes(bytes));
}
Err("unknown variant in tagged format".to_string())
}
serde_json::Value::Array(arr) => {
let bytes: Vec<u8> = arr
.iter()
.filter_map(|v| v.as_u64().map(|n| n as u8))
.collect();
Ok(ParamValue::Bytes(bytes))
}
_ => Err("invalid param value type".to_string()),
}
}
fn serialize_params<S>(
params: &HashMap<String, ParamValue>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(params.len()))?;
for (k, v) in params {
let json_val = param_value_to_json(v);
map.serialize_entry(k, &json_val)?;
}
map.end()
}
fn param_value_to_json(v: &ParamValue) -> serde_json::Value {
match v {
ParamValue::Float(f) => {
serde_json::Value::Number(serde_json::Number::from_f64(*f as f64).unwrap_or(0.into()))
}
ParamValue::Int(i) => serde_json::Value::Number(serde_json::Number::from(*i)),
ParamValue::Bool(b) => serde_json::Value::Bool(*b),
ParamValue::String(s) => serde_json::Value::String(s.clone()),
ParamValue::Choice(s) => serde_json::Value::String(s.clone()),
ParamValue::Bytes(b) => serde_json::Value::Array(
b.iter()
.map(|&x| serde_json::Value::Number(x.into()))
.collect(),
),
}
}
pub fn to_json<T: Transcendental, const B: usize>(
graph: &super::Graph<T, B>,
) -> Result<String, SerializationError> {
let doc = GraphDef::from_graph(graph);
serde_json::to_string_pretty(&doc).map_err(|e| SerializationError::InvalidFormat(e.to_string()))
}
pub fn from_json(json: &str) -> Result<GraphDef, SerializationError> {
serde_json::from_str(json).map_err(|e| SerializationError::InvalidFormat(e.to_string()))
}
pub fn to_cbor<T: Transcendental, const B: usize>(
graph: &super::Graph<T, B>,
) -> Result<Vec<u8>, SerializationError> {
let doc = GraphDef::from_graph(graph);
serde_cbor::to_vec(&doc).map_err(|e| SerializationError::InvalidFormat(e.to_string()))
}
pub fn from_cbor(bytes: &[u8]) -> Result<GraphDef, SerializationError> {
serde_cbor::from_slice(bytes).map_err(|e| SerializationError::InvalidFormat(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend_factory::BackendFactory;
use crate::factory::NodeConstructor;
use crate::graph::Graph;
use rill_core::math::Transcendental;
use rill_core::time::ClockTick;
use rill_core::traits::node::NodeState;
use rill_core::traits::port::Port;
use rill_core::traits::{
NodeCategory, NodeMetadata, ParamType, ParamValue as PV, ParameterId, ProcessResult,
Processor, Source,
};
use rill_core::ParamMetadata as PM;
use rill_core_actor::ActorSystem;
use std::sync::Arc;
fn test_system() -> ActorSystem {
ActorSystem::new()
}
struct TestNode<T: Transcendental, const B: usize> {
id: NodeId,
state: NodeState<T, B>,
output: Port<T, B>,
type_name: Option<String>,
cat: NodeCategory,
param_defs: Vec<PM>,
params: HashMap<String, f32>,
has_feedback: bool,
}
impl<T: Transcendental, const B: usize> TestNode<T, B> {
fn new_raw(cat: NodeCategory) -> Self {
Self {
id: NodeId(0),
state: NodeState::new(44100.0),
output: Port::output(NodeId(0), 0, "out"),
type_name: None,
cat,
param_defs: vec![],
params: HashMap::new(),
has_feedback: false,
}
}
fn source() -> Self {
Self::new_raw(NodeCategory::Source)
}
fn processor() -> Self {
let mut s = Self::new_raw(NodeCategory::Processor);
s.has_feedback = true;
s
}
fn with_type_name(mut self, tn: &str) -> Self {
self.type_name = Some(tn.to_string());
self
}
fn with_param(mut self, name: &str, default: f32) -> Self {
self.param_defs
.push(PM::new(name, ParamType::Float, PV::Float(default)));
self.params.insert(name.to_string(), default);
self
}
}
impl<T: Transcendental, const B: usize> Node<T, B> for TestNode<T, B> {
fn metadata(&self) -> rill_core::traits::NodeMetadata {
NodeMetadata {
name: "TestNode".to_string(),
type_name: self.type_name.clone(),
category: self.cat,
description: String::new(),
author: String::new(),
version: String::new(),
signal_inputs: if self.cat == NodeCategory::Source {
0
} else {
1
},
signal_outputs: 1,
control_inputs: 0,
control_outputs: 0,
clock_inputs: 0,
clock_outputs: 0,
feedback_ports: if self.has_feedback { 1 } else { 0 },
parameters: self.param_defs.clone(),
}
}
fn init(&mut self, _: f32) {}
fn reset(&mut self) {}
fn get_parameter(&self, id: &ParameterId) -> Option<PV> {
self.params.get(id.as_str()).map(|&v| PV::Float(v))
}
fn set_parameter(&mut self, id: &ParameterId, value: PV) -> ProcessResult<()> {
if let Some(f) = value.as_f32() {
self.params.insert(id.as_str().to_string(), f);
Ok(())
} else {
Err(rill_core::ProcessError::parameter("type mismatch"))
}
}
fn id(&self) -> NodeId {
self.id
}
fn set_id(&mut self, id: NodeId) {
self.id = id;
}
fn input_port(&self, _: usize) -> Option<&Port<T, B>> {
None
}
fn input_port_mut(&mut self, _: usize) -> Option<&mut Port<T, B>> {
None
}
fn output_port(&self, index: usize) -> Option<&Port<T, B>> {
if index == 0 {
Some(&self.output)
} else {
None
}
}
fn output_port_mut(&mut self, index: usize) -> Option<&mut Port<T, B>> {
if index == 0 {
Some(&mut self.output)
} else {
None
}
}
fn control_port(&self, _: usize) -> Option<&Port<T, B>> {
None
}
fn control_port_mut(&mut self, _: usize) -> Option<&mut Port<T, B>> {
None
}
fn state(&self) -> &NodeState<T, B> {
&self.state
}
fn state_mut(&mut self) -> &mut NodeState<T, B> {
&mut self.state
}
}
impl<T: Transcendental, const B: usize> Source<T, B> for TestNode<T, B> {
fn generate(&mut self, _: &ClockTick, _: &[T], _: &[ClockTick]) -> ProcessResult<()> {
Ok(())
}
}
impl<T: Transcendental, const B: usize> Processor<T, B> for TestNode<T, B> {
fn process(
&mut self,
_: &ClockTick,
_: &[&[T; B]],
_: &[T],
_: &[ClockTick],
_: &[&[T; B]],
) -> ProcessResult<()> {
Ok(())
}
fn latency(&self) -> usize {
0
}
}
struct TestCtor;
impl<T: Transcendental, const B: usize> NodeConstructor<T, B> for TestCtor {
fn type_name(&self) -> &'static str {
"rill/test"
}
fn construct(&self, id: NodeId, params: &Params) -> NodeVariant<T, B> {
let mut node = TestNode::<T, B>::source().with_type_name("rill/test");
node.set_id(id);
node.init(params.sample_rate);
NodeVariant::Source(Box::new(node))
}
fn clone_box(&self) -> Box<dyn NodeConstructor<T, B>> {
Box::new(Self)
}
}
struct ParamCtor;
impl<T: Transcendental, const B: usize> NodeConstructor<T, B> for ParamCtor {
fn type_name(&self) -> &'static str {
"rill/param"
}
fn construct(&self, id: NodeId, params: &Params) -> NodeVariant<T, B> {
let mut node = TestNode::<T, B>::processor()
.with_type_name("rill/param")
.with_param("frequency", 440.0)
.with_param("amplitude", 0.5);
if let Some(f) = params.get("frequency").and_then(|v| v.as_f32()) {
node.params.insert("frequency".into(), f);
}
if let Some(a) = params.get("amplitude").and_then(|v| v.as_f32()) {
node.params.insert("amplitude".into(), a);
}
node.set_id(id);
node.init(params.sample_rate);
NodeVariant::Processor(Box::new(node))
}
fn clone_box(&self) -> Box<dyn NodeConstructor<T, B>> {
Box::new(Self)
}
}
fn empty_factory() -> Arc<NodeFactory<f32, 64>> {
let mut r = NodeFactory::<f32, 64>::new();
r.register(TestCtor);
r.register(ParamCtor);
Arc::new(r)
}
fn empty_backends() -> Arc<BackendFactory<f32>> {
Arc::new(BackendFactory::new())
}
fn build_small_graph(factory: &NodeFactory<f32, 64>) -> Graph<f32, 64> {
let mut b = GraphBuilder::new(Arc::new(factory.clone()), empty_backends());
let src = b.add_node("rill/test", &Params::new(44100.0));
let proc = b.add_node("rill/test", &Params::new(44100.0));
b.connect_signal(src, 0, proc, 0);
b.build(&test_system()).expect("build")
}
#[test]
fn test_json_roundtrip() {
let reg = empty_factory();
let graph = build_small_graph(&*reg);
let json = to_json(&graph).expect("to_json");
assert!(json.contains("rill/test"));
assert!(json.contains("format_version"));
assert!(json.contains("connections"));
let def = from_json(&json).expect("from_json");
let mut restored = GraphBuilder::new(reg.clone(), empty_backends());
def.populate(&mut restored).expect("populate");
assert_eq!(restored.node_count(), 2);
restored.build(&test_system()).expect("rebuild");
}
#[test]
fn test_cbor_roundtrip() {
let reg = empty_factory();
let graph = build_small_graph(&*reg);
let cbor = to_cbor(&graph).expect("to_cbor");
assert!(!cbor.is_empty());
let def = from_cbor(&cbor).expect("from_cbor");
let mut restored = GraphBuilder::new(reg.clone(), empty_backends());
def.populate(&mut restored).expect("populate");
assert_eq!(restored.node_count(), 2);
}
#[test]
fn test_empty_graph_roundtrip() {
let reg = empty_factory();
let graph = GraphBuilder::new(empty_factory(), empty_backends())
.build(&test_system())
.expect("graph build");
let json = to_json(&graph).expect("to_json");
assert!(json.contains(r#""nodes": []"#));
assert!(json.contains(r#""connections": []"#));
let def = from_json(&json).expect("from_json");
let mut restored = GraphBuilder::new(reg.clone(), empty_backends());
def.populate(&mut restored).expect("populate");
assert_eq!(restored.node_count(), 0);
}
#[test]
fn test_export_parameters() {
let reg = empty_factory();
let mut b = GraphBuilder::new(reg.clone(), empty_backends());
b.add_node(
"rill/param",
&Params::new(44100.0)
.with("frequency", PV::Float(220.0))
.with("amplitude", PV::Float(0.8)),
);
let graph = b.build(&test_system()).expect("build");
let doc = GraphDef::from_graph(&graph);
assert_eq!(doc.nodes.len(), 1);
let nd = &doc.nodes[0];
assert_eq!(nd.type_name(), "rill/param");
assert_eq!(nd.parameters().get("frequency"), Some(&PV::Float(220.0)));
assert_eq!(nd.parameters().get("amplitude"), Some(&PV::Float(0.8)));
}
#[test]
fn test_roundtrip_parameters() {
let reg = empty_factory();
let mut b = GraphBuilder::new(reg.clone(), empty_backends());
b.add_node(
"rill/param",
&Params::new(48000.0)
.with("frequency", PV::Float(55.0))
.with("amplitude", PV::Float(0.25)),
);
let graph = b.build(&test_system()).expect("build");
let json = to_json(&graph).expect("to_json");
let def = from_json(&json).expect("from_json");
let mut restored = GraphBuilder::new(reg.clone(), empty_backends());
def.populate(&mut restored).expect("populate");
assert_eq!(restored.node_count(), 1);
restored.build(&test_system()).expect("rebuild");
}
#[test]
fn test_export_feedback_connection() {
let reg = empty_factory();
let mut b = GraphBuilder::new(reg.clone(), empty_backends());
let src = b.add_node("rill/test", &Params::new(44100.0));
let proc = b.add_node("rill/test", &Params::new(44100.0));
b.connect_signal(src, 0, proc, 0);
b.connect_feedback(proc, 0, src, 0);
let graph = b.build(&test_system()).expect("build");
let doc = GraphDef::from_graph(&graph);
let sigs: Vec<SignalKind> = doc.connections.iter().map(|c| c.kind).collect();
assert!(sigs.contains(&SignalKind::Signal));
assert!(sigs.contains(&SignalKind::Feedback));
assert_eq!(doc.connections.len(), 2);
}
#[test]
fn test_export_type_name_explicit() {
let reg = empty_factory();
let mut b = GraphBuilder::new(reg.clone(), empty_backends());
b.add_node("rill/param", &Params::new(44100.0));
let graph = b.build(&test_system()).expect("build");
let doc = GraphDef::from_graph(&graph);
assert_eq!(doc.nodes[0].type_name(), "rill/param");
}
#[test]
fn test_export_type_name_fallback_to_name() {
let mut reg = NodeFactory::<f32, 64>::new();
struct FallbackCtor;
impl<T: Transcendental, const B: usize> NodeConstructor<T, B> for FallbackCtor {
fn type_name(&self) -> &'static str {
"rill/fallback"
}
fn construct(&self, id: NodeId, params: &Params) -> NodeVariant<T, B> {
let mut node = TestNode::<T, B>::source();
node.set_id(id);
node.init(params.sample_rate);
NodeVariant::Source(Box::new(node))
}
fn clone_box(&self) -> Box<dyn NodeConstructor<T, B>> {
Box::new(Self)
}
}
reg.register(FallbackCtor);
let reg = Arc::new(reg);
let mut b = GraphBuilder::new(reg.clone(), empty_backends());
b.add_node("rill/fallback", &Params::new(44100.0));
let graph = b.build(&test_system()).expect("build");
let doc = GraphDef::from_graph(&graph);
assert_eq!(doc.nodes[0].type_name(), "TestNode");
}
#[test]
fn test_roundtrip_preserves_node_ids() {
let reg = empty_factory();
let mut b = GraphBuilder::new(reg.clone(), empty_backends());
b.add_node_with_id("rill/test", &Params::new(44100.0), NodeId(100));
b.add_node_with_id("rill/param", &Params::new(44100.0), NodeId(200));
b.connect_signal(0, 0, 1, 0);
let graph = b.build(&test_system()).expect("build");
let json = to_json(&graph).expect("to_json");
assert!(json.contains(r#""id": 100"#));
assert!(json.contains(r#""id": 200"#));
let def = from_json(&json).expect("from_json");
let mut rebuilt = GraphBuilder::new(reg.clone(), empty_backends());
def.populate(&mut rebuilt).expect("populate");
assert_eq!(rebuilt.node_count(), 2);
}
#[test]
fn test_roundtrip_complex_topology() {
let reg = empty_factory();
let mut b = GraphBuilder::new(reg.clone(), empty_backends());
let s0 = b.add_node("rill/test", &Params::new(44100.0));
let p1 = b.add_node("rill/param", &Params::new(44100.0));
let p2 = b.add_node("rill/param", &Params::new(44100.0));
b.connect_signal(s0, 0, p1, 0);
b.connect_signal(p1, 0, p2, 0);
let graph = b.build(&test_system()).expect("build");
let json = to_json(&graph).expect("to_json");
let def = from_json(&json).expect("from_json");
let mut restored = GraphBuilder::new(reg.clone(), empty_backends());
def.populate(&mut restored).expect("populate");
assert_eq!(restored.node_count(), 3);
let rebuilt = restored.build(&test_system()).expect("rebuild");
assert_eq!(rebuilt.topo_order().len(), 3);
}
#[test]
fn test_unknown_type_error() {
let reg = empty_factory();
let doc = GraphDef {
format_version: "rill/1".to_string(),
sample_rate: 44100.0,
block_size: 64,
resources: vec![],
nodes: vec![
NodeDef::Processor(ProcessorDef {
id: 0,
type_name: "rill/nonexistent".to_string(),
name: "x".to_string(),
parameters: HashMap::new(),
}),
NodeDef::Processor(ProcessorDef {
id: 0,
type_name: "rill/test".to_string(),
name: "b".to_string(),
parameters: HashMap::new(),
}),
],
connections: vec![],
description: None,
};
let mut b = GraphBuilder::new(reg.clone(), empty_backends());
match doc.populate(&mut b) {
Err(SerializationError::DuplicateNodeId(id)) => assert_eq!(id, 0),
_ => panic!("expected DuplicateNodeId"),
}
}
#[test]
fn test_block_size_mismatch() {
let doc = GraphDef {
format_version: "rill/1".to_string(),
sample_rate: 44100.0,
block_size: 128,
resources: vec![],
nodes: vec![],
connections: vec![],
description: None,
};
let r = Arc::new(NodeFactory::<f32, 256>::new());
let mut b = GraphBuilder::new(r.clone(), empty_backends());
match doc.populate(&mut b) {
Err(SerializationError::InvalidFormat(_)) => {}
_ => panic!("expected InvalidFormat"),
}
}
#[test]
fn test_invalid_json() {
let _reg = empty_factory();
assert!(from_json("not json").is_err());
}
}