#[derive(Debug, PartialEq, thiserror::Error)]
pub enum ParseMIDINoteTypeError {
#[error("Unknown note type {input}")]
UnknownNoteType { input: String },
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum MIDINoteType {
C,
CSharp,
D,
DSharp,
E,
F,
FSharp,
G,
GSharp,
A,
ASharp,
B,
Rest,
}
impl<'a> std::str::FromStr for MIDINoteType {
type Err = ParseMIDINoteTypeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"bsharp"
| "b#"
| "c" => Ok(Self::C),
"csharp"
| "c#"
| "dflat"
| "d♭"
| "db" => Ok(Self::CSharp),
"d" => Ok(Self::D),
"dsharp"
| "d#"
| "eflat"
| "e♭"
| "eb" => Ok(Self::DSharp),
"e"
| "fflat"
| "f♭"
| "fb" => Ok(Self::E),
"esharp"
| "e#"
| "f" => Ok(Self::F),
"fsharp"
| "f#"
| "gflat"
| "g♭"
| "gb" => Ok(Self::FSharp),
"g" => Ok(Self::G),
"gsharp"
| "g#"
| "aflat"
| "a♭"
| "ab" => Ok(Self::GSharp),
"a" => Ok(Self::A),
"asharp"
| "a#"
| "bflat"
| "b♭"
| "bb" => Ok(Self::ASharp),
"cflat"
| "c♭"
| "cb"
| "b" => Ok(Self::B),
"rest" | "empty" => Ok(Self::Rest),
_ => Err(ParseMIDINoteTypeError::UnknownNoteType {
input: s.to_string(),
}),
}
}
}
#[derive(Debug, PartialEq, thiserror::Error)]
pub enum ParseMIDINoteError {
#[error("Invalid note format (expected '<note>:<octave>', found {input})")]
InvalidNoteFormat { input: String },
#[error(transparent)]
InvalidOctave(#[from] std::num::ParseIntError),
#[error(transparent)]
UnknownNoteType(#[from] ParseMIDINoteTypeError),
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct MIDINote {
pub note_type: MIDINoteType,
pub octave: u32,
}
impl MIDINote {
pub fn new(note_type: MIDINoteType, octave: u32) -> Self {
Self { note_type, octave, }
}
pub fn convert(&self) -> u32 {
match &self.note_type {
MIDINoteType::Rest => u32::max_value(),
_ => (self.note_type as u32) + (self.octave + 1) * 12,
}
}
}
impl<'a> std::str::FromStr for MIDINote {
type Err = ParseMIDINoteError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let split_pair: Vec<&str> = s.split(':').collect();
if split_pair.len() != 2 {
return Err(ParseMIDINoteError::InvalidNoteFormat {
input: s.to_string(),
});
}
let note_type = MIDINoteType::from_str(split_pair[0])?;
let octave = split_pair[1].parse::<u32>()?;
Ok(Self { note_type, octave })
}
}
#[derive(Debug, PartialEq, thiserror::Error)]
pub enum ParseMIDINoteSequenceError {
#[error("Invalid note at index {0}")]
ParseMIDINote(usize, #[source] ParseMIDINoteError),
}
#[derive(Clone, Debug, PartialEq)]
pub struct MIDINoteSet(pub std::collections::BTreeSet<MIDINote>);
impl std::ops::Deref for MIDINoteSet {
type Target = std::collections::BTreeSet<MIDINote>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::str::FromStr for MIDINoteSet {
type Err = ParseMIDINoteSequenceError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let notes = s
.split(',')
.enumerate()
.map(|(idx, pair)| {
pair.parse::<MIDINote>()
.map_err(|err| ParseMIDINoteSequenceError::ParseMIDINote(idx, err))
})
.collect::<Result<std::collections::BTreeSet<MIDINote>, ParseMIDINoteSequenceError>>()?;
Ok(Self(notes))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct MIDINoteVec(pub Vec<MIDINote>);
impl std::ops::Deref for MIDINoteVec {
type Target = Vec<MIDINote>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::str::FromStr for MIDINoteVec {
type Err = ParseMIDINoteSequenceError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let notes = s
.split(',')
.enumerate()
.map(|(idx, pair)| {
pair.parse::<MIDINote>()
.map_err(|err| ParseMIDINoteSequenceError::ParseMIDINote(idx, err))
})
.collect::<Result<Vec<MIDINote>, ParseMIDINoteSequenceError>>()?;
Ok(Self(notes))
}
}
impl std::iter::FromIterator<MIDINote> for MIDINoteVec {
fn from_iter<I: IntoIterator<Item=MIDINote>>(iter: I) -> Self {
Self(iter.into_iter().collect::<Vec<MIDINote>>())
}
}
impl From<MIDINoteSet> for MIDINoteVec {
fn from(set: MIDINoteSet) -> Self {
set.0.into_iter().collect::<MIDINoteVec>()
}
}
impl From<&MIDINoteSet> for MIDINoteVec {
fn from(set: &MIDINoteSet) -> Self {
set.iter().map(|n| n.clone()).collect::<MIDINoteVec>()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_midi_note_from_str_valid() {
let observed = "C#:5".parse::<MIDINote>();
let expected = Ok(MIDINote::new(MIDINoteType::CSharp, 5));
assert_eq!(expected, observed);
}
#[test]
fn test_midi_note_from_str_invalid_note_type() {
let observed = "C$:5".parse::<MIDINote>();
let expected = Err(
ParseMIDINoteError::UnknownNoteType(
ParseMIDINoteTypeError::UnknownNoteType { input: "C$".to_string() }
)
);
assert_eq!(expected, observed);
}
#[test]
fn test_midi_note_from_str_invalid_format() {
let input = "C#;5".to_string();
let observed = input.as_str().parse::<MIDINote>();
let expected = Err(ParseMIDINoteError::InvalidNoteFormat { input });
assert_eq!(expected, observed);
}
#[test]
fn test_midi_note_from_str_invalid_octive() {
let input = "C#:12.3".to_string();
let observed = input.as_str().parse::<MIDINote>();
assert!(observed.is_err());
}
#[test]
fn test_midi_note_set_from_str_valid_no_duplicate() {
let observed = "C:4,D:4,E:4,F:4,F#:4,DFlat:5".parse::<MIDINoteSet>();
let expected = Ok(MIDINoteSet(vec![
MIDINote::new(MIDINoteType::C, 4),
MIDINote::new(MIDINoteType::D, 4),
MIDINote::new(MIDINoteType::E, 4),
MIDINote::new(MIDINoteType::F, 4),
MIDINote::new(MIDINoteType::FSharp, 4),
MIDINote::new(MIDINoteType::CSharp, 5),
].into_iter().collect::<std::collections::BTreeSet<MIDINote>>()));
assert_eq!(expected, observed);
}
#[test]
fn test_midi_note_set_from_str_valid_with_duplicate() {
let observed = "C:4,C:4,D:5".parse::<MIDINoteSet>();
let expected = Ok(MIDINoteSet(vec![
MIDINote::new(MIDINoteType::C, 4),
MIDINote::new(MIDINoteType::D, 5),
].into_iter().collect::<std::collections::BTreeSet<MIDINote>>()));
assert_eq!(expected, observed);
}
#[test]
fn test_midi_note_set_from_str_invalid_note_type() {
let input = "C:4,CL:8,D:6".to_string();
let observed = input.as_str().parse::<MIDINoteSet>();
let expected = Err(ParseMIDINoteSequenceError::ParseMIDINote(
1,
ParseMIDINoteError::UnknownNoteType(ParseMIDINoteTypeError::UnknownNoteType { input: "CL".to_string(), }),
));
assert_eq!(expected, observed);
}
#[test]
fn test_midi_note_set_from_str_extra_comma() {
let input = "C:4,C:8,D:6,".to_string();
let observed = input.as_str().parse::<MIDINoteSet>();
let expected = Err(ParseMIDINoteSequenceError::ParseMIDINote(
3,
ParseMIDINoteError::InvalidNoteFormat { input: "".to_string() },
));
assert_eq!(expected, observed);
}
}