This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

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}