1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
//! Defines the type for a MIDI [`Note`], and its basic methods.
use std::{
fmt::{Debug, Display, Formatter, Result as FmtResult},
num::ParseIntError,
str::FromStr,
};
/// A MIDI note. Note that `C4 = 60`, `A4 = 69`.
///
/// We use a 16-bit unsigned integer to store the MIDI note index. This is much larger than the MIDI
/// specification, which only uses values from 0-127. The main reason is so that methods that
/// convert [`RawFreq`](crate::prelude::RawFreq) into [`Note`] and viceversa don't run out of range.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct MidiNote {
/// The MIDI note index.
pub note: i16,
}
impl MidiNote {
/// Initializes a new [`Note`].
#[must_use]
pub const fn new(note: i16) -> Self {
Self { note }
}
}
/// We use `A4` as a default note.
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()))
}
}
/// Converts a letter to a numeric note, from 0 to 11.
///
/// Returns `None` if anything other than a letter `A` - `G` is passed.
#[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,
}
}
/// Converts a numeric note to a letter, from 0 to 11.
///
/// For consistency, we use sharps and no flats in the letter names.
///
/// ## Panics
///
/// Panics if anything other than a number from 0 to 11 is passed.
#[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"),
}
}
/// An error in [`Note::from_str`].
#[derive(Clone, Debug)]
pub enum NameError {
/// The string is not at least two characters long.
Short,
/// An invalid letter name for a note was read.
///
/// Note that this is case-sensitive.
Letter(char),
/// The integer after the letter name could not be parsed.
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 {
// Truncation is impossible.
#[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}")
}
}