Skip to main content

rustysynth_ext/
midifile.rs

1use std::io::Read;
2
3use crate::MidiFileError;
4use crate::MidiFileLoopType;
5use crate::binary_reader::BinaryReader;
6use crate::four_cc::FourCC;
7use crate::read_counter::ReadCounter;
8
9#[derive(Clone, Copy, Debug)]
10#[non_exhaustive]
11pub(crate) enum Message {
12    Normal { status: u8, data1: u8, data2: u8 },
13    TempoChange { bytes: [u8; 3] },
14    LoopStart,
15    LoopEnd,
16    EndOfTrack,
17}
18
19impl Message {
20    pub(crate) fn common1(status: u8, data1: u8) -> Self {
21        Self::Normal {
22            status,
23            data1,
24            data2: 0,
25        }
26    }
27
28    pub(crate) fn common2(status: u8, data1: u8, data2: u8, loop_type: MidiFileLoopType) -> Self {
29        let command = status & 0xF0;
30
31        if command == 0xB0 {
32            match loop_type {
33                MidiFileLoopType::RpgMaker if data1 == 111 => {
34                    return Message::LoopStart;
35                }
36
37                MidiFileLoopType::IncredibleMachine => {
38                    if data1 == 110 {
39                        return Message::LoopStart;
40                    }
41                    if data1 == 111 {
42                        return Message::LoopEnd;
43                    }
44                }
45
46                MidiFileLoopType::FinalFantasy => {
47                    if data1 == 116 {
48                        return Message::LoopStart;
49                    }
50                    if data1 == 117 {
51                        return Message::LoopEnd;
52                    }
53                }
54
55                _ => (),
56            }
57        }
58
59        Self::Normal {
60            status,
61            data1,
62            data2,
63        }
64    }
65
66    pub(crate) fn tempo_change(tempo: i32) -> Self {
67        // Truncate to u24
68        let bytes = tempo.to_be_bytes()[1..].try_into().unwrap();
69        Self::TempoChange { bytes }
70    }
71}
72
73/// Represents a standard MIDI file.
74#[derive(Debug)]
75#[non_exhaustive]
76pub struct MidiFile {
77    pub(crate) messages: Vec<Message>,
78    pub(crate) times: Vec<f64>,
79}
80
81impl MidiFile {
82    /// Loads a MIDI file from the stream.
83    ///
84    /// # Arguments
85    ///
86    /// * `reader` - The data stream used to load the MIDI file.
87    pub fn new<R: Read>(reader: &mut R) -> Result<Self, MidiFileError> {
88        MidiFile::new_with_loop_type(reader, MidiFileLoopType::LoopPoint(0))
89    }
90
91    /// Loads a MIDI file from the stream with a specified loop type.
92    ///
93    /// # Arguments
94    ///
95    /// * `reader` - The data stream used to load the MIDI file.
96    /// * `loop_type` - The type of the loop extension to be used.
97    ///
98    /// # Remarks
99    ///
100    /// `MidiFileLoopType` has the following variants:
101    /// * `LoopPoint(usize)` - Specifies the loop start point by a tick value.
102    /// * `RpgMaker` - The RPG Maker style loop.
103    ///   CC #111 will be the loop start point.
104    /// * `IncredibleMachine` - The Incredible Machine style loop.
105    ///   CC #110 and #111 will be the start and end points of the loop.
106    /// * `FinalFantasy` - The Final Fantasy style loop.
107    ///   CC #116 and #117 will be the start and end points of the loop.
108    pub fn new_with_loop_type<R: Read>(
109        reader: &mut R,
110        loop_type: MidiFileLoopType,
111    ) -> Result<Self, MidiFileError> {
112        let chunk_type = BinaryReader::read_four_cc(reader)?;
113        if chunk_type != b"MThd" {
114            return Err(MidiFileError::InvalidChunkType {
115                expected: FourCC::from_bytes(*b"MThd"),
116                actual: chunk_type,
117            });
118        }
119
120        let size = BinaryReader::read_i32_big_endian(reader)?;
121        if size != 6 {
122            return Err(MidiFileError::InvalidChunkData(FourCC::from_bytes(
123                *b"MThd",
124            )));
125        }
126
127        let format = BinaryReader::read_i16_big_endian(reader)?;
128        if !(format == 0 || format == 1) {
129            return Err(MidiFileError::UnsupportedFormat(format));
130        }
131
132        let track_count = BinaryReader::read_i16_big_endian(reader)? as i32;
133        let resolution = BinaryReader::read_i16_big_endian(reader)? as i32;
134
135        let mut message_lists: Vec<Vec<Message>> = Vec::with_capacity(track_count.max(0) as usize);
136        let mut tick_lists: Vec<Vec<i32>> = Vec::with_capacity(track_count.max(0) as usize);
137
138        for _i in 0..track_count {
139            let (message_list, tick_list) = MidiFile::read_track(reader, loop_type)?;
140            message_lists.push(message_list);
141            tick_lists.push(tick_list);
142        }
143
144        match loop_type {
145            MidiFileLoopType::LoopPoint(loop_point) if loop_point != 0 => {
146                let loop_point = loop_point as i32;
147                let tick_list = &mut tick_lists[0];
148                let message_list = &mut message_lists[0];
149
150                if loop_point <= *tick_list.last().unwrap() {
151                    for i in 0..tick_list.len() {
152                        if tick_list[i] >= loop_point {
153                            tick_list.insert(i, loop_point);
154                            message_list.insert(i, Message::LoopStart);
155                            break;
156                        }
157                    }
158                } else {
159                    tick_list.push(loop_point);
160                    message_list.push(Message::LoopStart);
161                }
162            }
163            _ => (),
164        }
165
166        let (messages, times) = MidiFile::merge_tracks(&message_lists, &tick_lists, resolution);
167
168        Ok(Self { messages, times })
169    }
170
171    fn discard_data<R: Read>(reader: &mut R) -> Result<(), MidiFileError> {
172        let size = BinaryReader::read_i32_variable_length(reader)? as usize;
173        BinaryReader::discard_data(reader, size)?;
174        Ok(())
175    }
176
177    fn read_tempo<R: Read>(reader: &mut R) -> Result<i32, MidiFileError> {
178        let size = BinaryReader::read_i32_variable_length(reader)?;
179        if size != 3 {
180            return Err(MidiFileError::InvalidTempoValue);
181        }
182
183        let b1 = BinaryReader::read_u8(reader)? as i32;
184        let b2 = BinaryReader::read_u8(reader)? as i32;
185        let b3 = BinaryReader::read_u8(reader)? as i32;
186
187        Ok((b1 << 16) | (b2 << 8) | b3)
188    }
189
190    fn read_track<R: Read>(
191        reader: &mut R,
192        loop_type: MidiFileLoopType,
193    ) -> Result<(Vec<Message>, Vec<i32>), MidiFileError> {
194        let chunk_type = BinaryReader::read_four_cc(reader)?;
195        if chunk_type != b"MTrk" {
196            return Err(MidiFileError::InvalidChunkType {
197                expected: FourCC::from_bytes(*b"MTrk"),
198                actual: chunk_type,
199            });
200        }
201
202        let size = BinaryReader::read_i32_big_endian(reader)? as usize;
203        let reader = &mut ReadCounter::new(reader);
204
205        let mut messages: Vec<Message> = Vec::new();
206        let mut ticks: Vec<i32> = Vec::new();
207
208        let mut tick: i32 = 0;
209        let mut last_status: u8 = 0;
210
211        loop {
212            let delta = BinaryReader::read_i32_variable_length(reader)?;
213            let first = BinaryReader::read_u8(reader)?;
214
215            tick += delta;
216
217            if (first & 128) == 0 {
218                let command = last_status & 0xF0;
219                if command == 0xC0 || command == 0xD0 {
220                    messages.push(Message::common1(last_status, first));
221                    ticks.push(tick);
222                } else {
223                    let data2 = BinaryReader::read_u8(reader)?;
224                    messages.push(Message::common2(last_status, first, data2, loop_type));
225                    ticks.push(tick);
226                }
227
228                continue;
229            }
230
231            match first {
232                0xF0 => MidiFile::discard_data(reader)?,
233                0xF7 => MidiFile::discard_data(reader)?,
234                0xFF => match BinaryReader::read_u8(reader)? {
235                    0x2F => {
236                        BinaryReader::read_u8(reader)?;
237                        messages.push(Message::EndOfTrack);
238                        ticks.push(tick);
239
240                        // Some MIDI files may have events inserted after the EOT.
241                        // Such events should be ignored.
242                        if reader.bytes_read() < size {
243                            BinaryReader::discard_data(reader, size - reader.bytes_read())?;
244                        }
245
246                        return Ok((messages, ticks));
247                    }
248                    0x51 => {
249                        messages.push(Message::tempo_change(MidiFile::read_tempo(reader)?));
250                        ticks.push(tick);
251                    }
252                    _ => MidiFile::discard_data(reader)?,
253                },
254                _ => {
255                    let command = first & 0xF0;
256                    if command == 0xC0 || command == 0xD0 {
257                        let data1 = BinaryReader::read_u8(reader)?;
258                        messages.push(Message::common1(first, data1));
259                        ticks.push(tick);
260                    } else {
261                        let data1 = BinaryReader::read_u8(reader)?;
262                        let data2 = BinaryReader::read_u8(reader)?;
263                        messages.push(Message::common2(first, data1, data2, loop_type));
264                        ticks.push(tick);
265                    }
266                }
267            }
268
269            last_status = first
270        }
271    }
272
273    fn merge_tracks(
274        message_lists: &[Vec<Message>],
275        tick_lists: &[Vec<i32>],
276        resolution: i32,
277    ) -> (Vec<Message>, Vec<f64>) {
278        // Every merged message corresponds to one entry in one of the tick lists.
279        let total_messages: usize = tick_lists.iter().map(Vec::len).sum();
280        let mut merged_messages: Vec<Message> = Vec::with_capacity(total_messages);
281        let mut merged_times: Vec<f64> = Vec::with_capacity(total_messages);
282
283        let mut indices: Vec<usize> = vec![0; message_lists.len()];
284
285        let mut current_tick: i32 = 0;
286        let mut current_time: f64 = 0.0;
287
288        let mut tempo: f64 = 120.0;
289
290        loop {
291            let mut min_tick = i32::MAX;
292            let mut min_index: i32 = -1;
293
294            for ch in 0..tick_lists.len() {
295                if indices[ch] < tick_lists[ch].len() {
296                    let tick = tick_lists[ch][indices[ch]];
297                    if tick < min_tick {
298                        min_tick = tick;
299                        min_index = ch as i32;
300                    }
301                }
302            }
303
304            if min_index == -1 {
305                break;
306            }
307
308            let next_tick = tick_lists[min_index as usize][indices[min_index as usize]];
309            let delta_tick = next_tick - current_tick;
310            let delta_time = 60.0 / (resolution as f64 * tempo) * delta_tick as f64;
311
312            current_tick += delta_tick;
313            current_time += delta_time;
314
315            let message = message_lists[min_index as usize][indices[min_index as usize]];
316            if let Message::TempoChange { bytes } = message {
317                let tempo_i32 = i32::from_be_bytes([0, bytes[0], bytes[1], bytes[2]]);
318                tempo = 60000000.0 / tempo_i32 as f64;
319            } else {
320                merged_messages.push(message);
321                merged_times.push(current_time);
322            }
323
324            indices[min_index as usize] += 1;
325        }
326
327        (merged_messages, merged_times)
328    }
329
330    /// Get the length of the MIDI file in seconds.
331    pub fn get_length(&self) -> f64 {
332        *self.times.last().unwrap()
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    #[test]
341    fn test_message_size() {
342        // Avoid increasing the size of the Message type
343        assert_eq!(size_of::<Message>(), 4);
344    }
345}