Skip to main content

sim_lib_stream_clock/
tempo.rs

1use sim_kernel::{Error, Result};
2
3use crate::Instant;
4
5/// One constant-tempo span of a MIDI [`TempoMap`], starting at a tick offset.
6///
7/// A segment holds the tempo (in microseconds per quarter note) that applies
8/// from `start_tick` until the next segment begins. The tempo must be
9/// non-zero.
10///
11/// # Examples
12///
13/// ```
14/// use sim_lib_stream_clock::TempoSegment;
15///
16/// let segment = TempoSegment::new(0, 500_000)?;
17/// assert_eq!(segment.start_tick, 0);
18/// assert_eq!(segment.us_per_quarter, 500_000);
19/// # Ok::<(), sim_kernel::Error>(())
20/// ```
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct TempoSegment {
23    /// Tick offset at which this tempo takes effect.
24    pub start_tick: u64,
25    /// Tempo of the segment, in microseconds per quarter note.
26    pub us_per_quarter: u32,
27}
28
29impl TempoSegment {
30    /// Builds a tempo segment beginning at `start_tick`.
31    ///
32    /// Returns an error when `us_per_quarter` is zero.
33    pub fn new(start_tick: u64, us_per_quarter: u32) -> Result<Self> {
34        if us_per_quarter == 0 {
35            return Err(Error::Eval(
36                "tempo segment microseconds-per-quarter must be non-zero".to_owned(),
37            ));
38        }
39        Ok(Self {
40            start_tick,
41            us_per_quarter,
42        })
43    }
44}
45
46/// Ordered list of [`TempoSegment`]s describing how MIDI tempo changes over a
47/// timeline.
48///
49/// A valid map starts with a segment at tick 0 and has strictly increasing
50/// segment ticks, so every tick maps to exactly one tempo. The map drives the
51/// tick <-> [`Instant`] conversions for MIDI clocks.
52///
53/// # Examples
54///
55/// ```
56/// use sim_lib_stream_clock::TempoMap;
57///
58/// let map = TempoMap::single(500_000)?;
59/// assert_eq!(map.segments().len(), 1);
60/// assert_eq!(map.segments()[0].start_tick, 0);
61/// # Ok::<(), sim_kernel::Error>(())
62/// ```
63#[derive(Clone, Debug, PartialEq, Eq)]
64pub struct TempoMap {
65    segments: Vec<TempoSegment>,
66}
67
68impl TempoMap {
69    /// Builds a tempo map from `segments`.
70    ///
71    /// Returns an error when `segments` is empty, when the first segment does
72    /// not start at tick 0, or when the segment ticks are not strictly
73    /// increasing.
74    pub fn new(segments: Vec<TempoSegment>) -> Result<Self> {
75        let first = segments
76            .first()
77            .ok_or_else(|| Error::Eval("tempo map must contain at least one segment".to_owned()))?;
78        if first.start_tick != 0 {
79            return Err(Error::Eval(
80                "tempo map must start with a segment at tick 0".to_owned(),
81            ));
82        }
83        for pair in segments.windows(2) {
84            if pair[0].start_tick >= pair[1].start_tick {
85                return Err(Error::Eval(
86                    "tempo map segment ticks must be strictly increasing".to_owned(),
87                ));
88            }
89        }
90        Ok(Self { segments })
91    }
92
93    /// Builds a single-segment map holding a constant tempo from tick 0.
94    ///
95    /// Returns an error when `us_per_quarter` is zero.
96    pub fn single(us_per_quarter: u32) -> Result<Self> {
97        Self::new(vec![TempoSegment::new(0, us_per_quarter)?])
98    }
99
100    /// Returns the map's segments in tick order.
101    pub fn segments(&self) -> &[TempoSegment] {
102        &self.segments
103    }
104
105    pub(crate) fn segment_start_instants(&self, tpq: u32) -> Result<Vec<Instant>> {
106        if tpq == 0 {
107            return Err(Error::Eval("midi clock TPQ must be non-zero".to_owned()));
108        }
109        let mut starts = Vec::with_capacity(self.segments.len());
110        starts.push(Instant::seconds(0));
111        let mut current = Instant::seconds(0);
112        for pair in self.segments.windows(2) {
113            let delta_ticks = pair[1].start_tick - pair[0].start_tick;
114            let duration = midi_tick_duration(delta_ticks, tpq, pair[0].us_per_quarter)?;
115            current = current.checked_add(duration)?;
116            starts.push(current);
117        }
118        Ok(starts)
119    }
120}
121
122pub(crate) fn midi_tick_duration(ticks: u64, tpq: u32, us_per_quarter: u32) -> Result<Instant> {
123    if tpq == 0 {
124        return Err(Error::Eval("midi clock TPQ must be non-zero".to_owned()));
125    }
126    if us_per_quarter == 0 {
127        return Err(Error::Eval(
128            "tempo segment microseconds-per-quarter must be non-zero".to_owned(),
129        ));
130    }
131    let numerator = i128::from(ticks)
132        .checked_mul(i128::from(us_per_quarter))
133        .ok_or_else(|| Error::Eval("midi tick duration overflowed".to_owned()))?;
134    let denominator = i128::from(tpq) * 1_000_000;
135    Instant::new(numerator, denominator)
136}