Skip to main content

sim_lib_midi_smf/
model.rs

1#![forbid(unsafe_code)]
2
3use std::{
4    cmp::Ordering,
5    num::{NonZeroU8, NonZeroU16},
6};
7
8use sim_lib_midi_core::{
9    ChannelMessage, MetaEvent, MidiError, MidiEvent, MidiPayload, MidiTempoMap, TickTime,
10    TrackedMidiEvent, synthetic_origin,
11};
12
13use crate::SmfError;
14
15/// The SMF header format field: how the file's tracks relate.
16#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
17pub enum SmfFormat {
18    /// Format 0: a single multi-channel track.
19    SingleTrack,
20    /// Format 1: several tracks played simultaneously.
21    Simultaneous,
22    /// Format 2: several independent single-track patterns.
23    Independent,
24}
25
26impl SmfFormat {
27    /// Returns the relationship between track-local times in this format.
28    pub const fn time_semantics(self) -> SmfTimeSemantics {
29        match self {
30            Self::SingleTrack | Self::Simultaneous => SmfTimeSemantics::SharedTimeline,
31            Self::Independent => SmfTimeSemantics::IndependentPatterns,
32        }
33    }
34}
35
36/// How track-local event times relate in an SMF format.
37#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
38pub enum SmfTimeSemantics {
39    /// All tracks share one time origin and may be merged chronologically.
40    SharedTimeline,
41    /// Every track is a separate pattern whose time starts at that track's
42    /// origin; tracks must not be merged as though they played together.
43    IndependentPatterns,
44}
45
46/// A valid SMPTE frame rate encoded by the signed high byte of an SMF
47/// division.
48#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
49pub enum SmpteRate {
50    /// 24 frames per second (`-24`).
51    Fps24,
52    /// 25 frames per second (`-25`).
53    Fps25,
54    /// 29.97 drop-frame timecode (`-29` in the SMF header).
55    Fps29Drop,
56    /// 30 frames per second (`-30`).
57    Fps30,
58}
59
60impl SmpteRate {
61    pub(crate) const fn from_header_byte(value: u8) -> Option<Self> {
62        match value as i8 {
63            -24 => Some(Self::Fps24),
64            -25 => Some(Self::Fps25),
65            -29 => Some(Self::Fps29Drop),
66            -30 => Some(Self::Fps30),
67            _ => None,
68        }
69    }
70
71    pub(crate) const fn header_byte(self) -> u8 {
72        match self {
73            Self::Fps24 => (-24_i8) as u8,
74            Self::Fps25 => (-25_i8) as u8,
75            Self::Fps29Drop => (-29_i8) as u8,
76            Self::Fps30 => (-30_i8) as u8,
77        }
78    }
79
80    /// Returns the exact frame-rate ratio as frames per second.
81    pub const fn frames_per_second_ratio(self) -> (u32, u32) {
82        match self {
83            Self::Fps24 => (24, 1),
84            Self::Fps25 => (25, 1),
85            Self::Fps29Drop => (30_000, 1_001),
86            Self::Fps30 => (30, 1),
87        }
88    }
89
90    const fn nominal_frames_per_second(self) -> u32 {
91        match self {
92            Self::Fps24 => 24,
93            Self::Fps25 => 25,
94            Self::Fps29Drop | Self::Fps30 => 30,
95        }
96    }
97}
98
99/// The lossless time-division field from an SMF header.
100#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
101pub enum SmfDivision {
102    /// Metrical timing in ticks per quarter note.
103    Metrical {
104        /// Non-zero ticks per quarter note.
105        ticks_per_quarter: NonZeroU16,
106    },
107    /// Timecode timing in subdivisions of an SMPTE frame.
108    Smpte {
109        /// One of the four frame rates permitted by SMF.
110        frames_per_second: SmpteRate,
111        /// Non-zero subdivisions per frame.
112        ticks_per_frame: NonZeroU8,
113    },
114}
115
116impl SmfDivision {
117    /// Constructs a metrical division, returning `None` for zero or a value
118    /// with the SMPTE high bit set.
119    pub const fn metrical(ticks_per_quarter: u16) -> Option<Self> {
120        if ticks_per_quarter >= 0x8000 {
121            return None;
122        }
123        match NonZeroU16::new(ticks_per_quarter) {
124            Some(ticks_per_quarter) => Some(Self::Metrical { ticks_per_quarter }),
125            None => None,
126        }
127    }
128
129    /// Constructs an SMPTE division, returning `None` for zero ticks per frame.
130    pub const fn smpte(frames_per_second: SmpteRate, ticks_per_frame: u8) -> Option<Self> {
131        match NonZeroU8::new(ticks_per_frame) {
132            Some(ticks_per_frame) => Some(Self::Smpte {
133                frames_per_second,
134                ticks_per_frame,
135            }),
136            None => None,
137        }
138    }
139
140    /// Returns metrical ticks per quarter note, or `None` for SMPTE timing.
141    pub const fn ticks_per_quarter(self) -> Option<NonZeroU16> {
142        match self {
143            Self::Metrical { ticks_per_quarter } => Some(ticks_per_quarter),
144            Self::Smpte { .. } => None,
145        }
146    }
147
148    /// Returns the exact number of SMF ticks per second as a ratio, or `None`
149    /// for metrical timing whose tempo is carried by events.
150    pub const fn ticks_per_second_ratio(self) -> Option<(u32, u32)> {
151        match self {
152            Self::Metrical { .. } => None,
153            Self::Smpte {
154                frames_per_second,
155                ticks_per_frame,
156            } => {
157                let (numerator, denominator) = frames_per_second.frames_per_second_ratio();
158                Some((numerator * ticks_per_frame.get() as u32, denominator))
159            }
160        }
161    }
162
163    /// Returns the resolution used in each event's [`TickTime`].
164    ///
165    /// For metrical files this is ticks per quarter note. For SMPTE files it
166    /// is nominal ticks per second (30 fps for the `-29` drop-frame code);
167    /// [`ticks_per_second_ratio`](Self::ticks_per_second_ratio) retains the
168    /// exact `30_000 / 1_001` rate for duration calculations.
169    pub const fn event_time_base(self) -> u32 {
170        match self {
171            Self::Metrical { ticks_per_quarter } => ticks_per_quarter.get() as u32,
172            Self::Smpte {
173                frames_per_second,
174                ticks_per_frame,
175            } => frames_per_second.nominal_frames_per_second() * ticks_per_frame.get() as u32,
176        }
177    }
178
179    pub(crate) const fn header_word(self) -> u16 {
180        match self {
181            Self::Metrical { ticks_per_quarter } => ticks_per_quarter.get(),
182            Self::Smpte {
183                frames_per_second,
184                ticks_per_frame,
185            } => u16::from_be_bytes([frames_per_second.header_byte(), ticks_per_frame.get()]),
186        }
187    }
188}
189
190/// One track: an ordered list of timestamped events.
191#[derive(Clone, Debug, PartialEq, Eq)]
192pub struct SmfTrack {
193    /// Events in this track, in absolute time order after canonicalisation.
194    pub events: Vec<MidiEvent>,
195}
196
197/// A parsed Standard MIDI File: its format, time division, and tracks.
198#[derive(Clone, Debug, PartialEq, Eq)]
199pub struct SmfFile {
200    /// Header format field.
201    pub format: SmfFormat,
202    /// Lossless metrical or SMPTE time division.
203    pub division: SmfDivision,
204    /// Tracks in file order.
205    pub tracks: Vec<SmfTrack>,
206}
207
208/// Tempo-map topology implied by an SMF file's format.
209///
210/// Formats 0 and 1 have one shared performance timeline. Format 2 retains one
211/// independent tempo map per track so callers cannot accidentally flatten
212/// unrelated patterns.
213#[derive(Clone, Debug, PartialEq, Eq)]
214pub enum SmfTempoMaps {
215    /// One map shared by every simultaneous track.
216    Shared(MidiTempoMap),
217    /// One track-local map per independent format-2 pattern, in track order.
218    Independent(Vec<MidiTempoMap>),
219}
220
221/// Options controlling SMF serialisation.
222#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
223pub struct SmfWriteOptions {
224    /// When set, omit a redundant status byte for consecutive channel messages
225    /// (MIDI running status).
226    pub running_status: bool,
227}
228
229/// An iterator that merges all tracks of an [`SmfFile`] into a single
230/// time-ordered, track-tagged stream.
231///
232/// Created by [`SmfFile::merge_cursor`]; yields
233/// [`TrackedMidiEvent`](sim_lib_midi_core::TrackedMidiEvent)s.
234pub struct SmfMergeCursor<'a> {
235    file: &'a SmfFile,
236    next_index: Vec<usize>,
237}
238
239impl SmfFile {
240    /// Returns this file's metrical ticks per quarter note, or `None` when its
241    /// division is SMPTE.
242    pub const fn ticks_per_quarter(&self) -> Option<u32> {
243        match self.division.ticks_per_quarter() {
244            Some(value) => Some(value.get() as u32),
245            None => None,
246        }
247    }
248
249    /// Builds exact tempo maps from the file's ordered tempo meta events.
250    ///
251    /// Metrical formats 0 and 1 produce one shared map. Format 2 produces one
252    /// map per independent track. SMPTE divisions have a direct timecode chart
253    /// rather than a tempo map and return [`MidiError::MetricalTempoRequired`].
254    pub fn tempo_maps(&self) -> Result<SmfTempoMaps, MidiError> {
255        let tpq = self
256            .ticks_per_quarter()
257            .ok_or(MidiError::MetricalTempoRequired)?;
258        if self.format == SmfFormat::Independent {
259            let maps = self
260                .tracks
261                .iter()
262                .map(|track| MidiTempoMap::from_ordered_events(tpq, &track.events))
263                .collect::<Result<Vec<_>, _>>()?;
264            return Ok(SmfTempoMaps::Independent(maps));
265        }
266
267        let mut events = self
268            .tracks
269            .iter()
270            .enumerate()
271            .flat_map(|(track, contents)| {
272                contents
273                    .events
274                    .iter()
275                    .enumerate()
276                    .map(move |(index, event)| (track, index, event))
277            })
278            .collect::<Vec<_>>();
279        events.sort_by(|left, right| {
280            compare_event_order(left.2, left.0, right.2, right.0).then_with(|| left.1.cmp(&right.1))
281        });
282        Ok(SmfTempoMaps::Shared(MidiTempoMap::from_ordered_events(
283            tpq,
284            events.into_iter().map(|(_, _, event)| event),
285        )?))
286    }
287
288    /// Sorts every track into canonical order and ensures each ends with an
289    /// end-of-track meta event.
290    pub fn canonicalize(&mut self) {
291        for track in &mut self.tracks {
292            canonicalize_track(track, self.division.event_time_base());
293        }
294    }
295
296    /// Returns a cursor that merges tracks sharing one timeline.
297    ///
298    /// Format-2 tracks are independent patterns, so merging them would invent
299    /// a relationship between their local time origins and is rejected.
300    pub fn merge_cursor(&self) -> Result<SmfMergeCursor<'_>, SmfError> {
301        if self.format.time_semantics() == SmfTimeSemantics::IndependentPatterns {
302            return Err(SmfError::IndependentPatternsCannotMerge);
303        }
304        Ok(SmfMergeCursor {
305            file: self,
306            next_index: vec![0; self.tracks.len()],
307        })
308    }
309
310    /// Collects every track's events into a single time-ordered,
311    /// track-tagged vector.
312    ///
313    /// Format-2 files return [`SmfError::IndependentPatternsCannotMerge`];
314    /// callers must choose and process a track as an independent pattern.
315    pub fn merged_events(&self) -> Result<Vec<TrackedMidiEvent>, SmfError> {
316        let mut merged = Vec::new();
317        for event in self.merge_cursor()? {
318            merged.push(event);
319        }
320        Ok(merged)
321    }
322}
323
324impl<'a> Iterator for SmfMergeCursor<'a> {
325    type Item = TrackedMidiEvent;
326
327    fn next(&mut self) -> Option<Self::Item> {
328        let mut best: Option<(usize, &MidiEvent)> = None;
329        for (track_idx, track) in self.file.tracks.iter().enumerate() {
330            let Some(event) = track.events.get(self.next_index[track_idx]) else {
331                continue;
332            };
333            match best {
334                None => best = Some((track_idx, event)),
335                Some((best_track, best_event)) => {
336                    if compare_event_order(event, track_idx, best_event, best_track)
337                        == Ordering::Less
338                    {
339                        best = Some((track_idx, event));
340                    }
341                }
342            }
343        }
344        let (track_idx, event) = best?;
345        self.next_index[track_idx] += 1;
346        Some(TrackedMidiEvent {
347            last_track: track_idx,
348            event: event.clone(),
349        })
350    }
351}
352
353pub(crate) fn canonicalize_track(track: &mut SmfTrack, tpq: u32) {
354    track.events.sort_by(compare_events_same_track);
355    if !track
356        .events
357        .iter()
358        .any(|event| matches!(event.payload, MidiPayload::Meta(MetaEvent::EndOfTrack)))
359    {
360        let last_ticks = track
361            .events
362            .last()
363            .map(|event| event.time.ticks)
364            .unwrap_or(0);
365        track.events.push(MidiEvent {
366            time: TickTime::new(last_ticks, tpq).unwrap_or(TickTime::ZERO),
367            origin: synthetic_origin(),
368            payload: MidiPayload::Meta(MetaEvent::EndOfTrack),
369        });
370    }
371    track.events.sort_by(compare_events_same_track);
372}
373
374pub(crate) fn compare_event_order(
375    left: &MidiEvent,
376    left_track: usize,
377    right: &MidiEvent,
378    right_track: usize,
379) -> Ordering {
380    compare_time(left.time, right.time)
381        .then_with(|| event_priority(left).cmp(&event_priority(right)))
382        .then_with(|| left_track.cmp(&right_track))
383}
384
385fn compare_events_same_track(left: &MidiEvent, right: &MidiEvent) -> Ordering {
386    compare_time(left.time, right.time)
387        .then_with(|| event_priority(left).cmp(&event_priority(right)))
388}
389
390fn compare_time(left: TickTime, right: TickTime) -> Ordering {
391    let left_scaled = i128::from(left.ticks) * i128::from(right.tpq);
392    let right_scaled = i128::from(right.ticks) * i128::from(left.tpq);
393    left_scaled.cmp(&right_scaled)
394}
395
396fn event_priority(event: &MidiEvent) -> u8 {
397    match event.payload {
398        MidiPayload::Meta(MetaEvent::EndOfTrack) => 4,
399        MidiPayload::Meta(_) => 0,
400        MidiPayload::Channel(ChannelMessage::NoteOff { .. }) => 1,
401        MidiPayload::Channel(ChannelMessage::NoteOn { .. }) => 2,
402        _ => 3,
403    }
404}