use std::{
fmt::{Debug, Display, Formatter, Result as FmtResult},
num::ParseIntError,
str::FromStr,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct MidiNote {
pub note: i16,
}
impl MidiNote {
#[must_use]
pub const fn new(note: i16) -> Self {
Self { note }
}
pointillism_macros::midi!();
}
impl Default for MidiNote {
fn default() -> Self {
Self::A4
}
}
#[cfg(feature = "midly")]
impl From<midly::num::u7> for MidiNote {
fn from(value: midly::num::u7) -> Self {
Self::new(i16::from(value.as_int()))
}
}
#[must_use]
pub const fn letter_to_note(letter: char) -> Option<u8> {
match letter {
'C' => Some(0),
'D' => Some(2),
'E' => Some(4),
'F' => Some(5),
'G' => Some(7),
'A' => Some(9),
'B' => Some(11),
_ => None,
}
}
#[must_use]
pub const fn note_to_letter(note: u8) -> &'static str {
match note {
0 => "C",
1 => "C#",
2 => "D",
3 => "D#",
4 => "E",
5 => "F",
6 => "F#",
7 => "G",
8 => "G#",
9 => "A",
10 => "A#",
11 => "B",
_ => panic!("invalid note"),
}
}
#[derive(Clone, Debug)]
pub enum NameError {
Short,
Letter(char),
Parse(ParseIntError),
}
impl From<ParseIntError> for NameError {
fn from(value: ParseIntError) -> Self {
NameError::Parse(value)
}
}
impl Display for NameError {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
match self {
Self::Short => write!(f, "the string was too short"),
Self::Letter(c) => write!(f, "letter {c} is invalid"),
Self::Parse(err) => write!(f, "integer parsing error: {err}"),
}
}
}
impl std::error::Error for NameError {}
impl FromStr for MidiNote {
type Err = NameError;
fn from_str(name: &str) -> Result<Self, NameError> {
let mut chars = name.chars();
if let (Some(letter), Some(next)) = (chars.next(), chars.next()) {
if let Some(note) = letter_to_note(letter) {
let mut note = i16::from(note);
let index = match next {
'#' => {
note += 1;
2
}
'b' => {
note -= 1;
2
}
_ => 1,
};
note += 12 * (name[index..].parse::<i16>()? + 1);
Ok(MidiNote::new(note))
} else {
Err(NameError::Letter(letter))
}
} else {
Err(NameError::Short)
}
}
}
impl Display for MidiNote {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
#[allow(clippy::cast_possible_truncation)]
let letter = note_to_letter(self.note.rem_euclid(12) as u8);
let octave = isize::from(self.note / 12) - 1;
write!(f, "{letter}{octave}")
}
}