sim_lib_midi_core/
tempo.rs1use sim_kernel::Symbol;
5use sim_lib_stream_clock::{Clock, ClockIndex, Instant, TempoMap as ClockTempoMap, TempoSegment};
6
7use crate::{MetaEvent, MidiError, MidiEvent, MidiPayload, TickTime};
8
9pub const DEFAULT_US_PER_QUARTER: u32 = 500_000;
11
12pub fn bpm_to_us_per_quarter(bpm: f64) -> u32 {
14 (60_000_000.0 / bpm).round() as u32
15}
16
17pub fn us_per_quarter_to_bpm(us_per_quarter: u32) -> f64 {
19 60_000_000.0 / us_per_quarter as f64
20}
21
22#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct MidiBeat {
29 numerator: i128,
30 denominator: i128,
31}
32
33impl MidiBeat {
34 pub fn new(numerator: i128, denominator: i128) -> Result<Self, MidiError> {
39 if denominator == 0 {
40 return Err(MidiError::InvalidRatio(
41 narrow_i128(numerator)?,
42 narrow_i128(denominator)?,
43 ));
44 }
45 let (numerator, denominator) = if denominator < 0 {
46 (
47 numerator.checked_neg().ok_or(MidiError::TempoOverflow)?,
48 denominator.checked_neg().ok_or(MidiError::TempoOverflow)?,
49 )
50 } else {
51 (numerator, denominator)
52 };
53 if numerator < 0 {
54 return Err(MidiError::NegativeTempoTick);
55 }
56 let divisor = gcd(numerator, denominator);
57 Ok(Self {
58 numerator: numerator / divisor,
59 denominator: denominator / divisor,
60 })
61 }
62
63 pub const fn numerator(self) -> i128 {
65 self.numerator
66 }
67
68 pub const fn denominator(self) -> i128 {
70 self.denominator
71 }
72}
73
74#[derive(Clone, Debug, PartialEq, Eq)]
81pub struct MidiTempoMap {
82 tpq: u32,
83 clock_map: ClockTempoMap,
84}
85
86impl MidiTempoMap {
87 pub fn from_ordered_events<'a>(
93 tpq: u32,
94 events: impl IntoIterator<Item = &'a MidiEvent>,
95 ) -> Result<Self, MidiError> {
96 if tpq == 0 {
97 return Err(MidiError::ZeroTpq);
98 }
99 let mut segments = vec![TempoSegment::new(0, DEFAULT_US_PER_QUARTER).map_err(chart_error)?];
100 let mut previous_tick = 0_u64;
101 for event in events {
102 let time = event.time.rebase(tpq)?;
103 let tick = u64::try_from(time.ticks).map_err(|_| MidiError::NegativeTempoTick)?;
104 if tick < previous_tick {
105 return Err(MidiError::TempoEventsOutOfOrder);
106 }
107 previous_tick = tick;
108 let MidiPayload::Meta(MetaEvent::Tempo { us_per_quarter }) = &event.payload else {
109 continue;
110 };
111 if *us_per_quarter == 0 {
112 return Err(MidiError::ZeroTempo);
113 }
114 if segments
115 .last()
116 .is_some_and(|segment| segment.start_tick == tick)
117 {
118 let last = segments
119 .last_mut()
120 .expect("the default tempo segment is always present");
121 *last = TempoSegment::new(tick, *us_per_quarter).map_err(chart_error)?;
122 } else {
123 segments.push(TempoSegment::new(tick, *us_per_quarter).map_err(chart_error)?);
124 }
125 }
126 let clock_map = ClockTempoMap::new(segments).map_err(chart_error)?;
127 Ok(Self { tpq, clock_map })
128 }
129
130 pub const fn tpq(&self) -> u32 {
132 self.tpq
133 }
134
135 pub fn segments(&self) -> &[TempoSegment] {
137 self.clock_map.segments()
138 }
139
140 pub fn beat_for_tick(&self, tick: TickTime) -> Result<MidiBeat, MidiError> {
142 let tick = tick.rebase(self.tpq)?;
143 MidiBeat::new(i128::from(tick.ticks), i128::from(self.tpq))
144 }
145
146 pub fn tick_for_beat(&self, beat: MidiBeat) -> Result<TickTime, MidiError> {
151 let scaled = beat
152 .numerator
153 .checked_mul(i128::from(self.tpq))
154 .ok_or(MidiError::TempoOverflow)?;
155 if scaled % beat.denominator != 0 {
156 return Err(MidiError::InexactTempoTick);
157 }
158 let ticks =
159 i64::try_from(scaled / beat.denominator).map_err(|_| MidiError::TempoOverflow)?;
160 TickTime::new(ticks, self.tpq)
161 }
162
163 pub fn wall_time_for_tick(&self, tick: TickTime) -> Result<Instant, MidiError> {
165 let tick = tick.rebase(self.tpq)?;
166 let index = u64::try_from(tick.ticks).map_err(|_| MidiError::NegativeTempoTick)?;
167 self.clock()?
168 .instant_for_index(ClockIndex::new(index))
169 .map_err(chart_error)
170 }
171
172 pub fn tick_for_wall_time(&self, wall_time: Instant) -> Result<TickTime, MidiError> {
177 let conversion = self
178 .clock()?
179 .index_for_instant(wall_time)
180 .map_err(chart_error)?;
181 if !conversion.is_exact() {
182 return Err(MidiError::InexactTempoTick);
183 }
184 let ticks =
185 i64::try_from(conversion.index().value()).map_err(|_| MidiError::TempoOverflow)?;
186 TickTime::new(ticks, self.tpq)
187 }
188
189 fn clock(&self) -> Result<Clock, MidiError> {
190 Clock::midi(
191 Symbol::qualified("midi/tempo", "timeline"),
192 self.tpq,
193 self.clock_map.clone(),
194 )
195 .map_err(chart_error)
196 }
197}
198
199fn chart_error(error: sim_kernel::Error) -> MidiError {
200 MidiError::TempoChart(error.to_string())
201}
202
203fn narrow_i128(value: i128) -> Result<i64, MidiError> {
204 i64::try_from(value).map_err(|_| MidiError::TempoOverflow)
205}
206
207fn gcd(mut left: i128, mut right: i128) -> i128 {
208 left = left.abs();
209 right = right.abs();
210 while right != 0 {
211 let remainder = left % right;
212 left = right;
213 right = remainder;
214 }
215 left.max(1)
216}