use std::collections::BTreeMap;
use sim_lib_music_core::{Music, MusicObject};
use sim_lib_pitch_chord::Chord;
use sim_lib_pitch_core::{Pitch, PitchClass};
use sim_lib_pitch_scale::Scale;
use crate::pitch_map::{MapError, PitchMap, PitchMapPolicy};
use crate::{
CallablePitchMap, TransformDiagnostic, TransformDiagnosticCode, TransformError,
TransformReport, map_pitches_with_diagnostics, nearest_pitch_in_scale, note_with_pitch,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TuningRemap {
pub cents: i32,
}
impl TuningRemap {
pub fn new(cents: i32) -> Self {
Self { cents }
}
pub fn semitone_delta(&self) -> i32 {
(f64::from(self.cents) / 100.0).round() as i32
}
pub fn pitch_map(&self) -> PitchMap {
PitchMap::chromatic_delta(self.semitone_delta())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IntMatrix {
pub degree_row: [i32; 3],
pub octave_row: [i32; 3],
pub divisor: i32,
}
impl IntMatrix {
pub fn new(degree_row: [i32; 3], octave_row: [i32; 3], divisor: i32) -> Self {
Self {
degree_row,
octave_row,
divisor,
}
}
pub fn identity() -> Self {
Self::new([1, 0, 0], [0, 1, 0], 1)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PitchRemap {
Chromatic(i32),
ScaleDegree {
scale: Scale,
steps: i32,
},
PitchClass {
from: PitchClass,
to: PitchClass,
},
DrumKey(BTreeMap<u8, u8>),
ChordTone {
scale: Scale,
degree: usize,
},
Tuning(TuningRemap),
Vector {
scale: Scale,
offsets: Vec<i32>,
},
Matrix {
scale: Scale,
matrix: IntMatrix,
},
Callable(CallablePitchMap),
Map(PitchMap),
}
impl PitchRemap {
pub fn apply(&self, object: &dyn MusicObject) -> Result<Music, TransformError> {
Ok(self.apply_report(object)?.music)
}
pub fn apply_report(
&self,
object: &dyn MusicObject,
) -> Result<TransformReport, TransformError> {
match self {
Self::Chromatic(semitones) => {
Ok(TransformReport::clean(crate::map_notes(object, |note| {
let pitch = note.pitch.transpose(*semitones);
note_with_pitch(note, pitch)
})?))
}
Self::ScaleDegree { scale, steps } => {
map_pitches_with_diagnostics(object, "pitch-remap", |pitch| {
scale
.transpose_diatonic(pitch, *steps)
.map_err(|_| out_of_scale_diagnostic("pitch-remap", pitch, "scale remap"))
})
}
Self::PitchClass { from, to } => {
Ok(TransformReport::clean(crate::map_notes(object, |note| {
let pitch = if note.pitch.class == *from {
Pitch {
class: *to,
octave: note.pitch.octave,
}
} else {
note.pitch
};
note_with_pitch(note, pitch)
})?))
}
Self::DrumKey(map) => map_pitches_with_diagnostics(object, "pitch-remap", |pitch| {
let Some(key) = pitch.to_midi() else {
return Err(TransformDiagnostic::new(
TransformDiagnosticCode::MissingMidiKey,
"pitch-remap",
"drum-key remap needs a MIDI key",
));
};
Ok(map
.get(&key)
.map(|mapped| Pitch::from_midi(*mapped))
.unwrap_or(pitch))
}),
Self::ChordTone { scale, degree } => {
map_pitches_with_diagnostics(object, "pitch-remap", |pitch| {
nearest_pitch_in_chord(pitch, *scale, *degree)
})
}
Self::Tuning(tuning) => Ok(TransformReport::clean(crate::map_notes(object, |note| {
let pitch = note.pitch.transpose(tuning.semitone_delta());
note_with_pitch(note, pitch)
})?)),
Self::Vector { scale, offsets } => {
map_pitches_with_diagnostics(object, "pitch-remap", |pitch| {
vector_remap(pitch, *scale, offsets)
})
}
Self::Matrix { scale, matrix } => {
map_pitches_with_diagnostics(object, "pitch-remap", |pitch| {
matrix_remap(pitch, *scale, matrix)
})
}
Self::Callable(map) => Ok(TransformReport::clean(crate::map_notes(object, |note| {
let pitch = map.map_pitch(note.pitch);
note_with_pitch(note, pitch)
})?)),
Self::Map(map) => map_pitches_with_diagnostics(object, "pitch-remap", |pitch| {
map.map_pitch(pitch)
.map(|result| result.pitch)
.map_err(|error| map_error_diagnostic(error, "pitch-remap"))
}),
}
}
pub fn as_pitch_map(&self) -> Option<PitchMap> {
match self {
Self::Chromatic(semitones) => Some(PitchMap::chromatic_delta(*semitones)),
Self::PitchClass { from, to } => Some(PitchMap::pitch_class_substitution(
*from,
*to,
PitchMapPolicy::Reject,
)),
Self::Tuning(tuning) => Some(tuning.pitch_map()),
Self::Map(map) => Some(map.clone()),
Self::ScaleDegree { .. }
| Self::DrumKey(_)
| Self::ChordTone { .. }
| Self::Vector { .. }
| Self::Matrix { .. }
| Self::Callable(_) => None,
}
}
}
fn map_error_diagnostic(error: MapError, transform: &'static str) -> TransformDiagnostic {
TransformDiagnostic::new(
TransformDiagnosticCode::UnsupportedMapping,
transform,
error.to_string(),
)
}
fn vector_remap(pitch: Pitch, scale: Scale, offsets: &[i32]) -> Result<Pitch, TransformDiagnostic> {
if offsets.is_empty() {
return Err(TransformDiagnostic::new(
TransformDiagnosticCode::UnsupportedMapping,
"pitch-remap",
"vector remap needs at least one offset",
));
}
let degree = scale
.degree_of(pitch.class)
.ok_or_else(|| out_of_scale_diagnostic("pitch-remap", pitch, "vector remap"))?;
let offset = offsets[(degree - 1) % offsets.len()];
Ok(pitch.transpose(offset))
}
fn matrix_remap(
pitch: Pitch,
scale: Scale,
matrix: &IntMatrix,
) -> Result<Pitch, TransformDiagnostic> {
if matrix.divisor <= 0 {
return Err(TransformDiagnostic::new(
TransformDiagnosticCode::InvalidMatrix,
"pitch-remap",
"matrix divisor must be positive",
));
}
let degree = scale
.degree_of(pitch.class)
.ok_or_else(|| out_of_scale_diagnostic("pitch-remap", pitch, "matrix remap"))?
as i32;
let input = [degree, i32::from(pitch.octave), 1];
let target_degree = dot(matrix.degree_row, input) / matrix.divisor;
if target_degree <= 0 {
return Err(TransformDiagnostic::new(
TransformDiagnosticCode::InvalidMatrix,
"pitch-remap",
"matrix remap produced a non-positive scale degree",
));
}
let target_octave = dot(matrix.octave_row, input) / matrix.divisor;
let octave = i16::try_from(target_octave).map_err(|_| {
TransformDiagnostic::new(
TransformDiagnosticCode::InvalidMatrix,
"pitch-remap",
"matrix remap produced an octave outside the supported range",
)
})?;
let target_degree = usize::try_from(target_degree).map_err(|_| {
TransformDiagnostic::new(
TransformDiagnosticCode::InvalidMatrix,
"pitch-remap",
"matrix remap produced a scale degree outside the supported range",
)
})?;
let class = scale.pitch_at_degree(target_degree).map_err(|_| {
TransformDiagnostic::new(
TransformDiagnosticCode::InvalidMatrix,
"pitch-remap",
"matrix remap produced a non-positive scale degree",
)
})?;
Ok(Pitch { class, octave })
}
fn nearest_pitch_in_chord(
pitch: Pitch,
scale: Scale,
degree: usize,
) -> Result<Pitch, TransformDiagnostic> {
let chord = Chord::chord_tones_in(scale, degree, pitch.octave).map_err(|_| {
TransformDiagnostic::new(
TransformDiagnosticCode::UnsupportedMapping,
"pitch-remap",
"chord-tone remap needs a one-based scale degree",
)
})?;
Ok(chord
.pitches()
.into_iter()
.min_by_key(|candidate| {
(
(candidate.semitone() - pitch.semitone()).abs(),
candidate.semitone(),
)
})
.unwrap_or_else(|| nearest_pitch_in_scale(pitch, &scale)))
}
fn out_of_scale_diagnostic(
transform: &'static str,
pitch: Pitch,
action: &'static str,
) -> TransformDiagnostic {
TransformDiagnostic::new(
TransformDiagnosticCode::PitchOutOfScale,
transform,
format!("{action} cannot place pitch class {}", pitch.class.value()),
)
}
fn dot(row: [i32; 3], input: [i32; 3]) -> i32 {
row.into_iter()
.zip(input)
.map(|(left, right)| left * right)
.sum()
}