Skip to main content

sim_lib_plugin_core/
citizen.rs

1use sim_citizen_derive::Citizen;
2use sim_kernel::{Error, Expr, NumberLiteral, Result, Symbol};
3use sim_lib_audio_graph_core::{PortDecl, PortDir, PortMedia};
4
5use crate::{ParameterDescriptor, ParameterKind, PluginDescriptor, PluginFormat, PluginId};
6
7const LIB_NS: &str = "plugin-core";
8
9/// A runtime citizen wrapping a [`PluginDescriptor`] in its encoded [`Expr`]
10/// form.
11///
12/// The record stores the descriptor as a `plugin-core/PluginDescriptor`-tagged
13/// expression so it can live in the object graph as a first-class citizen, and
14/// converts back to a typed [`PluginDescriptor`] on demand.
15#[derive(Clone, Debug, PartialEq, Citizen)]
16#[citizen(symbol = "plugin-core/PluginDescriptor", version = 1)]
17pub struct PluginDescriptorRecord {
18    #[citizen(with = "plugin_descriptor_expr")]
19    descriptor: Expr,
20}
21
22impl PluginDescriptorRecord {
23    /// Builds a record by encoding a typed [`PluginDescriptor`].
24    pub fn new(descriptor: PluginDescriptor) -> Self {
25        Self {
26            descriptor: descriptor_to_expr(&descriptor),
27        }
28    }
29
30    /// Builds a record from an already-encoded descriptor expression.
31    ///
32    /// # Errors
33    ///
34    /// Returns an error when `expr` does not decode to a valid plugin
35    /// descriptor.
36    pub fn from_expr(expr: Expr) -> Result<Self> {
37        plugin_descriptor_expr::decode(&expr)?;
38        Ok(Self { descriptor: expr })
39    }
40
41    /// Decodes the held expression back into a typed [`PluginDescriptor`].
42    ///
43    /// # Errors
44    ///
45    /// Returns an error when the stored expression is not a valid descriptor.
46    pub fn descriptor(&self) -> Result<PluginDescriptor> {
47        descriptor_from_expr(&self.descriptor)
48    }
49
50    /// Returns the underlying encoded descriptor expression.
51    pub fn as_expr(&self) -> &Expr {
52        &self.descriptor
53    }
54}
55
56impl Default for PluginDescriptorRecord {
57    fn default() -> Self {
58        let descriptor =
59            PluginDescriptor::audio_effect(PluginFormat::Sim, "org.sim.citizen", "Citizen", 2)
60                .expect("default plugin descriptor should be valid")
61                .with_parameter(
62                    ParameterDescriptor::new(0, "gain", "Gain", 0.0, 2.0, 1.0)
63                        .expect("default plugin parameter should be valid"),
64                );
65        Self::new(descriptor)
66    }
67}
68
69/// Returns the class symbol (`plugin-core/PluginDescriptor`) under which
70/// [`PluginDescriptorRecord`] is registered.
71pub fn plugin_descriptor_class_symbol() -> Symbol {
72    Symbol::qualified("plugin-core", "PluginDescriptor")
73}
74
75pub(crate) mod plugin_descriptor_expr {
76    use sim_kernel::{Expr, Result};
77
78    use super::descriptor_from_expr;
79
80    pub fn encode(expr: &Expr) -> Expr {
81        expr.clone()
82    }
83
84    pub fn decode(expr: &Expr) -> Result<Expr> {
85        descriptor_from_expr(expr)?;
86        Ok(expr.clone())
87    }
88}
89
90fn descriptor_to_expr(descriptor: &PluginDescriptor) -> Expr {
91    Expr::Map(vec![
92        (field("tag"), tag("descriptor")),
93        (
94            field("format"),
95            Expr::String(descriptor.id.format.as_str().to_owned()),
96        ),
97        (
98            field("stable-id"),
99            Expr::String(descriptor.id.stable_id.clone()),
100        ),
101        (field("name"), Expr::String(descriptor.name.clone())),
102        (field("vendor"), Expr::String(descriptor.vendor.clone())),
103        (field("version"), Expr::String(descriptor.version.clone())),
104        (
105            field("ports"),
106            Expr::Vector(descriptor.ports.iter().map(port_to_expr).collect()),
107        ),
108        (
109            field("parameters"),
110            Expr::Vector(
111                descriptor
112                    .parameters
113                    .iter()
114                    .map(parameter_to_expr)
115                    .collect(),
116            ),
117        ),
118        (
119            field("latency-frames"),
120            number_u32(descriptor.latency_frames),
121        ),
122    ])
123}
124
125fn descriptor_from_expr(expr: &Expr) -> Result<PluginDescriptor> {
126    let map = expr_map(expr, "plugin descriptor")?;
127    expect_tag(map, "descriptor")?;
128    let id = PluginId::new(
129        plugin_format(expr_string(lookup_required(map, "format")?, "format")?)?,
130        expr_string(lookup_required(map, "stable-id")?, "stable-id")?.to_owned(),
131    )?;
132    let mut descriptor = PluginDescriptor::new(
133        id,
134        expr_string(lookup_required(map, "name")?, "name")?.to_owned(),
135        expr_string(lookup_required(map, "vendor")?, "vendor")?.to_owned(),
136        expr_string(lookup_required(map, "version")?, "version")?.to_owned(),
137    )?;
138    descriptor.ports = expr_vector(lookup_required(map, "ports")?, "ports")?
139        .iter()
140        .map(port_from_expr)
141        .collect::<Result<Vec<_>>>()?;
142    descriptor.parameters = expr_vector(lookup_required(map, "parameters")?, "parameters")?
143        .iter()
144        .map(parameter_from_expr)
145        .collect::<Result<Vec<_>>>()?;
146    descriptor.latency_frames = expr_u32(lookup_required(map, "latency-frames")?, "latency")?;
147    Ok(descriptor)
148}
149
150fn port_to_expr(port: &PortDecl) -> Expr {
151    Expr::Map(vec![
152        (field("name"), Expr::String(port.name.clone())),
153        (
154            field("media"),
155            Expr::String(port_media_name(port.media).to_owned()),
156        ),
157        (
158            field("dir"),
159            Expr::String(port_dir_name(port.dir).to_owned()),
160        ),
161        (field("channels"), number_u16(port.channels)),
162    ])
163}
164
165fn port_from_expr(expr: &Expr) -> Result<PortDecl> {
166    let map = expr_map(expr, "plugin port")?;
167    let channels = expr_u16(lookup_required(map, "channels")?, "port channels")?;
168    if channels == 0 {
169        return Err(Error::Eval(
170            "plugin port channel count must be greater than zero".to_owned(),
171        ));
172    }
173    Ok(PortDecl::new(
174        expr_string(lookup_required(map, "name")?, "port name")?.to_owned(),
175        port_media(expr_string(lookup_required(map, "media")?, "port media")?)?,
176        port_dir(expr_string(lookup_required(map, "dir")?, "port direction")?)?,
177        channels,
178    ))
179}
180
181fn parameter_to_expr(parameter: &ParameterDescriptor) -> Expr {
182    Expr::Map(vec![
183        (field("id"), number_u32(parameter.id)),
184        (
185            field("stable-id"),
186            Expr::String(parameter.stable_id.clone()),
187        ),
188        (field("name"), Expr::String(parameter.name.clone())),
189        (
190            field("kind"),
191            Expr::String(parameter_kind_name(parameter.kind).to_owned()),
192        ),
193        (field("min"), number_f64(parameter.min)),
194        (field("max"), number_f64(parameter.max)),
195        (field("default"), number_f64(parameter.default)),
196        (field("automatable"), Expr::Bool(parameter.automatable)),
197    ])
198}
199
200fn parameter_from_expr(expr: &Expr) -> Result<ParameterDescriptor> {
201    let map = expr_map(expr, "plugin parameter")?;
202    let mut parameter = ParameterDescriptor::new(
203        expr_u32(lookup_required(map, "id")?, "parameter id")?,
204        expr_string(lookup_required(map, "stable-id")?, "parameter stable id")?.to_owned(),
205        expr_string(lookup_required(map, "name")?, "parameter name")?.to_owned(),
206        expr_f64(lookup_required(map, "min")?, "parameter min")?,
207        expr_f64(lookup_required(map, "max")?, "parameter max")?,
208        expr_f64(lookup_required(map, "default")?, "parameter default")?,
209    )?
210    .with_kind(parameter_kind(expr_string(
211        lookup_required(map, "kind")?,
212        "parameter kind",
213    )?)?);
214    parameter.automatable = expr_bool(lookup_required(map, "automatable")?, "automatable")?;
215    Ok(parameter)
216}
217
218fn field(name: &'static str) -> Expr {
219    sim_value::build::qsym(LIB_NS, name)
220}
221
222fn tag(name: &'static str) -> Expr {
223    Expr::Symbol(Symbol::qualified(LIB_NS, name))
224}
225
226fn number_u16(value: u16) -> Expr {
227    number_u32(u32::from(value))
228}
229
230fn number_u32(value: u32) -> Expr {
231    Expr::Number(NumberLiteral {
232        domain: Symbol::qualified("numbers", "i64"),
233        canonical: value.to_string(),
234    })
235}
236
237fn number_f64(value: f64) -> Expr {
238    Expr::Number(NumberLiteral {
239        domain: Symbol::qualified("numbers", "f64"),
240        canonical: value.to_string(),
241    })
242}
243
244fn plugin_format(text: &str) -> Result<PluginFormat> {
245    match text {
246        "clap" => Ok(PluginFormat::Clap),
247        "lv2" => Ok(PluginFormat::Lv2),
248        "vst3" => Ok(PluginFormat::Vst3),
249        "wasm" => Ok(PluginFormat::Wasm),
250        "sim" => Ok(PluginFormat::Sim),
251        _ => Err(Error::Eval(format!("unknown plugin format: {text}"))),
252    }
253}
254
255fn port_media_name(media: PortMedia) -> &'static str {
256    match media {
257        PortMedia::Audio => "audio",
258        PortMedia::Control => "control",
259        PortMedia::Event => "event",
260    }
261}
262
263fn port_media(text: &str) -> Result<PortMedia> {
264    match text {
265        "audio" => Ok(PortMedia::Audio),
266        "control" => Ok(PortMedia::Control),
267        "event" => Ok(PortMedia::Event),
268        _ => Err(Error::Eval(format!("unknown plugin port media: {text}"))),
269    }
270}
271
272fn port_dir_name(dir: PortDir) -> &'static str {
273    match dir {
274        PortDir::In => "in",
275        PortDir::Out => "out",
276    }
277}
278
279fn port_dir(text: &str) -> Result<PortDir> {
280    match text {
281        "in" => Ok(PortDir::In),
282        "out" => Ok(PortDir::Out),
283        _ => Err(Error::Eval(format!(
284            "unknown plugin port direction: {text}"
285        ))),
286    }
287}
288
289fn parameter_kind_name(kind: ParameterKind) -> &'static str {
290    match kind {
291        ParameterKind::Float => "float",
292        ParameterKind::Integer => "integer",
293        ParameterKind::Boolean => "boolean",
294    }
295}
296
297fn parameter_kind(text: &str) -> Result<ParameterKind> {
298    match text {
299        "float" => Ok(ParameterKind::Float),
300        "integer" => Ok(ParameterKind::Integer),
301        "boolean" => Ok(ParameterKind::Boolean),
302        _ => Err(Error::Eval(format!(
303            "unknown plugin parameter kind: {text}"
304        ))),
305    }
306}
307
308fn expr_map<'a>(expr: &'a Expr, context: &str) -> Result<&'a [(Expr, Expr)]> {
309    match expr {
310        Expr::Map(entries) => Ok(entries),
311        _ => Err(Error::Eval(format!("{context} must be a map"))),
312    }
313}
314
315fn expect_tag(map: &[(Expr, Expr)], expected: &str) -> Result<()> {
316    match lookup_required(map, "tag")? {
317        Expr::Symbol(symbol) if is_symbol(symbol, LIB_NS, expected) => Ok(()),
318        _ => Err(Error::Eval(format!(
319            "plugin descriptor tag must be {expected}"
320        ))),
321    }
322}
323
324fn expr_vector<'a>(expr: &'a Expr, context: &str) -> Result<&'a [Expr]> {
325    match expr {
326        Expr::Vector(items) => Ok(items),
327        _ => Err(Error::Eval(format!("{context} must be a vector"))),
328    }
329}
330
331fn expr_string<'a>(expr: &'a Expr, context: &str) -> Result<&'a str> {
332    match expr {
333        Expr::String(text) => Ok(text),
334        _ => Err(Error::Eval(format!("{context} must be a string"))),
335    }
336}
337
338fn expr_bool(expr: &Expr, context: &str) -> Result<bool> {
339    match expr {
340        Expr::Bool(value) => Ok(*value),
341        _ => Err(Error::Eval(format!("{context} must be a bool"))),
342    }
343}
344
345fn expr_u16(expr: &Expr, context: &str) -> Result<u16> {
346    expr_u32(expr, context)?
347        .try_into()
348        .map_err(|_| Error::Eval(format!("{context} is out of range for u16")))
349}
350
351fn expr_u32(expr: &Expr, context: &str) -> Result<u32> {
352    let text = number_text(expr, context)?;
353    text.parse::<u32>()
354        .map_err(|_| Error::Eval(format!("{context} must be a u32")))
355}
356
357fn expr_f64(expr: &Expr, context: &str) -> Result<f64> {
358    let text = number_text(expr, context)?;
359    let value = text
360        .parse::<f64>()
361        .map_err(|_| Error::Eval(format!("{context} must be an f64")))?;
362    if !value.is_finite() {
363        return Err(Error::Eval(format!("{context} must be finite")));
364    }
365    Ok(value)
366}
367
368fn number_text<'a>(expr: &'a Expr, context: &str) -> Result<&'a str> {
369    match expr {
370        Expr::Number(number) => Ok(number.canonical.as_str()),
371        Expr::String(text) => Ok(text),
372        _ => Err(Error::Eval(format!("{context} must be a number"))),
373    }
374}
375
376fn lookup_required<'a>(map: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
377    map.iter()
378        .find_map(|(key, value)| match key {
379            Expr::Symbol(symbol) if is_symbol(symbol, LIB_NS, name) => Some(value),
380            _ => None,
381        })
382        .ok_or_else(|| Error::Eval(format!("plugin descriptor field is missing: {name}")))
383}
384
385fn is_symbol(symbol: &Symbol, namespace: &str, name: &str) -> bool {
386    symbol.namespace.as_deref() == Some(namespace) && symbol.name.as_ref() == name
387}