Skip to main content

sim_lib_audio_dsp/
citizen.rs

1use sim_citizen_derive::Citizen;
2use sim_kernel::{Error, Expr, NumberLiteral, Result, Symbol};
3
4const LIB_NS: &str = "audio-dsp";
5
6/// Citizen descriptor for a DSP processor configuration: a kind name plus a
7/// list of named `f64` parameters.
8///
9/// The configuration is stored in its [`Expr`] encoding so it round-trips
10/// through the citizen protocol.
11#[derive(Clone, Debug, PartialEq, Citizen)]
12#[citizen(symbol = "audio-dsp/Config", version = 1)]
13pub struct DspConfigDescriptor {
14    #[citizen(with = "config_expr")]
15    config: Expr,
16}
17
18impl DspConfigDescriptor {
19    /// Builds a config descriptor, validating the kind and parameters.
20    pub fn new(kind: impl Into<String>, params: Vec<(String, f64)>) -> Result<Self> {
21        let kind = kind.into();
22        validate_kind(&kind)?;
23        validate_params(&params)?;
24        Ok(Self {
25            config: config_to_expr(&kind, &params),
26        })
27    }
28
29    /// Builds a `gain` config descriptor with a single `gain` parameter.
30    pub fn gain(gain: f64) -> Result<Self> {
31        Self::new("gain", vec![("gain".to_owned(), gain)])
32    }
33
34    /// Builds a descriptor from a config expression, validating that it decodes.
35    pub fn from_expr(expr: Expr) -> Result<Self> {
36        config_expr::decode(&expr)?;
37        Ok(Self { config: expr })
38    }
39
40    /// Decodes and returns the config kind name.
41    pub fn kind(&self) -> Result<String> {
42        let (kind, _) = config_from_expr(&self.config)?;
43        Ok(kind)
44    }
45
46    /// Decodes and returns the config parameters as name/value pairs.
47    pub fn params(&self) -> Result<Vec<(String, f64)>> {
48        let (_, params) = config_from_expr(&self.config)?;
49        Ok(params)
50    }
51
52    /// Returns the underlying config expression without decoding it.
53    pub fn as_expr(&self) -> &Expr {
54        &self.config
55    }
56}
57
58impl Default for DspConfigDescriptor {
59    fn default() -> Self {
60        Self::gain(1.0).expect("default DSP config descriptor should be valid")
61    }
62}
63
64/// Returns the class symbol under which DSP configs register as citizens.
65pub fn dsp_config_class_symbol() -> Symbol {
66    Symbol::qualified("audio-dsp", "Config")
67}
68
69pub(crate) mod config_expr {
70    use sim_kernel::{Expr, Result};
71
72    use super::config_from_expr;
73
74    pub fn encode(expr: &Expr) -> Expr {
75        expr.clone()
76    }
77
78    pub fn decode(expr: &Expr) -> Result<Expr> {
79        config_from_expr(expr)?;
80        Ok(expr.clone())
81    }
82}
83
84fn config_to_expr(kind: &str, params: &[(String, f64)]) -> Expr {
85    Expr::Map(vec![
86        (field("tag"), tag("config")),
87        (field("kind"), Expr::Symbol(Symbol::qualified(LIB_NS, kind))),
88        (
89            field("params"),
90            Expr::Vector(
91                params
92                    .iter()
93                    .map(|(key, value)| {
94                        Expr::Map(vec![
95                            (field("key"), Expr::String(key.clone())),
96                            (field("value"), number_f64(*value)),
97                        ])
98                    })
99                    .collect(),
100            ),
101        ),
102    ])
103}
104
105fn config_from_expr(expr: &Expr) -> Result<(String, Vec<(String, f64)>)> {
106    let map = expr_map(expr, "DSP config descriptor")?;
107    expect_tag(map, "config")?;
108    let kind = match lookup_required(map, "kind")? {
109        Expr::Symbol(symbol) if symbol.namespace.as_deref() == Some(LIB_NS) => {
110            symbol.name.to_string()
111        }
112        Expr::String(text) => text.clone(),
113        _ => return Err(Error::Eval("DSP config kind must be a symbol".to_owned())),
114    };
115    validate_kind(&kind)?;
116    let params = params_from_expr(lookup_required(map, "params")?)?;
117    validate_params(&params)?;
118    Ok((kind, params))
119}
120
121fn params_from_expr(expr: &Expr) -> Result<Vec<(String, f64)>> {
122    let Expr::Vector(items) = expr else {
123        return Err(Error::Eval("DSP config params must be a vector".to_owned()));
124    };
125    items
126        .iter()
127        .map(|item| {
128            let map = expr_map(item, "DSP config parameter")?;
129            Ok((
130                expr_string(lookup_required(map, "key")?, "parameter key")?.to_owned(),
131                expr_f64(lookup_required(map, "value")?, "parameter value")?,
132            ))
133        })
134        .collect()
135}
136
137fn validate_kind(kind: &str) -> Result<()> {
138    if kind.trim().is_empty() {
139        return Err(Error::Eval("DSP config kind cannot be empty".to_owned()));
140    }
141    Ok(())
142}
143
144fn validate_params(params: &[(String, f64)]) -> Result<()> {
145    for (key, value) in params {
146        if key.trim().is_empty() {
147            return Err(Error::Eval(
148                "DSP config parameter key cannot be empty".to_owned(),
149            ));
150        }
151        if !value.is_finite() {
152            return Err(Error::Eval(format!(
153                "DSP config parameter {key} must be finite"
154            )));
155        }
156    }
157    Ok(())
158}
159
160fn field(name: &'static str) -> Expr {
161    sim_value::build::qsym(LIB_NS, name)
162}
163
164fn tag(name: &'static str) -> Expr {
165    Expr::Symbol(Symbol::qualified(LIB_NS, name))
166}
167
168fn number_f64(value: f64) -> Expr {
169    Expr::Number(NumberLiteral {
170        domain: Symbol::qualified("numbers", "f64"),
171        canonical: value.to_string(),
172    })
173}
174
175fn expr_map<'a>(expr: &'a Expr, context: &str) -> Result<&'a [(Expr, Expr)]> {
176    match expr {
177        Expr::Map(entries) => Ok(entries),
178        _ => Err(Error::Eval(format!("{context} must be a map"))),
179    }
180}
181
182fn expect_tag(map: &[(Expr, Expr)], expected: &str) -> Result<()> {
183    match lookup_required(map, "tag")? {
184        Expr::Symbol(symbol) if is_symbol(symbol, LIB_NS, expected) => Ok(()),
185        _ => Err(Error::Eval(format!("DSP config tag must be {expected}"))),
186    }
187}
188
189fn expr_string<'a>(expr: &'a Expr, context: &str) -> Result<&'a str> {
190    match expr {
191        Expr::String(text) => Ok(text),
192        _ => Err(Error::Eval(format!("{context} must be a string"))),
193    }
194}
195
196fn expr_f64(expr: &Expr, context: &str) -> Result<f64> {
197    let text = match expr {
198        Expr::Number(number) => number.canonical.as_str(),
199        Expr::String(text) => text,
200        _ => return Err(Error::Eval(format!("{context} must be a number"))),
201    };
202    let value = text
203        .parse::<f64>()
204        .map_err(|_| Error::Eval(format!("{context} must be an f64")))?;
205    if !value.is_finite() {
206        return Err(Error::Eval(format!("{context} must be finite")));
207    }
208    Ok(value)
209}
210
211fn lookup_required<'a>(map: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
212    map.iter()
213        .find_map(|(key, value)| match key {
214            Expr::Symbol(symbol) if is_symbol(symbol, LIB_NS, name) => Some(value),
215            _ => None,
216        })
217        .ok_or_else(|| Error::Eval(format!("DSP config field is missing: {name}")))
218}
219
220fn is_symbol(symbol: &Symbol, namespace: &str, name: &str) -> bool {
221    symbol.namespace.as_deref() == Some(namespace) && symbol.name.as_ref() == name
222}