use std::str::FromStr;
use thiserror::Error;
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum PitchError {
#[error("invalid pitch class {0}")]
InvalidPitchClass(u8),
#[error("invalid pitch spelling")]
InvalidPitch,
#[error("invalid interval spelling")]
InvalidInterval,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PitchClass(pub u8);
impl PitchClass {
pub const C: Self = Self(0);
pub const CS: Self = Self(1);
pub const D: Self = Self(2);
pub const DS: Self = Self(3);
pub const E: Self = Self(4);
pub const F: Self = Self(5);
pub const FS: Self = Self(6);
pub const G: Self = Self(7);
pub const GS: Self = Self(8);
pub const A: Self = Self(9);
pub const AS: Self = Self(10);
pub const B: Self = Self(11);
pub fn new(value: u8) -> Result<Self, PitchError> {
if value < 12 {
Ok(Self(value))
} else {
Err(PitchError::InvalidPitchClass(value))
}
}
pub fn transpose(self, semitones: i32) -> Self {
Self(((self.0 as i32 + semitones).rem_euclid(12)) as u8)
}
pub fn invert(self, axis: PitchClass) -> Self {
Self(((2 * axis.0 as i32 - self.0 as i32).rem_euclid(12)) as u8)
}
pub fn interval_class(self, other: PitchClass) -> u8 {
let delta = (other.0 as i32 - self.0 as i32).rem_euclid(12) as u8;
delta.min(12 - delta)
}
pub fn canonical_name(self) -> &'static str {
match self.0 {
0 => "C",
1 => "C#",
2 => "D",
3 => "D#",
4 => "E",
5 => "F",
6 => "F#",
7 => "G",
8 => "G#",
9 => "A",
10 => "A#",
11 => "B",
_ => unreachable!(),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Pitch {
pub class: PitchClass,
pub octave: i16,
}
impl Pitch {
pub fn semitone(self) -> i32 {
(self.octave as i32 + 1) * 12 + self.class.0 as i32
}
pub fn from_semitone(semitone: i32) -> Self {
Self {
class: PitchClass(semitone.rem_euclid(12) as u8),
octave: (semitone.div_euclid(12) - 1) as i16,
}
}
pub fn to_midi(self) -> Option<u8> {
let semitone = self.semitone();
(0..=127).contains(&semitone).then_some(semitone as u8)
}
pub fn from_midi(value: u8) -> Self {
Self::from_semitone(value as i32)
}
pub fn transpose(self, semitones: i32) -> Self {
Self::from_semitone(self.semitone() + semitones)
}
pub fn invert(self, axis: Pitch) -> Self {
Self::from_semitone(2 * axis.semitone() - self.semitone())
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Letter {
C,
D,
E,
F,
G,
A,
B,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct SpelledPitch {
pub letter: Letter,
pub accidental: i8,
pub octave: i16,
}
impl SpelledPitch {
pub fn to_pitch(self) -> Pitch {
let base = match self.letter {
Letter::C => 0,
Letter::D => 2,
Letter::E => 4,
Letter::F => 5,
Letter::G => 7,
Letter::A => 9,
Letter::B => 11,
};
Pitch {
class: PitchClass((base + self.accidental as i32).rem_euclid(12) as u8),
octave: self.octave,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Interval {
pub semitones: i32,
}
impl Interval {
pub const UNISON: Self = Self { semitones: 0 };
pub const MINOR_3: Self = Self { semitones: 3 };
pub const MAJOR_3: Self = Self { semitones: 4 };
pub const PERFECT_5: Self = Self { semitones: 7 };
pub const TRITONE: Self = Self { semitones: 6 };
pub const MAJOR_7: Self = Self { semitones: 11 };
pub fn between(a: Pitch, b: Pitch) -> Self {
Self {
semitones: b.semitone() - a.semitone(),
}
}
pub fn class(self) -> u8 {
let delta = self.semitones.rem_euclid(12) as u8;
delta.min(12 - delta)
}
}
impl FromStr for Pitch {
type Err = PitchError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
parse_pitch(value)
}
}
impl FromStr for Interval {
type Err = PitchError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
parse_interval(value)
}
}
pub fn parse_pitch(value: &str) -> Result<Pitch, PitchError> {
let mut chars = value.chars();
let letter = match chars.next() {
Some('C') => Letter::C,
Some('D') => Letter::D,
Some('E') => Letter::E,
Some('F') => Letter::F,
Some('G') => Letter::G,
Some('A') => Letter::A,
Some('B') => Letter::B,
_ => return Err(PitchError::InvalidPitch),
};
let rest = chars.as_str();
let (accidental, octave_str) = if let Some(rest) = rest.strip_prefix('#') {
(1, rest)
} else if let Some(rest) = rest.strip_prefix('s') {
(1, rest)
} else if let Some(rest) = rest.strip_prefix('b') {
(-1, rest)
} else {
(0, rest)
};
if octave_str.is_empty() {
return Err(PitchError::InvalidPitch);
}
let octave = octave_str
.parse::<i16>()
.map_err(|_| PitchError::InvalidPitch)?;
Ok(SpelledPitch {
letter,
accidental,
octave,
}
.to_pitch())
}
pub fn parse_interval(value: &str) -> Result<Interval, PitchError> {
match value {
"P5" => Ok(Interval::PERFECT_5),
"m3" => Ok(Interval::MINOR_3),
"M7" => Ok(Interval::MAJOR_7),
"TT" => Ok(Interval::TRITONE),
_ => Err(PitchError::InvalidInterval),
}
}