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}