diff --git a/midi-file/src/lib.rs b/midi-file/src/lib.rs index 59004107..eabe553b 100644 --- a/midi-file/src/lib.rs +++ b/midi-file/src/lib.rs @@ -1,5 +1,6 @@ mod midi; pub mod playback; +pub mod program_map; mod track; mod utils; diff --git a/midi-file/src/midi.rs b/midi-file/src/midi.rs index d031007d..49a4bd27 100644 --- a/midi-file/src/midi.rs +++ b/midi-file/src/midi.rs @@ -1,4 +1,4 @@ -use crate::{utils, MidiTrack}; +use crate::{program_map::ProgramMap, utils, MidiTrack}; use midly::{Format, Smf, Timing}; use std::{fs, path::Path}; @@ -7,6 +7,7 @@ pub struct Midi { pub format: Format, pub tracks: Vec, pub merged_track: MidiTrack, + pub program_map: ProgramMap, } impl Midi { @@ -70,10 +71,13 @@ impl Midi { note.id = i; } + let program_map = ProgramMap::new(&merged_track.events); + Ok(Self { format: smf.header.format, tracks, merged_track, + program_map, }) } } diff --git a/midi-file/src/program_map.rs b/midi-file/src/program_map.rs new file mode 100644 index 00000000..a4ff8c5e --- /dev/null +++ b/midi-file/src/program_map.rs @@ -0,0 +1,55 @@ +use crate::MidiEvent; +use std::{collections::HashMap, sync::OnceLock, time::Duration}; + +/// HashMap +fn default_programs() -> &'static HashMap { + static DEFAULT_PROGRAMS: OnceLock> = OnceLock::new(); + DEFAULT_PROGRAMS.get_or_init(|| (0..16).map(|ch| (ch, 0)).collect()) +} + +#[derive(Debug, Clone)] +struct Bucket { + timestamp: Duration, + map: HashMap, +} + +#[derive(Debug, Clone)] +pub struct ProgramMap { + timestamps: Vec, +} + +impl ProgramMap { + pub fn new(events: &[MidiEvent]) -> Self { + let mut map = default_programs().clone(); + + let mut timestamps = Vec::new(); + + for event in events { + if let midly::MidiMessage::ProgramChange { program } = event.message { + *map.entry(event.channel).or_default() = program.as_int(); + + timestamps.push(Bucket { + timestamp: event.timestamp, + map: map.clone(), + }); + } + } + + Self { timestamps } + } + + /// Search for program at certain timestamp + pub fn program_for_timestamp(&self, timestamp: &Duration) -> &HashMap { + let res = self + .timestamps + .binary_search_by_key(timestamp, |bucket| bucket.timestamp); + + let id = match res { + Ok(id) => Some(id), + Err(id) => id.checked_sub(1), + }; + + id.map(|id| &self.timestamps[id].map) + .unwrap_or_else(|| default_programs()) + } +} diff --git a/neothesia/src/scene/playing_scene/midi_player.rs b/neothesia/src/scene/playing_scene/midi_player.rs index ea752241..cd51f0bc 100644 --- a/neothesia/src/scene/playing_scene/midi_player.rs +++ b/neothesia/src/scene/playing_scene/midi_player.rs @@ -26,6 +26,10 @@ impl MidiPlayer { midi_file: midi_file.clone(), play_along: PlayAlong::new(user_keyboard_range), }; + // Let's reset programs, + // for timestamp 0 most likely all programs will be 0, so this should clean any leftovers + // from previous songs + player.send_midi_programs_for_timestamp(&player.playback.time()); player.update(target, Duration::ZERO); player @@ -106,6 +110,23 @@ impl MidiPlayer { self.playback.resume(); } + fn send_midi_programs_for_timestamp(&self, time: &Duration) { + for (&channel, &p) in self.midi_file.program_map.program_for_timestamp(time) { + self.output_manager + .borrow_mut() + .midi_event(&midi_file::MidiEvent { + channel, + delta: 0, + timestamp: Duration::ZERO, + message: midi_file::midly::MidiMessage::ProgramChange { + program: midi_file::midly::num::u7::new(p), + }, + track_id: 0, + track_color_id: 0, + }); + } + } + fn set_time(&mut self, time: Duration) { self.playback.set_time(time); @@ -116,6 +137,7 @@ impl MidiPlayer { std::mem::drop(events); self.clear(); + self.send_midi_programs_for_timestamp(&time); } pub fn rewind(&mut self, delta: i64) {