Skip to main content

sim_lib_audio_graph_core/
patch.rs

1use sim_kernel::{Error, Expr, NumberLiteral, Result, Symbol};
2
3use crate::{Cable, PortUri};
4
5/// Portable description of one graph node: its id and channel counts.
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub struct PatchNode {
8    /// Node id, unique within the patch.
9    pub id: String,
10    /// Number of input channels.
11    pub in_channels: u16,
12    /// Number of output channels.
13    pub out_channels: u16,
14}
15
16/// Portable, serializable description of a graph: its nodes and cables.
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct Patch {
19    /// Nodes in the patch.
20    pub nodes: Vec<PatchNode>,
21    /// Cables connecting node ports.
22    pub cables: Vec<Cable>,
23}
24
25impl Patch {
26    /// Encodes the patch as a tagged expression map.
27    pub fn to_expr(&self) -> Expr {
28        Expr::Map(vec![
29            (
30                field("tag"),
31                Expr::Symbol(Symbol::qualified("audio-graph", "patch")),
32            ),
33            (
34                field("nodes"),
35                Expr::Vector(self.nodes.iter().map(node_to_expr).collect()),
36            ),
37            (
38                field("cables"),
39                Expr::Vector(self.cables.iter().map(cable_to_expr).collect()),
40            ),
41        ])
42    }
43
44    /// Decodes a patch from its expression form, validating the tag and fields.
45    pub fn from_expr(expr: &Expr) -> Result<Self> {
46        let map = expr_map(expr, "patch")?;
47        match lookup(map, "tag") {
48            Some(Expr::Symbol(symbol)) if is_symbol(symbol, "audio-graph", "patch") => {}
49            Some(_) => return Err(Error::Eval("audio graph patch tag is invalid".to_owned())),
50            None => return Err(Error::Eval("audio graph patch tag is missing".to_owned())),
51        }
52        let nodes = expr_vector(
53            lookup(map, "nodes").ok_or_else(|| missing("nodes"))?,
54            "patch nodes",
55        )?
56        .iter()
57        .map(node_from_expr)
58        .collect::<Result<Vec<_>>>()?;
59        let cables = expr_vector(
60            lookup(map, "cables").ok_or_else(|| missing("cables"))?,
61            "patch cables",
62        )?
63        .iter()
64        .map(cable_from_expr)
65        .collect::<Result<Vec<_>>>()?;
66        Ok(Self { nodes, cables })
67    }
68}
69
70fn node_to_expr(node: &PatchNode) -> Expr {
71    Expr::Map(vec![
72        (field("id"), Expr::String(node.id.clone())),
73        (field("in-channels"), number(node.in_channels)),
74        (field("out-channels"), number(node.out_channels)),
75    ])
76}
77
78fn node_from_expr(expr: &Expr) -> Result<PatchNode> {
79    let map = expr_map(expr, "patch node")?;
80    Ok(PatchNode {
81        id: expr_string(
82            lookup(map, "id").ok_or_else(|| missing("node id"))?,
83            "node id",
84        )?
85        .to_owned(),
86        in_channels: expr_u16(
87            lookup(map, "in-channels").ok_or_else(|| missing("node in-channels"))?,
88            "node in-channels",
89        )?,
90        out_channels: expr_u16(
91            lookup(map, "out-channels").ok_or_else(|| missing("node out-channels"))?,
92            "node out-channels",
93        )?,
94    })
95}
96
97fn cable_to_expr(cable: &Cable) -> Expr {
98    Expr::Map(vec![
99        (field("from"), Expr::String(cable.from.to_string())),
100        (field("to"), Expr::String(cable.to.to_string())),
101    ])
102}
103
104fn cable_from_expr(expr: &Expr) -> Result<Cable> {
105    let map = expr_map(expr, "patch cable")?;
106    let from = expr_string(
107        lookup(map, "from").ok_or_else(|| missing("cable from"))?,
108        "cable from",
109    )?
110    .parse::<PortUri>()?;
111    let to = expr_string(
112        lookup(map, "to").ok_or_else(|| missing("cable to"))?,
113        "cable to",
114    )?
115    .parse::<PortUri>()?;
116    Ok(Cable { from, to })
117}
118
119fn field(name: &'static str) -> Expr {
120    sim_value::build::qsym("audio-graph", name)
121}
122
123fn number(value: u16) -> Expr {
124    Expr::Number(NumberLiteral {
125        domain: Symbol::qualified("numbers", "i64"),
126        canonical: value.to_string(),
127    })
128}
129
130fn expr_map<'a>(expr: &'a Expr, context: &str) -> Result<&'a [(Expr, Expr)]> {
131    match expr {
132        Expr::Map(entries) => Ok(entries),
133        other => Err(Error::Eval(format!(
134            "expected {context} map, found {}",
135            expr_kind(other)
136        ))),
137    }
138}
139
140fn expr_vector<'a>(expr: &'a Expr, context: &str) -> Result<&'a [Expr]> {
141    match expr {
142        Expr::Vector(items) => Ok(items),
143        other => Err(Error::Eval(format!(
144            "expected {context} vector, found {}",
145            expr_kind(other)
146        ))),
147    }
148}
149
150fn expr_string<'a>(expr: &'a Expr, context: &str) -> Result<&'a str> {
151    match expr {
152        Expr::String(text) => Ok(text),
153        other => Err(Error::Eval(format!(
154            "expected {context} string, found {}",
155            expr_kind(other)
156        ))),
157    }
158}
159
160fn expr_u16(expr: &Expr, context: &str) -> Result<u16> {
161    match expr {
162        Expr::Number(number) => number.canonical.parse::<u16>().map_err(|_| {
163            Error::Eval(format!(
164                "expected {context} u16 number, found {}",
165                number.canonical
166            ))
167        }),
168        Expr::String(text) => text
169            .parse::<u16>()
170            .map_err(|_| Error::Eval(format!("expected {context} u16 string, found {text}"))),
171        other => Err(Error::Eval(format!(
172            "expected {context} u16, found {}",
173            expr_kind(other)
174        ))),
175    }
176}
177
178fn lookup<'a>(map: &'a [(Expr, Expr)], name: &str) -> Option<&'a Expr> {
179    map.iter().find_map(|(key, value)| match key {
180        Expr::Symbol(symbol) if is_symbol(symbol, "audio-graph", name) => Some(value),
181        _ => None,
182    })
183}
184
185fn is_symbol(symbol: &Symbol, namespace: &str, name: &str) -> bool {
186    symbol.namespace.as_deref() == Some(namespace) && symbol.name.as_ref() == name
187}
188
189fn missing(field: &str) -> Error {
190    Error::Eval(format!("audio graph patch field is missing: {field}"))
191}
192
193use sim_value::kind::expr_kind;