use thiserror::Error;
use sim_lib_pitch_core::{Pitch, PitchClass};
use sim_lib_pitch_set::PitchClassMask;
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum PitchScaleError {
#[error("pitch class {0} is not in the scale")]
PitchClassOutOfScale(u8),
#[error("scale must contain at least one pitch class")]
EmptyScale,
#[error("scale interval {0} is outside 0..12")]
InvalidScaleInterval(u8),
#[error("invalid scale degree {0}")]
InvalidScaleDegree(usize),
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Mode {
Major,
MinorNatural,
MinorHarmonic,
MinorMelodic,
Dorian,
Phrygian,
Lydian,
Mixolydian,
Aeolian,
Locrian,
WholeTone,
Diminished,
Chromatic,
}
impl Mode {
pub fn intervals(self) -> &'static [u8] {
match self {
Self::Major => &[0, 2, 4, 5, 7, 9, 11],
Self::MinorNatural | Self::Aeolian => &[0, 2, 3, 5, 7, 8, 10],
Self::MinorHarmonic => &[0, 2, 3, 5, 7, 8, 11],
Self::MinorMelodic => &[0, 2, 3, 5, 7, 9, 11],
Self::Dorian => &[0, 2, 3, 5, 7, 9, 10],
Self::Phrygian => &[0, 1, 3, 5, 7, 8, 10],
Self::Lydian => &[0, 2, 4, 6, 7, 9, 11],
Self::Mixolydian => &[0, 2, 4, 5, 7, 9, 10],
Self::Locrian => &[0, 1, 3, 5, 6, 8, 10],
Self::WholeTone => &[0, 2, 4, 6, 8, 10],
Self::Diminished => &[0, 2, 3, 5, 6, 8, 9, 11],
Self::Chromatic => &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
}
}
pub fn name(self) -> &'static str {
match self {
Self::Major => "major",
Self::MinorNatural => "minor-natural",
Self::MinorHarmonic => "minor-harmonic",
Self::MinorMelodic => "minor-melodic",
Self::Dorian => "dorian",
Self::Phrygian => "phrygian",
Self::Lydian => "lydian",
Self::Mixolydian => "mixolydian",
Self::Aeolian => "aeolian",
Self::Locrian => "locrian",
Self::WholeTone => "whole-tone",
Self::Diminished => "diminished",
Self::Chromatic => "chromatic",
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Key {
pub tonic: PitchClass,
pub mode: Mode,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Scale {
pub tonic: PitchClass,
pub mode: Mode,
}
impl Scale {
pub const fn new(tonic: PitchClass, mode: Mode) -> Self {
Self { tonic, mode }
}
pub const fn major(tonic: PitchClass) -> Self {
Self::new(tonic, Mode::Major)
}
pub const fn minor_natural(tonic: PitchClass) -> Self {
Self::new(tonic, Mode::MinorNatural)
}
pub const fn minor_harmonic(tonic: PitchClass) -> Self {
Self::new(tonic, Mode::MinorHarmonic)
}
pub const fn minor_melodic(tonic: PitchClass) -> Self {
Self::new(tonic, Mode::MinorMelodic)
}
pub const fn dorian(tonic: PitchClass) -> Self {
Self::new(tonic, Mode::Dorian)
}
pub const fn phrygian(tonic: PitchClass) -> Self {
Self::new(tonic, Mode::Phrygian)
}
pub const fn lydian(tonic: PitchClass) -> Self {
Self::new(tonic, Mode::Lydian)
}
pub const fn mixolydian(tonic: PitchClass) -> Self {
Self::new(tonic, Mode::Mixolydian)
}
pub const fn aeolian(tonic: PitchClass) -> Self {
Self::new(tonic, Mode::Aeolian)
}
pub const fn locrian(tonic: PitchClass) -> Self {
Self::new(tonic, Mode::Locrian)
}
pub const fn whole_tone(tonic: PitchClass) -> Self {
Self::new(tonic, Mode::WholeTone)
}
pub const fn diminished(tonic: PitchClass) -> Self {
Self::new(tonic, Mode::Diminished)
}
pub const fn chromatic(tonic: PitchClass) -> Self {
Self::new(tonic, Mode::Chromatic)
}
pub fn pitch_classes(self) -> Vec<PitchClass> {
self.mode
.intervals()
.iter()
.map(|step| self.tonic.transpose(i32::from(*step)))
.collect()
}
pub fn mask(self) -> PitchClassMask {
PitchClassMask::from_pitch_classes(&self.pitch_classes())
}
pub fn degree_of(self, pitch_class: PitchClass) -> Option<usize> {
self.pitch_classes()
.iter()
.position(|candidate| *candidate == pitch_class)
.map(|index| index + 1)
}
pub fn pitch_at_degree(self, degree: usize) -> Result<PitchClass, PitchScaleError> {
self.try_pitch_at_degree(degree)
}
pub fn try_pitch_at_degree(self, degree: usize) -> Result<PitchClass, PitchScaleError> {
let index = degree
.checked_sub(1)
.ok_or(PitchScaleError::InvalidScaleDegree(degree))?;
let intervals = self.mode.intervals();
Ok(self
.tonic
.transpose(i32::from(intervals[index % intervals.len()])))
}
pub fn transpose_diatonic(self, pitch: Pitch, steps: i32) -> Result<Pitch, PitchScaleError> {
let degree = self
.degree_of(pitch.class)
.ok_or(PitchScaleError::PitchClassOutOfScale(pitch.class.value()))?;
let intervals = self.mode.intervals();
let start = degree as i32 - 1;
let target = start + steps;
let width = intervals.len() as i32;
let octave_delta = target.div_euclid(width);
let target_index = target.rem_euclid(width) as usize;
let start_semitones = i32::from(intervals[start as usize]);
let end_semitones = i32::from(intervals[target_index]) + octave_delta * 12;
Ok(pitch.transpose(end_semitones - start_semitones))
}
pub fn chord_tone_to_scale_tone(chord_tone: usize) -> Result<usize, PitchScaleError> {
let offset = chord_tone
.checked_sub(1)
.ok_or(PitchScaleError::InvalidScaleDegree(chord_tone))?;
offset
.checked_mul(2)
.and_then(|value| value.checked_add(1))
.ok_or(PitchScaleError::InvalidScaleDegree(chord_tone))
}
pub fn scale_tone_to_diatonic(scale_tone: usize) -> Result<i32, PitchScaleError> {
let offset = scale_tone
.checked_sub(1)
.ok_or(PitchScaleError::InvalidScaleDegree(scale_tone))?;
i32::try_from(offset).map_err(|_| PitchScaleError::InvalidScaleDegree(scale_tone))
}
}