use std::collections::BTreeMap;
use num_rational::Ratio;
use thiserror::Error;
use sim_lib_music_core::{
Articulation, AtomRef, Channel, Melody, MelodyItem, Music, MusicObject, Note, PianoRoll, Rest,
Time, TimedNote,
};
use sim_lib_pitch_core::{Pitch, PitchClass};
use sim_lib_pitch_scale::{Key, Mode, Scale};
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum TransformError {
#[error("transform factor must be positive")]
InvalidFactor,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum RetrogradeMode {
Cutout,
PinnedNoteOn,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FunctionMap {
Major,
MinorNatural,
MinorHarmonic,
MinorMelodicAsc,
Dorian,
Phrygian,
Lydian,
Mixolydian,
Locrian,
Custom(Scale),
}
impl FunctionMap {
pub fn name(&self) -> &'static str {
match self {
Self::Major => "Major",
Self::MinorNatural => "MinorNatural",
Self::MinorHarmonic => "MinorHarmonic",
Self::MinorMelodicAsc => "MinorMelodicAsc",
Self::Dorian => "Dorian",
Self::Phrygian => "Phrygian",
Self::Lydian => "Lydian",
Self::Mixolydian => "Mixolydian",
Self::Locrian => "Locrian",
Self::Custom(_) => "Custom",
}
}
pub fn scale_for_key(&self, key: &Key) -> Scale {
match self {
Self::Major => Scale::new(key.tonic, Mode::Major),
Self::MinorNatural => Scale::new(key.tonic, Mode::MinorNatural),
Self::MinorHarmonic => Scale::new(key.tonic, Mode::MinorHarmonic),
Self::MinorMelodicAsc => Scale::new(key.tonic, Mode::MinorMelodic),
Self::Dorian => Scale::new(key.tonic, Mode::Dorian),
Self::Phrygian => Scale::new(key.tonic, Mode::Phrygian),
Self::Lydian => Scale::new(key.tonic, Mode::Lydian),
Self::Mixolydian => Scale::new(key.tonic, Mode::Mixolydian),
Self::Locrian => Scale::new(key.tonic, Mode::Locrian),
Self::Custom(scale) => *scale,
}
}
pub fn degree_to_pitch(&self, degree: usize, key: &Key, octave: i16) -> Pitch {
Pitch {
class: self.scale_for_key(key).pitch_at_degree(degree),
octave,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct FunctionMapRegistry {
maps: BTreeMap<String, FunctionMap>,
}
impl FunctionMapRegistry {
pub fn new_with_builtins() -> Self {
let mut registry = Self::default();
for map in [
FunctionMap::Major,
FunctionMap::MinorNatural,
FunctionMap::MinorHarmonic,
FunctionMap::MinorMelodicAsc,
FunctionMap::Dorian,
FunctionMap::Phrygian,
FunctionMap::Lydian,
FunctionMap::Mixolydian,
FunctionMap::Locrian,
] {
registry.register(map);
}
registry
}
pub fn register(&mut self, map: FunctionMap) {
self.maps.insert(map.name().to_owned(), map);
}
pub fn get(&self, name: &str) -> Option<&FunctionMap> {
self.maps.get(name)
}
pub fn names(&self) -> Vec<&str> {
self.maps.keys().map(String::as_str).collect()
}
}
pub fn augment(object: &dyn MusicObject, factor: Time) -> Result<Music, TransformError> {
scale_time(object, factor)
}
pub fn diminish(object: &dyn MusicObject, factor: Time) -> Result<Music, TransformError> {
if factor <= Time::from_integer(0) {
return Err(TransformError::InvalidFactor);
}
scale_time(object, factor.recip())
}
pub fn retrograde(object: &dyn MusicObject) -> Music {
retrograde_with_mode(object, RetrogradeMode::Cutout)
}
pub fn retrograde_with_mode(object: &dyn MusicObject, mode: RetrogradeMode) -> Music {
let roll = to_piano_roll(object);
let total = object.duration();
let items = match mode {
RetrogradeMode::Cutout => roll
.items
.into_iter()
.map(|item| TimedNote {
onset: total - item.onset - item.note.duration,
note: item.note,
})
.collect(),
RetrogradeMode::PinnedNoteOn => {
let mut onsets: Vec<Time> = roll.items.iter().map(|item| item.onset).collect();
onsets.sort();
let notes: Vec<Note> = roll.items.into_iter().rev().map(|item| item.note).collect();
onsets
.into_iter()
.zip(notes)
.map(|(onset, note)| TimedNote { onset, note })
.collect()
}
};
Music::PianoRoll(canonical_roll(items))
}
pub fn time_invert(object: &dyn MusicObject) -> Music {
let roll = to_piano_roll(object);
if roll.items.is_empty() {
return Music::PianoRoll(roll);
}
let total = object.duration();
let mut items: Vec<TimedNote> = roll
.items
.into_iter()
.map(|item| TimedNote {
onset: total - item.onset,
note: item.note,
})
.collect();
let min_onset = items
.iter()
.map(|item| item.onset)
.min()
.unwrap_or_else(|| Time::from_integer(0));
for item in &mut items {
item.onset -= min_onset;
}
Music::PianoRoll(canonical_roll(items))
}
pub fn loop_n(object: &dyn MusicObject, n: usize) -> Music {
let roll = to_piano_roll(object);
let span = object.duration();
let items = (0..n)
.flat_map(|index| {
let offset = span * Time::from_integer(index as i64);
roll.items.iter().cloned().map(move |mut item| {
item.onset += offset;
item
})
})
.collect();
Music::PianoRoll(canonical_roll(items))
}
pub fn slice(object: &dyn MusicObject, start: Time, end: Time) -> Music {
let roll = to_piano_roll(object);
let items = roll
.items
.into_iter()
.filter_map(|item| {
let item_start = item.onset;
let item_end = item.onset + item.note.duration;
let clipped_start = item_start.max(start);
let clipped_end = item_end.min(end);
(clipped_start < clipped_end).then(|| TimedNote {
onset: clipped_start - start,
note: Note {
duration: clipped_end - clipped_start,
..item.note
},
})
})
.collect();
Music::PianoRoll(canonical_roll(items))
}
pub fn transpose(object: &dyn MusicObject, semitones: i32) -> Music {
map_notes(object, |note| Note {
pitch: note.pitch.transpose(semitones),
..note
})
}
pub fn transpose_diatonic(object: &dyn MusicObject, scale: &Scale, steps: i32) -> Music {
map_notes(object, |note| Note {
pitch: scale
.transpose_diatonic(note.pitch, steps)
.unwrap_or(note.pitch),
..note
})
}
pub fn pitch_invert(object: &dyn MusicObject, axis: Pitch) -> Music {
map_notes(object, |note| Note {
pitch: note.pitch.invert(axis),
..note
})
}
pub fn retrograde_invert(object: &dyn MusicObject, axis: Pitch) -> Music {
retrograde(&pitch_invert(object, axis))
}
pub fn shift_octave(object: &dyn MusicObject, octaves: i16) -> Music {
map_notes(object, |note| Note {
pitch: note.pitch.transpose(i32::from(octaves) * 12),
..note
})
}
pub fn chord_tones_in(object: &dyn MusicObject, scale: &Scale) -> Music {
map_notes(object, |note| Note {
pitch: nearest_pitch_in_scale(note.pitch, scale),
..note
})
}
pub fn map_to_function(object: &dyn MusicObject, key: &Key, fmap: &FunctionMap) -> Music {
let source_scale = Scale::new(key.tonic, key.mode);
map_notes(object, |note| {
match source_scale.degree_of(note.pitch.class) {
Some(degree) => Note {
pitch: fmap.degree_to_pitch(degree, key, note.pitch.octave),
..note
},
None => note,
}
})
}
fn scale_time(object: &dyn MusicObject, factor: Time) -> Result<Music, TransformError> {
if factor <= Time::from_integer(0) {
return Err(TransformError::InvalidFactor);
}
Ok(map_roll(object, |mut item| {
item.onset *= factor;
item.note.duration *= factor;
item
}))
}
pub(crate) fn map_notes(object: &dyn MusicObject, f: impl Fn(Note) -> Note) -> Music {
map_roll(object, |mut item| {
item.note = f(item.note);
item
})
}
pub(crate) fn map_roll(object: &dyn MusicObject, f: impl Fn(TimedNote) -> TimedNote) -> Music {
let roll = to_piano_roll(object);
let items = roll.items.into_iter().map(f).collect();
Music::PianoRoll(canonical_roll(items))
}
pub(crate) fn to_piano_roll(object: &dyn MusicObject) -> PianoRoll {
let mut atoms = Vec::new();
object.voices(Time::from_integer(0), &mut atoms);
let items = atoms
.into_iter()
.filter_map(|atom| match atom.atom {
AtomRef::Note(note) => Some(TimedNote {
onset: atom.onset,
note,
}),
AtomRef::Rest(_) | AtomRef::Phantom(_) => None,
})
.collect();
canonical_roll(items)
}
pub(crate) fn canonical_roll(items: Vec<TimedNote>) -> PianoRoll {
PianoRoll::new(items).expect("transform output is canonical")
}
pub(crate) fn nearest_pitch_in_scale(pitch: Pitch, scale: &Scale) -> Pitch {
if scale.degree_of(pitch.class).is_some() {
return pitch;
}
let candidates = scale
.pitch_classes()
.into_iter()
.flat_map(|class| {
[
Pitch {
class,
octave: pitch.octave - 1,
},
Pitch {
class,
octave: pitch.octave,
},
Pitch {
class,
octave: pitch.octave + 1,
},
]
})
.collect::<Vec<_>>();
candidates
.into_iter()
.min_by_key(|candidate| {
(
(candidate.semitone() - pitch.semitone()).abs(),
candidate.semitone(),
)
})
.unwrap_or(pitch)
}
pub fn simple_melody(items: &[(u8, Time)]) -> Melody {
Melody::new(
items
.iter()
.map(|(midi, duration)| {
MelodyItem::Note(
Note::new(
*duration,
Pitch::from_midi(*midi),
100,
Channel::new(0).expect("channel"),
Articulation::Normal,
)
.expect("note"),
)
})
.collect(),
)
.expect("melody")
}
pub fn silence(duration: Time) -> Rest {
Rest::new(duration).expect("rest")
}
pub fn quarter() -> Time {
Ratio::new(1, 4)
}
pub fn pitch_class_name(class: PitchClass) -> &'static str {
class.canonical_name()
}