Skip to content

Commit

Permalink
WIP: MIDI: split by instruments
Browse files Browse the repository at this point in the history
  • Loading branch information
tsionyx committed Aug 22, 2024
1 parent adeea8a commit 6bf3864
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 42 deletions.
3 changes: 2 additions & 1 deletion examples/hsom-exercises/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
ch6::shepard_scale::music(
-Interval::semi_tone(),
&[
// ascending by Instrument to ease debug
(AcousticGrandPiano, 2323),
(ElectricGuitarClean, 9940),
(Flute, 7899),
(Cello, 15000),
(Flute, 7899),
],
)
}
Expand Down
2 changes: 1 addition & 1 deletion src/music/iter_like.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ impl<P> Music<P> {
pub fn chord(musics: Vec<Self>) -> Self {
musics
.into_iter()
.rfold(Self::rest(Dur::ZERO), |acc, m| m | acc)
.rfold(Self::rest(Dur::ZERO), |acc, m| acc | m)
}

/// Strip away the [`Dur::ZERO`] occurrences that could appear
Expand Down
57 changes: 24 additions & 33 deletions src/output/midi/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#![cfg_attr(not(feature = "play-midi"), allow(dead_code))]

use std::{borrow::Cow, collections::BTreeMap as Map, fmt, iter, time::Duration};
use std::{fmt, iter, time::Duration};

use itertools::Itertools as _;
use midly::{
Expand All @@ -15,7 +15,7 @@ use crate::{
instruments::InstrumentName,
music::perf::{Event, Performance},
prim::volume::Volume,
utils::{append_with_last, merge_pairs_by},
utils::{append_with_last, merge_pairs_by, partition, LazyList},
};

use super::{Channel, ProgNum, UserPatchMap};
Expand All @@ -39,7 +39,7 @@ impl Performance {
///
/// Optionally, the [patch map][UserPatchMap] could be provided to
/// explicitly assign MIDI channels to instruments.
pub fn into_midi(self, user_patch: Option<&UserPatchMap>) -> Result<Smf<'_>, Error> {
pub fn into_midi(self, user_patch: Option<UserPatchMap>) -> Result<Smf<'static>, Error> {
let (tracks, timing) = self.into_lazy_midi(user_patch);
let tracks: Result<Vec<_>, _> = tracks.collect();
let tracks: Vec<_> = tracks?.into_iter().map(Iterator::collect).collect();
Expand All @@ -62,34 +62,16 @@ impl Performance {
/// explicitly assign MIDI channels to instruments.
pub fn into_lazy_midi<'a>(
self,
user_patch: Option<&'a UserPatchMap>,
user_patch: Option<UserPatchMap>,
) -> (
impl Iterator<Item = Result<Box<dyn Iterator<Item = TrackEvent<'static>> + 'a>, Error>> + 'a,
Timing,
) {
// TODO: split lazily with `&mut UserPatchMap`
let split = self.split_by_instruments();
let user_patch = user_patch.and_then(|user_patch| {
let instruments = split.keys();
user_patch
.contains_all(instruments)
.then_some(Cow::Borrowed(user_patch))
});

let user_patch = user_patch.map_or_else(
|| {
let instruments = split.keys().cloned().collect();
UserPatchMap::with_instruments(instruments).map(Cow::Owned)
},
Ok,
);

let stream = split.into_iter().map(move |(i, p)| {
let user_patch = user_patch.as_ref().map_err(Error::clone)?;
let mut user_patch = user_patch.unwrap_or_default();

let (channel, program) = user_patch
.lookup(&i)
.ok_or_else(|| Error::NotFoundInstrument(i.clone()))?;
let split = self.split_by_instruments();
let stream = split.map(move |(i, p)| {
let (channel, program) = user_patch.get_or_insert(i)?;

let track = into_relative_time(p.as_midi_track(channel, program));
let track = track.chain(iter::once(TrackEvent {
Expand All @@ -104,13 +86,22 @@ impl Performance {
(stream, Timing::Metrical(DEFAULT_TIME_DIV))
}

fn split_by_instruments(self) -> Map<InstrumentName, Self> {
self.iter()
.map(|e| (e.instrument.clone(), e))
.into_group_map()
.into_iter()
.map(|(k, v)| (k, Self::with_events(v.into_iter())))
.collect()
fn split_by_instruments(self) -> impl Iterator<Item = (InstrumentName, Self)> {
let mut stream = {
let x: LazyList<_> = self.into_iter();
Some(x.peekable())
};

iter::from_fn(move || {
let mut current_stream = stream.take()?;
let head = current_stream.peek()?;
let instrument = head.instrument.clone();
let i = instrument.clone();

let (this_instrument, other) = partition(current_stream, move |e| e.instrument == i);
stream = Some(LazyList(Box::new(other)).peekable());
Some((instrument, Self::with_events(this_instrument)))
})
}

fn as_midi_track(
Expand Down
46 changes: 40 additions & 6 deletions src/output/midi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use std::{collections::BTreeMap as Map, path::Path};

use enum_map::Enum;
use log::info;
use log::{info, trace};
use midly::num::{u4, u7};

use crate::{instruments::InstrumentName, music::perf::Performance};
Expand All @@ -29,6 +29,16 @@ impl Performance {
pub fn save_to_file<P: AsRef<Path>>(self, path: P) -> Result<(), AnyError> {
let midi = self.into_midi(None)?;
info!("Saving to MIDI file {}", path.as_ref().display());

if log::log_enabled!(log::Level::Trace) {
trace!("{:?}", midi.header);
for (i, tr) in midi.tracks.iter().enumerate() {
trace!("Track #{i}");
for (j, ev) in tr.iter().enumerate() {
trace!("{i}.{j}.{ev:?}");
}
}
}
midi.save(path)?;
Ok(())
}
Expand Down Expand Up @@ -57,7 +67,7 @@ type Channel = u4;
// up to 128 instruments
type ProgNum = u7;

#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
/// The [patch map][UserPatchMap]
/// assigns MIDI channels to instruments.
pub struct UserPatchMap {
Expand Down Expand Up @@ -102,7 +112,7 @@ impl UserPatchMap {

/// Given the [instrument][InstrumentName],
/// find the MIDI channel for it, and its Program Number (ID).
pub fn lookup(&self, instrument: &InstrumentName) -> Option<(Channel, ProgNum)> {
fn lookup(&self, instrument: &InstrumentName) -> Option<(Channel, ProgNum)> {
let channel = self.repr.get(instrument)?;
let prog_num = match instrument {
InstrumentName::Midi(i) => i
Expand All @@ -117,8 +127,32 @@ impl UserPatchMap {
))
}

#[allow(single_use_lifetimes)] // false positive
fn contains_all<'i>(&self, mut instruments: impl Iterator<Item = &'i InstrumentName>) -> bool {
instruments.all(|i| self.lookup(i).is_some())
fn get_or_insert(&mut self, instrument: InstrumentName) -> Result<(Channel, ProgNum), Error> {
if let Some(x) = self.lookup(&instrument) {
return Ok(x);
}

let available_channels = Self::available_channels();
let occupied: Vec<_> = self.repr.values().copied().collect();

if occupied.len() >= available_channels.len() {
return Err(Error::TooManyInstruments(available_channels.len()));
}

if instrument == InstrumentName::Percussion {
let x = self.repr.insert(instrument.clone(), Self::PERCUSSION);
assert!(x.is_none());
return Ok(self.lookup(&instrument).expect("Just inserted"));
}

for i in available_channels {
if !occupied.contains(&i) {
let x = self.repr.insert(instrument.clone(), i);
assert!(x.is_none());
return Ok(self.lookup(&instrument).expect("Just inserted"));
}
}

Err(Error::NotFoundInstrument(instrument))
}
}
17 changes: 17 additions & 0 deletions src/utils/iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,20 @@ where
(lo * 2, hi.map(|hi| hi * 2))
}
}

pub fn partition<I, T, F>(
iter: I,
predicate: F,
) -> (
impl CloneableIterator<Item = T>,
impl CloneableIterator<Item = T>,
)
where
I: Iterator<Item = T> + Clone,
F: Fn(&T) -> bool + Clone + 'static,
{
let f = predicate.clone();
let left = iter.clone().filter(move |x| f(x));
let right = iter.filter(move |x| !predicate(x));
(left, right)
}
2 changes: 1 addition & 1 deletion src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ pub use self::{
};

pub(crate) use self::{
iter::{append_with_last, merge_pairs_by},
iter::{append_with_last, merge_pairs_by, partition},
r#ref::to_static,
};

0 comments on commit 6bf3864

Please sign in to comment.