midi-reader-writer 0.1.3

Facilitate reading and writing midi files.
Documentation
//     Facilitate reading and writing midi files.
//
//     Copyright (C) 2021 Pieter Penninckx
//
//     `midi-reader-writer` is licensed under the Apache License, Version 2.0
//     or the MIT license, at your option.
//
//     For the application of the MIT license, the examples included in the doc comments are not
//     considered "substantial portions of this Software".
//
//     License texts can be found:
//     * for the Apache License, Version 2.0: <LICENSE-APACHE.txt> or
//         <http://www.apache.org/licenses/LICENSE-2.0>
//     * for the MIT license: <LICENSE-MIT.txt> or
//         <http://opensource.org/licenses/MIT>.
//
//
//! Facilitate reading and writing midi files. This library does not serialise or deserialise
//! midi files, but uses another library for that. Currently supported is using the `midly` library,
//! behind the `engine-midly-0-5` feature.
//!
//! In particular this create supports,
//! * creating an iterator over all the tracks, merged;
//! * given an iterator, separating the tracks, and
//! * converting time stamps from ticks to microseconds and vice versa.
//!
//! # Example
//! The following example illustrates the steps that could typically be used in an application
//! that transforms midi data.
//!
//! _Note_: this example requires the `convert-time` feature, the `engine-midly-0-5` feature, and the `read` feature.
#![cfg_attr(
    all(
        feature = "convert-time",
        feature = "engine-midly-0-5",
        feature = "read"
    ),
    doc = "\
```
"
)]
#![cfg_attr(
    not(all(
        feature = "convert-time",
        feature = "engine-midly-0-5",
        feature = "read"
    )),
    doc = "\
```ignore
"
)]
//! use midi_reader_writer::{
//!     ConvertTicksToMicroseconds, ConvertMicroSecondsToTicks,
//!     midly_0_5::{exports::Smf, merge_tracks, TrackSeparator},
//! };
//! use std::{fs, error::Error, convert::TryFrom};
//!
//! fn example(input_filename: &str, output_filename: &str) -> Result<(), Box<dyn Error>> {
//!     // Read the midi file
//!     let bytes = fs::read(input_filename)?;
//!     let input_midi_file = Smf::parse(&bytes)?;
//!
//!
//!     let mut ticks_to_microseconds = ConvertTicksToMicroseconds::try_from(input_midi_file.header)?;
//!     let mut microseconds_to_ticks = ConvertMicroSecondsToTicks::from(input_midi_file.header);
//!     let mut separator = TrackSeparator::new();
//!
//!     // Iterate over the events from all tracks:
//!     for (ticks, track_index, event) in merge_tracks(&input_midi_file.tracks) {
//!
//!         // Convert the ticks to microseconds:
//!         let microseconds = ticks_to_microseconds.convert(ticks, &event);
//!
//!         // Do something with the event or with the timing, or both, or ...
//!         // ... <- Insert your code here
//!
//!         // Convert from microseconds to ticks:
//!         let new_ticks = microseconds_to_ticks.convert(microseconds, &event)?;
//!
//!         // Push the event to the appropriate track.
//!         separator.push(new_ticks, track_index, event)?;
//!     }
//!
//!     // Save the output:
//!     let tracks = separator.collect();
//!     let output_midi_file = Smf {
//!         header: input_midi_file.header,
//!         tracks,
//!     };
//!     output_midi_file.save(output_filename)?;
//!     Ok(())
//! }
//! ```

#[cfg(feature = "engine-midly-0-5")]
pub mod midly_0_5;

#[cfg(feature = "convert-time")]
use std::num::NonZeroU64;
#[cfg(feature = "convert-time")]
use std::{
    error::Error,
    fmt::{Display, Formatter},
};
#[cfg(feature = "convert-time")]
use timestamp_stretcher::TimestampStretcher;

/// Error type for failed time conversions.
#[derive(Debug)]
#[non_exhaustive]
#[cfg(feature = "convert-time")]
pub enum TimeConversionError {
    /// The header indicates that there are zero ticks per beat.
    ZeroTicksPerBeatNotSupported,
    /// The header indicates that there are zero ticks per frame.
    ZeroTicksPerFrameNotSupported,
    /// An event indicates that the tempo is zero microseconds per beat.
    ZeroTempoNotSupported,
}

#[cfg(feature = "convert-time")]
impl Display for TimeConversionError {
    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
        use TimeConversionError::*;
        match self {
            ZeroTicksPerBeatNotSupported => {
                write!(f, "zero ticks per beat is not supported")
            }
            ZeroTicksPerFrameNotSupported => {
                write!(f, "zero ticks per frame is not supported")
            }
            ZeroTempoNotSupported => {
                write!(f, "a tempo of zero is not supported")
            }
        }
    }
}

#[cfg(feature = "convert-time")]
impl Error for TimeConversionError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        None
    }
}

/// Convert timings of midi events from ticks to microseconds.
#[cfg(feature = "convert-time")]
pub struct ConvertTicksToMicroseconds {
    #[allow(dead_code)]
    time_stretcher: TimestampStretcher,
    #[allow(dead_code)]
    ticks_per_beat: Option<NonZeroU64>,
}

/// Implement this trait to use [`ConvertTicksToMicroseconds`] and [`ConvertMicroSecondsToTicks`]
/// for this type of midi event.
#[cfg(feature = "convert-time")]
pub trait MidiEvent {
    /// If `&self` is a meta-event indicating a tempo change, return `Some(t)`, where `t` is
    /// the number of microseconds per beat.
    /// Return `None` otherwise.
    fn tempo(&self) -> Option<u32>;
}

#[cfg(feature = "convert-time")]
impl ConvertTicksToMicroseconds {
    /// Return the time of the event, in microseconds, relative to the beginning of the track.
    ///
    /// # Parameters
    /// `ticks`: the time, in ticks, relative to the beginning of the track.
    /// It is assumed that this only increases with subsequent calls to this method.
    ///
    /// `event`: the event.
    ///
    /// # Return value
    /// The  time, in microseconds, relative to the beginning of the track.
    pub fn convert<T>(&mut self, ticks: u64, event: &T) -> u64
    where
        T: MidiEvent,
    {
        let new_factor = if let Some(ticks_per_beat) = self.ticks_per_beat {
            if let Some(tempo) = event.tempo() {
                Some((tempo as u64, ticks_per_beat))
            } else {
                None
            }
        } else {
            None
        };
        self.time_stretcher.stretch(ticks, new_factor)
    }
}

/// Convert timings of midi events from microseconds to ticks.
#[cfg(feature = "convert-time")]
pub struct ConvertMicroSecondsToTicks {
    #[allow(dead_code)]
    time_stretcher: TimestampStretcher,
    #[allow(dead_code)]
    ticks_per_beat: Option<u64>,
}

#[cfg(feature = "convert-time")]
impl ConvertMicroSecondsToTicks {
    /// Return the time of the midi event, in ticks, relative to the beginning of the track.
    ///
    /// # Parameters
    /// `microseconds`: the time, in ticks, relative to the beginning of the track.
    /// It is assumed that this only increases with subsequent calls to this method.
    ///
    /// `event`: the event.
    ///
    /// # Return value
    /// The method returns `Err` if the event does not indicate a tempo of zero microseconds per beat.
    /// Otherwise, it returns `Ok(time_in_ticks)`, where `time_in_ticks` is the time, in ticks,
    /// relative to the beginning of the track.
    pub fn convert<T>(&mut self, microseconds: u64, event: &T) -> Result<u64, TimeConversionError>
    where
        T: MidiEvent,
    {
        let new_factor = if let Some(ticks_per_beat) = self.ticks_per_beat {
            if let Some(tempo) = event.tempo() {
                Some((
                    ticks_per_beat,
                    NonZeroU64::new(tempo as u64)
                        .ok_or(TimeConversionError::ZeroTempoNotSupported)?,
                ))
            } else {
                None
            }
        } else {
            None
        };
        Ok(self.time_stretcher.stretch(microseconds, new_factor))
    }
}