use std::collections::BTreeMap;
use num_rational::Ratio;
use thiserror::Error;
use sim_lib_music_core::{
Articulation, AtomRef, Channel, ConversionError, Melody, MelodyItem, Music, MusicError,
MusicObject, Note, PianoRoll, Rest, Time, TimedNote,
};
use sim_lib_pitch_core::{Pitch, PitchClass};
use sim_lib_pitch_scale::{Key, Mode, PitchScaleError, Scale};
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum TransformError {
#[error("transform factor must be positive")]
InvalidFactor,
#[error(transparent)]
InvalidMusic(#[from] MusicError),
#[error("{transform} transform returned invalid output: {reason}")]
InvalidTransformOutput {
transform: &'static str,
reason: &'static str,
},
#[error(transparent)]
InvalidStaff(#[from] ConversionError),
#[error(transparent)]
Assignment(#[from] sim_lib_discrete_graph::GraphError),
}
#[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,
) -> Result<Pitch, PitchScaleError> {
Ok(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) -> Result<Music, TransformError> {
retrograde_with_mode(object, RetrogradeMode::Cutout)
}
pub fn retrograde_with_mode(
object: &dyn MusicObject,
mode: RetrogradeMode,
) -> Result<Music, TransformError> {
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()
}
};
Ok(Music::PianoRoll(canonical_roll(items)?))
}
pub fn time_invert(object: &dyn MusicObject) -> Result<Music, TransformError> {
let roll = to_piano_roll(object)?;
if roll.items.is_empty() {
return Ok(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;
}
Ok(Music::PianoRoll(canonical_roll(items)?))
}
pub fn loop_n(object: &dyn MusicObject, n: usize) -> Result<Music, TransformError> {
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();
Ok(Music::PianoRoll(canonical_roll(items)?))
}
pub fn slice(object: &dyn MusicObject, start: Time, end: Time) -> Result<Music, TransformError> {
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();
Ok(Music::PianoRoll(canonical_roll(items)?))
}
pub fn transpose(object: &dyn MusicObject, semitones: i32) -> Result<Music, TransformError> {
map_notes(object, |note| Note {
pitch: note.pitch.transpose(semitones),
..note
})
}
pub fn transpose_diatonic(
object: &dyn MusicObject,
scale: &Scale,
steps: i32,
) -> Result<Music, TransformError> {
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) -> Result<Music, TransformError> {
map_notes(object, |note| Note {
pitch: note.pitch.invert(axis),
..note
})
}
pub fn retrograde_invert(object: &dyn MusicObject, axis: Pitch) -> Result<Music, TransformError> {
let inverted = pitch_invert(object, axis)?;
retrograde(&inverted)
}
pub fn shift_octave(object: &dyn MusicObject, octaves: i16) -> Result<Music, TransformError> {
map_notes(object, |note| Note {
pitch: note.pitch.transpose(i32::from(octaves) * 12),
..note
})
}
pub fn chord_tones_in(object: &dyn MusicObject, scale: &Scale) -> Result<Music, TransformError> {
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,
) -> Result<Music, TransformError> {
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)
.unwrap_or(note.pitch),
..note
},
None => note,
}
})
}
fn scale_time(object: &dyn MusicObject, factor: Time) -> Result<Music, TransformError> {
if factor <= Time::from_integer(0) {
return Err(TransformError::InvalidFactor);
}
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,
) -> Result<Music, TransformError> {
map_roll(object, |mut item| {
item.note = f(item.note);
item
})
}
pub(crate) fn map_roll(
object: &dyn MusicObject,
f: impl Fn(TimedNote) -> TimedNote,
) -> Result<Music, TransformError> {
let roll = to_piano_roll(object)?;
let items = roll.items.into_iter().map(f).collect();
Ok(Music::PianoRoll(canonical_roll(items)?))
}
pub(crate) fn to_piano_roll(object: &dyn MusicObject) -> Result<PianoRoll, TransformError> {
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>) -> Result<PianoRoll, TransformError> {
Ok(PianoRoll::new(items)?)
}
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()
}