Skip to main content

oxisound_smf/
lib.rs

1//! Standard MIDI File (SMF / `.mid`) parser and player.
2//!
3//! # Usage
4//!
5//! ```rust
6//! use oxisound_smf::{parse, SmfFormat, SmfEvent};
7//!
8//! // Minimal Format-0 SMF (1 track, 480 ticks/beat, one NoteOn)
9//! let bytes: Vec<u8> = vec![
10//!     0x4D, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06,
11//!     0x00, 0x00, 0x00, 0x01, 0x01, 0xE0,
12//!     0x4D, 0x54, 0x72, 0x6B, 0x00, 0x00, 0x00, 0x08,
13//!     0x00, 0x90, 0x3C, 0x40,
14//!     0x00, 0xFF, 0x2F, 0x00,
15//! ];
16//! let file = parse(&bytes).unwrap();
17//! assert_eq!(file.format, SmfFormat::SingleTrack);
18//! assert_eq!(file.tracks.len(), 1);
19//! ```
20
21#![forbid(unsafe_code)]
22#![cfg_attr(not(feature = "std"), no_std)]
23extern crate alloc;
24
25use alloc::{string::String, vec::Vec};
26
27mod parser;
28mod player;
29mod timing;
30mod writer;
31
32pub use parser::parse;
33pub use player::SmfPlayer;
34pub use timing::{TempoMap, ticks_to_seconds};
35pub use writer::write as write_smf;
36
37/// SMF file format variant (header word 0).
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub enum SmfFormat {
41    SingleTrack = 0,
42    MultiTrack = 1,
43    MultiSong = 2,
44}
45
46/// SMF time division — either ticks-per-beat or SMPTE frame-based.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49pub enum Division {
50    TicksPerBeat(u16),
51    Smpte { fps: u8, subframes: u8 },
52}
53
54/// A fully parsed SMF file.
55#[derive(Debug, Clone)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57pub struct SmfFile {
58    pub format: SmfFormat,
59    pub division: Division,
60    pub tracks: Vec<SmfTrack>,
61}
62
63/// One track extracted from an SMF file.
64#[derive(Debug, Clone)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66pub struct SmfTrack {
67    pub name: Option<String>,
68    pub events: Vec<TrackEvent>,
69}
70
71/// A single event within a track, with its delta-tick offset from the previous event.
72#[derive(Debug, Clone)]
73#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
74pub struct TrackEvent {
75    pub delta_ticks: u32,
76    pub event: SmfEvent,
77}
78
79/// All event kinds that can appear in an SMF track.
80#[derive(Debug, Clone)]
81#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
82pub enum SmfEvent {
83    /// A MIDI channel message. The channel nibble is encoded in `MidiMessage::status`.
84    Midi(oxisound_core::MidiMessage),
85    /// Tempo change in microseconds per quarter note.
86    Tempo(u32),
87    TimeSignature {
88        numerator: u8,
89        /// Log₂ of the denominator (e.g. 2 → quarter note = denominator 4).
90        denominator_pow2: u8,
91        clocks_per_click: u8,
92        notated_32nds_per_beat: u8,
93    },
94    KeySignature {
95        /// Negative = flats, positive = sharps.
96        sharps_flats: i8,
97        is_minor: bool,
98    },
99    TrackName(String),
100    EndOfTrack,
101    /// Unknown/unsupported meta event — preserved for round-trip fidelity.
102    UnknownMeta {
103        meta_type: u8,
104        data: Vec<u8>,
105    },
106}
107
108/// Parse error returned by [`parse`].
109#[derive(Debug, Clone, PartialEq, Eq)]
110#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
111pub struct SmfError(pub String);
112
113impl core::fmt::Display for SmfError {
114    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
115        write!(f, "SMF parse error: {}", self.0)
116    }
117}
118
119#[cfg(feature = "std")]
120impl std::error::Error for SmfError {}