1use sim_kernel::{Cx, Diagnostic, Error, Expr, NumberLiteral, Result, Severity, Symbol, Tick};
2use sim_lib_stream_core::{ClockDomain, clock_index_ref};
3
4use crate::{Instant, TempoMap, tempo::midi_tick_duration};
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
18pub struct ClockIndex(u64);
19
20impl ClockIndex {
21 pub fn new(value: u64) -> Self {
23 Self(value)
24 }
25
26 pub fn value(self) -> u64 {
28 self.0
29 }
30}
31
32#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct IndexConversion {
53 index: ClockIndex,
54 diagnostics: Vec<Diagnostic>,
55}
56
57impl IndexConversion {
58 fn exact(index: u64) -> Self {
59 Self {
60 index: ClockIndex::new(index),
61 diagnostics: Vec::new(),
62 }
63 }
64
65 fn inexact(index: u64, message: impl Into<String>) -> Self {
66 Self {
67 index: ClockIndex::new(index),
68 diagnostics: vec![Diagnostic {
69 severity: Severity::Warning,
70 message: message.into(),
71 source: None,
72 span: None,
73 code: Some(Symbol::qualified("stream/clock", "inexact-conversion")),
74 related: Vec::new(),
75 }],
76 }
77 }
78
79 pub fn index(&self) -> ClockIndex {
81 self.index
82 }
83
84 pub fn diagnostics(&self) -> &[Diagnostic] {
86 &self.diagnostics
87 }
88
89 pub fn is_exact(&self) -> bool {
91 self.diagnostics.is_empty()
92 }
93}
94
95#[derive(Clone, Debug, PartialEq, Eq)]
100pub enum ClockChart {
101 Frames {
103 frames_per_second: u64,
105 },
106 Midi {
109 tpq: u32,
111 tempo_map: TempoMap,
113 },
114}
115
116#[derive(Clone, Debug, PartialEq, Eq)]
137pub struct Clock {
138 id: Symbol,
139 domain: ClockDomain,
140 chart: ClockChart,
141}
142
143impl Clock {
144 pub fn frame(id: Symbol, frames_per_second: u64) -> Result<Self> {
148 Self::frame_with_domain(id, ClockDomain::Sample, frames_per_second)
149 }
150
151 pub fn frame_with_domain(
156 id: Symbol,
157 domain: ClockDomain,
158 frames_per_second: u64,
159 ) -> Result<Self> {
160 if domain == ClockDomain::MidiTick {
161 return Err(Error::Eval(
162 "frame clock domain must not be midi-tick".to_owned(),
163 ));
164 }
165 if frames_per_second == 0 {
166 return Err(Error::Eval("frame clock rate must be non-zero".to_owned()));
167 }
168 Ok(Self {
169 id,
170 domain,
171 chart: ClockChart::Frames { frames_per_second },
172 })
173 }
174
175 pub fn midi(id: Symbol, tpq: u32, tempo_map: TempoMap) -> Result<Self> {
179 if tpq == 0 {
180 return Err(Error::Eval("midi clock TPQ must be non-zero".to_owned()));
181 }
182 Ok(Self {
183 id,
184 domain: ClockDomain::MidiTick,
185 chart: ClockChart::Midi { tpq, tempo_map },
186 })
187 }
188
189 pub fn id(&self) -> &Symbol {
191 &self.id
192 }
193
194 pub fn domain(&self) -> ClockDomain {
196 self.domain
197 }
198
199 pub fn chart(&self) -> &ClockChart {
201 &self.chart
202 }
203
204 pub fn index_for_instant(&self, instant: Instant) -> Result<IndexConversion> {
210 match &self.chart {
211 ClockChart::Frames { frames_per_second } => {
212 frame_index_for_instant(instant, *frames_per_second)
213 }
214 ClockChart::Midi { tpq, tempo_map } => midi_index_for_instant(instant, *tpq, tempo_map),
215 }
216 }
217
218 pub fn instant_for_index(&self, index: ClockIndex) -> Result<Instant> {
222 match &self.chart {
223 ClockChart::Frames { frames_per_second } => {
224 Instant::new(i128::from(index.value()), i128::from(*frames_per_second))
225 }
226 ClockChart::Midi { tpq, tempo_map } => midi_instant_for_index(index, *tpq, tempo_map),
227 }
228 }
229
230 pub fn tick_for_index(&self, _cx: &mut Cx, index: ClockIndex) -> Result<Tick> {
236 Ok(Tick::new(self.id.clone(), clock_index_ref(index.value())))
237 }
238
239 pub fn index_expr(&self, index: ClockIndex) -> Expr {
241 Expr::Extension {
242 tag: Symbol::qualified("stream/clock", "index"),
243 payload: Box::new(Expr::Map(vec![
244 (
245 Expr::Symbol(Symbol::new("clock")),
246 Expr::Symbol(self.id.clone()),
247 ),
248 (
249 Expr::Symbol(Symbol::new("index")),
250 Expr::Number(NumberLiteral {
251 domain: Symbol::qualified("stream/clock", "index"),
252 canonical: index.value().to_string(),
253 }),
254 ),
255 ])),
256 }
257 }
258}
259
260fn frame_index_for_instant(instant: Instant, frames_per_second: u64) -> Result<IndexConversion> {
261 let numerator = instant
262 .numerator()
263 .checked_mul(i128::from(frames_per_second))
264 .ok_or_else(|| Error::Eval("frame clock conversion overflowed".to_owned()))?;
265 let denominator = instant.denominator();
266 let index = checked_index(numerator / denominator)?;
267 if numerator % denominator == 0 {
268 Ok(IndexConversion::exact(index))
269 } else {
270 Ok(IndexConversion::inexact(
271 index,
272 "instant is not an exact frame boundary",
273 ))
274 }
275}
276
277fn midi_index_for_instant(
278 instant: Instant,
279 tpq: u32,
280 tempo_map: &TempoMap,
281) -> Result<IndexConversion> {
282 let starts = tempo_map.segment_start_instants(tpq)?;
283 let segment_index = starts.partition_point(|start| *start <= instant) - 1;
284 let segment = tempo_map.segments()[segment_index];
285 let elapsed = instant.checked_sub(starts[segment_index])?;
286 let numerator = elapsed
287 .numerator()
288 .checked_mul(1_000_000)
289 .and_then(|value| value.checked_mul(i128::from(tpq)))
290 .ok_or_else(|| Error::Eval("midi clock conversion overflowed".to_owned()))?;
291 let denominator = elapsed
292 .denominator()
293 .checked_mul(i128::from(segment.us_per_quarter))
294 .ok_or_else(|| Error::Eval("midi clock denominator overflowed".to_owned()))?;
295 let delta_ticks = checked_index(numerator / denominator)?;
296 let index = segment
297 .start_tick
298 .checked_add(delta_ticks)
299 .ok_or_else(|| Error::Eval("midi clock index overflowed".to_owned()))?;
300 if numerator % denominator == 0 {
301 Ok(IndexConversion::exact(index))
302 } else {
303 Ok(IndexConversion::inexact(
304 index,
305 "instant is not an exact midi tick boundary",
306 ))
307 }
308}
309
310fn midi_instant_for_index(index: ClockIndex, tpq: u32, tempo_map: &TempoMap) -> Result<Instant> {
311 let starts = tempo_map.segment_start_instants(tpq)?;
312 let segments = tempo_map.segments();
313 let segment_index = segments.partition_point(|segment| segment.start_tick <= index.value()) - 1;
314 let segment = segments[segment_index];
315 let elapsed_ticks = index.value() - segment.start_tick;
316 starts[segment_index].checked_add(midi_tick_duration(
317 elapsed_ticks,
318 tpq,
319 segment.us_per_quarter,
320 )?)
321}
322
323fn checked_index(value: i128) -> Result<u64> {
324 u64::try_from(value)
325 .map_err(|_| Error::Eval("clock index must fit in an unsigned 64-bit value".to_owned()))
326}