use sim_kernel::Symbol;
use sim_lib_stream_clock::{Clock, ClockIndex, Instant, TempoMap as ClockTempoMap, TempoSegment};
use crate::{MetaEvent, MidiError, MidiEvent, MidiPayload, TickTime};
pub const DEFAULT_US_PER_QUARTER: u32 = 500_000;
pub fn bpm_to_us_per_quarter(bpm: f64) -> u32 {
(60_000_000.0 / bpm).round() as u32
}
pub fn us_per_quarter_to_bpm(us_per_quarter: u32) -> f64 {
60_000_000.0 / us_per_quarter as f64
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MidiBeat {
numerator: i128,
denominator: i128,
}
impl MidiBeat {
pub fn new(numerator: i128, denominator: i128) -> Result<Self, MidiError> {
if denominator == 0 {
return Err(MidiError::InvalidRatio(
narrow_i128(numerator)?,
narrow_i128(denominator)?,
));
}
let (numerator, denominator) = if denominator < 0 {
(
numerator.checked_neg().ok_or(MidiError::TempoOverflow)?,
denominator.checked_neg().ok_or(MidiError::TempoOverflow)?,
)
} else {
(numerator, denominator)
};
if numerator < 0 {
return Err(MidiError::NegativeTempoTick);
}
let divisor = gcd(numerator, denominator);
Ok(Self {
numerator: numerator / divisor,
denominator: denominator / divisor,
})
}
pub const fn numerator(self) -> i128 {
self.numerator
}
pub const fn denominator(self) -> i128 {
self.denominator
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MidiTempoMap {
tpq: u32,
clock_map: ClockTempoMap,
}
impl MidiTempoMap {
pub fn from_ordered_events<'a>(
tpq: u32,
events: impl IntoIterator<Item = &'a MidiEvent>,
) -> Result<Self, MidiError> {
if tpq == 0 {
return Err(MidiError::ZeroTpq);
}
let mut segments = vec![TempoSegment::new(0, DEFAULT_US_PER_QUARTER).map_err(chart_error)?];
let mut previous_tick = 0_u64;
for event in events {
let time = event.time.rebase(tpq)?;
let tick = u64::try_from(time.ticks).map_err(|_| MidiError::NegativeTempoTick)?;
if tick < previous_tick {
return Err(MidiError::TempoEventsOutOfOrder);
}
previous_tick = tick;
let MidiPayload::Meta(MetaEvent::Tempo { us_per_quarter }) = &event.payload else {
continue;
};
if *us_per_quarter == 0 {
return Err(MidiError::ZeroTempo);
}
if segments
.last()
.is_some_and(|segment| segment.start_tick == tick)
{
let last = segments
.last_mut()
.expect("the default tempo segment is always present");
*last = TempoSegment::new(tick, *us_per_quarter).map_err(chart_error)?;
} else {
segments.push(TempoSegment::new(tick, *us_per_quarter).map_err(chart_error)?);
}
}
let clock_map = ClockTempoMap::new(segments).map_err(chart_error)?;
Ok(Self { tpq, clock_map })
}
pub const fn tpq(&self) -> u32 {
self.tpq
}
pub fn segments(&self) -> &[TempoSegment] {
self.clock_map.segments()
}
pub fn beat_for_tick(&self, tick: TickTime) -> Result<MidiBeat, MidiError> {
let tick = tick.rebase(self.tpq)?;
MidiBeat::new(i128::from(tick.ticks), i128::from(self.tpq))
}
pub fn tick_for_beat(&self, beat: MidiBeat) -> Result<TickTime, MidiError> {
let scaled = beat
.numerator
.checked_mul(i128::from(self.tpq))
.ok_or(MidiError::TempoOverflow)?;
if scaled % beat.denominator != 0 {
return Err(MidiError::InexactTempoTick);
}
let ticks =
i64::try_from(scaled / beat.denominator).map_err(|_| MidiError::TempoOverflow)?;
TickTime::new(ticks, self.tpq)
}
pub fn wall_time_for_tick(&self, tick: TickTime) -> Result<Instant, MidiError> {
let tick = tick.rebase(self.tpq)?;
let index = u64::try_from(tick.ticks).map_err(|_| MidiError::NegativeTempoTick)?;
self.clock()?
.instant_for_index(ClockIndex::new(index))
.map_err(chart_error)
}
pub fn tick_for_wall_time(&self, wall_time: Instant) -> Result<TickTime, MidiError> {
let conversion = self
.clock()?
.index_for_instant(wall_time)
.map_err(chart_error)?;
if !conversion.is_exact() {
return Err(MidiError::InexactTempoTick);
}
let ticks =
i64::try_from(conversion.index().value()).map_err(|_| MidiError::TempoOverflow)?;
TickTime::new(ticks, self.tpq)
}
fn clock(&self) -> Result<Clock, MidiError> {
Clock::midi(
Symbol::qualified("midi/tempo", "timeline"),
self.tpq,
self.clock_map.clone(),
)
.map_err(chart_error)
}
}
fn chart_error(error: sim_kernel::Error) -> MidiError {
MidiError::TempoChart(error.to_string())
}
fn narrow_i128(value: i128) -> Result<i64, MidiError> {
i64::try_from(value).map_err(|_| MidiError::TempoOverflow)
}
fn gcd(mut left: i128, mut right: i128) -> i128 {
left = left.abs();
right = right.abs();
while right != 0 {
let remainder = left % right;
left = right;
right = remainder;
}
left.max(1)
}