use sim_citizen_derive::Citizen;
use sim_kernel::{Error, Expr, NumberLiteral, Result, Symbol};
use crate::{PcmSampleFormat, PcmSpec};
const LIB_NS: &str = "stream-audio";
#[derive(Clone, Debug, PartialEq, Citizen)]
#[citizen(symbol = "stream/PcmFormat", version = 1)]
pub struct PcmFormatDescriptor {
#[citizen(with = "pcm_spec_expr")]
spec: Expr,
}
impl PcmFormatDescriptor {
pub fn new(spec: PcmSpec) -> Self {
Self {
spec: spec_to_expr(spec),
}
}
pub fn from_expr(expr: Expr) -> Result<Self> {
pcm_spec_expr::decode(&expr)?;
Ok(Self { spec: expr })
}
pub fn spec(&self) -> Result<PcmSpec> {
spec_from_expr(&self.spec)
}
pub fn as_expr(&self) -> &Expr {
&self.spec
}
}
impl Default for PcmFormatDescriptor {
fn default() -> Self {
Self::new(PcmSpec::f32(2, 48_000).expect("default PCM descriptor should be valid"))
}
}
pub fn pcm_format_class_symbol() -> Symbol {
Symbol::qualified("stream", "PcmFormat")
}
pub(crate) mod pcm_spec_expr {
use sim_kernel::{Expr, Result};
use super::spec_from_expr;
pub fn encode(expr: &Expr) -> Expr {
expr.clone()
}
pub fn decode(expr: &Expr) -> Result<Expr> {
spec_from_expr(expr)?;
Ok(expr.clone())
}
}
fn spec_to_expr(spec: PcmSpec) -> Expr {
Expr::Map(vec![
(field("tag"), tag("pcm-format")),
(field("channels"), number_usize(spec.channels())),
(field("sample-rate-hz"), number_u32(spec.sample_rate_hz())),
(
field("sample-format"),
Expr::Symbol(match spec.sample_format() {
PcmSampleFormat::I16 => Symbol::qualified("pcm", "i16"),
PcmSampleFormat::F32 => Symbol::qualified("pcm", "f32"),
}),
),
])
}
fn spec_from_expr(expr: &Expr) -> Result<PcmSpec> {
let map = expr_map(expr, "PCM format descriptor")?;
expect_tag(map, "pcm-format")?;
let channels = expr_usize(lookup_required(map, "channels")?, "channels")?;
let sample_rate_hz = expr_u32(lookup_required(map, "sample-rate-hz")?, "sample-rate-hz")?;
match lookup_required(map, "sample-format")? {
Expr::Symbol(symbol) if symbol.namespace.as_deref() == Some("pcm") => {
match symbol.name.as_ref() {
"i16" => PcmSpec::i16(channels, sample_rate_hz),
"f32" => PcmSpec::f32(channels, sample_rate_hz),
other => Err(Error::Eval(format!("unknown PCM sample format: {other}"))),
}
}
_ => Err(Error::Eval(
"PCM sample format must be a pcm/* symbol".to_owned(),
)),
}
}
fn field(name: &'static str) -> Expr {
sim_value::build::qsym(LIB_NS, name)
}
fn tag(name: &'static str) -> Expr {
Expr::Symbol(Symbol::qualified(LIB_NS, name))
}
fn number_u32(value: u32) -> Expr {
number_usize(value as usize)
}
fn number_usize(value: usize) -> Expr {
Expr::Number(NumberLiteral {
domain: Symbol::qualified("numbers", "i64"),
canonical: value.to_string(),
})
}
fn expr_map<'a>(expr: &'a Expr, context: &str) -> Result<&'a [(Expr, Expr)]> {
match expr {
Expr::Map(entries) => Ok(entries),
_ => Err(Error::Eval(format!("{context} must be a map"))),
}
}
fn expect_tag(map: &[(Expr, Expr)], expected: &str) -> Result<()> {
match lookup_required(map, "tag")? {
Expr::Symbol(symbol) if is_symbol(symbol, LIB_NS, expected) => Ok(()),
_ => Err(Error::Eval(format!(
"PCM descriptor tag must be {expected}"
))),
}
}
fn expr_u32(expr: &Expr, context: &str) -> Result<u32> {
expr_usize(expr, context)?
.try_into()
.map_err(|_| Error::Eval(format!("{context} is out of range for u32")))
}
fn expr_usize(expr: &Expr, context: &str) -> Result<usize> {
let text = match expr {
Expr::Number(number) => number.canonical.as_str(),
Expr::String(text) => text,
_ => return Err(Error::Eval(format!("{context} must be a number"))),
};
text.parse::<usize>()
.map_err(|_| Error::Eval(format!("{context} must be an unsigned integer")))
}
fn lookup_required<'a>(map: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
map.iter()
.find_map(|(key, value)| match key {
Expr::Symbol(symbol) if is_symbol(symbol, LIB_NS, name) => Some(value),
_ => None,
})
.ok_or_else(|| Error::Eval(format!("PCM descriptor field is missing: {name}")))
}
fn is_symbol(symbol: &Symbol, namespace: &str, name: &str) -> bool {
symbol.namespace.as_deref() == Some(namespace) && symbol.name.as_ref() == name
}