Skip to main content

aether_manifest/
lib.rs

1//! Aether project manifest — the declarative project format.
2//!
3//! A manifest describes a complete audio application:
4//! nodes, connections, parameters, and build targets.
5//!
6//! ```json
7//! {
8//!   "name": "my-synth",
9//!   "version": "0.1.0",
10//!   "engine": "aether-dsp",
11//!   "sample_rate": 48000,
12//!   "block_size": 64,
13//!   "nodes": [
14//!     { "id": "osc", "type": "Oscillator", "params": { "Frequency": 440.0 } },
15//!     { "id": "filt", "type": "StateVariableFilter", "params": { "Cutoff": 2000.0 } },
16//!     { "id": "out", "type": "Gain", "params": { "Gain": 0.8 } }
17//!   ],
18//!   "connections": [
19//!     { "from": "osc", "to": "filt", "slot": 0 },
20//!     { "from": "filt", "to": "out", "slot": 0 }
21//!   ],
22//!   "output_node": "out",
23//!   "plugin_targets": ["clap"]
24//! }
25//! ```
26
27use 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/// Top-level project manifest.
46#[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/// A node instance in the manifest.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct NodeDef {
73    /// Unique identifier within this project (e.g. "osc1").
74    pub id: String,
75    /// Registered type name (e.g. "Oscillator").
76    #[serde(rename = "type")]
77    pub node_type: String,
78    /// Initial parameter values by name.
79    #[serde(default)]
80    pub params: HashMap<String, f32>,
81}
82
83/// A connection between two nodes.
84#[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    /// Parse a manifest from a JSON string.
94    pub fn from_json(json: &str) -> Result<Self, ManifestError> {
95        Ok(serde_json::from_str(json)?)
96    }
97
98    /// Parse a manifest from a file.
99    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    /// Serialize to pretty JSON.
105    pub fn to_json(&self) -> String {
106        serde_json::to_string_pretty(self).unwrap_or_default()
107    }
108
109    /// Validate the manifest against a node registry.
110    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    /// Instantiate a `DspGraph` from this manifest using the given registry.
138    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            // Apply param overrides
160            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
184/// Generate a starter manifest for a new project.
185pub 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}