Skip to content

Commit

Permalink
Track program changes across rewinds (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
PolyMeilex authored Sep 17, 2023
1 parent 577fc26 commit a525b9c
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 1 deletion.
1 change: 1 addition & 0 deletions midi-file/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod midi;
pub mod playback;
pub mod program_map;
mod track;
mod utils;

Expand Down
6 changes: 5 additions & 1 deletion midi-file/src/midi.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -7,6 +7,7 @@ pub struct Midi {
pub format: Format,
pub tracks: Vec<MidiTrack>,
pub merged_track: MidiTrack,
pub program_map: ProgramMap,
}

impl Midi {
Expand Down Expand Up @@ -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,
})
}
}
55 changes: 55 additions & 0 deletions midi-file/src/program_map.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use crate::MidiEvent;
use std::{collections::HashMap, sync::OnceLock, time::Duration};

/// HashMap<Channel, Program>
fn default_programs() -> &'static HashMap<u8, u8> {
static DEFAULT_PROGRAMS: OnceLock<HashMap<u8, u8>> = OnceLock::new();
DEFAULT_PROGRAMS.get_or_init(|| (0..16).map(|ch| (ch, 0)).collect())
}

#[derive(Debug, Clone)]
struct Bucket {
timestamp: Duration,
map: HashMap<u8, u8>,
}

#[derive(Debug, Clone)]
pub struct ProgramMap {
timestamps: Vec<Bucket>,
}

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<u8, u8> {
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())
}
}
22 changes: 22 additions & 0 deletions neothesia/src/scene/playing_scene/midi_player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand All @@ -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) {
Expand Down

0 comments on commit a525b9c

Please sign in to comment.