1use sim_citizen_derive::Citizen;
2use sim_kernel::{Error, Expr, NumberLiteral, Result, Symbol};
3
4const LIB_NS: &str = "audio-dsp";
5
6#[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 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(¶ms)?;
24 Ok(Self {
25 config: config_to_expr(&kind, ¶ms),
26 })
27 }
28
29 pub fn gain(gain: f64) -> Result<Self> {
31 Self::new("gain", vec![("gain".to_owned(), gain)])
32 }
33
34 pub fn from_expr(expr: Expr) -> Result<Self> {
36 config_expr::decode(&expr)?;
37 Ok(Self { config: expr })
38 }
39
40 pub fn kind(&self) -> Result<String> {
42 let (kind, _) = config_from_expr(&self.config)?;
43 Ok(kind)
44 }
45
46 pub fn params(&self) -> Result<Vec<(String, f64)>> {
48 let (_, params) = config_from_expr(&self.config)?;
49 Ok(params)
50 }
51
52 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
64pub 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(¶ms)?;
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}