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#[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 pub fn new(descriptor: PluginDescriptor) -> Self {
25 Self {
26 descriptor: descriptor_to_expr(&descriptor),
27 }
28 }
29
30 pub fn from_expr(expr: Expr) -> Result<Self> {
37 plugin_descriptor_expr::decode(&expr)?;
38 Ok(Self { descriptor: expr })
39 }
40
41 pub fn descriptor(&self) -> Result<PluginDescriptor> {
47 descriptor_from_expr(&self.descriptor)
48 }
49
50 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
69pub 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}