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#[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 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 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 pub fn from_clock(clock: &Clock) -> Self {
54 Self {
55 clock: clock_to_expr(clock),
56 }
57 }
58
59 pub fn from_expr(expr: Expr) -> Result<Self> {
63 clock_expr::decode(&expr)?;
64 Ok(Self { clock: expr })
65 }
66
67 pub fn clock(&self) -> Result<Clock> {
71 clock_from_expr(&self.clock)
72 }
73
74 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
87pub 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}