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}
59
60impl Pitch {
61 pub fn new(step: Step, octave: i8) -> Self {
62 Self {
63 step,
64 octave,
65 alter: 0,
66 }
67 }
68
69 pub fn with_alter(step: Step, octave: i8, alter: i8) -> Self {
70 Self {
71 step,
72 octave,
73 alter,
74 }
75 }
76
77 pub fn to_midi(&self) -> i16 {
79 let semitone = self.step.to_semitone() as i16;
80 let base = (self.octave as i16 + 1) * 12;
81 base + semitone + self.alter as i16
82 }
83
84 pub fn from_midi(midi: u8, prefer_flat: bool) -> Pitch {
90 let pc = midi % 12;
91 let (step, alter): (Step, i8) = if prefer_flat {
92 match pc {
93 0 => (Step::C, 0),
94 1 => (Step::D, -1),
95 2 => (Step::D, 0),
96 3 => (Step::E, -1),
97 4 => (Step::E, 0),
98 5 => (Step::F, 0),
99 6 => (Step::G, -1),
100 7 => (Step::G, 0),
101 8 => (Step::A, -1),
102 9 => (Step::A, 0),
103 10 => (Step::B, -1),
104 11 => (Step::B, 0),
105 _ => (Step::C, 0),
106 }
107 } else {
108 match pc {
109 0 => (Step::C, 0),
110 1 => (Step::C, 1),
111 2 => (Step::D, 0),
112 3 => (Step::D, 1),
113 4 => (Step::E, 0),
114 5 => (Step::F, 0),
115 6 => (Step::F, 1),
116 7 => (Step::G, 0),
117 8 => (Step::G, 1),
118 9 => (Step::A, 0),
119 10 => (Step::A, 1),
120 11 => (Step::B, 0),
121 _ => (Step::C, 0),
122 }
123 };
124 let step_semitone = step.to_semitone() as i16 + alter as i16;
125 let octave = ((midi as i16 - step_semitone) / 12 - 1) as i8;
126 Pitch::with_alter(step, octave, alter)
127 }
128
129 pub fn to_scientific_name(&self) -> String {
131 let accidental = match self.alter {
132 2 => "##",
133 1 => "#",
134 0 => "",
135 -1 => "b",
136 -2 => "bb",
137 _ => "",
138 };
139 format!("{}{}{}", self.step.to_char(), accidental, self.octave)
140 }
141
142 pub fn respell(&self, prefer_flat: bool) -> Pitch {
149 Pitch::from_midi(self.to_midi().clamp(0, 127) as u8, prefer_flat)
150 }
151}
152
153impl std::str::FromStr for Pitch {
154 type Err = ();
155
156 fn from_str(s: &str) -> Result<Self, Self::Err> {
160 let mut chars = s.chars().peekable();
161 let step = Step::from_char(chars.next().ok_or(())?).ok_or(())?;
162 let mut alter: i8 = 0;
163 loop {
164 match chars.peek() {
165 Some('#') => {
166 alter += 1;
167 chars.next();
168 }
169 Some('b') => {
170 alter -= 1;
171 chars.next();
172 }
173 _ => break,
174 }
175 }
176 let octave: i8 = chars.collect::<String>().parse().map_err(|_| ())?;
177 Ok(Pitch::with_alter(step, octave, alter))
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn middle_c_midi() {
187 assert_eq!(Pitch::new(Step::C, 4).to_midi(), 60);
188 }
189
190 #[test]
191 fn a4_midi() {
192 assert_eq!(Pitch::new(Step::A, 4).to_midi(), 69);
193 }
194
195 #[test]
196 fn scientific_name_sharp() {
197 let p = Pitch::with_alter(Step::F, 5, 1);
198 assert_eq!(p.to_scientific_name(), "F#5");
199 }
200
201 #[test]
202 fn scientific_name_flat() {
203 let p = Pitch::with_alter(Step::B, 3, -1);
204 assert_eq!(p.to_scientific_name(), "Bb3");
205 }
206
207 #[test]
208 fn respell_natural_unchanged() {
209 let p = Pitch::new(Step::C, 4);
210 assert_eq!(p.respell(true), Pitch::new(Step::C, 4));
211 assert_eq!(p.respell(false), Pitch::new(Step::C, 4));
212 }
213
214 #[test]
215 fn respell_csharp_to_db() {
216 let p = Pitch::with_alter(Step::C, 4, 1); let flat = p.respell(true);
218 assert_eq!(flat.step, Step::D);
219 assert_eq!(flat.alter, -1);
220 assert_eq!(flat.octave, 4);
221 assert_eq!(flat.to_midi(), 61);
222 }
223
224 #[test]
225 fn respell_db_to_csharp() {
226 let p = Pitch::with_alter(Step::D, 4, -1); let sharp = p.respell(false);
228 assert_eq!(sharp.step, Step::C);
229 assert_eq!(sharp.alter, 1);
230 assert_eq!(sharp.octave, 4);
231 assert_eq!(sharp.to_midi(), 61);
232 }
233
234 #[test]
235 fn respell_bsharp_to_c_next_octave() {
236 let p = Pitch::with_alter(Step::B, 4, 1); let resolved = p.respell(true);
238 assert_eq!(resolved.step, Step::C);
239 assert_eq!(resolved.alter, 0);
240 assert_eq!(resolved.octave, 5);
241 assert_eq!(resolved.to_midi(), 72);
242 }
243
244 #[test]
245 fn respell_cb_to_b_prev_octave() {
246 let p = Pitch::with_alter(Step::C, 5, -1); let resolved = p.respell(false);
248 assert_eq!(resolved.step, Step::B);
249 assert_eq!(resolved.alter, 0);
250 assert_eq!(resolved.octave, 4);
251 assert_eq!(resolved.to_midi(), 71);
252 }
253
254 #[test]
255 fn from_midi_middle_c() {
256 let p = Pitch::from_midi(60, false);
257 assert_eq!(p.step, Step::C);
258 assert_eq!(p.octave, 4);
259 assert_eq!(p.alter, 0);
260 }
261
262 #[test]
263 fn from_midi_c_sharp_prefer_sharp() {
264 let p = Pitch::from_midi(61, false);
265 assert_eq!(p.step, Step::C);
266 assert_eq!(p.octave, 4);
267 assert_eq!(p.alter, 1);
268 }
269
270 #[test]
271 fn from_midi_d_flat_prefer_flat() {
272 let p = Pitch::from_midi(61, true);
273 assert_eq!(p.step, Step::D);
274 assert_eq!(p.octave, 4);
275 assert_eq!(p.alter, -1);
276 }
277
278 #[test]
279 fn from_midi_a4() {
280 let p = Pitch::from_midi(69, false);
281 assert_eq!(p.step, Step::A);
282 assert_eq!(p.octave, 4);
283 assert_eq!(p.alter, 0);
284 }
285
286 #[test]
287 fn from_midi_respell_roundtrip() {
288 for midi in 21u8..=108 {
289 for prefer_flat in [false, true] {
290 let p = Pitch::from_midi(midi, prefer_flat);
291 assert_eq!(
292 p.to_midi() as u8,
293 midi,
294 "from_midi({midi},{prefer_flat}) roundtrip failed: {:?}",
295 p
296 );
297 }
298 }
299 }
300
301 #[test]
302 fn from_str_c4() {
303 let p: Pitch = "C4".parse().unwrap();
304 assert_eq!(p, Pitch::new(Step::C, 4));
305 }
306
307 #[test]
308 fn from_str_fsharp5() {
309 let p: Pitch = "F#5".parse().unwrap();
310 assert_eq!(p, Pitch::with_alter(Step::F, 5, 1));
311 }
312
313 #[test]
314 fn from_str_bflat3() {
315 let p: Pitch = "Bb3".parse().unwrap();
316 assert_eq!(p.step, Step::B);
317 assert_eq!(p.alter, -1);
318 assert_eq!(p.octave, 3);
319 assert_eq!(p.to_midi(), 58);
320 }
321
322 #[test]
323 fn from_str_double_sharp() {
324 let p: Pitch = "C##4".parse().unwrap();
325 assert_eq!(p.step, Step::C);
326 assert_eq!(p.alter, 2);
327 assert_eq!(p.octave, 4);
328 }
329
330 #[test]
331 fn from_str_invalid_step_returns_err() {
332 assert!("X4".parse::<Pitch>().is_err());
333 assert!("".parse::<Pitch>().is_err());
334 assert!("C".parse::<Pitch>().is_err()); }
336
337 #[test]
338 fn from_str_roundtrip() {
339 for midi in 21u8..=108 {
340 for prefer_flat in [false, true] {
341 let p = Pitch::from_midi(midi, prefer_flat);
342 let name = p.to_scientific_name();
343 let parsed: Pitch = name
344 .parse()
345 .unwrap_or_else(|_| panic!("parse failed for {:?}", name));
346 assert_eq!(
347 parsed.to_midi() as u8,
348 midi,
349 "roundtrip failed for {:?} (midi {})",
350 name,
351 midi
352 );
353 }
354 }
355 }
356
357 #[test]
358 fn respell_fsharp_to_gb() {
359 let p = Pitch::with_alter(Step::F, 4, 1); let flat = p.respell(true);
361 assert_eq!(flat.step, Step::G);
362 assert_eq!(flat.alter, -1);
363 assert_eq!(flat.to_midi(), 66);
364 }
365}