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 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}