1use sim_kernel::{
2 Cx, Datum, DatumStore, Diagnostic, Error, Expr, NumberLiteral, Ref, Result, Severity, Symbol,
3 Tick,
4};
5use sim_lib_stream_core::ClockDomain;
6
7use crate::{Instant, TempoMap, tempo::midi_tick_duration};
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
21pub struct ClockIndex(u64);
22
23impl ClockIndex {
24 pub fn new(value: u64) -> Self {
26 Self(value)
27 }
28
29 pub fn value(self) -> u64 {
31 self.0
32 }
33}
34
35#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct IndexConversion {
56 index: ClockIndex,
57 diagnostics: Vec<Diagnostic>,
58}
59
60impl IndexConversion {
61 fn exact(index: u64) -> Self {
62 Self {
63 index: ClockIndex::new(index),
64 diagnostics: Vec::new(),
65 }
66 }
67
68 fn inexact(index: u64, message: impl Into<String>) -> Self {
69 Self {
70 index: ClockIndex::new(index),
71 diagnostics: vec![Diagnostic {
72 severity: Severity::Warning,
73 message: message.into(),
74 source: None,
75 span: None,
76 code: Some(Symbol::qualified("stream/clock", "inexact-conversion")),
77 related: Vec::new(),
78 }],
79 }
80 }
81
82 pub fn index(&self) -> ClockIndex {
84 self.index
85 }
86
87 pub fn diagnostics(&self) -> &[Diagnostic] {
89 &self.diagnostics
90 }
91
92 pub fn is_exact(&self) -> bool {
94 self.diagnostics.is_empty()
95 }
96}
97
98#[derive(Clone, Debug, PartialEq, Eq)]
103pub enum ClockChart {
104 Frames {
106 frames_per_second: u64,
108 },
109 Midi {
112 tpq: u32,
114 tempo_map: TempoMap,
116 },
117}
118
119#[derive(Clone, Debug, PartialEq, Eq)]
140pub struct Clock {
141 id: Symbol,
142 domain: ClockDomain,
143 chart: ClockChart,
144}
145
146impl Clock {
147 pub fn frame(id: Symbol, frames_per_second: u64) -> Result<Self> {
151 Self::frame_with_domain(id, ClockDomain::Sample, frames_per_second)
152 }
153
154 pub fn frame_with_domain(
159 id: Symbol,
160 domain: ClockDomain,
161 frames_per_second: u64,
162 ) -> Result<Self> {
163 if domain == ClockDomain::MidiTick {
164 return Err(Error::Eval(
165 "frame clock domain must not be midi-tick".to_owned(),
166 ));
167 }
168 if frames_per_second == 0 {
169 return Err(Error::Eval("frame clock rate must be non-zero".to_owned()));
170 }
171 Ok(Self {
172 id,
173 domain,
174 chart: ClockChart::Frames { frames_per_second },
175 })
176 }
177
178 pub fn midi(id: Symbol, tpq: u32, tempo_map: TempoMap) -> Result<Self> {
182 if tpq == 0 {
183 return Err(Error::Eval("midi clock TPQ must be non-zero".to_owned()));
184 }
185 Ok(Self {
186 id,
187 domain: ClockDomain::MidiTick,
188 chart: ClockChart::Midi { tpq, tempo_map },
189 })
190 }
191
192 pub fn id(&self) -> &Symbol {
194 &self.id
195 }
196
197 pub fn domain(&self) -> ClockDomain {
199 self.domain
200 }
201
202 pub fn chart(&self) -> &ClockChart {
204 &self.chart
205 }
206
207 pub fn index_for_instant(&self, instant: Instant) -> Result<IndexConversion> {
213 match &self.chart {
214 ClockChart::Frames { frames_per_second } => {
215 frame_index_for_instant(instant, *frames_per_second)
216 }
217 ClockChart::Midi { tpq, tempo_map } => midi_index_for_instant(instant, *tpq, tempo_map),
218 }
219 }
220
221 pub fn instant_for_index(&self, index: ClockIndex) -> Result<Instant> {
225 match &self.chart {
226 ClockChart::Frames { frames_per_second } => {
227 Instant::new(i128::from(index.value()), i128::from(*frames_per_second))
228 }
229 ClockChart::Midi { tpq, tempo_map } => midi_instant_for_index(index, *tpq, tempo_map),
230 }
231 }
232
233 pub fn tick_for_index(&self, cx: &mut Cx, index: ClockIndex) -> Result<Tick> {
240 let id = cx.datum_store_mut().intern(Datum::Node {
241 tag: Symbol::qualified("stream/clock", "index"),
242 fields: vec![
243 (Symbol::new("clock"), Datum::Symbol(self.id.clone())),
244 (
245 Symbol::new("index"),
246 Datum::Number(NumberLiteral {
247 domain: Symbol::qualified("stream/clock", "index"),
248 canonical: index.value().to_string(),
249 }),
250 ),
251 ],
252 })?;
253 Ok(Tick::new(self.id.clone(), Ref::Content(id)))
254 }
255
256 pub fn index_expr(&self, index: ClockIndex) -> Expr {
258 Expr::Extension {
259 tag: Symbol::qualified("stream/clock", "index"),
260 payload: Box::new(Expr::Map(vec![
261 (
262 Expr::Symbol(Symbol::new("clock")),
263 Expr::Symbol(self.id.clone()),
264 ),
265 (
266 Expr::Symbol(Symbol::new("index")),
267 Expr::Number(NumberLiteral {
268 domain: Symbol::qualified("stream/clock", "index"),
269 canonical: index.value().to_string(),
270 }),
271 ),
272 ])),
273 }
274 }
275}
276
277fn frame_index_for_instant(instant: Instant, frames_per_second: u64) -> Result<IndexConversion> {
278 let numerator = instant
279 .numerator()
280 .checked_mul(i128::from(frames_per_second))
281 .ok_or_else(|| Error::Eval("frame clock conversion overflowed".to_owned()))?;
282 let denominator = instant.denominator();
283 let index = checked_index(numerator / denominator)?;
284 if numerator % denominator == 0 {
285 Ok(IndexConversion::exact(index))
286 } else {
287 Ok(IndexConversion::inexact(
288 index,
289 "instant is not an exact frame boundary",
290 ))
291 }
292}
293
294fn midi_index_for_instant(
295 instant: Instant,
296 tpq: u32,
297 tempo_map: &TempoMap,
298) -> Result<IndexConversion> {
299 let starts = tempo_map.segment_start_instants(tpq)?;
300 let segment_index = starts.partition_point(|start| *start <= instant) - 1;
301 let segment = tempo_map.segments()[segment_index];
302 let elapsed = instant.checked_sub(starts[segment_index])?;
303 let numerator = elapsed
304 .numerator()
305 .checked_mul(1_000_000)
306 .and_then(|value| value.checked_mul(i128::from(tpq)))
307 .ok_or_else(|| Error::Eval("midi clock conversion overflowed".to_owned()))?;
308 let denominator = elapsed
309 .denominator()
310 .checked_mul(i128::from(segment.us_per_quarter))
311 .ok_or_else(|| Error::Eval("midi clock denominator overflowed".to_owned()))?;
312 let delta_ticks = checked_index(numerator / denominator)?;
313 let index = segment
314 .start_tick
315 .checked_add(delta_ticks)
316 .ok_or_else(|| Error::Eval("midi clock index overflowed".to_owned()))?;
317 if numerator % denominator == 0 {
318 Ok(IndexConversion::exact(index))
319 } else {
320 Ok(IndexConversion::inexact(
321 index,
322 "instant is not an exact midi tick boundary",
323 ))
324 }
325}
326
327fn midi_instant_for_index(index: ClockIndex, tpq: u32, tempo_map: &TempoMap) -> Result<Instant> {
328 let starts = tempo_map.segment_start_instants(tpq)?;
329 let segments = tempo_map.segments();
330 let segment_index = segments.partition_point(|segment| segment.start_tick <= index.value()) - 1;
331 let segment = segments[segment_index];
332 let elapsed_ticks = index.value() - segment.start_tick;
333 starts[segment_index].checked_add(midi_tick_duration(
334 elapsed_ticks,
335 tpq,
336 segment.us_per_quarter,
337 )?)
338}
339
340fn checked_index(value: i128) -> Result<u64> {
341 u64::try_from(value)
342 .map_err(|_| Error::Eval("clock index must fit in an unsigned 64-bit value".to_owned()))
343}