pitch_detector/note/
note_detection_result.rs

1use crate::core::{
2    constants::{A4_FREQ, MAX_CENTS_OFFSET, MIN_FREQ, NOTES},
3    NoteName,
4};
5
6/// The resut of a pitch detection expressed as a note.
7/// You will rarely need to instantiate this struct directly. Most commonly this will be returned from
8/// [`detect_note`](crate::note::detect_note).
9#[derive(Debug, Clone, PartialEq, PartialOrd)]
10pub struct NoteDetectionResult {
11    /// The predominant frequency detected from a signal.
12    pub actual_freq: f64,
13
14    /// The note name of the detected note.
15    pub note_name: NoteName,
16
17    /// The expected frequency of the detected note.
18    pub note_freq: f64,
19
20    /// The octave of the detected note.
21    pub octave: i32,
22
23    /// The degree to which the detected not is in tune, expressed in cents. The absolute maximum `cents_offset` is
24    /// 50, since anything larger than 50 would be considered the next or previous note.
25    pub cents_offset: f64,
26
27    /// The note name of the note that comes before the detected note. Not commonly used.
28    pub previous_note_name: NoteName,
29
30    /// The note name of the note that comes after the detected note. Not commonly used.
31    pub next_note_name: NoteName,
32
33    /// A `NoteDetectionResult` will be marked as `in_tune` if the `cents_offset` is less than
34    /// [`MAX_CENTS_OFFSET`](crate::core::constants::MAX_CENTS_OFFSET).
35    pub in_tune: bool,
36}
37
38impl TryFrom<f64> for NoteDetectionResult {
39    type Error = anyhow::Error;
40    fn try_from(freq: f64) -> Result<Self, Self::Error> {
41        if freq < MIN_FREQ {
42            return Err(anyhow::anyhow!("Invalid frequency: {}", freq));
43        }
44        let steps_from_a4 = (freq / A4_FREQ).log2() * 12.0;
45        let note_freq = A4_FREQ * 2f64.powf(steps_from_a4.round() / 12.0);
46        let steps_from_c5 = steps_from_a4 - 2.0;
47        let cents_offset = (steps_from_a4 - steps_from_a4.round()) * 100.0;
48        Ok(Self {
49            actual_freq: freq,
50            note_name: NOTES
51                [(steps_from_a4.round() as isize).rem_euclid(NOTES.len() as isize) as usize]
52                .into(),
53            note_freq,
54            octave: (5. + (steps_from_c5 / 12.0).floor()) as i32,
55            cents_offset,
56            previous_note_name: NOTES
57                [(steps_from_a4.round() as isize - 1).rem_euclid(NOTES.len() as isize) as usize]
58                .into(),
59            next_note_name: NOTES
60                [(steps_from_a4.round() as isize + 1).rem_euclid(NOTES.len() as isize) as usize]
61                .into(),
62            in_tune: cents_offset.abs() < MAX_CENTS_OFFSET,
63        })
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use anyhow::Result;
71    use float_cmp::ApproxEq;
72    fn test_pitch_from_f64(
73        actual_freq: f64,
74        note_name: NoteName,
75        note_freq: f64,
76        octave: i32,
77        cents_offset: f64,
78        previous_note_name: NoteName,
79        next_note_name: NoteName,
80        in_tune: bool,
81    ) -> Result<()> {
82        let pitch = NoteDetectionResult::try_from(actual_freq)?;
83        assert_eq!(
84            pitch.note_name, note_name,
85            "Expected note name {}, got {}",
86            note_name, pitch.note_name
87        );
88        assert!(
89            pitch.note_freq.approx_eq(note_freq, (0.1, 1)),
90            "Expected note_freq: {}, actual note_freq: {}",
91            note_freq,
92            pitch.note_freq
93        );
94        assert_eq!(
95            pitch.octave, octave,
96            "Expected octave {}, got {}",
97            octave, pitch.octave
98        );
99        assert!(
100            pitch.cents_offset.approx_eq(cents_offset, (0.1, 1)),
101            "Expected cents_offset: {}, actual cents_offset: {}",
102            cents_offset,
103            pitch.cents_offset
104        );
105        assert_eq!(
106            pitch.previous_note_name, previous_note_name,
107            "Expected previous note name {}, got {}",
108            previous_note_name, pitch.previous_note_name
109        );
110        assert_eq!(
111            pitch.next_note_name, next_note_name,
112            "Expected next note name {}, got {}",
113            next_note_name, pitch.next_note_name
114        );
115        assert_eq!(
116            pitch.in_tune, in_tune,
117            "Expected in tune {}, got {}",
118            in_tune, pitch.in_tune
119        );
120        Ok(())
121    }
122
123    #[test]
124    fn pitch_from_f64_works() -> Result<()> {
125        test_pitch_from_f64(
126            311.13,
127            NoteName::DSharp,
128            311.13,
129            4,
130            0.,
131            NoteName::D,
132            NoteName::E,
133            true,
134        )?;
135        test_pitch_from_f64(
136            329.63,
137            NoteName::E,
138            329.63,
139            4,
140            0.,
141            NoteName::DSharp,
142            NoteName::F,
143            true,
144        )?;
145        test_pitch_from_f64(
146            349.23,
147            NoteName::F,
148            349.23,
149            4,
150            0.,
151            NoteName::E,
152            NoteName::FSharp,
153            true,
154        )?;
155        test_pitch_from_f64(
156            369.99,
157            NoteName::FSharp,
158            369.99,
159            4,
160            0.,
161            NoteName::F,
162            NoteName::G,
163            true,
164        )?;
165        test_pitch_from_f64(
166            392.,
167            NoteName::G,
168            392.,
169            4,
170            0.,
171            NoteName::FSharp,
172            NoteName::GSharp,
173            true,
174        )?;
175        test_pitch_from_f64(
176            440.,
177            NoteName::A,
178            440.,
179            4,
180            0.,
181            NoteName::GSharp,
182            NoteName::ASharp,
183            true,
184        )?;
185        test_pitch_from_f64(
186            493.88,
187            NoteName::B,
188            493.88,
189            4,
190            0.,
191            NoteName::ASharp,
192            NoteName::C,
193            true,
194        )?;
195        test_pitch_from_f64(
196            523.25,
197            NoteName::C,
198            523.25,
199            5,
200            0.,
201            NoteName::B,
202            NoteName::CSharp,
203            true,
204        )?;
205        test_pitch_from_f64(
206            880.,
207            NoteName::A,
208            880.,
209            5,
210            0.,
211            NoteName::GSharp,
212            NoteName::ASharp,
213            true,
214        )?;
215        test_pitch_from_f64(
216            220.,
217            NoteName::A,
218            220.,
219            3,
220            0.,
221            NoteName::GSharp,
222            NoteName::ASharp,
223            true,
224        )?;
225        test_pitch_from_f64(
226            448., // Slighly sharp A
227            NoteName::A,
228            440.,
229            4,
230            31.194,
231            NoteName::GSharp,
232            NoteName::ASharp,
233            false,
234        )?;
235        assert!(test_pitch_from_f64(
236            0.,
237            NoteName::A,
238            0.,
239            0,
240            0.,
241            NoteName::GSharp,
242            NoteName::ASharp,
243            true
244        )
245        .is_err());
246        Ok(())
247    }
248}