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