miami/lib.rs
1//! # miami
2//!
3//! A minimal dependency MIDI file parser designed for both standard and WASM targets,
4//! This crate provides core MIDI "chunks" and utilities for reading and parsing them,
5//! without introducing any extra overhead or dependencies.
6//!
7//! ## Overview
8//!
9//! MIDI files are structured as a series of chunks. Each chunk contains a 4-character ASCII
10//! type identifier and a 32-bit length that specifies how many bytes of data follow. The
11//! `Chunk` struct and related APIs in this crate make it straightforward to inspect and
12//! parse these sections of a MIDI file.
13//!
14//! - **Minimal dependencies**: Keeps your application lightweight and minimizes build complexity.
15//! Opt in to serde support.
16//! - **Streaming-friendly**: Exposes traits and functions that can parse MIDI data from any
17//! implementor of [`reader::MidiStream`], making it easier to handle data on the fly.
18//!
19//! ## Example Usage
20//!
21//! ```rust
22//! use miami::{reader::MidiReadable, Midi, RawMidi};
23//!
24//! // Load MIDI bytes (replace with your own source as needed).
25//! let mut data = "test/test.mid"
26//! .get_midi_bytes()
27//! .expect("Get `test.mid` file and read bytes");
28//!
29//! let midi: Midi = RawMidi::try_from_midi_stream(data)
30//! .expect("Parse data as a MIDI stream")
31//! .check_into_midi()
32//! .expect("Sanitize MIDI into formatted MIDI");
33//!
34//! println!("Header: {:?}", midi.header);
35//! for chunk in midi.tracks.iter() {
36//! println!("Track: {:?}", chunk);
37//! }
38//! ```
39//!
40//!
41//! The above example illustrates how to read chunks from a MIDI stream and use
42//! [`ParsedChunk::try_from`] to parse them into known types (header or track chunks).
43//!
44//! ## Library Structure
45//!
46//! - **[`chunk`]**: Contains the [`Chunk`] struct and associated utilities for identifying
47//! chunk types and lengths.
48//! - **[`reader`]**: Provides traits and types for streaming MIDI data. The [`MidiStream`]
49//! trait and related helpers allow on-the-fly parsing from any data source.
50//! - **`chunk_types`, `header`, and `track`**: Provide definitions for recognized MIDI
51//! chunk types (e.g., `MThd` for the header and `MTrk` for track data) and the logic for
52//! parsing their contents.
53//!
54//! ## Extensibility
55//!
56//! While this crate focuses on parsing the structural aspects of MIDI files (chunks and headers),
57//! you can use the raw track data to implement custom handling of MIDI events or other logic
58//! as needed. Because `miami` exposes chunks in a straightforward format, you remain in full
59//! control of the MIDI event parsing layer.
60//!
61
62pub mod chunk;
63pub mod reader;
64pub mod writer;
65
66use chunk::{header::HeaderChunk, track::TrackChunk, ChunkParseError, ParsedChunk};
67use reader::MidiStream;
68#[cfg(feature = "serde")]
69use serde::{Deserialize, Serialize};
70use writer::MidiWriteable;
71
72/// An entire MIDI file as a raw sequence of parsed chunks
73#[derive(Debug, Clone, PartialEq)]
74#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
75pub struct RawMidi {
76 /// All raw chunks as ParsedChunks
77 pub chunks: Vec<ParsedChunk>,
78}
79
80impl RawMidi {
81 /// Constructs a new MIDI instance from a stream of MIDI bytes
82 pub fn try_from_midi_stream<STREAM>(stream: STREAM) -> Result<Self, ChunkParseError>
83 where
84 STREAM: MidiStream,
85 {
86 Self::try_from(StreamWrapper(stream))
87 }
88
89 /// Attempts to upgrade a `RawMidi` stream into a sanitized `Midi` struct. This means there
90 /// must be a single starting header and only track chunks afterwards
91 pub fn check_into_midi(self) -> Result<Midi, MidiSanitizerError> {
92 self.try_into()
93 }
94}
95
96impl MidiWriteable for RawMidi {
97 fn to_midi_bytes(self) -> Vec<u8> {
98 let mut res = vec![];
99 for chunk in self.chunks {
100 res.extend(chunk.to_midi_bytes());
101 }
102
103 res
104 }
105}
106
107/// A MIDI File "cleaned" by enforcing a single header chunk and an arbitrary amount of Track
108/// chunks
109#[derive(Debug, Clone, PartialEq)]
110#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
111pub struct Midi {
112 /// The header chunk
113 pub header: HeaderChunk,
114 /// All subsequent track chunks
115 pub tracks: Vec<TrackChunk>,
116}
117
118impl MidiWriteable for Midi {
119 fn to_midi_bytes(self) -> Vec<u8> {
120 let mut res = vec![];
121 res.extend(ParsedChunk::Header(self.header).to_midi_bytes());
122 for track in self.tracks {
123 let wrapped = ParsedChunk::Track(track);
124 res.extend(wrapped.to_midi_bytes());
125 }
126
127 res
128 }
129}
130
131/// An error that may occur when verifying that a Raw Midi struct is sanitized into a clean MIDI
132/// format
133#[derive(Debug, Clone, Copy, PartialEq)]
134pub enum MidiSanitizerError {
135 /// Sequence doesn't start with a header
136 NoStartHeader,
137 /// Too many headers
138 TooManyHeaders,
139 /// No chunks at all
140 NoChunks,
141}
142impl core::error::Error for MidiSanitizerError {}
143impl core::fmt::Display for MidiSanitizerError {
144 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
145 match self {
146 Self::NoStartHeader => write![f, "First ParsedChunk in sequence isn't a header"],
147 Self::TooManyHeaders => write![f, "More than one header chunk identified"],
148 Self::NoChunks => write![f, "No chunks present"],
149 }
150 }
151}
152
153impl TryFrom<RawMidi> for Midi {
154 type Error = MidiSanitizerError;
155 fn try_from(value: RawMidi) -> Result<Self, Self::Error> {
156 let mut chunks = value.chunks.into_iter();
157 let first = chunks.next().ok_or(MidiSanitizerError::NoChunks)?;
158 let header = match first {
159 ParsedChunk::Header(header) => header,
160 _ => return Err(MidiSanitizerError::NoStartHeader),
161 };
162 let mut tracks = vec![];
163
164 for track in chunks {
165 match track {
166 ParsedChunk::Track(track) => tracks.push(track),
167 _ => return Err(MidiSanitizerError::TooManyHeaders),
168 }
169 }
170
171 Ok(Self { header, tracks })
172 }
173}
174
175/// A wrapper to allow TryFrom implementations for `MidiStream` implementors
176pub struct StreamWrapper<STREAM>(STREAM)
177where
178 STREAM: MidiStream;
179impl<STREAM> TryFrom<StreamWrapper<STREAM>> for RawMidi
180where
181 STREAM: MidiStream,
182{
183 type Error = ChunkParseError;
184 fn try_from(value: StreamWrapper<STREAM>) -> Result<Self, Self::Error> {
185 let mut data = value.0;
186 let mut chunks = vec![];
187
188 while let Some(parsed) = data.read_chunk_data_pair().map(ParsedChunk::try_from) {
189 let parsed = parsed?;
190 chunks.push(parsed);
191 }
192
193 Ok(Self { chunks })
194 }
195}
196
197/// Represents a raw MIDI Chunk.
198/// A MIDI Chunk consists of a 4-character ASCII type identifier and a 32-bit unsigned integer specifying the length of its data.
199#[derive(Debug, Clone, Copy, PartialEq)]
200#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
201pub struct Chunk {
202 /// 4 character ASCII chunk type
203 pub chunk_type: [char; 4],
204 /// Length of the data that follows
205 length: u32,
206}
207
208impl Chunk {
209 /// Gets the length of the chunk as a usize
210 pub fn len(&self) -> usize {
211 self.length as usize
212 }
213
214 /// Returns if the chunk has no attributed data
215 pub fn is_empty(&self) -> bool {
216 self.length == 0
217 }
218}
219
220impl From<u64> for Chunk {
221 fn from(value: u64) -> Self {
222 let high = (value >> 32) as u32;
223 let low = value as u32;
224
225 let a = (high >> 24) as u8 as char;
226 let b = (high >> 16) as u8 as char;
227 let c = (high >> 8) as u8 as char;
228 let d = high as u8 as char;
229
230 Self {
231 chunk_type: [a, b, c, d],
232 length: low,
233 }
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use crate::Chunk;
240
241 #[test]
242 fn chunk_from_raw_u64_behaves_normally() {
243 let message = 0x74657374_0000000au64;
244 let expected = Chunk {
245 chunk_type: ['t', 'e', 's', 't'],
246 length: 10,
247 };
248
249 assert_eq!(expected, message.into())
250 }
251}