lyrica 0.2.1

Phantasmically simple MIDI file handling
Documentation
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,
    // borrowed for life from `nodi`
    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"),
        };

        // This looks like this performs far too many allocations, but
        // in the optimal case, the parsing library would make most of
        // the allocations here. There are only `tracks.len()` + 1 extra allocations:
        // `tracks.len()` to collect into `VecDeque`s, and one to collect into
        // a `Vec`. If the parsing library also used `Vec<VecDeque<_>>`, this would
        // need no extra allocations.
        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(())
    }

    // TODO: is passing `None` here useful?
    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()
    }

    /// Like [`Self::is_finished`], but ignores the loop point.
    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()
    }

    /// Seek to the given time in seconds.
    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();
            }

            // `cumulative_delta` is the time needed to get to the event before
            // `self.progress[track_id].next_event` in ticks.
            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 {
                // Not ready to proceed yet
                break;
            }

            // update!
            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) {
                        // This track is finished; play the next track.
                        // If this is the last track, this will cause
                        // `current` to go out of the range of valid track
                        // indices. This will make `Self::is_finished`
                        // return `true`, skipping any future updates and
                        // avoiding "index out of bounds" panics.
                        // TODO: this will have to reset if looping is
                        // enabled
                        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(())
    }
}