Skip to content

Commit

Permalink
Make MIDI-to-Pitch conversion more explicit
Browse files Browse the repository at this point in the history
  • Loading branch information
hedgecrw committed Dec 8, 2024
1 parent b103087 commit 5c0d177
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 51 deletions.
21 changes: 20 additions & 1 deletion amm_sdk/src/context/key.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::note::Accidental;
use crate::note::{Accidental, PitchName};
use amm_internal::amm_prelude::*;
use amm_macros::{JsonDeserialize, JsonSerialize};
#[cfg(target_arch = "wasm32")]
Expand Down Expand Up @@ -194,6 +194,25 @@ impl Key {
}
}

/// Returns whether the key contains an accidental on the given pitch.
#[must_use]
pub fn contains(&self, pitch: PitchName) -> bool {
let key_accidentals = self.accidentals();
key_accidentals[pitch.index()] != Accidental::None
}

/// Returns whether the key contains one or more flat accidentals.
#[must_use]
pub fn is_flat_key(&self) -> bool {
self.fifths() < 0
}

/// Returns whether the key contains one or more sharp accidentals.
#[must_use]
pub fn is_sharp_key(&self) -> bool {
self.fifths() > 0
}

/// Returns a new key with the same tonic (root note) as the current key,
/// but with the opposite mode (i.e., the parallel key of C-Major
/// would be C-Minor and vice versa).
Expand Down
80 changes: 40 additions & 40 deletions amm_sdk/src/note/note.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,55 +41,55 @@ impl Note {
/// Creates a new note from the given MIDI number, duration, and optional key signature.
#[must_use]
pub fn from_midi(mut midi_number: u8, duration: Duration, key: Option<Key>) -> Self {
if midi_number == 255 {
if midi_number == 0 || midi_number == 255 {
Self::new(Pitch::new_rest(), duration, None)
} else {
let fifths = key.unwrap_or_default().fifths();
let key = key.unwrap_or_default();
let (pitch_name, accidental) = match midi_number % 12 {
0 if fifths > -6 && fifths < 2 => (PitchName::C, Accidental::None),
0 if fifths >= 7 => {
0 if key.is_sharp_key() && key.contains(PitchName::B) => {
midi_number -= 12;
(PitchName::B, Accidental::None)
}
0 => (PitchName::C, Accidental::Natural),
1 if fifths > -4 && fifths <= 0 => (PitchName::D, Accidental::Flat),
1 if fifths <= -4 => (PitchName::D, Accidental::None),
1 if fifths < 2 => (PitchName::C, Accidental::Sharp),
1 => (PitchName::C, Accidental::None),
2 if fifths > -4 && fifths < 4 => (PitchName::D, Accidental::None),
2 => (PitchName::D, Accidental::Natural),
3 if fifths > -2 && fifths <= 0 => (PitchName::E, Accidental::Flat),
3 if fifths <= -2 => (PitchName::E, Accidental::None),
3 if fifths < 4 => (PitchName::D, Accidental::Sharp),
3 => (PitchName::D, Accidental::None),
4 if fifths > -2 && fifths < 6 => (PitchName::E, Accidental::None),
4 if fifths <= -7 => (PitchName::F, Accidental::None),
4 => (PitchName::E, Accidental::Natural),
5 if fifths > -7 && fifths < 1 => (PitchName::F, Accidental::None),
5 if fifths >= 6 => (PitchName::E, Accidental::None),
5 => (PitchName::F, Accidental::Natural),
6 if fifths > -5 && fifths < 0 => (PitchName::G, Accidental::Flat),
6 if fifths <= -5 => (PitchName::G, Accidental::None),
6 if fifths < 1 => (PitchName::F, Accidental::Sharp),
6 => (PitchName::F, Accidental::None),
7 if fifths > -5 && fifths < 3 => (PitchName::G, Accidental::None),
7 => (PitchName::G, Accidental::Natural),
8 if fifths > -3 && fifths <= 0 => (PitchName::A, Accidental::Flat),
8 if fifths <= -3 => (PitchName::A, Accidental::None),
8 if fifths < 3 => (PitchName::G, Accidental::Sharp),
8 => (PitchName::G, Accidental::None),
9 if fifths > -3 && fifths < 5 => (PitchName::A, Accidental::None),
9 => (PitchName::A, Accidental::Natural),
10 if fifths == 0 => (PitchName::B, Accidental::Flat),
10 if fifths < 0 => (PitchName::B, Accidental::None),
10 if fifths < 5 => (PitchName::A, Accidental::Sharp),
10 => (PitchName::A, Accidental::None),
11 if fifths > -1 && fifths < 7 => (PitchName::B, Accidental::None),
11 if fifths <= -6 => {
0 if key.contains(PitchName::C) => (PitchName::C, Accidental::Natural),
0 => (PitchName::C, Accidental::None),
1 if key.is_sharp_key() && key.contains(PitchName::C) => (PitchName::C, Accidental::None),
1 if key.is_flat_key() && key.contains(PitchName::D) => (PitchName::D, Accidental::None),
1 if key.is_sharp_key() => (PitchName::C, Accidental::Sharp),
1 => (PitchName::D, Accidental::Flat),
2 if key.contains(PitchName::D) => (PitchName::D, Accidental::Natural),
2 => (PitchName::D, Accidental::None),
3 if key.is_sharp_key() && key.contains(PitchName::D) => (PitchName::D, Accidental::None),
3 if key.is_flat_key() && key.contains(PitchName::E) => (PitchName::E, Accidental::None),
3 if key.is_sharp_key() => (PitchName::D, Accidental::Sharp),
3 => (PitchName::E, Accidental::Flat),
4 if key.is_flat_key() && key.contains(PitchName::F) => (PitchName::F, Accidental::None),
4 if key.contains(PitchName::E) => (PitchName::E, Accidental::Natural),
4 => (PitchName::E, Accidental::None),
5 if key.is_sharp_key() && key.contains(PitchName::E) => (PitchName::E, Accidental::None),
5 if key.contains(PitchName::F) => (PitchName::F, Accidental::Natural),
5 => (PitchName::F, Accidental::None),
6 if key.is_sharp_key() && key.contains(PitchName::F) => (PitchName::F, Accidental::None),
6 if key.is_flat_key() && key.contains(PitchName::G) => (PitchName::G, Accidental::None),
6 if key.is_sharp_key() => (PitchName::F, Accidental::Sharp),
6 => (PitchName::G, Accidental::Flat),
7 if key.contains(PitchName::G) => (PitchName::G, Accidental::Natural),
7 => (PitchName::G, Accidental::None),
8 if key.is_sharp_key() && key.contains(PitchName::G) => (PitchName::G, Accidental::None),
8 if key.is_flat_key() && key.contains(PitchName::A) => (PitchName::A, Accidental::None),
8 if key.is_sharp_key() => (PitchName::G, Accidental::Sharp),
8 => (PitchName::A, Accidental::Flat),
9 if key.contains(PitchName::A) => (PitchName::A, Accidental::Natural),
9 => (PitchName::A, Accidental::None),
10 if key.is_sharp_key() && key.contains(PitchName::A) => (PitchName::A, Accidental::None),
10 if key.is_flat_key() && key.contains(PitchName::B) => (PitchName::B, Accidental::None),
10 if key.is_sharp_key() => (PitchName::A, Accidental::Sharp),
10 => (PitchName::B, Accidental::Flat),
11 if key.is_flat_key() && key.contains(PitchName::C) => {
midi_number += 12;
(PitchName::C, Accidental::None)
}
11 => (PitchName::B, Accidental::Natural),
11 if key.contains(PitchName::B) => (PitchName::B, Accidental::Natural),
11 => (PitchName::B, Accidental::None),
_ => (PitchName::Rest, Accidental::None),
};
Self::new(Pitch::new(pitch_name, midi_number / 12 - 1), duration, Some(accidental))
Expand Down
18 changes: 18 additions & 0 deletions amm_sdk/src/note/pitch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,24 @@ pub enum PitchName {
G,
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
impl PitchName {
/// Returns the index of the pitch name within the `PitchName` enum.
#[must_use]
pub(crate) fn index(self) -> usize {
match self {
PitchName::Rest => 0,
PitchName::A => 1,
PitchName::B => 2,
PitchName::C => 3,
PitchName::D => 4,
PitchName::E => 5,
PitchName::F => 6,
PitchName::G => 7,
}
}
}

/// Represents a musical pitch, which is a combination of a pitch name and octave.
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, JsonDeserialize, JsonSerialize)]
Expand Down
19 changes: 9 additions & 10 deletions amm_sdk/src/storage/midi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,18 +262,17 @@ pub struct MidiConverter;
impl MidiConverter {
fn load_from_midi(data: &[u8]) -> Result<Composition, String> {
// Parse the MIDI representation
let _midi = Smf::parse(data).map_err(|err| err.to_string())?;
let track_count = _midi.tracks.len();
let ticks_per_beat = get_ticks_per_beat(&_midi.header);
let control_track = parse_control_track(&_midi.tracks[0]);
let midi = Smf::parse(data).map_err(|err| err.to_string())?;
let ticks_per_beat = get_ticks_per_beat(&midi.header);
let control_track = parse_control_track(&midi.tracks[0]);

// Generate the initial composition structure and fill in known metadata
// Generate the composition structure and fill in known data
let mut composition = Composition::new("Default", None, None, None);
let part = composition.add_part("todo_part_name");
let section = part.add_section("Default");
for i in 1..track_count {
let staff = section.add_staff(&format!("{}", i));
load_staff_content(staff, control_track.clone(), &_midi.tracks[i], ticks_per_beat);
let part = composition.add_part("MIDI Track");
let section = part.add_section("Top-Level Section");
for i in 1..midi.tracks.len() {
let staff = section.add_staff(&format!("Section {i}"));
load_staff_content(staff, control_track.clone(), &midi.tracks[i], ticks_per_beat);
}

// Return the fully constructed composition
Expand Down

0 comments on commit 5c0d177

Please sign in to comment.