Skip to main content

sim_lib_stream_clock/
clock.rs

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/// Position on a clock timeline, counted in that clock's own units (frames for
10/// frame clocks, ticks for MIDI clocks).
11///
12/// # Examples
13///
14/// ```
15/// use sim_lib_stream_clock::ClockIndex;
16///
17/// let index = ClockIndex::new(48_000);
18/// assert_eq!(index.value(), 48_000);
19/// ```
20#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
21pub struct ClockIndex(u64);
22
23impl ClockIndex {
24    /// Wraps a raw count into a clock index.
25    pub fn new(value: u64) -> Self {
26        Self(value)
27    }
28
29    /// Returns the underlying count.
30    pub fn value(self) -> u64 {
31        self.0
32    }
33}
34
35/// Result of converting an [`Instant`] to a [`ClockIndex`], paired with any
36/// diagnostics raised by the conversion.
37///
38/// A conversion is exact when the instant lands on a clock boundary; otherwise
39/// the index is rounded toward zero and a warning diagnostic records that the
40/// instant was not on an exact boundary.
41///
42/// # Examples
43///
44/// ```
45/// use sim_kernel::Symbol;
46/// use sim_lib_stream_clock::{Clock, Instant};
47///
48/// let clock = Clock::frame(Symbol::new("audio"), 48_000)?;
49/// let conversion = clock.index_for_instant(Instant::seconds(1))?;
50/// assert_eq!(conversion.index().value(), 48_000);
51/// assert!(conversion.is_exact());
52/// # Ok::<(), sim_kernel::Error>(())
53/// ```
54#[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    /// Returns the converted index.
83    pub fn index(&self) -> ClockIndex {
84        self.index
85    }
86
87    /// Returns the diagnostics raised by the conversion, empty when exact.
88    pub fn diagnostics(&self) -> &[Diagnostic] {
89        &self.diagnostics
90    }
91
92    /// Returns `true` when the instant landed exactly on a clock boundary.
93    pub fn is_exact(&self) -> bool {
94        self.diagnostics.is_empty()
95    }
96}
97
98/// Timing law that maps a clock's indexes to [`Instant`]s and back.
99///
100/// A chart is either a fixed-rate frame clock or a MIDI clock whose tempo is
101/// governed by a [`TempoMap`].
102#[derive(Clone, Debug, PartialEq, Eq)]
103pub enum ClockChart {
104    /// Fixed-rate clock advancing at `frames_per_second` frames each second.
105    Frames {
106        /// Frames per second of the clock.
107        frames_per_second: u64,
108    },
109    /// MIDI clock whose ticks are paced by `tempo_map` at `tpq` ticks per
110    /// quarter note.
111    Midi {
112        /// Ticks per quarter note.
113        tpq: u32,
114        /// Tempo timeline driving tick pacing.
115        tempo_map: TempoMap,
116    },
117}
118
119/// Named clock with a [`ClockDomain`] and a [`ClockChart`], the unit of clock
120/// math in this crate.
121///
122/// A clock converts between [`Instant`]s and [`ClockIndex`]es, and mints the
123/// kernel [`Tick`] / [`Expr`] forms that carry a clock index across the
124/// runtime.
125///
126/// # Examples
127///
128/// ```
129/// use sim_kernel::Symbol;
130/// use sim_lib_stream_clock::{Clock, ClockChart, ClockIndex};
131///
132/// let clock = Clock::frame(Symbol::new("audio"), 48_000)?;
133/// assert!(matches!(clock.chart(), ClockChart::Frames { frames_per_second: 48_000 }));
134/// let instant = clock.instant_for_index(ClockIndex::new(48_000))?;
135/// assert_eq!(instant.numerator(), 1);
136/// assert_eq!(instant.denominator(), 1);
137/// # Ok::<(), sim_kernel::Error>(())
138/// ```
139#[derive(Clone, Debug, PartialEq, Eq)]
140pub struct Clock {
141    id: Symbol,
142    domain: ClockDomain,
143    chart: ClockChart,
144}
145
146impl Clock {
147    /// Builds a frame clock in the [`ClockDomain::Sample`] domain.
148    ///
149    /// Returns an error when `frames_per_second` is zero.
150    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    /// Builds a frame clock in an explicit `domain`.
155    ///
156    /// Returns an error when `domain` is [`ClockDomain::MidiTick`] (which
157    /// requires a MIDI chart) or when `frames_per_second` is zero.
158    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    /// Builds a MIDI clock in the [`ClockDomain::MidiTick`] domain.
179    ///
180    /// Returns an error when `tpq` (ticks per quarter note) is zero.
181    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    /// Returns the clock's identifying symbol.
193    pub fn id(&self) -> &Symbol {
194        &self.id
195    }
196
197    /// Returns the clock's domain.
198    pub fn domain(&self) -> ClockDomain {
199        self.domain
200    }
201
202    /// Returns the clock's timing chart.
203    pub fn chart(&self) -> &ClockChart {
204        &self.chart
205    }
206
207    /// Converts `instant` to this clock's index, reporting whether the result
208    /// is exact via the returned [`IndexConversion`].
209    ///
210    /// Returns an error when the conversion arithmetic overflows or the index
211    /// does not fit in a `u64`.
212    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    /// Converts a clock `index` back to its [`Instant`].
222    ///
223    /// Returns an error when the conversion arithmetic overflows.
224    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    /// Interns `index` as datum content and returns the kernel [`Tick`] that
234    /// carries it on this clock.
235    ///
236    /// The clock index is stored as a datum node referenced by content, so
237    /// kernel events keep carrying only `Tick` values. Returns an error when
238    /// interning into the datum store fails.
239    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    /// Builds the codec [`Expr`] extension form encoding `index` on this clock.
257    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}