Skip to main content

sim_lib_audio_graph_core/
citizen.rs

1use 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/// Citizen descriptor wrapping a single [`PatchNode`] configuration.
9///
10/// The node is stored in its [`Expr`] encoding so it round-trips through the
11/// citizen protocol; [`AudioGraphNodeConfig::node`] decodes it back.
12#[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/// Citizen descriptor wrapping a whole [`Patch`].
20///
21/// The patch is stored in its [`Expr`] encoding; [`AudioGraphPatchDescriptor::patch`]
22/// decodes it back.
23#[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    /// Wraps a patch node, validating its id is non-empty.
32    pub fn new(node: PatchNode) -> Result<Self> {
33        validate_node(&node)?;
34        Ok(Self {
35            node: node_to_expr(&node),
36        })
37    }
38
39    /// Builds a config from a node expression, validating that it decodes.
40    pub fn from_expr(expr: Expr) -> Result<Self> {
41        node_expr::decode(&expr)?;
42        Ok(Self { node: expr })
43    }
44
45    /// Decodes and returns the wrapped [`PatchNode`].
46    pub fn node(&self) -> Result<PatchNode> {
47        node_from_expr(&self.node)
48    }
49
50    /// Returns the underlying node expression without decoding it.
51    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    /// Wraps a patch, validating that it round-trips through its expression.
69    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    /// Builds a descriptor from a patch expression, validating that it decodes.
77    pub fn from_expr(expr: Expr) -> Result<Self> {
78        patch_expr::decode(&expr)?;
79        Ok(Self { patch: expr })
80    }
81
82    /// Decodes and returns the wrapped [`Patch`].
83    pub fn patch(&self) -> Result<Patch> {
84        Patch::from_expr(&self.patch)
85    }
86
87    /// Returns the underlying patch expression without decoding it.
88    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
107/// Returns the class symbol under which node configs register as citizens.
108pub fn audio_graph_node_config_class_symbol() -> Symbol {
109    Symbol::qualified("audio-graph", "NodeConfig")
110}
111
112/// Returns the class symbol under which patches register as citizens.
113pub 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}