-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
288 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Track<Ticks>>, | ||
} | ||
|
||
#[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<T> { | ||
repr: Vec<(T, Message)>, | ||
} | ||
|
||
impl Track<Ticks> { | ||
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<Midi, String> { | ||
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<InstrumentName, Self> { | ||
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<Ticks> { | ||
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, T); | ||
|
||
impl Event { | ||
fn as_midi(&self, channel: Channel) -> Pair<TimedMessage> { | ||
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, | ||
}, | ||
), | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<InstrumentName, Channel>, | ||
} | ||
|
||
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<InstrumentName>) -> Result<Self, String> { | ||
// 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()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pub mod midi; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters