sim_lib_audio_graph_core/
citizen.rs1use sim_citizen_derive::Citizen;
2use sim_kernel::{Error, Expr, NumberLiteral, Result, Symbol};
3
4use crate::{Patch, PatchNode};
5
6const LIB_NS: &str = "audio-graph";
7
8#[derive(Clone, Debug, PartialEq, Citizen)]
13#[citizen(symbol = "audio-graph/NodeConfig", version = 1)]
14pub struct AudioGraphNodeConfig {
15 #[citizen(with = "node_expr")]
16 node: Expr,
17}
18
19#[derive(Clone, Debug, PartialEq, Citizen)]
24#[citizen(symbol = "audio-graph/Patch", version = 1)]
25pub struct AudioGraphPatchDescriptor {
26 #[citizen(with = "patch_expr")]
27 patch: Expr,
28}
29
30impl AudioGraphNodeConfig {
31 pub fn new(node: PatchNode) -> Result<Self> {
33 validate_node(&node)?;
34 Ok(Self {
35 node: node_to_expr(&node),
36 })
37 }
38
39 pub fn from_expr(expr: Expr) -> Result<Self> {
41 node_expr::decode(&expr)?;
42 Ok(Self { node: expr })
43 }
44
45 pub fn node(&self) -> Result<PatchNode> {
47 node_from_expr(&self.node)
48 }
49
50 pub fn as_expr(&self) -> &Expr {
52 &self.node
53 }
54}
55
56impl Default for AudioGraphNodeConfig {
57 fn default() -> Self {
58 Self::new(PatchNode {
59 id: "citizen-node".to_owned(),
60 in_channels: 2,
61 out_channels: 2,
62 })
63 .expect("default audio graph node config should be valid")
64 }
65}
66
67impl AudioGraphPatchDescriptor {
68 pub fn new(patch: Patch) -> Result<Self> {
70 Patch::from_expr(&patch.to_expr())?;
71 Ok(Self {
72 patch: patch.to_expr(),
73 })
74 }
75
76 pub fn from_expr(expr: Expr) -> Result<Self> {
78 patch_expr::decode(&expr)?;
79 Ok(Self { patch: expr })
80 }
81
82 pub fn patch(&self) -> Result<Patch> {
84 Patch::from_expr(&self.patch)
85 }
86
87 pub fn as_expr(&self) -> &Expr {
89 &self.patch
90 }
91}
92
93impl Default for AudioGraphPatchDescriptor {
94 fn default() -> Self {
95 Self::new(Patch {
96 nodes: vec![
97 AudioGraphNodeConfig::default()
98 .node()
99 .expect("default node should decode"),
100 ],
101 cables: Vec::new(),
102 })
103 .expect("default audio graph patch should be valid")
104 }
105}
106
107pub fn audio_graph_node_config_class_symbol() -> Symbol {
109 Symbol::qualified("audio-graph", "NodeConfig")
110}
111
112pub fn audio_graph_patch_class_symbol() -> Symbol {
114 Symbol::qualified("audio-graph", "Patch")
115}
116
117pub(crate) mod node_expr {
118 use sim_kernel::{Expr, Result};
119
120 use super::node_from_expr;
121
122 pub fn encode(expr: &Expr) -> Expr {
123 expr.clone()
124 }
125
126 pub fn decode(expr: &Expr) -> Result<Expr> {
127 node_from_expr(expr)?;
128 Ok(expr.clone())
129 }
130}
131
132pub(crate) mod patch_expr {
133 use sim_kernel::{Expr, Result};
134
135 use crate::Patch;
136
137 pub fn encode(expr: &Expr) -> Expr {
138 expr.clone()
139 }
140
141 pub fn decode(expr: &Expr) -> Result<Expr> {
142 Patch::from_expr(expr)?;
143 Ok(expr.clone())
144 }
145}
146
147fn node_to_expr(node: &PatchNode) -> Expr {
148 Expr::Map(vec![
149 (field("tag"), tag("node-config")),
150 (field("id"), Expr::String(node.id.clone())),
151 (field("in-channels"), number_u16(node.in_channels)),
152 (field("out-channels"), number_u16(node.out_channels)),
153 ])
154}
155
156fn node_from_expr(expr: &Expr) -> Result<PatchNode> {
157 let map = expr_map(expr, "audio graph node config")?;
158 expect_tag(map, "node-config")?;
159 let node = PatchNode {
160 id: expr_string(lookup_required(map, "id")?, "node id")?.to_owned(),
161 in_channels: expr_u16(lookup_required(map, "in-channels")?, "in-channels")?,
162 out_channels: expr_u16(lookup_required(map, "out-channels")?, "out-channels")?,
163 };
164 validate_node(&node)?;
165 Ok(node)
166}
167
168fn validate_node(node: &PatchNode) -> Result<()> {
169 if node.id.trim().is_empty() {
170 return Err(Error::Eval(
171 "audio graph node config id cannot be empty".to_owned(),
172 ));
173 }
174 Ok(())
175}
176
177fn field(name: &'static str) -> Expr {
178 sim_value::build::qsym(LIB_NS, name)
179}
180
181fn tag(name: &'static str) -> Expr {
182 Expr::Symbol(Symbol::qualified(LIB_NS, name))
183}
184
185fn number_u16(value: u16) -> Expr {
186 Expr::Number(NumberLiteral {
187 domain: Symbol::qualified("numbers", "i64"),
188 canonical: value.to_string(),
189 })
190}
191
192fn expr_map<'a>(expr: &'a Expr, context: &str) -> Result<&'a [(Expr, Expr)]> {
193 match expr {
194 Expr::Map(entries) => Ok(entries),
195 _ => Err(Error::Eval(format!("{context} must be a map"))),
196 }
197}
198
199fn expect_tag(map: &[(Expr, Expr)], expected: &str) -> Result<()> {
200 match lookup_required(map, "tag")? {
201 Expr::Symbol(symbol) if is_symbol(symbol, LIB_NS, expected) => Ok(()),
202 _ => Err(Error::Eval(format!(
203 "audio graph descriptor tag must be {expected}"
204 ))),
205 }
206}
207
208fn expr_string<'a>(expr: &'a Expr, context: &str) -> Result<&'a str> {
209 match expr {
210 Expr::String(text) => Ok(text),
211 _ => Err(Error::Eval(format!("{context} must be a string"))),
212 }
213}
214
215fn expr_u16(expr: &Expr, context: &str) -> Result<u16> {
216 let text = match expr {
217 Expr::Number(number) => number.canonical.as_str(),
218 Expr::String(text) => text,
219 _ => return Err(Error::Eval(format!("{context} must be a number"))),
220 };
221 text.parse::<u16>()
222 .map_err(|_| Error::Eval(format!("{context} must be a u16")))
223}
224
225fn lookup_required<'a>(map: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
226 map.iter()
227 .find_map(|(key, value)| match key {
228 Expr::Symbol(symbol) if is_symbol(symbol, LIB_NS, name) => Some(value),
229 _ => None,
230 })
231 .ok_or_else(|| Error::Eval(format!("audio graph descriptor field is missing: {name}")))
232}
233
234fn is_symbol(symbol: &Symbol, namespace: &str, name: &str) -> bool {
235 symbol.namespace.as_deref() == Some(namespace) && symbol.name.as_ref() == name
236}