midi_reader_writer/
lib.rs

1//     Facilitate reading and writing midi files.
2//
3//     Copyright (C) 2021 Pieter Penninckx
4//
5//     `midi-reader-writer` is licensed under the Apache License, Version 2.0
6//     or the MIT license, at your option.
7//
8//     For the application of the MIT license, the examples included in the doc comments are not
9//     considered "substantial portions of this Software".
10//
11//     License texts can be found:
12//     * for the Apache License, Version 2.0: <LICENSE-APACHE.txt> or
13//         <http://www.apache.org/licenses/LICENSE-2.0>
14//     * for the MIT license: <LICENSE-MIT.txt> or
15//         <http://opensource.org/licenses/MIT>.
16//
17//
18//! Facilitate reading and writing midi files. This library does not serialise or deserialise
19//! midi files, but uses another library for that. Currently supported is using the `midly` library,
20//! behind the `engine-midly-0-5` feature.
21//!
22//! In particular this create supports,
23//! * creating an iterator over all the tracks, merged;
24//! * given an iterator, separating the tracks, and
25//! * converting time stamps from ticks to microseconds and vice versa.
26//!
27//! # Example
28//! The following example illustrates the steps that could typically be used in an application
29//! that transforms midi data.
30//!
31//! _Note_: this example requires the `convert-time` feature, the `engine-midly-0-5` feature, and the `read` feature.
32#![cfg_attr(
33    all(
34        feature = "convert-time",
35        feature = "engine-midly-0-5",
36        feature = "read"
37    ),
38    doc = "\
39```
40"
41)]
42#![cfg_attr(
43    not(all(
44        feature = "convert-time",
45        feature = "engine-midly-0-5",
46        feature = "read"
47    )),
48    doc = "\
49```ignore
50"
51)]
52//! use midi_reader_writer::{
53//!     ConvertTicksToMicroseconds, ConvertMicroSecondsToTicks,
54//!     midly_0_5::{exports::Smf, merge_tracks, TrackSeparator},
55//! };
56//! use std::{fs, error::Error, convert::TryFrom};
57//!
58//! fn example(input_filename: &str, output_filename: &str) -> Result<(), Box<dyn Error>> {
59//!     // Read the midi file
60//!     let bytes = fs::read(input_filename)?;
61//!     let input_midi_file = Smf::parse(&bytes)?;
62//!
63//!
64//!     let mut ticks_to_microseconds = ConvertTicksToMicroseconds::try_from(input_midi_file.header)?;
65//!     let mut microseconds_to_ticks = ConvertMicroSecondsToTicks::from(input_midi_file.header);
66//!     let mut separator = TrackSeparator::new();
67//!
68//!     // Iterate over the events from all tracks:
69//!     for (ticks, track_index, event) in merge_tracks(&input_midi_file.tracks) {
70//!
71//!         // Convert the ticks to microseconds:
72//!         let microseconds = ticks_to_microseconds.convert(ticks, &event);
73//!
74//!         // Do something with the event or with the timing, or both, or ...
75//!         // ... <- Insert your code here
76//!
77//!         // Convert from microseconds to ticks:
78//!         let new_ticks = microseconds_to_ticks.convert(microseconds, &event)?;
79//!
80//!         // Push the event to the appropriate track.
81//!         separator.push(ticks, track_index, event)?;
82//!     }
83//!
84//!     // Save the output:
85//!     let tracks = separator.collect();
86//!     let output_midi_file = Smf {
87//!         header: input_midi_file.header,
88//!         tracks,
89//!     };
90//!     output_midi_file.save(output_filename)?;
91//!     Ok(())
92//! }
93//! ```
94
95#[cfg(feature = "engine-midly-0-5")]
96pub mod midly_0_5;
97
98#[cfg(feature = "convert-time")]
99use std::num::NonZeroU64;
100#[cfg(feature = "convert-time")]
101use std::{
102    error::Error,
103    fmt::{Display, Formatter},
104};
105#[cfg(feature = "convert-time")]
106use timestamp_stretcher::TimestampStretcher;
107
108/// Error type for failed time conversions.
109#[derive(Debug)]
110#[non_exhaustive]
111#[cfg(feature = "convert-time")]
112pub enum TimeConversionError {
113    /// The header indicates that there are zero ticks per beat.
114    ZeroTicksPerBeatNotSupported,
115    /// The header indicates that there are zero ticks per frame.
116    ZeroTicksPerFrameNotSupported,
117    /// An event indicates that the tempo is zero microseconds per beat.
118    ZeroTempoNotSupported,
119}
120
121#[cfg(feature = "convert-time")]
122impl Display for TimeConversionError {
123    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
124        use TimeConversionError::*;
125        match self {
126            ZeroTicksPerBeatNotSupported => {
127                write!(f, "zero ticks per beat is not supported")
128            }
129            ZeroTicksPerFrameNotSupported => {
130                write!(f, "zero ticks per frame is not supported")
131            }
132            ZeroTempoNotSupported => {
133                write!(f, "a tempo of zero is not supported")
134            }
135        }
136    }
137}
138
139#[cfg(feature = "convert-time")]
140impl Error for TimeConversionError {
141    fn source(&self) -> Option<&(dyn Error + 'static)> {
142        None
143    }
144}
145
146/// Convert timings of midi events from ticks to microseconds.
147#[cfg(feature = "convert-time")]
148pub struct ConvertTicksToMicroseconds {
149    #[allow(dead_code)]
150    time_stretcher: TimestampStretcher,
151    #[allow(dead_code)]
152    ticks_per_beat: Option<NonZeroU64>,
153}
154
155/// Implement this trait to use [`ConvertTicksToMicroseconds`] and [`ConvertMicroSecondsToTicks`]
156/// for this type of midi event.
157#[cfg(feature = "convert-time")]
158pub trait MidiEvent {
159    /// If `&self` is a meta-event indicating a tempo change, return `Some(t)`, where `t` is
160    /// the number of microseconds per beat.
161    /// Return `None` otherwise.
162    fn tempo(&self) -> Option<u32>;
163}
164
165#[cfg(feature = "convert-time")]
166impl ConvertTicksToMicroseconds {
167    /// Return the time of the event, in microseconds, relative to the beginning of the track.
168    ///
169    /// # Parameters
170    /// `ticks`: the time, in ticks, relative to the beginning of the track.
171    /// It is assumed that this only increases with subsequent calls to this method.
172    ///
173    /// `event`: the event.
174    ///
175    /// # Return value
176    /// The  time, in microseconds, relative to the beginning of the track.
177    pub fn convert<T>(&mut self, ticks: u64, event: &T) -> u64
178    where
179        T: MidiEvent,
180    {
181        let new_factor = if let Some(ticks_per_beat) = self.ticks_per_beat {
182            if let Some(tempo) = event.tempo() {
183                Some((tempo as u64, ticks_per_beat))
184            } else {
185                None
186            }
187        } else {
188            None
189        };
190        self.time_stretcher.stretch(ticks, new_factor)
191    }
192}
193
194/// Convert timings of midi events from microseconds to ticks.
195#[cfg(feature = "convert-time")]
196pub struct ConvertMicroSecondsToTicks {
197    #[allow(dead_code)]
198    time_stretcher: TimestampStretcher,
199    #[allow(dead_code)]
200    ticks_per_beat: Option<u64>,
201}
202
203#[cfg(feature = "convert-time")]
204impl ConvertMicroSecondsToTicks {
205    /// Return the time of the midi event, in ticks, relative to the beginning of the track.
206    ///
207    /// # Parameters
208    /// `microseconds`: the time, in ticks, relative to the beginning of the track.
209    /// It is assumed that this only increases with subsequent calls to this method.
210    ///
211    /// `event`: the event.
212    ///
213    /// # Return value
214    /// The method returns `Err` if the event does not indicate a tempo of zero microseconds per beat.
215    /// Otherwise, it returns `Ok(time_in_ticks)`, where `time_in_ticks` is the time, in ticks,
216    /// relative to the beginning of the track.
217    pub fn convert<T>(&mut self, microseconds: u64, event: &T) -> Result<u64, TimeConversionError>
218    where
219        T: MidiEvent,
220    {
221        let new_factor = if let Some(ticks_per_beat) = self.ticks_per_beat {
222            if let Some(tempo) = event.tempo() {
223                Some((
224                    ticks_per_beat,
225                    NonZeroU64::new(tempo as u64)
226                        .ok_or(TimeConversionError::ZeroTempoNotSupported)?,
227                ))
228            } else {
229                None
230            }
231        } else {
232            None
233        };
234        Ok(self.time_stretcher.stretch(microseconds, new_factor))
235    }
236}