Skip to main content

sim_lib_stream_audio/
citizen.rs

1use sim_citizen_derive::Citizen;
2use sim_kernel::{Error, Expr, NumberLiteral, Result, Symbol};
3
4use crate::{PcmSampleFormat, PcmSpec};
5
6const LIB_NS: &str = "stream-audio";
7
8/// Runtime citizen that carries a [`PcmSpec`] as a `stream/PcmFormat` object.
9///
10/// The descriptor stores the audio format as a kernel [`Expr`] map so it can be
11/// exposed to the SIM runtime as a first-class object (registered under the
12/// [`pcm_format_class_symbol`] class). [`spec`](PcmFormatDescriptor::spec)
13/// decodes that expression back into a [`PcmSpec`].
14///
15/// # Examples
16///
17/// ```
18/// use sim_lib_stream_audio::{PcmFormatDescriptor, PcmSpec};
19///
20/// let spec = PcmSpec::f32(2, 48_000)?;
21/// let descriptor = PcmFormatDescriptor::new(spec);
22/// assert_eq!(descriptor.spec()?, spec);
23/// # Ok::<(), sim_kernel::Error>(())
24/// ```
25#[derive(Clone, Debug, PartialEq, Citizen)]
26#[citizen(symbol = "stream/PcmFormat", version = 1)]
27pub struct PcmFormatDescriptor {
28    #[citizen(with = "pcm_spec_expr")]
29    spec: Expr,
30}
31
32impl PcmFormatDescriptor {
33    /// Builds a descriptor from an audio format.
34    pub fn new(spec: PcmSpec) -> Self {
35        Self {
36            spec: spec_to_expr(spec),
37        }
38    }
39
40    /// Builds a descriptor from an already-encoded format expression.
41    ///
42    /// Returns an error when `expr` is not a valid PCM format map.
43    pub fn from_expr(expr: Expr) -> Result<Self> {
44        pcm_spec_expr::decode(&expr)?;
45        Ok(Self { spec: expr })
46    }
47
48    /// Decodes the stored expression back into a [`PcmSpec`].
49    ///
50    /// Returns an error when the stored expression is not a valid PCM format
51    /// map.
52    pub fn spec(&self) -> Result<PcmSpec> {
53        spec_from_expr(&self.spec)
54    }
55
56    /// Returns the underlying format expression.
57    pub fn as_expr(&self) -> &Expr {
58        &self.spec
59    }
60}
61
62impl Default for PcmFormatDescriptor {
63    fn default() -> Self {
64        Self::new(PcmSpec::f32(2, 48_000).expect("default PCM descriptor should be valid"))
65    }
66}
67
68/// Returns the `stream/PcmFormat` class symbol under which
69/// [`PcmFormatDescriptor`] is registered.
70pub fn pcm_format_class_symbol() -> Symbol {
71    Symbol::qualified("stream", "PcmFormat")
72}
73
74pub(crate) mod pcm_spec_expr {
75    use sim_kernel::{Expr, Result};
76
77    use super::spec_from_expr;
78
79    pub fn encode(expr: &Expr) -> Expr {
80        expr.clone()
81    }
82
83    pub fn decode(expr: &Expr) -> Result<Expr> {
84        spec_from_expr(expr)?;
85        Ok(expr.clone())
86    }
87}
88
89fn spec_to_expr(spec: PcmSpec) -> Expr {
90    Expr::Map(vec![
91        (field("tag"), tag("pcm-format")),
92        (field("channels"), number_usize(spec.channels())),
93        (field("sample-rate-hz"), number_u32(spec.sample_rate_hz())),
94        (
95            field("sample-format"),
96            Expr::Symbol(match spec.sample_format() {
97                PcmSampleFormat::I16 => Symbol::qualified("pcm", "i16"),
98                PcmSampleFormat::F32 => Symbol::qualified("pcm", "f32"),
99            }),
100        ),
101    ])
102}
103
104fn spec_from_expr(expr: &Expr) -> Result<PcmSpec> {
105    let map = expr_map(expr, "PCM format descriptor")?;
106    expect_tag(map, "pcm-format")?;
107    let channels = expr_usize(lookup_required(map, "channels")?, "channels")?;
108    let sample_rate_hz = expr_u32(lookup_required(map, "sample-rate-hz")?, "sample-rate-hz")?;
109    match lookup_required(map, "sample-format")? {
110        Expr::Symbol(symbol) if symbol.namespace.as_deref() == Some("pcm") => {
111            match symbol.name.as_ref() {
112                "i16" => PcmSpec::i16(channels, sample_rate_hz),
113                "f32" => PcmSpec::f32(channels, sample_rate_hz),
114                other => Err(Error::Eval(format!("unknown PCM sample format: {other}"))),
115            }
116        }
117        _ => Err(Error::Eval(
118            "PCM sample format must be a pcm/* symbol".to_owned(),
119        )),
120    }
121}
122
123fn field(name: &'static str) -> Expr {
124    sim_value::build::qsym(LIB_NS, name)
125}
126
127fn tag(name: &'static str) -> Expr {
128    Expr::Symbol(Symbol::qualified(LIB_NS, name))
129}
130
131fn number_u32(value: u32) -> Expr {
132    number_usize(value as usize)
133}
134
135fn number_usize(value: usize) -> Expr {
136    Expr::Number(NumberLiteral {
137        domain: Symbol::qualified("numbers", "i64"),
138        canonical: value.to_string(),
139    })
140}
141
142fn expr_map<'a>(expr: &'a Expr, context: &str) -> Result<&'a [(Expr, Expr)]> {
143    match expr {
144        Expr::Map(entries) => Ok(entries),
145        _ => Err(Error::Eval(format!("{context} must be a map"))),
146    }
147}
148
149fn expect_tag(map: &[(Expr, Expr)], expected: &str) -> Result<()> {
150    match lookup_required(map, "tag")? {
151        Expr::Symbol(symbol) if is_symbol(symbol, LIB_NS, expected) => Ok(()),
152        _ => Err(Error::Eval(format!(
153            "PCM descriptor tag must be {expected}"
154        ))),
155    }
156}
157
158fn expr_u32(expr: &Expr, context: &str) -> Result<u32> {
159    expr_usize(expr, context)?
160        .try_into()
161        .map_err(|_| Error::Eval(format!("{context} is out of range for u32")))
162}
163
164fn expr_usize(expr: &Expr, context: &str) -> Result<usize> {
165    let text = match expr {
166        Expr::Number(number) => number.canonical.as_str(),
167        Expr::String(text) => text,
168        _ => return Err(Error::Eval(format!("{context} must be a number"))),
169    };
170    text.parse::<usize>()
171        .map_err(|_| Error::Eval(format!("{context} must be an unsigned integer")))
172}
173
174fn lookup_required<'a>(map: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
175    map.iter()
176        .find_map(|(key, value)| match key {
177            Expr::Symbol(symbol) if is_symbol(symbol, LIB_NS, name) => Some(value),
178            _ => None,
179        })
180        .ok_or_else(|| Error::Eval(format!("PCM descriptor field is missing: {name}")))
181}
182
183fn is_symbol(symbol: &Symbol, namespace: &str, name: &str) -> bool {
184    symbol.namespace.as_deref() == Some(namespace) && symbol.name.as_ref() == name
185}