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