1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4pub enum Step {
5 C,
6 D,
7 E,
8 F,
9 G,
10 A,
11 B,
12}
13
14impl Step {
15 pub fn to_semitone(&self) -> u8 {
16 match self {
17 Step::C => 0,
18 Step::D => 2,
19 Step::E => 4,
20 Step::F => 5,
21 Step::G => 7,
22 Step::A => 9,
23 Step::B => 11,
24 }
25 }
26
27 pub fn from_char(c: char) -> Option<Self> {
28 match c.to_ascii_uppercase() {
29 'C' => Some(Step::C),
30 'D' => Some(Step::D),
31 'E' => Some(Step::E),
32 'F' => Some(Step::F),
33 'G' => Some(Step::G),
34 'A' => Some(Step::A),
35 'B' => Some(Step::B),
36 _ => None,
37 }
38 }
39
40 pub fn to_char(&self) -> char {
41 match self {
42 Step::C => 'C',
43 Step::D => 'D',
44 Step::E => 'E',
45 Step::F => 'F',
46 Step::G => 'G',
47 Step::A => 'A',
48 Step::B => 'B',
49 }
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54pub struct Pitch {
55 pub step: Step,
56 pub octave: i8,
57 pub alter: i8,
58 #[serde(default)]
60 pub microtone_cents: i16,
61}
62
63impl Pitch {
64 pub fn new(step: Step, octave: i8) -> Self {
65 Self {
66 step,
67 octave,
68 alter: 0,
69 microtone_cents: 0,
70 }
71 }
72
73 pub fn with_alter(step: Step, octave: i8, alter: i8) -> Self {
74 Self {
75 step,
76 octave,
77 alter,
78 microtone_cents: 0,
79 }
80 }
81
82 pub fn with_microtone(step: Step, octave: i8, alter: i8, microtone_cents: i16) -> Self {
83 let mut pitch = Self::with_alter(step, octave, alter);
84 pitch.microtone_cents = microtone_cents.clamp(-99, 99);
85 pitch
86 }
87
88 pub fn to_midi(&self) -> i16 {
90 let semitone = self.step.to_semitone() as i16;
91 let base = (self.octave as i16 + 1) * 12;
92 (base as f32 + semitone as f32 + self.alter as f32 + self.microtone_cents as f32 / 100.0)
93 .round() as i16
94 }
95
96 pub fn from_midi(midi: u8, prefer_flat: bool) -> Pitch {
102 let pc = midi % 12;
103 let (step, alter): (Step, i8) = if prefer_flat {
104 match pc {
105 0 => (Step::C, 0),
106 1 => (Step::D, -1),
107 2 => (Step::D, 0),
108 3 => (Step::E, -1),
109 4 => (Step::E, 0),
110 5 => (Step::F, 0),
111 6 => (Step::G, -1),
112 7 => (Step::G, 0),
113 8 => (Step::A, -1),
114 9 => (Step::A, 0),
115 10 => (Step::B, -1),
116 11 => (Step::B, 0),
117 _ => (Step::C, 0),
118 }
119 } else {
120 match pc {
121 0 => (Step::C, 0),
122 1 => (Step::C, 1),
123 2 => (Step::D, 0),
124 3 => (Step::D, 1),
125 4 => (Step::E, 0),
126 5 => (Step::F, 0),
127 6 => (Step::F, 1),
128 7 => (Step::G, 0),
129 8 => (Step::G, 1),
130 9 => (Step::A, 0),
131 10 => (Step::A, 1),
132 11 => (Step::B, 0),
133 _ => (Step::C, 0),
134 }
135 };
136 let step_semitone = step.to_semitone() as i16 + alter as i16;
137 let octave = ((midi as i16 - step_semitone) / 12 - 1) as i8;
138 Pitch::with_alter(step, octave, alter)
139 }
140
141 pub fn to_scientific_name(&self) -> String {
143 let accidental = match self.alter {
144 2 => "##",
145 1 => "#",
146 0 => "",
147 -1 => "b",
148 -2 => "bb",
149 _ => "",
150 };
151 format!("{}{}{}", self.step.to_char(), accidental, self.octave)
152 }
153
154 pub fn respell(&self, prefer_flat: bool) -> Pitch {
161 let mut pitch = Pitch::from_midi(self.to_midi().clamp(0, 127) as u8, prefer_flat);
162 pitch.microtone_cents = self.microtone_cents;
163 pitch
164 }
165}
166
167impl std::str::FromStr for Pitch {
168 type Err = ();
169
170 fn from_str(s: &str) -> Result<Self, Self::Err> {
174 let mut chars = s.chars().peekable();
175 let step = Step::from_char(chars.next().ok_or(())?).ok_or(())?;
176 let mut alter: i8 = 0;
177 loop {
178 match chars.peek() {
179 Some('#') => {
180 alter += 1;
181 chars.next();
182 }
183 Some('b') => {
184 alter -= 1;
185 chars.next();
186 }
187 _ => break,
188 }
189 }
190 let octave: i8 = chars.collect::<String>().parse().map_err(|_| ())?;
191 Ok(Pitch::with_alter(step, octave, alter))
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn middle_c_midi() {
201 assert_eq!(Pitch::new(Step::C, 4).to_midi(), 60);
202 }
203
204 #[test]
205 fn a4_midi() {
206 assert_eq!(Pitch::new(Step::A, 4).to_midi(), 69);
207 }
208
209 #[test]
210 fn scientific_name_sharp() {
211 let p = Pitch::with_alter(Step::F, 5, 1);
212 assert_eq!(p.to_scientific_name(), "F#5");
213 }
214
215 #[test]
216 fn scientific_name_flat() {
217 let p = Pitch::with_alter(Step::B, 3, -1);
218 assert_eq!(p.to_scientific_name(), "Bb3");
219 }
220
221 #[test]
222 fn respell_natural_unchanged() {
223 let p = Pitch::new(Step::C, 4);
224 assert_eq!(p.respell(true), Pitch::new(Step::C, 4));
225 assert_eq!(p.respell(false), Pitch::new(Step::C, 4));
226 }
227
228 #[test]
229 fn respell_csharp_to_db() {
230 let p = Pitch::with_alter(Step::C, 4, 1); let flat = p.respell(true);
232 assert_eq!(flat.step, Step::D);
233 assert_eq!(flat.alter, -1);
234 assert_eq!(flat.octave, 4);
235 assert_eq!(flat.to_midi(), 61);
236 }
237
238 #[test]
239 fn respell_db_to_csharp() {
240 let p = Pitch::with_alter(Step::D, 4, -1); let sharp = p.respell(false);
242 assert_eq!(sharp.step, Step::C);
243 assert_eq!(sharp.alter, 1);
244 assert_eq!(sharp.octave, 4);
245 assert_eq!(sharp.to_midi(), 61);
246 }
247
248 #[test]
249 fn respell_bsharp_to_c_next_octave() {
250 let p = Pitch::with_alter(Step::B, 4, 1); let resolved = p.respell(true);
252 assert_eq!(resolved.step, Step::C);
253 assert_eq!(resolved.alter, 0);
254 assert_eq!(resolved.octave, 5);
255 assert_eq!(resolved.to_midi(), 72);
256 }
257
258 #[test]
259 fn respell_cb_to_b_prev_octave() {
260 let p = Pitch::with_alter(Step::C, 5, -1); let resolved = p.respell(false);
262 assert_eq!(resolved.step, Step::B);
263 assert_eq!(resolved.alter, 0);
264 assert_eq!(resolved.octave, 4);
265 assert_eq!(resolved.to_midi(), 71);
266 }
267
268 #[test]
269 fn from_midi_middle_c() {
270 let p = Pitch::from_midi(60, false);
271 assert_eq!(p.step, Step::C);
272 assert_eq!(p.octave, 4);
273 assert_eq!(p.alter, 0);
274 }
275
276 #[test]
277 fn from_midi_c_sharp_prefer_sharp() {
278 let p = Pitch::from_midi(61, false);
279 assert_eq!(p.step, Step::C);
280 assert_eq!(p.octave, 4);
281 assert_eq!(p.alter, 1);
282 }
283
284 #[test]
285 fn from_midi_d_flat_prefer_flat() {
286 let p = Pitch::from_midi(61, true);
287 assert_eq!(p.step, Step::D);
288 assert_eq!(p.octave, 4);
289 assert_eq!(p.alter, -1);
290 }
291
292 #[test]
293 fn from_midi_a4() {
294 let p = Pitch::from_midi(69, false);
295 assert_eq!(p.step, Step::A);
296 assert_eq!(p.octave, 4);
297 assert_eq!(p.alter, 0);
298 }
299
300 #[test]
301 fn from_midi_respell_roundtrip() {
302 for midi in 21u8..=108 {
303 for prefer_flat in [false, true] {
304 let p = Pitch::from_midi(midi, prefer_flat);
305 assert_eq!(
306 p.to_midi() as u8,
307 midi,
308 "from_midi({midi},{prefer_flat}) roundtrip failed: {:?}",
309 p
310 );
311 }
312 }
313 }
314
315 #[test]
316 fn from_str_c4() {
317 let p: Pitch = "C4".parse().unwrap();
318 assert_eq!(p, Pitch::new(Step::C, 4));
319 }
320
321 #[test]
322 fn from_str_fsharp5() {
323 let p: Pitch = "F#5".parse().unwrap();
324 assert_eq!(p, Pitch::with_alter(Step::F, 5, 1));
325 }
326
327 #[test]
328 fn from_str_bflat3() {
329 let p: Pitch = "Bb3".parse().unwrap();
330 assert_eq!(p.step, Step::B);
331 assert_eq!(p.alter, -1);
332 assert_eq!(p.octave, 3);
333 assert_eq!(p.to_midi(), 58);
334 }
335
336 #[test]
337 fn from_str_double_sharp() {
338 let p: Pitch = "C##4".parse().unwrap();
339 assert_eq!(p.step, Step::C);
340 assert_eq!(p.alter, 2);
341 assert_eq!(p.octave, 4);
342 }
343
344 #[test]
345 fn from_str_invalid_step_returns_err() {
346 assert!("X4".parse::<Pitch>().is_err());
347 assert!("".parse::<Pitch>().is_err());
348 assert!("C".parse::<Pitch>().is_err()); }
350
351 #[test]
352 fn from_str_roundtrip() {
353 for midi in 21u8..=108 {
354 for prefer_flat in [false, true] {
355 let p = Pitch::from_midi(midi, prefer_flat);
356 let name = p.to_scientific_name();
357 let parsed: Pitch = name
358 .parse()
359 .unwrap_or_else(|_| panic!("parse failed for {:?}", name));
360 assert_eq!(
361 parsed.to_midi() as u8,
362 midi,
363 "roundtrip failed for {:?} (midi {})",
364 name,
365 midi
366 );
367 }
368 }
369 }
370
371 #[test]
372 fn respell_fsharp_to_gb() {
373 let p = Pitch::with_alter(Step::F, 4, 1); let flat = p.respell(true);
375 assert_eq!(flat.step, Step::G);
376 assert_eq!(flat.alter, -1);
377 assert_eq!(flat.to_midi(), 66);
378 }
379}