diff --git a/src/instruments.rs b/src/instruments.rs index d214513..48d3f62 100644 --- a/src/instruments.rs +++ b/src/instruments.rs @@ -9,7 +9,7 @@ use enum_map::Enum; use crate::{AbsPitch, Dur, Music}; // https://github.com/rust-lang/rfcs/issues/284#issuecomment-1592343574 -#[derive(Debug, PartialEq, Eq, Copy, Clone, PartialOrd, Ord, Enum, Sequence)] +#[derive(Debug, PartialEq, Eq, Copy, Clone, PartialOrd, Ord, Hash, Enum, Sequence)] pub enum StandardMidiInstrument { AcousticGrandPiano, BrightAcousticPiano, @@ -141,7 +141,7 @@ pub enum StandardMidiInstrument { Gunshot, } -#[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] +#[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord, Hash)] pub enum InstrumentName { Standard(StandardMidiInstrument), /// Marks the pitches in the [`Music`] as the specific [`PercussionSound`]. diff --git a/src/io/midi/codec.rs b/src/io/midi/codec.rs new file mode 100644 index 0000000..952f641 --- /dev/null +++ b/src/io/midi/codec.rs @@ -0,0 +1,208 @@ +use std::{borrow::Cow, collections::HashMap, iter}; + +use itertools::Itertools as _; + +use crate::{ + instruments::InstrumentName, + music::{ + performance::{Event, Performance}, + Volume, + }, +}; + +use super::{Channel, UserPatchMap}; + +#[derive(Debug, Eq, PartialEq)] +pub struct Midi { + file_type: FileType, + time_div: TimeDiv, + tracks: Vec>, +} + +#[derive(Debug, Eq, PartialEq)] +pub enum FileType { + SingleTrack, + MultiTrack, + MultiPattern, +} + +#[derive(Debug, Eq, PartialEq)] +enum TimeDiv { + /// Ticks per quarter note + TicksPerBeat(u16), +} + +impl Default for TimeDiv { + fn default() -> Self { + Self::TicksPerBeat(96) + } +} + +#[derive(Debug, Eq, PartialEq)] +pub struct Track { + repr: Vec<(T, Message)>, +} + +impl Track { + pub fn into_relative_time(self) -> Self { + let (ts, msgs): (Vec<_>, Vec<_>) = self.repr.into_iter().unzip(); + let repr = ts + .into_iter() + .scan(0, |acc, t| { + let rel_time = t - *acc; + *acc = t; + Some(rel_time) + }) + .zip(msgs) + .collect(); + Self { repr } + } +} + +type Ticks = u32; // 0..2^28 + +#[derive(Debug, Eq, PartialEq)] +pub enum Message { + /// Turns off the key on the given MIDI channel. + NoteOff { + channel: Channel, + key: Key, + velocity: Velocity, + }, + + /// Turns on the key on the given MIDI channel. + NoteOn { + channel: Channel, + /// Pitch + key: Key, + velocity: Velocity, + }, + + /// Select an instrument for a given channel. + ProgramChange { channel: Channel, preset: Preset }, + + /// The time, in microseconds, of one whole note. + /// + /// E.g. using 120 beats per minute (2 beats per second), + /// that works out to 500_000 microseconds per beat, + /// which is the default value that we will use. + TempoChange(Tempo), +} + +type Key = u8; // 0..=127 +type Velocity = Volume; +type Preset = u8; // 0..=127 +type Tempo = u32; // microseconds per beat, 1..2^24 + +impl Performance { + pub fn into_midi(self, user_patch: Option<&UserPatchMap>) -> Result { + let split = self.split_by_instruments(); + let instruments: Vec<_> = split.keys().cloned().collect(); + let user_patch = user_patch.and_then(|user_patch| { + user_patch + .contains_all(&instruments) + .then_some(Cow::Borrowed(user_patch)) + }); + + let user_patch = match user_patch { + Some(x) => x, + None => Cow::Owned(UserPatchMap::with_instruments(instruments)?), + }; + + let file_type = if split.len() == 1 { + FileType::SingleTrack + } else { + FileType::MultiTrack + }; + + let tracks = split + .iter() + .map(|(i, p)| p.as_midi_track(i, &user_patch).into_relative_time()) + .collect(); + Ok(Midi { + file_type, + time_div: TimeDiv::default(), + tracks, + }) + } + + fn split_by_instruments(self) -> HashMap { + self.into_events() + .into_iter() + .map(|e| (e.instrument.clone(), e)) + .into_group_map() + .into_iter() + .map(|(k, v)| (k, Self::with_events(v))) + .collect() + } + + fn as_midi_track( + &self, + instrument: &InstrumentName, + user_patch: &UserPatchMap, + ) -> Track { + let (channel, preset) = match user_patch.lookup(instrument) { + Some(x) => x, + None => { + // TODO: Error + return Track { repr: vec![] }; + } + }; + + let setup_instrument = (0, Message::ProgramChange { channel, preset }); + let tempo = 1_000_000 / BEATS_PER_SECOND; + let set_tempo = (0, Message::TempoChange(tempo)); + + let messages = self + .iter() + .flat_map(|e| { + // TODO: sort the off properly for the infinite events + let (on, off) = e.as_midi(channel); + iter::once(on).chain(iter::once(off)) + }) + .sorted_by_key(|(t, _)| *t); + Track { + repr: iter::once(setup_instrument) + .chain(iter::once(set_tempo)) + .chain(messages) + .collect(), + } + } +} + +// beat is a quarter note +const BEATS_PER_SECOND: u32 = 2; + +type TimedMessage = (u32, Message); +type Pair = (T, T); + +impl Event { + fn as_midi(&self, channel: Channel) -> Pair { + let ticks_per_second = match TimeDiv::default() { + TimeDiv::TicksPerBeat(x) => u32::from(x) * BEATS_PER_SECOND, + }; + + let start = (self.start_time * ticks_per_second).to_integer(); + let end = ((self.start_time + self.duration) * ticks_per_second).to_integer(); + let key = self.pitch.get_inner().try_into().expect("Bad pitch"); + let velocity = self.volume.clamp(Volume::softest(), Volume::loudest()); + ( + ( + start, + Message::NoteOn { + channel, + key, + velocity, + }, + ), + ( + end, + Message::NoteOff { + channel, + key, + velocity, + }, + ), + ) + } +} diff --git a/src/io/midi/mod.rs b/src/io/midi/mod.rs new file mode 100644 index 0000000..8a3d052 --- /dev/null +++ b/src/io/midi/mod.rs @@ -0,0 +1,62 @@ +mod codec; + +use std::collections::HashMap; + +use enum_map::Enum; + +use crate::instruments::InstrumentName; + +pub use self::codec::{FileType, Message, Midi, Track}; + +// up to 16 channels +type Channel = u8; + +// up to 128 instruments +type ProgNum = u8; + +#[derive(Debug, Clone)] +pub struct UserPatchMap { + repr: HashMap, +} + +impl UserPatchMap { + const PERCUSSION: Channel = 9; + // all but Percussion + const AVAILABLE_CHANNELS: [Channel; 15] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15]; + + pub fn with_instruments(instruments: Vec) -> Result { + // TODO: extend the range of instruments by combining non-overlapping tracks + if instruments.len() > Self::AVAILABLE_CHANNELS.len() { + return Err(format!("Too many instruments: {}", instruments.len())); + } + + let map = instruments.into_iter().scan(0, |idx, instrument| { + if instrument == InstrumentName::Percussion { + Some((instrument, Self::PERCUSSION)) + } else { + let channel = Self::AVAILABLE_CHANNELS[*idx]; + *idx += 1; + Some((instrument, channel)) + } + }); + + Ok(Self { + repr: map.collect(), + }) + } + + pub fn lookup(&self, instrument: &InstrumentName) -> Option<(Channel, ProgNum)> { + self.repr.get(instrument).map(|x| { + let prog_num = match instrument { + InstrumentName::Standard(i) => i.into_usize(), + InstrumentName::Percussion => 0, + InstrumentName::Custom(_) => 0, + }; + (*x, u8::try_from(prog_num).expect("128 instruments only")) + }) + } + + pub fn contains_all(&self, instruments: &[InstrumentName]) -> bool { + instruments.iter().all(|i| self.lookup(i).is_some()) + } +} diff --git a/src/io/mod.rs b/src/io/mod.rs new file mode 100644 index 0000000..419b825 --- /dev/null +++ b/src/io/mod.rs @@ -0,0 +1 @@ +pub mod midi; diff --git a/src/lib.rs b/src/lib.rs index 1935479..19fc4f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,15 @@ pub mod instruments; +mod io; pub mod music; -pub use music::{ - duration::Dur, - interval::{AbsPitch, Interval, Octave}, - performance::{self, Performance, Player}, - phrases::{PhraseAttribute, TrillOptions}, - pitch::{Pitch, PitchClass}, - rests, Music, +pub use self::{ + io::midi, + music::{ + duration::Dur, + interval::{AbsPitch, Interval, Octave}, + performance::{self, Performance, Player}, + phrases::{PhraseAttribute, TrillOptions}, + pitch::{Pitch, PitchClass}, + rests, Music, + }, }; diff --git a/src/music/performance.rs b/src/music/performance.rs index 5a62b75..3027539 100644 --- a/src/music/performance.rs +++ b/src/music/performance.rs @@ -30,6 +30,10 @@ impl Performance { pub fn into_events(self) -> Vec { self.repr } + + pub(crate) fn iter(&self) -> impl Iterator { + self.repr.iter() + } } pub trait Performable

{