sim_lib_stream_audio/
citizen.rs1use 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#[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 pub fn new(spec: PcmSpec) -> Self {
35 Self {
36 spec: spec_to_expr(spec),
37 }
38 }
39
40 pub fn from_expr(expr: Expr) -> Result<Self> {
44 pcm_spec_expr::decode(&expr)?;
45 Ok(Self { spec: expr })
46 }
47
48 pub fn spec(&self) -> Result<PcmSpec> {
53 spec_from_expr(&self.spec)
54 }
55
56 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
68pub 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}