1use crate::core::{
2 constants::{A4_FREQ, MAX_CENTS_OFFSET, MIN_FREQ, NOTES},
3 NoteName,
4};
5
6#[derive(Debug, Clone, PartialEq, PartialOrd)]
10pub struct NoteDetectionResult {
11 pub actual_freq: f64,
13
14 pub note_name: NoteName,
16
17 pub note_freq: f64,
19
20 pub octave: i32,
22
23 pub cents_offset: f64,
26
27 pub previous_note_name: NoteName,
29
30 pub next_note_name: NoteName,
32
33 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., 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}