use midir::MidiOutputConnection;
use midly::{Smf, Timing};
use std::{collections::VecDeque, time::Duration};
use crate::{
all_sound_off,
events::{OwnedTrackEvent, OwnedTrackEventKind},
LyricaError,
};
enum MidiFileFormat {
Sequential { current: usize },
Parallel,
}
impl From<midly::Format> for MidiFileFormat {
fn from(midly_format: midly::Format) -> Self {
match midly_format {
midly::Format::SingleTrack | midly::Format::Sequential => {
Self::Sequential { current: 0 }
}
midly::Format::Parallel => Self::Parallel,
}
}
}
#[derive(Clone, Copy, Default)]
struct TrackProgress {
ticks_since_last_update: u32,
next_event: usize,
}
pub struct MidiFile {
ticks_per_beat: u16,
microseconds_per_tick: f64,
timer: f64,
loop_point: Option<f64>,
format: MidiFileFormat,
tracks: Vec<VecDeque<OwnedTrackEvent>>,
progress: Vec<TrackProgress>,
paused: bool,
}
impl MidiFile {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, LyricaError> {
let parsed_file = Smf::parse(bytes).map_err(|err| LyricaError::ParsingFailed(err))?;
let ticks_per_beat = match parsed_file.header.timing {
Timing::Metrical(ticks_per_beat) => ticks_per_beat.into(),
Timing::Timecode(_, _) => todo!("timecode timing is unimplemented"),
};
let tracks: Vec<VecDeque<OwnedTrackEvent>> = parsed_file
.tracks
.into_iter()
.map(|track| track.into_iter().map(OwnedTrackEvent::from).collect())
.collect();
let progress = vec![Default::default(); tracks.len()];
Ok(Self {
ticks_per_beat,
microseconds_per_tick: 0.0,
timer: 0.0,
loop_point: None,
format: parsed_file.header.format.into(),
tracks,
progress,
paused: false,
})
}
pub fn set_paused(
&mut self,
paused: bool,
connection: &mut MidiOutputConnection,
) -> Result<(), LyricaError> {
self.paused = paused;
if paused {
all_sound_off(connection)?;
}
Ok(())
}
pub fn set_loop_point(&mut self, loop_point: Option<f64>) {
self.loop_point = loop_point;
}
fn at_end_of_track(&self, track_id: usize) -> bool {
self.progress[track_id].next_event >= self.tracks[track_id].len()
}
fn at_end_of_file(&self) -> bool {
match self.format {
MidiFileFormat::Sequential { current } => self.tracks.len() <= current,
MidiFileFormat::Parallel => self
.tracks
.iter()
.zip(self.progress.iter())
.all(|(track, progress)| progress.next_event >= track.len()),
}
}
pub fn is_finished(&self) -> bool {
if self.loop_point.is_some() {
return false;
}
self.at_end_of_file()
}
pub fn seek_to(
&mut self,
seconds: f64,
connection: &mut MidiOutputConnection,
) -> Result<(), LyricaError> {
all_sound_off(connection)?;
let loop_point_in_ticks = (seconds * 1_000_000.0 / self.microseconds_per_tick) as u32;
for track_id in 0..self.tracks.len() {
let mut cumulative_delta = 0;
for (i, event) in self.tracks[track_id].iter().enumerate() {
if cumulative_delta + event.delta.as_int() > loop_point_in_ticks {
self.progress[track_id].next_event = i;
break;
}
cumulative_delta += event.delta.as_int();
}
self.progress[track_id].ticks_since_last_update =
loop_point_in_ticks.saturating_sub(cumulative_delta);
}
Ok(())
}
fn update_track(&mut self, track_id: usize, connection: &mut MidiOutputConnection) {
let track = &self.tracks[track_id];
let progress = &mut self.progress[track_id];
progress.ticks_since_last_update += 1;
while progress.next_event < track.len() {
let event = &track[progress.next_event];
if event.delta > progress.ticks_since_last_update {
break;
}
progress.ticks_since_last_update = 0;
progress.next_event += 1;
match &event.kind {
OwnedTrackEventKind::ToSynth(event_bytes) => {
connection.send(event_bytes).unwrap();
}
OwnedTrackEventKind::Tempo(tempo) => {
self.microseconds_per_tick =
u32::from(*tempo) as f64 / self.ticks_per_beat as f64;
}
OwnedTrackEventKind::InessentialMeta => {}
}
}
}
pub fn update(
&mut self,
delta_time: Duration,
connection: &mut MidiOutputConnection,
) -> Result<(), LyricaError> {
if self.paused || self.is_finished() {
return Ok(());
}
self.timer += delta_time.as_micros() as f64;
while self.timer > self.microseconds_per_tick {
match self.format {
MidiFileFormat::Sequential { current } => {
self.update_track(current, connection);
if self.at_end_of_track(current) {
self.format = MidiFileFormat::Sequential {
current: current + 1,
};
}
}
MidiFileFormat::Parallel => {
for track_id in 0..self.tracks.len() {
self.update_track(track_id, connection);
}
}
}
if self.at_end_of_file() {
if let Some(loop_point) = self.loop_point {
self.seek_to(loop_point, connection)?;
}
}
self.timer -= self.microseconds_per_tick;
}
Ok(())
}
}