This repository has no description
1use super::audio::{self, Stem};
2use super::sync::{SyncData, Syncable};
3use crate::synchronization::sync::TimestampMS;
4use crate::ui::MaybeProgressBar;
5use indicatif::ProgressBar;
6use itertools::Itertools;
7use measure_time::debug_time;
8use midly::{MetaMessage, MidiMessage, TrackEvent, TrackEventKind};
9use std::io::Read;
10use std::{collections::HashMap, fmt::Debug, path::PathBuf};
11
12pub struct MidiSynchronizer {
13 pub midi_path: PathBuf,
14}
15
16trait Averageable {
17 fn average(&self) -> f32;
18}
19
20impl Averageable for Vec<f32> {
21 fn average(&self) -> f32 {
22 self.iter().sum::<f32>() / self.len() as f32
23 }
24}
25
26impl Syncable for MidiSynchronizer {
27 fn new(path: impl Into<PathBuf>) -> Self {
28 Self {
29 midi_path: path.into(),
30 }
31 }
32
33 fn load(&self, progressbar: Option<&ProgressBar>) -> SyncData {
34 let (now, notes_per_instrument, markers) =
35 load_midi_file(&self.midi_path, progressbar);
36
37 SyncData {
38 markers,
39 bpm: Some(tempo_to_bpm(now.tempo)),
40 stems: HashMap::from_iter(notes_per_instrument.iter().map(
41 |(name, notes)| {
42 let mut notes_per_ms =
43 HashMap::<usize, Vec<audio::Note>>::new();
44
45 if let Some(pb) = progressbar {
46 pb.set_length(notes.len() as u64);
47 pb.set_position(0);
48 }
49 progressbar
50 .set_message(format!("Adding loaded notes for {name}"));
51
52 for note in notes.iter() {
53 notes_per_ms.entry(note.ms as usize).or_default().push(
54 audio::Note {
55 pitch: note.key,
56 tick: note.tick,
57 velocity: note.vel,
58 },
59 );
60 progressbar.inc(1);
61 }
62
63 let duration_ms = *notes_per_ms.keys().max().unwrap_or(&0);
64
65 if let Some(pb) = progressbar {
66 pb.set_length(duration_ms as u64 - 1);
67 pb.set_position(0);
68 }
69 progressbar
70 .set_message(format!("Infering amplitudes for {name}"));
71
72 let mut amplitudes = Vec::<f32>::new();
73 let mut last_amplitude = 0.0;
74 for i in 0..duration_ms {
75 if let Some(notes) = notes_per_ms.get(&i) {
76 last_amplitude = notes
77 .iter()
78 .map(|n| n.velocity as f32)
79 .collect::<Vec<f32>>()
80 .average();
81 }
82 amplitudes.push(last_amplitude);
83 progressbar.inc(1);
84 }
85
86 (
87 name.clone(),
88 Stem {
89 amplitude_max: notes
90 .iter()
91 .map(|n| n.vel)
92 .max()
93 .unwrap_or(0)
94 as f32,
95 amplitude_db: amplitudes,
96 duration_ms,
97 notes: notes_per_ms,
98 name: name.clone(),
99 },
100 )
101 },
102 )),
103 }
104 }
105}
106
107#[derive(Clone)]
108struct Note {
109 tick: u32,
110 ms: u32,
111 key: u8,
112 vel: u8,
113}
114
115struct Now {
116 ms: usize,
117 tempo: usize,
118 ticks_per_beat: u16,
119}
120
121type Timeline<'a> = HashMap<u32, HashMap<String, TrackEvent<'a>>>;
122
123type StemNotes = HashMap<u32, HashMap<String, Note>>;
124
125impl Note {
126 fn is_off(&self) -> bool {
127 self.vel == 0
128 }
129}
130
131fn tempo_to_bpm(µs_per_beat: usize) -> usize {
132 (60_000_000.0 / µs_per_beat as f32).round() as usize
133}
134
135// fn to_ms(delta: u32, bpm: f32) -> f32 {
136// (delta as f32) * (60.0 / bpm) * 1000.0
137// }
138
139impl Debug for Note {
140 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141 write!(
142 f,
143 "{}{}",
144 self.key,
145 if self.is_off() {
146 "↓".to_string()
147 } else if self.vel == 100 {
148 "".to_string()
149 } else {
150 format!("@{}", self.vel)
151 }
152 )
153 }
154}
155
156fn load_midi_file(
157 source: &PathBuf,
158 progressbar: Option<&ProgressBar>,
159) -> (
160 Now,
161 HashMap<String, Vec<Note>>,
162 HashMap<TimestampMS, String>,
163) {
164 debug_time!("load_midi_notes");
165
166 let mut markers = HashMap::<TimestampMS, String>::new();
167
168 // Read midi file using midly
169 if let Some(pb) = progressbar {
170 pb.set_length(1);
171 pb.set_prefix("Loading");
172 pb.set_message("reading MIDI file");
173 pb.set_position(0);
174 }
175
176 let raw = std::fs::read(source).unwrap_or_else(|_| {
177 panic!("Failed to read MIDI file {}", source.to_str().unwrap())
178 });
179 let midifile = midly::Smf::parse(&raw).unwrap();
180
181 let mut timeline = Timeline::new();
182 progressbar
183 .set_message(format!("MIDI file has {} tracks", midifile.tracks.len()));
184
185 let mut now = Now {
186 ms: 0,
187 tempo: 0,
188 ticks_per_beat: match midifile.header.timing {
189 midly::Timing::Metrical(ticks_per_beat) => ticks_per_beat.as_int(),
190 midly::Timing::Timecode(fps, subframe) => {
191 (1.0 / fps.as_f32() / subframe as f32) as u16
192 }
193 },
194 };
195
196 // Get track names and (initial) BPM
197 let mut track_no = 0;
198 let mut track_names = HashMap::<usize, String>::new();
199 for track in midifile.tracks.iter() {
200 track_no += 1;
201 let mut track_name = String::new();
202 for event in track {
203 match event.kind {
204 TrackEventKind::Meta(MetaMessage::TrackName(name_bytes)) => {
205 track_name = String::from_utf8(name_bytes.to_vec())
206 .unwrap_or_default();
207 }
208 TrackEventKind::Meta(MetaMessage::Tempo(tempo)) => {
209 if now.tempo == 0 {
210 now.tempo = tempo.as_int() as usize;
211 }
212 }
213 _ => {}
214 }
215 }
216 track_names.insert(
217 track_no,
218 if !track_name.is_empty() {
219 track_name
220 } else {
221 format!("Track #{}", track_no)
222 },
223 );
224 }
225
226 // Convert ticks to absolute
227 let mut track_no = 0;
228 for track in midifile.tracks.iter() {
229 track_no += 1;
230 let mut absolute_tick = 0;
231 for event in track {
232 absolute_tick += event.delta.as_int();
233 timeline
234 .entry(absolute_tick)
235 .or_default()
236 .insert(track_names[&track_no].clone(), *event);
237 }
238 }
239
240 // Convert ticks to ms
241 let mut absolute_tick_to_ms = HashMap::<u32, usize>::new();
242 let mut last_tick = 0;
243 for (tick, tracks) in timeline.iter().sorted_by_key(|(tick, _)| *tick) {
244 for event in tracks.values() {
245 if let TrackEventKind::Meta(MetaMessage::Tempo(tempo)) = event.kind {
246 now.tempo = tempo.as_int() as usize;
247 }
248 }
249 let delta = tick - last_tick;
250 last_tick = *tick;
251 now.ms += midi_tick_to_ms(delta, now.tempo, now.ticks_per_beat as usize);
252 absolute_tick_to_ms.insert(*tick, now.ms);
253 }
254
255 if let Some(pb) = progressbar {
256 pb.set_length(
257 midifile.tracks.iter().map(|t| t.len() as u64).sum::<u64>(),
258 );
259 pb.set_prefix("Loading");
260 pb.set_message("parsing MIDI events");
261 pb.set_position(0);
262 }
263
264 // Add notes
265 let mut stem_notes = StemNotes::new();
266 for (tick, tracks) in timeline.iter().sorted_by_key(|(tick, _)| *tick) {
267 for (track_name, event) in tracks {
268 if let TrackEventKind::Meta(MetaMessage::Marker(mut marker)) =
269 event.kind
270 {
271 let mut text = String::new();
272
273 marker
274 .read_to_string(&mut text)
275 .expect("Marker is not valid UTF8");
276
277 markers.insert(absolute_tick_to_ms[tick], text);
278 }
279
280 if let TrackEventKind::Midi {
281 channel: _,
282 message,
283 } = event.kind
284 {
285 match message {
286 MidiMessage::NoteOn { key, vel }
287 | MidiMessage::NoteOff { key, vel } => {
288 stem_notes
289 .entry(absolute_tick_to_ms[tick] as u32)
290 .or_default()
291 .insert(
292 track_name.clone(),
293 Note {
294 tick: *tick,
295 ms: absolute_tick_to_ms[tick] as u32,
296 key: key.as_int(),
297 vel: if matches!(
298 message,
299 MidiMessage::NoteOff { .. }
300 ) {
301 0
302 } else {
303 vel.as_int()
304 },
305 },
306 );
307 }
308 _ => {}
309 }
310 }
311 progressbar.inc(1)
312 }
313 }
314
315 let mut result = HashMap::<String, Vec<Note>>::new();
316
317 for (_ms, notes) in stem_notes.iter().sorted_by_key(|(ms, _)| *ms) {
318 for (track_name, note) in notes {
319 result
320 .entry(track_name.clone())
321 .or_default()
322 .push(note.clone());
323 }
324 }
325
326 (now, result, markers)
327}
328
329fn midi_tick_to_ms(tick: u32, tempo: usize, ppq: usize) -> usize {
330 let with_floats = (tempo as f32 / 1e3) / ppq as f32 * tick as f32;
331 with_floats.round() as usize
332}