Skip to main content

sim_lib_stream_clock/
citizen.rs

1use sim_citizen_derive::Citizen;
2use sim_kernel::{Error, Expr, NumberLiteral, Result, Symbol};
3use sim_lib_stream_core::ClockDomain;
4
5use crate::{Clock, ClockChart, TempoMap, TempoSegment};
6
7const LIB_NS: &str = "stream-clock";
8
9/// Citizen wrapper that carries a [`Clock`] as a self-describing `stream/Clock`
10/// value.
11///
12/// The descriptor stores the clock in its encoded [`Expr`] form so it can move
13/// across codec surfaces and the kernel object system, while still decoding
14/// back to a [`Clock`] for clock math.
15///
16/// # Examples
17///
18/// ```
19/// use sim_kernel::Symbol;
20/// use sim_lib_stream_clock::StreamClockDescriptor;
21///
22/// let descriptor = StreamClockDescriptor::frame(Symbol::new("audio"), 48_000)?;
23/// let clock = descriptor.clock()?;
24/// assert_eq!(clock.id(), &Symbol::new("audio"));
25/// # Ok::<(), sim_kernel::Error>(())
26/// ```
27#[derive(Clone, Debug, PartialEq, Citizen)]
28#[citizen(symbol = "stream/Clock", version = 1)]
29pub struct StreamClockDescriptor {
30    #[citizen(with = "clock_expr")]
31    clock: Expr,
32}
33
34impl StreamClockDescriptor {
35    /// Builds a descriptor for a frame clock with the given id and rate.
36    ///
37    /// Returns an error when `frames_per_second` is zero.
38    pub fn frame(id: Symbol, frames_per_second: u64) -> Result<Self> {
39        let clock = Clock::frame(id, frames_per_second)?;
40        Ok(Self::from_clock(&clock))
41    }
42
43    /// Builds a descriptor for a MIDI clock with the given id, resolution, and
44    /// tempo map.
45    ///
46    /// Returns an error when `tpq` is zero.
47    pub fn midi(id: Symbol, tpq: u32, tempo_map: TempoMap) -> Result<Self> {
48        let clock = Clock::midi(id, tpq, tempo_map)?;
49        Ok(Self::from_clock(&clock))
50    }
51
52    /// Builds a descriptor by encoding an existing [`Clock`].
53    pub fn from_clock(clock: &Clock) -> Self {
54        Self {
55            clock: clock_to_expr(clock),
56        }
57    }
58
59    /// Builds a descriptor from an already-encoded clock [`Expr`].
60    ///
61    /// Returns an error when `expr` is not a valid stream clock encoding.
62    pub fn from_expr(expr: Expr) -> Result<Self> {
63        clock_expr::decode(&expr)?;
64        Ok(Self { clock: expr })
65    }
66
67    /// Decodes the wrapped [`Clock`].
68    ///
69    /// Returns an error when the stored encoding is invalid.
70    pub fn clock(&self) -> Result<Clock> {
71        clock_from_expr(&self.clock)
72    }
73
74    /// Returns the wrapped clock in its encoded [`Expr`] form.
75    pub fn as_expr(&self) -> &Expr {
76        &self.clock
77    }
78}
79
80impl Default for StreamClockDescriptor {
81    fn default() -> Self {
82        Self::frame(Symbol::qualified("clock", "citizen"), 48_000)
83            .expect("default stream clock descriptor should be valid")
84    }
85}
86
87/// Returns the `stream/Clock` class symbol used to register and look up
88/// [`StreamClockDescriptor`] values.
89pub fn stream_clock_class_symbol() -> Symbol {
90    Symbol::qualified("stream", "Clock")
91}
92
93pub(crate) mod clock_expr {
94    use sim_kernel::{Expr, Result};
95
96    use super::clock_from_expr;
97
98    pub fn encode(expr: &Expr) -> Expr {
99        expr.clone()
100    }
101
102    pub fn decode(expr: &Expr) -> Result<Expr> {
103        clock_from_expr(expr)?;
104        Ok(expr.clone())
105    }
106}
107
108fn clock_to_expr(clock: &Clock) -> Expr {
109    match clock.chart() {
110        ClockChart::Frames { frames_per_second } => Expr::Map(vec![
111            (field("tag"), tag("clock")),
112            (field("id"), Expr::Symbol(clock.id().clone())),
113            (field("domain"), Expr::Symbol(clock.domain().symbol())),
114            (field("kind"), tag("frame")),
115            (field("frames-per-second"), number_u64(*frames_per_second)),
116        ]),
117        ClockChart::Midi { tpq, tempo_map } => Expr::Map(vec![
118            (field("tag"), tag("clock")),
119            (field("id"), Expr::Symbol(clock.id().clone())),
120            (field("domain"), Expr::Symbol(clock.domain().symbol())),
121            (field("kind"), tag("midi")),
122            (field("tpq"), number_u32(*tpq)),
123            (
124                field("tempo-map"),
125                Expr::Vector(
126                    tempo_map
127                        .segments()
128                        .iter()
129                        .map(tempo_segment_to_expr)
130                        .collect(),
131                ),
132            ),
133        ]),
134    }
135}
136
137fn clock_from_expr(expr: &Expr) -> Result<Clock> {
138    let map = expr_map(expr, "clock descriptor")?;
139    expect_tag(map, "clock")?;
140    let id = expr_symbol(lookup_required(map, "id")?, "clock id")?;
141    let domain = ClockDomain::from_symbol(&expr_symbol(
142        lookup_required(map, "domain")?,
143        "clock domain",
144    )?)?;
145    let kind = tag_name(lookup_required(map, "kind")?, "clock kind")?;
146    match kind {
147        "frame" => Clock::frame_with_domain(
148            id,
149            domain,
150            expr_u64(
151                lookup_required(map, "frames-per-second")?,
152                "frames-per-second",
153            )?,
154        ),
155        "midi" => {
156            if domain != ClockDomain::MidiTick {
157                return Err(Error::Eval(
158                    "MIDI stream clock domain must be midi-tick".to_owned(),
159                ));
160            }
161            Clock::midi(
162                id,
163                expr_u32(lookup_required(map, "tpq")?, "tpq")?,
164                tempo_map_from_expr(lookup_required(map, "tempo-map")?)?,
165            )
166        }
167        other => Err(Error::Eval(format!("unknown stream clock kind: {other}"))),
168    }
169}
170
171fn tempo_segment_to_expr(segment: &TempoSegment) -> Expr {
172    Expr::Map(vec![
173        (field("start-tick"), number_u64(segment.start_tick)),
174        (field("us-per-quarter"), number_u32(segment.us_per_quarter)),
175    ])
176}
177
178fn tempo_map_from_expr(expr: &Expr) -> Result<TempoMap> {
179    let Expr::Vector(items) = expr else {
180        return Err(Error::Eval("tempo map must be a vector".to_owned()));
181    };
182    TempoMap::new(
183        items
184            .iter()
185            .map(|item| {
186                let map = expr_map(item, "tempo segment")?;
187                TempoSegment::new(
188                    expr_u64(lookup_required(map, "start-tick")?, "start-tick")?,
189                    expr_u32(lookup_required(map, "us-per-quarter")?, "us-per-quarter")?,
190                )
191            })
192            .collect::<Result<Vec<_>>>()?,
193    )
194}
195
196fn field(name: &'static str) -> Expr {
197    sim_value::build::qsym(LIB_NS, name)
198}
199
200fn tag(name: &'static str) -> Expr {
201    Expr::Symbol(Symbol::qualified(LIB_NS, name))
202}
203
204fn number_u32(value: u32) -> Expr {
205    number_u64(u64::from(value))
206}
207
208fn number_u64(value: u64) -> Expr {
209    Expr::Number(NumberLiteral {
210        domain: Symbol::qualified("numbers", "i64"),
211        canonical: value.to_string(),
212    })
213}
214
215fn expr_map<'a>(expr: &'a Expr, context: &str) -> Result<&'a [(Expr, Expr)]> {
216    match expr {
217        Expr::Map(entries) => Ok(entries),
218        _ => Err(Error::Eval(format!("{context} must be a map"))),
219    }
220}
221
222fn expect_tag(map: &[(Expr, Expr)], expected: &str) -> Result<()> {
223    match lookup_required(map, "tag")? {
224        Expr::Symbol(symbol) if is_symbol(symbol, LIB_NS, expected) => Ok(()),
225        _ => Err(Error::Eval(format!("stream clock tag must be {expected}"))),
226    }
227}
228
229fn expr_symbol(expr: &Expr, context: &str) -> Result<Symbol> {
230    match expr {
231        Expr::Symbol(symbol) => Ok(symbol.clone()),
232        _ => Err(Error::Eval(format!("{context} must be a symbol"))),
233    }
234}
235
236fn tag_name<'a>(expr: &'a Expr, context: &str) -> Result<&'a str> {
237    match expr {
238        Expr::Symbol(symbol) if symbol.namespace.as_deref() == Some(LIB_NS) => {
239            Ok(symbol.name.as_ref())
240        }
241        _ => Err(Error::Eval(format!("{context} must be a {LIB_NS} symbol"))),
242    }
243}
244
245fn expr_u32(expr: &Expr, context: &str) -> Result<u32> {
246    expr_u64(expr, context)?
247        .try_into()
248        .map_err(|_| Error::Eval(format!("{context} is out of range for u32")))
249}
250
251fn expr_u64(expr: &Expr, context: &str) -> Result<u64> {
252    let text = match expr {
253        Expr::Number(number) => number.canonical.as_str(),
254        Expr::String(text) => text,
255        _ => return Err(Error::Eval(format!("{context} must be a number"))),
256    };
257    text.parse::<u64>()
258        .map_err(|_| Error::Eval(format!("{context} must be an unsigned integer")))
259}
260
261fn lookup_required<'a>(map: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
262    map.iter()
263        .find_map(|(key, value)| match key {
264            Expr::Symbol(symbol) if is_symbol(symbol, LIB_NS, name) => Some(value),
265            _ => None,
266        })
267        .ok_or_else(|| Error::Eval(format!("stream clock field is missing: {name}")))
268}
269
270fn is_symbol(symbol: &Symbol, namespace: &str, name: &str) -> bool {
271    symbol.namespace.as_deref() == Some(namespace) && symbol.name.as_ref() == name
272}