Skip to main content

sim_lib_music_core/
time.rs

1use sim_kernel::{Ref, Symbol, Tick as KernelTick};
2use sim_lib_midi_core::{DEFAULT_US_PER_QUARTER, TickTime};
3
4use crate::{MusicError, Time};
5
6/// A musical position or duration measured in beats, as a rational [`Time`].
7pub type Beat = Time;
8/// A musical position or duration in beats, as a rational [`Time`].
9pub type MusicalTime = Time;
10/// A position or duration in MIDI-style ticks, re-exporting `TickTime`.
11pub type Tick = TickTime;
12
13/// A reference to a tempo map, carrying its id and constant tempo.
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct TempoMapRef {
16    /// The qualified identifier of the tempo map.
17    pub id: Symbol,
18    /// Microseconds per quarter note for the (constant) tempo.
19    pub us_per_quarter: u32,
20}
21
22impl TempoMapRef {
23    /// Builds a constant-tempo reference from an id and microseconds per quarter.
24    pub fn constant(id: impl Into<String>, us_per_quarter: u32) -> Self {
25        Self {
26            id: Symbol::qualified("music/tempo", id.into()),
27            us_per_quarter,
28        }
29    }
30}
31
32impl Default for TempoMapRef {
33    fn default() -> Self {
34        Self::constant("default", DEFAULT_US_PER_QUARTER)
35    }
36}
37
38/// A half-open span of ticks `[start, end)`.
39#[derive(Copy, Clone, Debug, PartialEq, Eq)]
40pub struct TimeRange {
41    /// The inclusive start tick of the range.
42    pub start: Tick,
43    /// The exclusive end tick of the range.
44    pub end: Tick,
45}
46
47impl TimeRange {
48    /// Builds a range, quantizing `end` to `start`'s resolution.
49    ///
50    /// Returns [`MusicError::InvalidTimeRange`] when `end` precedes `start`.
51    pub fn new(start: Tick, end: Tick) -> Result<Self, MusicError> {
52        let end = end.quantize(start.tpq);
53        if end.ticks < start.ticks {
54            return Err(MusicError::InvalidTimeRange);
55        }
56        Ok(Self { start, end })
57    }
58
59    /// Builds a range from raw tick counts at the given pulses-per-quarter.
60    ///
61    /// Returns [`MusicError::InvalidPpq`] when `ppq` is zero.
62    pub fn from_ticks(start: i64, end: i64, ppq: u32) -> Result<Self, MusicError> {
63        if ppq == 0 {
64            return Err(MusicError::InvalidPpq);
65        }
66        Self::new(
67            Tick {
68                ticks: start,
69                tpq: ppq,
70            },
71            Tick {
72                ticks: end,
73                tpq: ppq,
74            },
75        )
76    }
77
78    /// Builds a range from tick zero to `end`, sharing `end`'s resolution.
79    pub fn starts_at_zero(end: Tick) -> Result<Self, MusicError> {
80        Self::new(
81            Tick {
82                ticks: 0,
83                tpq: end.tpq,
84            },
85            end,
86        )
87    }
88
89    /// Reports whether `tick` falls within the half-open range.
90    pub fn contains(self, tick: Tick) -> bool {
91        let tick = tick.quantize(self.start.tpq);
92        tick.ticks >= self.start.ticks && tick.ticks < self.end.ticks
93    }
94
95    /// Clips a `(start, duration)` span to this range.
96    ///
97    /// Returns the clipped start and duration, or `None` when the span lies
98    /// entirely outside the range.
99    pub fn clip_span(self, start: Tick, duration: Tick) -> Option<(Tick, Tick)> {
100        let start = start.quantize(self.start.tpq);
101        let end = (start + duration).quantize(self.start.tpq);
102        if end.ticks <= self.start.ticks || start.ticks >= self.end.ticks {
103            return None;
104        }
105        let clipped_start = Tick::new(start.ticks.max(self.start.ticks), self.start.tpq).ok()?;
106        let clipped_end = Tick::new(end.ticks.min(self.end.ticks), self.start.tpq).ok()?;
107        let clipped_duration =
108            Tick::new(clipped_end.ticks - clipped_start.ticks, self.start.tpq).ok()?;
109        Some((clipped_start, clipped_duration))
110    }
111}
112
113/// Converts a musical time in beats to ticks at the given pulses-per-quarter.
114///
115/// Returns [`MusicError::InvalidPpq`] when `ppq` is zero.
116pub fn time_to_tick(time: MusicalTime, ppq: u32) -> Result<Tick, MusicError> {
117    if ppq == 0 {
118        return Err(MusicError::InvalidPpq);
119    }
120    let ticks = *time.numer() * 4 * i64::from(ppq) / *time.denom();
121    Ok(Tick { ticks, tpq: ppq })
122}
123
124/// Encodes a tick on a named clock as a kernel `Tick` reference.
125pub fn tick_to_kernel_tick(tick: Tick, clock: Symbol) -> KernelTick {
126    KernelTick::new(
127        clock,
128        Ref::Symbol(Symbol::qualified(
129            "music/tick",
130            format!("{}@{}", tick.ticks, tick.tpq),
131        )),
132    )
133}