1use std::collections::HashMap;
9use std::error::Error;
10use std::fmt;
11use std::io;
12use std::path::{Path, PathBuf};
13
14use midly::{Format, MetaMessage, MidiMessage, Smf, Timing, TrackEventKind};
15
16use super::{Note, NoteError, NoteList, Velocity};
17use crate::cast;
18use crate::units::{Bpm, MidiPitch, Seconds};
19
20const DEFAULT_TEMPO_US: u32 = 500_000;
22pub const BPM_TOLERANCE: f64 = 0.01;
25pub const DRUM_CHANNEL: u8 = 9;
28
29pub fn read_midi(path: &Path, bpm: Bpm) -> Result<NoteList, MidiError> {
45 read_midi_near(path, bpm, None)
46}
47
48pub fn read_midi_near(path: &Path, bpm: Bpm, refined: Option<Bpm>) -> Result<NoteList, MidiError> {
57 let bytes = std::fs::read(path).map_err(|source| MidiError {
58 path: path.to_path_buf(),
59 kind: Kind::Open(source),
60 })?;
61 notes_near(path, &bytes, bpm, refined)
62}
63
64pub fn read_midi_bytes(path: &Path, bpm: Bpm) -> Result<(NoteList, Vec<u8>), MidiError> {
72 let bytes = std::fs::read(path).map_err(|source| MidiError {
73 path: path.to_path_buf(),
74 kind: Kind::Open(source),
75 })?;
76 Ok((notes_near(path, &bytes, bpm, None)?, bytes))
77}
78
79fn notes_near(
80 path: &Path,
81 bytes: &[u8],
82 bpm: Bpm,
83 refined: Option<Bpm>,
84) -> Result<NoteList, MidiError> {
85 let fail = |kind| MidiError {
86 path: path.to_path_buf(),
87 kind,
88 };
89 let smf = Smf::parse(bytes).map_err(|source| fail(Kind::Parse(source.to_string())))?;
90 match (notes_of(&smf, bpm), refined) {
91 (Err(Kind::Tempo { .. }), Some(refined)) if refined != bpm => notes_of(&smf, refined)
92 .map_err(|kind| match kind {
93 Kind::Tempo {
94 file_bpm, default, ..
95 } => Kind::Tempo {
96 file_bpm,
97 bpm,
98 refined: Some(refined),
99 default,
100 },
101 other => other,
102 }),
103 (result, _) => result,
104 }
105 .map_err(fail)
106}
107
108fn notes_of(smf: &Smf<'_>, bpm: Bpm) -> Result<NoteList, Kind> {
109 if smf.header.format == Format::Sequential {
110 return Err(Kind::Sequential);
111 }
112 let clock = match smf.header.timing {
113 Timing::Metrical(ticks) => Clock::metrical(smf, ticks.as_int(), bpm)?,
114 Timing::Timecode(fps, subframes) => Clock::Timecode {
115 ticks_per_second: f64::from(fps.as_f32()) * f64::from(subframes),
116 },
117 };
118 let mut notes = Vec::new();
119 let mut drums = 0_usize;
120 for (track, events) in smf.tracks.iter().enumerate() {
121 let mut tick = 0_u64;
122 let mut open: HashMap<(u8, u8), Vec<(u64, u8)>> = HashMap::new();
123 for event in events {
124 tick += u64::from(event.delta.as_int());
125 let TrackEventKind::Midi { channel, message } = event.kind else {
126 continue;
127 };
128 if channel.as_int() == DRUM_CHANNEL {
129 if matches!(message, MidiMessage::NoteOn { vel, .. } if vel.as_int() > 0) {
130 drums += 1;
131 }
132 continue;
133 }
134 let slot = |key: midly::num::u7| (channel.as_int(), key.as_int());
135 match message {
136 MidiMessage::NoteOn { key, vel } if vel.as_int() > 0 => {
137 open.entry(slot(key))
138 .or_default()
139 .push((tick, vel.as_int()));
140 }
141 MidiMessage::NoteOn { key, .. } | MidiMessage::NoteOff { key, .. } => {
142 if let Some(queue) = open.get_mut(&slot(key))
143 && !queue.is_empty()
144 {
145 let (onset, velocity) = queue.remove(0);
146 push_note(
147 &mut notes,
148 &clock,
149 track,
150 onset,
151 tick,
152 key.as_int(),
153 velocity,
154 )?;
155 }
156 }
157 _ => {}
158 }
159 }
160 let mut unclosed: Vec<_> = open
161 .into_iter()
162 .flat_map(|((_, key), queue)| queue.into_iter().map(move |(onset, v)| (onset, key, v)))
163 .collect();
164 unclosed.sort_unstable();
165 for (onset, key, velocity) in unclosed {
166 push_note(&mut notes, &clock, track, onset, tick, key, velocity)?;
167 }
168 }
169 if notes.is_empty() {
170 return Err(Kind::NoNotes { drums });
171 }
172 Ok(NoteList::new(notes))
173}
174
175fn push_note(
176 notes: &mut Vec<Note>,
177 clock: &Clock,
178 track: usize,
179 onset: u64,
180 offset: u64,
181 key: u8,
182 velocity: u8,
183) -> Result<(), Kind> {
184 if offset <= onset {
185 return Ok(());
186 }
187 let note = Note::new(
188 clock.seconds(onset),
189 clock.seconds(offset),
190 MidiPitch::clamped(f64::from(key)),
191 )
192 .and_then(|note| Ok(note.with_velocity(Velocity::new(velocity)?)))
193 .map_err(|source| Kind::Note { track, source })?;
194 notes.push(note);
195 Ok(())
196}
197
198enum Clock {
200 Metrical { map: Vec<(u64, f64, f64)> },
203 Timecode { ticks_per_second: f64 },
205}
206
207impl Clock {
208 fn metrical(smf: &Smf<'_>, ticks_per_beat: u16, bpm: Bpm) -> Result<Self, Kind> {
212 let mut tempos = Vec::new();
213 for events in &smf.tracks {
214 let mut tick = 0_u64;
215 for event in events {
216 tick += u64::from(event.delta.as_int());
217 if let TrackEventKind::Meta(MetaMessage::Tempo(us)) = event.kind {
218 tempos.push((tick, us.as_int(), false));
219 }
220 }
221 }
222 tempos.sort_by_key(|&(tick, _, _)| tick);
223 if tempos.first().is_none_or(|&(tick, _, _)| tick > 0) {
224 tempos.insert(0, (0, DEFAULT_TEMPO_US, true));
225 }
226 let per_beat = f64::from(ticks_per_beat.max(1));
227 let mut map: Vec<(u64, f64, f64)> = Vec::with_capacity(tempos.len());
228 for (tick, us, default) in tempos {
229 let file_bpm = 60e6 / f64::from(us.max(1));
230 if (file_bpm - bpm.get()).abs() > BPM_TOLERANCE {
231 return Err(Kind::Tempo {
232 file_bpm,
233 bpm,
234 refined: None,
235 default,
236 });
237 }
238 let seconds = map.last().map_or(0.0, |&(start, at, per_tick)| {
239 at + cast::u64_to_f64(tick - start) * per_tick
240 });
241 map.push((tick, seconds, f64::from(us) * 1e-6 / per_beat));
242 }
243 Ok(Self::Metrical { map })
244 }
245
246 fn seconds(&self, tick: u64) -> Seconds {
247 let seconds = match self {
248 Self::Metrical { map } => {
249 let index = map.partition_point(|&(start, _, _)| start <= tick);
250 map.get(index.saturating_sub(1))
251 .map_or(0.0, |&(start, at, per_tick)| {
252 at + cast::u64_to_f64(tick - start) * per_tick
253 })
254 }
255 Self::Timecode { ticks_per_second } => {
256 cast::u64_to_f64(tick) / ticks_per_second.max(1.0)
257 }
258 };
259 Seconds::clamped(seconds)
260 }
261}
262
263#[derive(Debug)]
265pub struct MidiError {
266 path: PathBuf,
267 kind: Kind,
268}
269
270impl MidiError {
271 #[must_use]
273 pub fn path(&self) -> &Path {
274 &self.path
275 }
276}
277
278#[derive(Debug)]
279enum Kind {
280 Open(io::Error),
281 Parse(String),
282 Sequential,
283 Tempo {
284 file_bpm: f64,
285 bpm: Bpm,
286 refined: Option<Bpm>,
287 default: bool,
288 },
289 Note {
290 track: usize,
291 source: NoteError,
292 },
293 NoNotes {
294 drums: usize,
295 },
296}
297
298impl fmt::Display for MidiError {
299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300 write!(f, "cannot read the notes of {}: ", self.path.display())?;
301 match &self.kind {
302 Kind::Open(source) => write!(f, "cannot open the file ({source}). Check the path"),
303 Kind::Parse(message) => write!(
304 f,
305 "the file is not a valid MIDI file ({message}). Export the notes again as a Standard MIDI File"
306 ),
307 Kind::Sequential => write!(
308 f,
309 "the file is a sequential (format 2) MIDI file. Export the notes as format 0 or 1"
310 ),
311 Kind::Tempo {
312 file_bpm,
313 bpm,
314 refined,
315 default,
316 } => {
317 let origin = if *default {
318 " (the MIDI default; the file has no tempo event at its start)"
319 } else {
320 ""
321 };
322 let refined = refined.map_or_else(String::new, |refined| {
323 format!(" and the tempo refined from the stem is {}", refined.get())
324 });
325 write!(
326 f,
327 "the file plays at {file_bpm:.3} BPM{origin}, but --bpm is {}{refined}. Export the notes at the stem's tempo or pass the file's tempo as --bpm",
328 bpm.get()
329 )
330 }
331 Kind::Note { track, source } => write!(f, "track {track} holds a bad note: {source}"),
332 Kind::NoNotes { drums: 0 } => {
333 write!(f, "the file holds no notes. Export the notes again")
334 }
335 Kind::NoNotes { drums } => write!(
336 f,
337 "the file holds only drum notes ({drums} on channel 10), which have no pitch. Export the pitched part"
338 ),
339 }
340 }
341}
342
343impl Error for MidiError {
344 fn source(&self) -> Option<&(dyn Error + 'static)> {
345 match &self.kind {
346 Kind::Open(source) => Some(source),
347 Kind::Note { source, .. } => Some(source),
348 _ => None,
349 }
350 }
351}