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