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}