1use std::collections::HashMap;
28use serde::{Deserialize, Serialize};
29use thiserror::Error;
30
31#[derive(Debug, Error)]
32pub enum ManifestError {
33 #[error("JSON parse error: {0}")]
34 Json(#[from] serde_json::Error),
35 #[error("Unknown node type: {0}")]
36 UnknownNode(String),
37 #[error("Unknown node id: {0}")]
38 UnknownId(String),
39 #[error("Duplicate node id: {0}")]
40 DuplicateId(String),
41 #[error("IO error: {0}")]
42 Io(#[from] std::io::Error),
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct Manifest {
48 pub name: String,
49 pub version: String,
50 #[serde(default = "default_engine")]
51 pub engine: String,
52 #[serde(default = "default_sample_rate")]
53 pub sample_rate: u32,
54 #[serde(default = "default_block_size")]
55 pub block_size: usize,
56 pub nodes: Vec<NodeDef>,
57 #[serde(default)]
58 pub connections: Vec<ConnectionDef>,
59 pub output_node: String,
60 #[serde(default)]
61 pub plugin_targets: Vec<String>,
62 #[serde(default)]
63 pub metadata: HashMap<String, serde_json::Value>,
64}
65
66fn default_engine() -> String { "aether-dsp".into() }
67fn default_sample_rate() -> u32 { 48_000 }
68fn default_block_size() -> usize { 64 }
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct NodeDef {
73 pub id: String,
75 #[serde(rename = "type")]
77 pub node_type: String,
78 #[serde(default)]
80 pub params: HashMap<String, f32>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct ConnectionDef {
86 pub from: String,
87 pub to: String,
88 #[serde(default)]
89 pub slot: usize,
90}
91
92impl Manifest {
93 pub fn from_json(json: &str) -> Result<Self, ManifestError> {
95 Ok(serde_json::from_str(json)?)
96 }
97
98 pub fn from_file(path: &std::path::Path) -> Result<Self, ManifestError> {
100 let json = std::fs::read_to_string(path)?;
101 Self::from_json(&json)
102 }
103
104 pub fn to_json(&self) -> String {
106 serde_json::to_string_pretty(self).unwrap_or_default()
107 }
108
109 pub fn validate(
111 &self,
112 registry: &aether_ndk::node::NodeRegistry,
113 ) -> Result<(), ManifestError> {
114 let mut ids = std::collections::HashSet::new();
115 for node in &self.nodes {
116 if !ids.insert(node.id.as_str()) {
117 return Err(ManifestError::DuplicateId(node.id.clone()));
118 }
119 if registry.create(&node.node_type).is_none() {
120 return Err(ManifestError::UnknownNode(node.node_type.clone()));
121 }
122 }
123 for conn in &self.connections {
124 if !ids.contains(conn.from.as_str()) {
125 return Err(ManifestError::UnknownId(conn.from.clone()));
126 }
127 if !ids.contains(conn.to.as_str()) {
128 return Err(ManifestError::UnknownId(conn.to.clone()));
129 }
130 }
131 if !ids.contains(self.output_node.as_str()) {
132 return Err(ManifestError::UnknownId(self.output_node.clone()));
133 }
134 Ok(())
135 }
136
137 pub fn build_graph(
139 &self,
140 registry: &aether_ndk::node::NodeRegistry,
141 _sample_rate: f32,
142 ) -> Result<aether_core::graph::DspGraph, ManifestError> {
143 use aether_core::graph::DspGraph;
144 use aether_core::arena::NodeId;
145 use std::collections::HashMap;
146
147 self.validate(registry)?;
148
149 let mut graph = DspGraph::new();
150 let mut id_map: HashMap<&str, NodeId> = HashMap::new();
151
152 for node_def in &self.nodes {
153 let node = registry
154 .create(&node_def.node_type)
155 .ok_or_else(|| ManifestError::UnknownNode(node_def.node_type.clone()))?;
156
157 let node_id = graph.add_node(node).expect("graph full");
158
159 if let Some(defs) = registry.param_defs(&node_def.node_type) {
161 let record = graph.arena.get_mut(node_id).unwrap();
162 for def in defs {
163 let value = node_def.params.get(def.name).copied().unwrap_or(def.default);
164 record.params.add(value);
165 }
166 }
167
168 id_map.insert(&node_def.id, node_id);
169 }
170
171 for conn in &self.connections {
172 let src = *id_map.get(conn.from.as_str()).unwrap();
173 let dst = *id_map.get(conn.to.as_str()).unwrap();
174 graph.connect(src, dst, conn.slot);
175 }
176
177 let out_id = *id_map.get(self.output_node.as_str()).unwrap();
178 graph.set_output_node(out_id);
179
180 Ok(graph)
181 }
182}
183
184pub fn new_project_manifest(name: &str) -> Manifest {
186 Manifest {
187 name: name.to_string(),
188 version: "0.1.0".into(),
189 engine: "aether-dsp".into(),
190 sample_rate: 48_000,
191 block_size: 64,
192 nodes: vec![
193 NodeDef {
194 id: "osc".into(),
195 node_type: "Oscillator".into(),
196 params: [("Frequency".into(), 440.0)].into(),
197 },
198 NodeDef {
199 id: "out".into(),
200 node_type: "Gain".into(),
201 params: [("Gain".into(), 0.8)].into(),
202 },
203 ],
204 connections: vec![ConnectionDef {
205 from: "osc".into(),
206 to: "out".into(),
207 slot: 0,
208 }],
209 output_node: "out".into(),
210 plugin_targets: vec!["clap".into()],
211 metadata: HashMap::new(),
212 }
213}