Skip to content

Commit

Permalink
Add documentation for "note" elements
Browse files Browse the repository at this point in the history
  • Loading branch information
hedgecrw committed Nov 19, 2024
1 parent 0c16a3c commit 95750d5
Show file tree
Hide file tree
Showing 10 changed files with 133 additions and 11 deletions.
Binary file added amm_sdk/assets/images/note-type-2048th.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions amm_sdk/src/context/clef.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ use wasm_bindgen::prelude::*;
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, JsonDeserialize, JsonSerialize)]
pub enum ClefSymbol {
/// ![G Clef](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/clef-G.png)
///
///
/// The middle curl of the G clef wraps around the staff line used to notate a pitch of G4.
#[default]
GClef,
/// ![C Clef](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/clef-C.png)
///
///
/// The middle of the C clef indicates the staff line used to notate a pitch of C4 (middle C).
CClef,
/// ![F Clef](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/clef-F.png)
///
///
/// The two dots of the F clef surround the staff line used to notate a pitch of F3.
FClef,
}
Expand Down
8 changes: 4 additions & 4 deletions amm_sdk/src/context/dynamic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,26 @@ pub enum Dynamic {
///
/// A `forte` dynamic marking indicates that corresponding music should be played
/// relatively loudly.
///
///
/// The magnitude of the notated forte is specified by the `u8` value.
/// For example, `Forte(3)` represents a dynamic marking of `fff`.
Forte(u8),
#[default]
/// ![Mezzo Forte](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/mf.png)
///
///
/// A `mezzo-forte` dynamic marking indicates that corresponding music should be played
/// only slightly louder than average.
MezzoForte,
/// ![Mezzo Piano](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/mp.png)
///
///
/// A `mezzo-piano` dynamic marking indicates that corresponding music should be played
/// only slightly softer than average.
MezzoPiano,
/// ![Piano](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/p.png)
///
/// A `piano` dynamic marking indicates that corresponding music should be played
/// relatively softly.
///
///
/// The magnitude of the notated piano is specified by the `u8` value.
/// For example, `Piano(3)` represents a dynamic marking of `ppp`.
Piano(u8),
Expand Down
2 changes: 1 addition & 1 deletion amm_sdk/src/modification/direction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ impl Direction {
self.id
}

/// Converts the direction into a [Timeslice].
/// Converts the direction into a [`Timeslice`].
#[must_use]
pub fn to_timeslice(&self) -> Timeslice {
let mut timeslice = Timeslice::new();
Expand Down
3 changes: 3 additions & 0 deletions amm_sdk/src/modification/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//! This module contains the various modifications that can be applied
//! to the musical elements in a score.
mod chord;
mod direction;
mod note;
Expand Down
27 changes: 27 additions & 0 deletions amm_sdk/src/note/accidental.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,46 @@ use amm_macros::{JsonDeserialize, JsonSerialize};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;

/// Represents a musical pitch modification.
///
/// Common examples include sharps and flats, which raise or lower the
/// pitch of a note by a half step (semitone).
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, JsonDeserialize, JsonSerialize)]
pub enum Accidental {
/// Represents an explicit lack of an accidental.
///
/// This means that any accidentals will be inferred from either the key
/// signature or a previously played accidental in the same measure.
#[default]
None,
/// <span class="smufl">TODO</span>
///
/// Represents a natural pitch, which is neither sharp nor flat. Used primarily to
/// negate the effect of a previous accidental or a key signature.
Natural,
/// <span class="smufl">TODO</span>
///
/// Represents a sharp pitch, which raises the pitch of a note by a half step (semitone).
Sharp,
/// <span class="smufl">TODO</span>
///
/// Represents a flat pitch, which lowers the pitch of a note by a half step (semitone).
Flat,
/// <span class="smufl">TODO</span>
///
/// Represents a double-sharp pitch, which raises the pitch of a
/// note by a whole step (2 semitones).
DoubleSharp,
/// <span class="smufl">TODO</span>
///
/// Represents a double-flat pitch, which lowers the pitch of a
/// note by a whole step (2 semitones).
DoubleFlat,
}

impl Accidental {
/// Returns the number of semitones that this accidental raises or lowers a pitch.
#[must_use]
pub fn value(&self) -> i8 {
match self {
Expand Down
47 changes: 44 additions & 3 deletions amm_sdk/src/note/duration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,41 +20,68 @@ const FIVE_HUNDRED_TWELFTH_VALUE: f64 = 0.001_953_125;
const ONE_THOUSAND_TWENTY_FOURTH_VALUE: f64 = 0.000_976_562_5;
const TWO_THOUSAND_FOURTH_EIGHTH_VALUE: f64 = 0.000_488_281_25;

/// Represents the type of duration of a note.
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, JsonDeserialize, JsonSerialize)]
pub enum DurationType {
/// ![Maxima Duration](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/note-type-maxima.png)
Maxima,
/// ![Long Duration](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/note-type-long.png)
Long,
/// ![Breve Duration](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/note-type-breve.png)
Breve,
/// ![Whole Duration](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/note-type-whole.png)
Whole,
/// ![Half Duration](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/note-type-half.png)
Half,
/// ![Quarter Duration](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/note-type-quarter.png)
#[default]
Quarter,
/// ![8th Duration](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/note-type-eighth.png)
Eighth,
/// ![16th Duration](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/note-type-16th.png)
Sixteenth,
/// ![32nd Duration](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/note-type-32nd.png)
ThirtySecond,
/// ![64th Duration](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/note-type-64th.png)
SixtyFourth,
/// ![128th Duration](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/note-type-128th.png)
OneHundredTwentyEighth,
/// ![256th Duration](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/note-type-256th.png)
TwoHundredFiftySixth,
/// ![512th Duration](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/note-type-512th.png)
FiveHundredTwelfth,
/// ![1024th Duration](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/note-type-1024th.png)
OneThousandTwentyFourth,
/// ![2048th Duration](https://hedgetechllc.github.io/amm-sdk/amm_sdk/images/note-type-2048th.png)
TwoThousandFortyEighth,
}

/// Represents the duration of a note as a combination of note type and dots.
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, JsonDeserialize, JsonSerialize)]
pub struct Duration {
/// The type of duration of the note.
pub value: DurationType,
/// The number of dots after the note.
///
/// Each dot increases the duration of a note by half of its original value, compounded.
///
/// For example, a quarter note with one dot is equivalent in length to a quarter note
/// and an eighth note. A quarter note with two dots is equivalent to a quarter
/// note, an eighth note, and a sixteenth note.
pub dots: u8,
}

#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
impl Duration {
/// Creates a new [`Duration`] with the given note type and number of dots.
#[must_use]
pub fn new(value: DurationType, dots: u8) -> Self {
Self { value, dots }
}

/// Returns the number of dots required to represent the remainder of a note's value.
#[must_use]
fn dots_from_remainder(base_value: f64, full_value: f64) -> u8 {
let (mut current_value, mut dots) = (base_value, 0);
Expand All @@ -65,6 +92,9 @@ impl Duration {
dots
}

/// Creates a new [`Duration`] from the given beat base value and number of beats.
///
/// The `beat_base_value` defines the type of note that represents a single beat.
#[must_use]
pub fn from_beats(beat_base_value: &Duration, beats: f64) -> Self {
let value = beats * beat_base_value.value();
Expand Down Expand Up @@ -110,11 +140,14 @@ impl Duration {
}
}

/// Creates a new [`Duration`] from the given tempo and note duration in seconds.
#[must_use]
pub fn from_duration(tempo: &Tempo, duration: f64) -> Self {
Duration::from_beats(&tempo.base_note, duration * f64::from(tempo.beats_per_minute) / 60.0)
}

/// Returns the minimum number and type of notes that can be used to
/// represent the specified number of beats.
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub(crate) fn get_minimum_divisible_notes(beats: f64) -> (DurationType, u32) {
Expand Down Expand Up @@ -142,6 +175,7 @@ impl Duration {
}
}

/// Returns the value of the duration as its fractional representation.
#[must_use]
pub fn value(&self) -> f64 {
let base_duration = match self.value {
Expand All @@ -166,17 +200,23 @@ impl Duration {
.sum()
}

/// Returns the number of beats that the duration represents.
///
/// The `base_beat_value` parameter defines the type of note that represents a single beat.
#[must_use]
pub fn beats(&self, base_beat_value: f64) -> f64 {
self.value() / base_beat_value
}

/// Splits the duration into `into_notes` number of notes.
///
/// **Note:** The `into_notes` parameter **must** be a power of 2.
#[must_use]
pub fn split(&self, mut times: u8) -> Self {
pub fn split(&self, mut into_notes: u8) -> Self {
// Note: `times` must be a power of 2
let mut duration = self.value;
while times > 1 {
times /= 2;
while into_notes > 1 {
into_notes /= 2;
duration = match duration {
DurationType::Maxima => DurationType::Long,
DurationType::Long => DurationType::Breve,
Expand All @@ -200,6 +240,7 @@ impl Duration {
}
}

/// Converts the number of dots in the duration into a textual representation.
#[must_use]
fn dots_to_text(dots: u8) -> String {
match dots {
Expand Down
2 changes: 2 additions & 0 deletions amm_sdk/src/note/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! This module contains all musical note-related structs and enums.
mod accidental;
mod duration;
mod note;
Expand Down
37 changes: 37 additions & 0 deletions amm_sdk/src/note/note.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,23 @@ use wasm_bindgen::prelude::*;
const A4_FREQUENCY_HZ: f32 = 440.0;
const MIDI_NUMBER_A4: i8 = 69;

/// Represents a note in a musical composition.
#[derive(Debug, Default, Eq, JsonDeserialize, JsonSerialize)]
pub struct Note {
/// The unique identifier of the note.
pub id: usize,
/// The pitch of the note.
pub pitch: Pitch,
/// The duration of the note.
pub duration: Duration,
/// An accidental modifier on the note (if any).
pub accidental: Accidental,
/// A list of modifications on the note.
modifications: BTreeSet<NoteModification>,
}

impl Note {
/// Creates a new note with the given pitch, duration, and optional accidental modifier.
#[must_use]
pub fn new(pitch: Pitch, duration: Duration, accidental: Option<Accidental>) -> Self {
Self {
Expand All @@ -31,11 +38,14 @@ impl Note {
}
}

/// Returns the unique identifier of the note.
#[must_use]
pub fn get_id(&self) -> usize {
self.id
}

/// Returns the number of semitones between the note and A4, taking into
/// account the accidentals for a given key signature.
#[must_use]
fn semitone_distance(&self, key_accidentals: [Accidental; 8]) -> i8 {
let (pitch_index, num_semitones) = self.pitch.value();
Expand All @@ -47,16 +57,19 @@ impl Note {
}
}

/// Returns whether the note is the same pitch as another note.
#[must_use]
pub fn is_same_pitch(&self, other: &Note) -> bool {
self.pitch == other.pitch
}

/// Returns whether the note is a rest (i.e., unvoiced).
#[must_use]
pub fn is_rest(&self) -> bool {
self.pitch.is_rest()
}

/// Returns whether the note is a grace note.
#[must_use]
pub fn is_grace_note(&self) -> bool {
self
Expand All @@ -65,19 +78,26 @@ impl Note {
.any(|modification| matches!(modification.r#type, NoteModificationType::Grace { .. }))
}

/// Returns the pitch of the note in Hertz,
/// optionally taking into account a key signature.
#[must_use]
pub fn pitch_hz(&self, key: Option<Key>, a4_frequency_hz: Option<f32>) -> f32 {
let accidentals = key.unwrap_or_default().accidentals();
a4_frequency_hz.unwrap_or(A4_FREQUENCY_HZ) * 2f32.powf(f32::from(self.semitone_distance(accidentals)) / 12.0)
}

/// Returns the pitch of the note in MIDI number format,
/// optionally taking into account a key signature.
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn midi_number(&self, key: Option<Key>) -> u8 {
let accidentals = key.unwrap_or_default().accidentals();
(MIDI_NUMBER_A4 + self.semitone_distance(accidentals)) as u8
}

/// Returns the duration of the note in beats,
///
/// The `base_beat_value` parameter defines the type of note that represents a single beat.
#[must_use]
pub fn beats(&self, base_beat_value: f64) -> f64 {
if self.is_grace_note() {
Expand All @@ -87,39 +107,56 @@ impl Note {
}
}

/// Adds a modification to the note and returns a unique identifier for the modification.
pub fn add_modification(&mut self, mod_type: NoteModificationType) -> usize {
let modification = NoteModification::new(mod_type);
let modification_id = modification.get_id();
self.modifications.replace(modification);
modification_id
}

/// Returns a note modification based on the specified unique identifier.
#[must_use]
pub fn get_modification(&self, id: usize) -> Option<&NoteModification> {
self
.iter_modifications()
.find(|modification| modification.get_id() == id)
}

/// Returns the number of beats for the note, taking into account
/// a base beat value and optional tuplet ratio.
///
/// The `beat_base` parameter defines the type of note that represents a single beat.
///
/// The `tuplet_ratio` parameter defines the ratio of the note's target duration to
/// its original, unmodified duration.
#[must_use]
pub fn get_beats(&self, beat_base: &Duration, tuplet_ratio: Option<f64>) -> f64 {
self.beats(beat_base.value()) * tuplet_ratio.unwrap_or(1.0)
}

/// Returns the duration of the note in seconds, taking into account
/// a tempo and optional tuplet ratio.
///
/// The `tuplet_ratio` parameter defines the ratio of the note's target duration to
/// its original, unmodified duration.
#[must_use]
pub fn get_duration(&self, tempo: &Tempo, tuplet_ratio: Option<f64>) -> f64 {
self.get_beats(&tempo.base_note, tuplet_ratio) * 60.0 / f64::from(tempo.beats_per_minute)
}

/// Removes a modification from the note based on the specified unique identifier.
pub fn remove_modification(&mut self, id: usize) -> &mut Self {
self.modifications.retain(|modification| modification.get_id() != id);
self
}

/// Returns an iterator over the note's modifications.
pub fn iter_modifications(&self) -> alloc::collections::btree_set::Iter<'_, NoteModification> {
self.modifications.iter()
}

/// Returns a [`Timeslice`] containing only this single note.
#[must_use]
pub fn to_timeslice(&self) -> Timeslice {
let mut timeslice = Timeslice::new();
Expand Down
Loading

0 comments on commit 95750d5

Please sign in to comment.