Skip to main content

sim_lib_stream_clock/
clock.rs

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