1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
use crate::errors::NoteError;
use crate::num::u7;
use crate::Result;

/// Represents a music note, with a pitch, a rhythm, and a dynamic (volume)
#[derive(Debug, Clone, PartialEq)]
pub struct Note {
    /// the rhythm value is a floating point value of a beat (no maximum).
    /// Some defaults are available in the rhythms_constants submodule.
    rhythm: f64,
    /// the pitch must be between 0 and 127 (included)
    pitch: u7,
    /// the dynamic describes the volume of a note. Some defaults are available
    /// in the dynamics_constants submodule
    dynamic: u7,
}

/// Represents a note by name without a specific octave or accidental
/// Supports both letters from A to G and traditional Do Re Mi ... names
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum NoteName {
    Do = 0,
    Re = 2,
    Mi = 4,
    Fa = 5,
    Sol = 7,
    La = 9,
    Si = 11,
}

impl NoteName {
    pub const C: NoteName = NoteName::Do;
    pub const D: NoteName = NoteName::Re;
    pub const E: NoteName = NoteName::Mi;
    pub const F: NoteName = NoteName::Fa;
    pub const G: NoteName = NoteName::Sol;
    pub const A: NoteName = NoteName::La;
    pub const B: NoteName = NoteName::Si;
}

/// Represents a note accidental
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Accidental {
    Flat,
    Natural,
    Sharp,
}

impl Note {
    /// Returns a Note with the given rhythm, pitch, and dynamic
    ///
    /// # Arguments
    ///
    /// * `pitch` - The pitch of the note (between 0 and 127)
    /// * `rhythm` - The rhythm value of the note
    /// * `dynamic` - The dynamic (volume) of the note
    ///
    /// # Errors
    ///
    /// * `Error::Note(Invalid::Rhythm)` if rhythm is below `0.000_001`
    pub fn new(pitch: u7, rhythm: f64, dynamic: u7) -> Result<Note> {
        if rhythm < 0.000_001 {
            return Err(NoteError::InvalidRhythm(rhythm).into());
        }
        Ok(Note {
            pitch,
            rhythm,
            dynamic,
        })
    }

    /// Creates an iterator of notes with idential rhythms and dynamic which can be added directly
    /// to a phrase using `phrase.add_sequential_notes` to be played sequentially or collected as
    /// a vector to use in a `Chord`.
    pub fn new_sequence<'a, PitchIter: IntoIterator<Item = u7> + 'a>(
        rhythm: f64,
        dynamic: u7,
        pitches: PitchIter,
    ) -> impl std::iter::Iterator<Item = Result<Note>> + 'a {
        pitches
            .into_iter()
            .map(move |p| Note::new(p, rhythm, dynamic))
    }

    /// Returns the pitch of the note
    pub fn pitch(&self) -> u7 {
        self.pitch
    }

    /// Returns the rhythm value of the note
    pub fn rhythm(&self) -> f64 {
        self.rhythm
    }

    /// Returns the dynamic value of the note
    pub fn dynamic(&self) -> u7 {
        self.dynamic
    }

    /// Returns the note name, accidental, and octave of the `Note`'s pitch
    ///
    /// # Arguments
    ///
    /// * `pitch`: pitch to analyse
    /// * `sharps`: specifies if an accidental should be returned as a sharp
    ///   (if false, an accidentals will be returned as a flat). This does not
    ///   affect naturals.
    pub fn pitch_info(&self, sharps: bool) -> (NoteName, Accidental, u8) {
        pitch_info(self.pitch, sharps)
    }
}

/// Returns a pitch value based on the given pitch name, octave, and accidental
///
/// # Arguments
///
/// * `letter` - The note name (between `A` and `G`)
/// * `accidental` - The accidental of the note
/// * `octave` - Which octave the note is in (`12` pitches per octave,
///   pitch `0` is a `C`, final pitch must be `127` max)
///
/// # Errors
///
/// Will return `Error::Note(Invalid::Pitch)` if final pitch is above `127`
/// or underflowed below `0`
pub fn compute_pitch(note: NoteName, accidental: Accidental, octave: u8) -> Result<u7> {
    // we use u32 to avoid an uint overflow before the value check
    let base_pitch = note as u32;
    let nat_pitch = 12 * octave as u32 + base_pitch;
    let pitch = match accidental {
        Accidental::Natural => nat_pitch,
        Accidental::Sharp => nat_pitch + 1,
        Accidental::Flat => nat_pitch - 1,
    };
    if pitch > 127 {
        return Err(NoteError::InvalidPitch(pitch).into());
    }
    Ok(u7::new(pitch as u8))
}

/// Returns the note name, accidental, and octave of the given pitch
///
/// # Arguments
///
/// * `pitch`: pitch to analyse
/// * `sharps`: specifies if an accidental should be returned as a sharp
///   (if false, an accidentals will be returned as a flat). This does not
///   affect naturals.
pub fn pitch_info(pitch: u7, sharps: bool) -> (NoteName, Accidental, u8) {
    let pitch = u8::from(pitch);
    let octave = pitch / 12;
    let mut remainder_pitch = pitch % 12;
    let mut acc = Accidental::Natural;
    if matches!(remainder_pitch, 1 | 3 | 6 | 8 | 10) {
        (acc, remainder_pitch) = if sharps {
            (Accidental::Sharp, remainder_pitch - 1)
        } else {
            (Accidental::Flat, remainder_pitch + 1)
        };
    }
    let name = match remainder_pitch {
        0 => NoteName::Do,
        2 => NoteName::Re,
        4 => NoteName::Mi,
        5 => NoteName::Fa,
        7 => NoteName::Sol,
        9 => NoteName::La,
        11 => NoteName::Si,
        _ => NoteName::Do, // This is supposedly impossible
    };
    (name, acc, octave)
}

#[cfg(test)]
mod tests {
    use super::{compute_pitch, pitch_info, u7, Accidental, NoteName};

    #[test]
    fn pitch_test() {
        let test_cases = vec![
            (NoteName::C, Accidental::Sharp, 2, true, 25),
            (NoteName::B, Accidental::Natural, 1, false, 23),
            (NoteName::B, Accidental::Natural, 1, true, 23),
            (NoteName::C, Accidental::Natural, 2, true, 24),
            (NoteName::C, Accidental::Natural, 2, false, 24),
            (NoteName::D, Accidental::Natural, 2, false, 26),
            (NoteName::E, Accidental::Natural, 2, false, 28),
            (NoteName::F, Accidental::Natural, 2, true, 29),
            (NoteName::G, Accidental::Sharp, 5, true, 68),
            (NoteName::A, Accidental::Flat, 9, false, 116),
        ];

        let compute_only_cases = vec![
            (NoteName::B, Accidental::Sharp, 1, true, 24),
            (NoteName::C, Accidental::Flat, 2, false, 23),
            (NoteName::E, Accidental::Sharp, 2, true, 29),
        ];

        for (name, acc, octave, sharps, out) in test_cases {
            assert_eq!(compute_pitch(name, acc, octave).unwrap(), out);
            assert_eq!(pitch_info(u7::new(out), sharps), (name, acc, octave));
        }
        for (name, acc, octave, _, out) in compute_only_cases {
            assert_eq!(compute_pitch(name, acc, octave).unwrap(), out);
        }
    }
}