- crates: common→application, api→presentation, infrastructure/*→adapters/* - new crates: api-types, infra-wiring - domain: errors/, models/, value_objects/, ports/, services/ - application: CQRS use cases (songs/, tabs/) w/ commands, queries, deps - unified DomainError replaces RepositoryError - workspace deps, unused dep cleanup - fix: parse plain-text chord lines (UG drops spans mid-song) - tests extracted to separate modules (tests/ dirs)
130 lines
3.1 KiB
Rust
130 lines
3.1 KiB
Rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Note {
|
|
A,
|
|
ASharpBFlat,
|
|
B,
|
|
C,
|
|
CSharpDFlat,
|
|
D,
|
|
DSharpEFlat,
|
|
E,
|
|
F,
|
|
FSharpGFlat,
|
|
G,
|
|
GSharpAFlat,
|
|
}
|
|
|
|
impl Note {
|
|
pub fn semitone(&self) -> u8 {
|
|
match self {
|
|
Note::C => 0,
|
|
Note::CSharpDFlat => 1,
|
|
Note::D => 2,
|
|
Note::DSharpEFlat => 3,
|
|
Note::E => 4,
|
|
Note::F => 5,
|
|
Note::FSharpGFlat => 6,
|
|
Note::G => 7,
|
|
Note::GSharpAFlat => 8,
|
|
Note::A => 9,
|
|
Note::ASharpBFlat => 10,
|
|
Note::B => 11,
|
|
}
|
|
}
|
|
|
|
pub fn from_semitone(s: u8) -> Note {
|
|
match s % 12 {
|
|
0 => Note::C,
|
|
1 => Note::CSharpDFlat,
|
|
2 => Note::D,
|
|
3 => Note::DSharpEFlat,
|
|
4 => Note::E,
|
|
5 => Note::F,
|
|
6 => Note::FSharpGFlat,
|
|
7 => Note::G,
|
|
8 => Note::GSharpAFlat,
|
|
9 => Note::A,
|
|
10 => Note::ASharpBFlat,
|
|
11 => Note::B,
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
pub fn to_sharp_str(&self) -> &'static str {
|
|
match self {
|
|
Note::C => "C",
|
|
Note::CSharpDFlat => "C#",
|
|
Note::D => "D",
|
|
Note::DSharpEFlat => "D#",
|
|
Note::E => "E",
|
|
Note::F => "F",
|
|
Note::FSharpGFlat => "F#",
|
|
Note::G => "G",
|
|
Note::GSharpAFlat => "G#",
|
|
Note::A => "A",
|
|
Note::ASharpBFlat => "A#",
|
|
Note::B => "B",
|
|
}
|
|
}
|
|
|
|
pub fn to_flat_str(&self) -> &'static str {
|
|
match self {
|
|
Note::C => "C",
|
|
Note::CSharpDFlat => "Db",
|
|
Note::D => "D",
|
|
Note::DSharpEFlat => "Eb",
|
|
Note::E => "E",
|
|
Note::F => "F",
|
|
Note::FSharpGFlat => "Gb",
|
|
Note::G => "G",
|
|
Note::GSharpAFlat => "Ab",
|
|
Note::A => "A",
|
|
Note::ASharpBFlat => "Bb",
|
|
Note::B => "B",
|
|
}
|
|
}
|
|
|
|
pub fn parse_prefix(s: &str) -> Option<(Note, usize)> {
|
|
let mut chars = s.chars();
|
|
let root = match chars.next()? {
|
|
'A' => Note::A,
|
|
'B' => Note::B,
|
|
'C' => Note::C,
|
|
'D' => Note::D,
|
|
'E' => Note::E,
|
|
'F' => Note::F,
|
|
'G' => Note::G,
|
|
_ => return None,
|
|
};
|
|
match chars.next() {
|
|
Some('#') => Some((Self::sharp_of(root), 2)),
|
|
Some('b') if s.len() > 1 => {
|
|
let flatted = Self::flat_of(root)?;
|
|
Some((flatted, 2))
|
|
}
|
|
_ => Some((root, 1)),
|
|
}
|
|
}
|
|
|
|
pub fn parse(s: &str) -> Option<Note> {
|
|
let (note, consumed) = Self::parse_prefix(s)?;
|
|
if consumed == s.len() {
|
|
Some(note)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn sharp_of(root: Note) -> Note {
|
|
Note::from_semitone((root.semitone() + 1) % 12)
|
|
}
|
|
|
|
fn flat_of(root: Note) -> Option<Note> {
|
|
Some(Note::from_semitone((root.semitone() + 11) % 12))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "../tests/note.rs"]
|
|
mod tests;
|