rust_music/
note.rs

1use crate::errors::NoteError;
2use crate::num::u7;
3use crate::Result;
4
5/// Represents a music note, with a pitch, a rhythm, and a dynamic (volume)
6#[derive(Debug, Default, Clone, PartialEq)]
7pub struct Note {
8    /// the rhythm value is a floating point value of a beat (no maximum).
9    /// Some defaults are available in the rhythms_constants submodule.
10    rhythm: f64,
11    /// the pitch must be between 0 and 127 (included)
12    pitch: u7,
13    /// the dynamic describes the volume of a note. Some defaults are available
14    /// in the dynamics_constants submodule
15    dynamic: u7,
16}
17
18/// Represents a note by name without a specific octave or accidental
19/// Supports both letters from A to G and traditional Do Re Mi ... names
20#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
21pub enum NoteName {
22    Do = 0,
23    Re = 2,
24    Mi = 4,
25    Fa = 5,
26    Sol = 7,
27    La = 9,
28    Si = 11,
29}
30
31impl NoteName {
32    pub const C: NoteName = NoteName::Do;
33    pub const D: NoteName = NoteName::Re;
34    pub const E: NoteName = NoteName::Mi;
35    pub const F: NoteName = NoteName::Fa;
36    pub const G: NoteName = NoteName::Sol;
37    pub const A: NoteName = NoteName::La;
38    pub const B: NoteName = NoteName::Si;
39}
40
41/// Represents a note accidental
42#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)]
43pub enum Accidental {
44    Flat,
45    #[default]
46    Natural,
47    Sharp,
48}
49
50impl Note {
51    /// Returns a Note with the given rhythm, pitch, and dynamic
52    ///
53    /// # Arguments
54    ///
55    /// * `pitch` - The pitch of the note (between 0 and 127)
56    /// * `rhythm` - The rhythm value of the note
57    /// * `dynamic` - The dynamic (volume) of the note
58    ///
59    /// # Errors
60    ///
61    /// * `Error::Note(Invalid::Rhythm)` if rhythm is below `0.000_001`
62    pub fn new(pitch: u7, rhythm: f64, dynamic: u7) -> Result<Note> {
63        if rhythm < 0.000_001 {
64            return Err(NoteError::InvalidRhythm(rhythm).into());
65        }
66        Ok(Note {
67            pitch,
68            rhythm,
69            dynamic,
70        })
71    }
72
73    /// Creates an iterator of notes with idential rhythms and dynamic which can be added directly
74    /// to a phrase using `phrase.add_sequential_notes` to be played sequentially or collected as
75    /// a vector to use in a `Chord`.
76    pub fn new_sequence<'a, PitchIter: IntoIterator<Item = u7> + 'a>(
77        rhythm: f64,
78        dynamic: u7,
79        pitches: PitchIter,
80    ) -> impl std::iter::Iterator<Item = Result<Note>> + 'a {
81        pitches
82            .into_iter()
83            .map(move |p| Note::new(p, rhythm, dynamic))
84    }
85
86    /// Returns the pitch of the note
87    pub fn pitch(&self) -> u7 {
88        self.pitch
89    }
90
91    /// Returns the rhythm value of the note
92    pub fn rhythm(&self) -> f64 {
93        self.rhythm
94    }
95
96    /// Returns the dynamic value of the note
97    pub fn dynamic(&self) -> u7 {
98        self.dynamic
99    }
100
101    /// Returns the note name, accidental, and octave of the `Note`'s pitch
102    ///
103    /// # Arguments
104    ///
105    /// * `pitch`: pitch to analyse
106    /// * `sharps`: specifies if an accidental should be returned as a sharp
107    ///   (if false, an accidentals will be returned as a flat). This does not
108    ///   affect naturals.
109    pub fn pitch_info(&self, sharps: bool) -> (NoteName, Accidental, u8) {
110        pitch_info(self.pitch, sharps)
111    }
112}
113
114/// Returns a pitch value based on the given pitch name, octave, and accidental
115///
116/// # Arguments
117///
118/// * `letter` - The note name (between `A` and `G`)
119/// * `accidental` - The accidental of the note
120/// * `octave` - Which octave the note is in (`12` pitches per octave,
121///   pitch `0` is a `C`, final pitch must be `127` max)
122///
123/// # Errors
124///
125/// Will return `Error::Note(Invalid::Pitch)` if final pitch is above `127`
126/// or underflowed below `0`
127pub fn compute_pitch(note: NoteName, accidental: Accidental, octave: u8) -> Result<u7> {
128    // we use u32 to avoid an uint overflow before the value check
129    let base_pitch = note as u32;
130    let nat_pitch = 12 * octave as u32 + base_pitch;
131    let pitch = match accidental {
132        Accidental::Natural => nat_pitch,
133        Accidental::Sharp => nat_pitch + 1,
134        Accidental::Flat => nat_pitch - 1,
135    };
136    if pitch > 127 {
137        return Err(NoteError::InvalidPitch(pitch).into());
138    }
139    Ok(u7::new(pitch as u8))
140}
141
142/// Returns the note name, accidental, and octave of the given pitch
143///
144/// # Arguments
145///
146/// * `pitch`: pitch to analyse
147/// * `sharps`: specifies if an accidental should be returned as a sharp
148///   (if false, an accidentals will be returned as a flat). This does not
149///   affect naturals.
150pub fn pitch_info(pitch: u7, sharps: bool) -> (NoteName, Accidental, u8) {
151    let pitch = u8::from(pitch);
152    let octave = pitch / 12;
153    let mut remainder_pitch = pitch % 12;
154    let mut acc = Accidental::Natural;
155    if matches!(remainder_pitch, 1 | 3 | 6 | 8 | 10) {
156        (acc, remainder_pitch) = if sharps {
157            (Accidental::Sharp, remainder_pitch - 1)
158        } else {
159            (Accidental::Flat, remainder_pitch + 1)
160        };
161    }
162    let name = match remainder_pitch {
163        0 => NoteName::Do,
164        2 => NoteName::Re,
165        4 => NoteName::Mi,
166        5 => NoteName::Fa,
167        7 => NoteName::Sol,
168        9 => NoteName::La,
169        11 => NoteName::Si,
170        _ => NoteName::Do, // This is supposedly impossible
171    };
172    (name, acc, octave)
173}
174
175#[cfg(test)]
176mod tests {
177    use super::{compute_pitch, pitch_info, u7, Accidental, NoteName};
178
179    #[test]
180    fn pitch_test() {
181        let test_cases = vec![
182            (NoteName::C, Accidental::Sharp, 2, true, 25),
183            (NoteName::B, Accidental::Natural, 1, false, 23),
184            (NoteName::B, Accidental::Natural, 1, true, 23),
185            (NoteName::C, Accidental::Natural, 2, true, 24),
186            (NoteName::C, Accidental::Natural, 2, false, 24),
187            (NoteName::D, Accidental::Natural, 2, false, 26),
188            (NoteName::E, Accidental::Natural, 2, false, 28),
189            (NoteName::F, Accidental::Natural, 2, true, 29),
190            (NoteName::G, Accidental::Sharp, 5, true, 68),
191            (NoteName::A, Accidental::Flat, 9, false, 116),
192        ];
193
194        let compute_only_cases = vec![
195            (NoteName::B, Accidental::Sharp, 1, true, 24),
196            (NoteName::C, Accidental::Flat, 2, false, 23),
197            (NoteName::E, Accidental::Sharp, 2, true, 29),
198        ];
199
200        for (name, acc, octave, sharps, out) in test_cases {
201            assert_eq!(compute_pitch(name, acc, octave).unwrap(), out);
202            assert_eq!(pitch_info(u7::new(out), sharps), (name, acc, octave));
203        }
204        for (name, acc, octave, _, out) in compute_only_cases {
205            assert_eq!(compute_pitch(name, acc, octave).unwrap(), out);
206        }
207    }
208}