1use crate::{solfa::Solfa, key::Key};
2use crate::octave::Octave;
3use crate::sharp_flat::SharpFlat;
4use std::fmt::{self};
5use derive_builder::Builder;
6use super::octave;
7
8#[derive(Debug)]
10pub enum PitchError {
11 TooLow(Solfa, Octave, SharpFlat, i32),
13 TooHigh(Solfa, Octave, SharpFlat, i32),
15 InvalidScoreOffset(i32),
17}
18
19impl fmt::Display for PitchError {
20 fn fmt(self: &PitchError, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 match self {
22 PitchError::TooLow(solfa, octave, sharp_flat, value) => f.write_fmt(
23 format_args!("Pitch({:?}, {:?}, {:?}) is too low {}", solfa, octave, sharp_flat, value)
24 ),
25 PitchError::TooHigh(solfa, octave, sharp_flat, value) => f.write_fmt(
26 format_args!("Pitch({:?}, {:?}, {:?}) is too high {}", solfa, octave, sharp_flat, value)
27 ),
28 PitchError::InvalidScoreOffset(score_offset) => f.write_fmt(
29 format_args!("Score offset error({})", score_offset)
30 )
31 }
32 }
33}
34
35#[derive(serde::Deserialize, serde::Serialize)]
52#[serde(from = "PitchSerializedForm")]
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Builder)]
54#[builder(default)]
55pub struct Pitch {
56 solfa: Solfa,
58 octave: Octave,
60 sharp_flat: SharpFlat,
62
63 #[serde(skip)]
65 value: u8,
66 #[serde(skip)]
68 score_offset: i8,
69}
70
71impl Default for Pitch {
72 fn default() -> Self {
73 DEFAULT
74 }
75}
76
77impl From<PitchSerializedForm> for Pitch {
78 fn from(from: PitchSerializedForm) -> Self {
79 Self::new(from.solfa, from.octave, from.sharp_flat)
80 }
81}
82
83#[derive(serde::Deserialize)]
84struct PitchSerializedForm {
85 solfa: Solfa,
86 octave: Octave,
87 sharp_flat: SharpFlat,
88}
89
90pub const MAX_VALUE: i8 = 127;
92pub const MIN_VALUE: i8 = 0;
94
95pub const MIN: Pitch = Pitch::new(Solfa::C, Octave::OctM2, SharpFlat::Null);
97pub const MAX: Pitch = Pitch::new(Solfa::G, Octave::Oct8, SharpFlat::Null);
99const DEFAULT: Pitch = Pitch::new(Solfa::A, Octave::Oct4, SharpFlat::Null);
101
102pub const MIN_SCORE_OFFSET: i32 = 0;
104pub const MAX_SCORE_OFFSET: i32 = 74;
106
107impl Pitch {
108 pub const fn new(solfa: Solfa, octave: Octave, sharp_flat: SharpFlat) -> Self {
120 match Self::value_of(solfa, octave, sharp_flat) {
121 Err(_pe) => panic!("Logic error."),
122 Ok(p) => p
123 }
124 }
125
126 pub fn apply_key(self, key: Key) -> Result<Self, PitchError> {
139 let solfas = Key::SOLFAS;
140 if self.sharp_flat == SharpFlat::Null {
141 match solfas.get(&key) {
142 Some(solfas) =>
143 if solfas.contains(&self.solfa) {
144 let sharp_flat = if key.is_flat() { SharpFlat::Flat } else { SharpFlat::Sharp };
145 Pitch::value_of(self.solfa, self.octave, sharp_flat)
146 } else {
147 Ok(self)
148 }
149 None => Ok(self)
150 }
151 } else {
152 Ok(self)
153 }
154 }
155
156 pub const fn to_value(solfa: Solfa, octave: Octave, sharp_flat: SharpFlat) -> i32 {
157 solfa.pitch_offset() + (octave.value() + Octave::BIAS_VALUE) * 12 + sharp_flat.offset()
158 }
159
160 pub const fn value_of(solfa: Solfa, octave: Octave, sharp_flat: SharpFlat) -> Result<Self, PitchError> {
161 let v: i32 = Self::to_value(solfa, octave, sharp_flat);
162 if v < (MIN_VALUE as i32) {
163 Err(PitchError::TooLow(solfa, octave, sharp_flat, v))
164 } else if (MAX_VALUE as i32) < v {
165 Err(PitchError::TooHigh(solfa, octave, sharp_flat, v))
166 } else {
167 let so = (solfa.score_offset() + 7 * octave.offset()) as i8;
168 Ok(
169 Self {
170 solfa,
171 octave,
172 sharp_flat,
173 value: v as u8,
174 score_offset: so
175 }
176 )
177 }
178 }
179
180 pub fn toggle_sharp(self) -> Result<Self, PitchError> {
181 let sharp_flat = match self.sharp_flat {
182 SharpFlat::Sharp => SharpFlat::DoubleSharp,
183 SharpFlat::DoubleSharp => SharpFlat::Null,
184 _ => SharpFlat::Sharp
185 };
186 Self::value_of(self.solfa, self.octave, sharp_flat)
187 }
188
189 pub fn toggle_flat(self) -> Result<Self, PitchError> {
190 let sharp_flat = match self.sharp_flat {
191 SharpFlat::Flat => SharpFlat::DoubleFlat,
192 SharpFlat::DoubleFlat => SharpFlat::Null,
193 _ => SharpFlat::Flat
194 };
195 Self::value_of(self.solfa, self.octave, sharp_flat)
196 }
197
198 pub fn toggle_natural(self) -> Result<Self, PitchError> {
199 let sharp_flat = match self.sharp_flat {
200 SharpFlat::Natural => SharpFlat::Null,
201 _ => SharpFlat::Natural
202 };
203 Self::value_of(self.solfa, self.octave, sharp_flat)
204 }
205
206 pub fn from_score_offset(score_offset: i32) -> Self {
207 let score_offset = score_offset.clamp(MIN_SCORE_OFFSET, MAX_SCORE_OFFSET);
208 let (solfa, octave) = Self::score_offset_to_solfa_octave(score_offset);
209 Self::value_of(solfa, octave, SharpFlat::Null).unwrap()
210 }
211
212 pub fn with_score_offset_delta(self, score_offset_delta: i32) -> Result<Self, PitchError> {
213 let score_offset = self.score_offset as i32 + score_offset_delta;
214 if !(MIN_SCORE_OFFSET..=MAX_SCORE_OFFSET).contains(&score_offset) {
215 return Err(PitchError::InvalidScoreOffset(score_offset));
216 }
217
218 let (solfa, octave) = Self::score_offset_to_solfa_octave(score_offset);
219 Self::value_of(solfa, octave, self.sharp_flat)
220 }
221
222 pub fn score_offset_to_solfa_octave(score_offset: i32) -> (Solfa, Octave) {
223 let octave_offset = score_offset / 7;
224 let solfa_offset = score_offset - (octave_offset * 7);
225 let solfa = Solfa::from_score_offset(solfa_offset);
226 let octave = Octave::from_score_offset(octave_offset).unwrap();
227 (solfa, octave)
228 }
229
230 #[inline]
231 pub const fn score_offset(self) -> i8 { self.score_offset }
232
233 #[inline]
234 pub const fn sharp_flat(self) -> SharpFlat {
235 self.sharp_flat
236 }
237
238 pub fn up(self) -> Result<Self, PitchError> {
245 let mut solfa = self.solfa;
246 let mut octave = self.octave;
247
248 if solfa == Solfa::B {
249 if octave != octave::MAX {
250 octave += 1;
251 solfa = Solfa::C;
252 } else {
253 return Err(PitchError::TooHigh(solfa, octave, self.sharp_flat, -1));
254 }
255 } else {
256 solfa += 1;
257 }
258 Self::value_of(solfa, octave, self.sharp_flat)
259 }
260
261 pub fn down(self) -> Result<Self, PitchError> {
268 let mut solfa = self.solfa;
269 let mut octave = self.octave;
270
271 if solfa == Solfa::C {
272 if octave != octave::MIN {
273 octave -= 1;
274 solfa = Solfa::B;
275 } else {
276 return Err(PitchError::TooLow(solfa, octave, self.sharp_flat, -1));
277 }
278 } else {
279 solfa -= 1;
280 }
281 Self::value_of(solfa, octave, self.sharp_flat)
282 }
283
284 #[inline]
286 pub fn solfa(self) -> Solfa {
287 self.solfa
288 }
289
290 #[inline]
292 pub fn octave(self) -> Octave {
293 self.octave
294 }
295
296 #[inline]
298 pub fn value(self) -> u8 {
299 self.value
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use crate::key::Key;
306 use crate::pitch::MAX_SCORE_OFFSET;
307 use crate::pitch::MIN_SCORE_OFFSET;
308 use crate::pitch::Pitch;
309 use crate::pitch::MIN;
310 use crate::pitch::MAX;
311 use crate::solfa::Solfa;
312 use crate::octave::Octave;
313 use crate::sharp_flat::SharpFlat;
314 use serde_json::Value;
315 use serde_json::json;
316
317 #[test]
318 #[should_panic]
319 fn too_low() {
320 Pitch::new(Solfa::C, Octave::value_of(-2).unwrap(), SharpFlat::Flat);
321 }
322
323 #[test]
324 fn lowest() {
325 assert_eq!(0, MIN.value);
326 assert_eq!(MIN_SCORE_OFFSET, MIN.score_offset() as i32);
327 }
328
329 #[test]
330 fn f7() {
331 assert_eq!(113, Pitch::new(Solfa::F, Octave::Oct7, SharpFlat::Null).value);
332 }
333
334 #[test]
335 fn highest() {
336 assert_eq!(127, MAX.value);
337 assert_eq!(MAX_SCORE_OFFSET, MAX.score_offset() as i32);
338 }
339
340 #[test]
341 #[should_panic]
342 fn too_high() {
343 Pitch::new(Solfa::G, Octave::Oct8, SharpFlat::Sharp);
344 }
345
346 #[test]
347 fn lowest_score_offset() {
348 assert_eq!(0, Pitch::new(Solfa::C, Octave::OctM2, SharpFlat::Null).score_offset());
349 }
350
351 #[test]
352 fn score_offset() {
353 assert_eq!(1, Pitch::new(Solfa::D, Octave::OctM2, SharpFlat::Null).score_offset());
354 assert_eq!(6, Pitch::new(Solfa::B, Octave::OctM2, SharpFlat::Null).score_offset());
355 assert_eq!(7, Pitch::new(Solfa::C, Octave::OctM1, SharpFlat::Null).score_offset());
356 }
357
358 #[test]
359 #[should_panic]
360 fn up_err() {
361 MAX.up().unwrap();
362 }
363
364 #[test]
365 fn up() {
366 assert_eq!(
367 Pitch::new(Solfa::F, Octave::Oct8, SharpFlat::Null).up().unwrap(),
368 Pitch::new(Solfa::G, Octave::Oct8, SharpFlat::Null)
369 );
370 assert_eq!(
371 Pitch::new(Solfa::B, Octave::Oct7, SharpFlat::Null).up().unwrap(),
372 Pitch::new(Solfa::C, Octave::Oct8, SharpFlat::Null)
373 );
374 }
375
376 #[test]
377 fn down() {
378 assert_eq!(
379 Pitch::new(Solfa::C, Octave::Oct8, SharpFlat::Null).down().unwrap(),
380 Pitch::new(Solfa::B, Octave::Oct7, SharpFlat::Null)
381 );
382 assert_eq!(
383 Pitch::new(Solfa::D, Octave::Oct7, SharpFlat::Null).down().unwrap(),
384 Pitch::new(Solfa::C, Octave::Oct7, SharpFlat::Null)
385 );
386 }
387
388 #[test]
389 #[should_panic]
390 fn down_err() {
391 MIN.down().unwrap();
392 }
393
394 #[test]
395 fn from_score_offset() {
396 assert_eq!(MIN, Pitch::from_score_offset(MIN_SCORE_OFFSET - 1));
397 assert_eq!(MAX, Pitch::from_score_offset(MAX_SCORE_OFFSET + 1));
398
399 assert_eq!(MIN, Pitch::from_score_offset(MIN_SCORE_OFFSET));
400 assert_eq!(MAX, Pitch::from_score_offset(MAX_SCORE_OFFSET));
401 }
402
403 #[test]
404 fn with_score_offset_delta() {
405 assert_eq!(
406 Pitch::new(Solfa::C, Octave::Oct8, SharpFlat::Flat).with_score_offset_delta(1).unwrap(),
407 Pitch::new(Solfa::D, Octave::Oct8, SharpFlat::Flat)
408 );
409
410 assert_eq!(
411 Pitch::new(Solfa::C, Octave::Oct7, SharpFlat::Sharp).with_score_offset_delta(8).unwrap(),
412 Pitch::new(Solfa::D, Octave::Oct8, SharpFlat::Sharp)
413 );
414
415 assert_eq!(
416 Pitch::new(Solfa::C, Octave::Oct7, SharpFlat::Sharp).with_score_offset_delta(-8).unwrap(),
417 Pitch::new(Solfa::B, Octave::Oct5, SharpFlat::Sharp)
418 );
419 }
420
421 #[test]
422 #[should_panic]
423 fn with_score_offset_delta_min_error() {
424 let _ = MIN.with_score_offset_delta(-1).unwrap();
425 }
426
427 #[test]
428 #[should_panic]
429 fn with_score_offset_delta_max_error() {
430 let _ = MAX.with_score_offset_delta(1).unwrap();
431 }
432
433 #[test]
434 fn can_serialize_pitch() {
435 let json_str = serde_json::to_string(&Pitch::new(
436 Solfa::C, Octave::Oct2, SharpFlat::DoubleFlat
437 )).unwrap();
438 let json: Value = serde_json::from_str(&json_str).unwrap();
439 assert_eq!(
440 json,
441 json!({
442 "octave": "Oct2",
443 "sharp_flat": "DoubleFlat",
444 "solfa": "C"
445 })
446 );
447 }
448
449 #[test]
450 fn can_deserialize_pitch() {
451 let pitch: Pitch = serde_json::from_str(r#"
452 {
453 "octave": "Oct2",
454 "sharp_flat": "DoubleFlat",
455 "solfa": "C"
456 }"#).unwrap();
457 let expected = Pitch::new(Solfa::C, Octave::Oct2, SharpFlat::DoubleFlat);
458 assert_eq!(pitch, expected);
459 assert_eq!(pitch.score_offset, expected.score_offset);
460 }
461
462 #[test]
463 fn apply() {
464 let pitch = Pitch::new(Solfa::F, Octave::Oct1, SharpFlat::Null);
465 assert_eq!(pitch.apply_key(Key::SHARP_1).unwrap(), Pitch::new(Solfa::F, Octave::Oct1, SharpFlat::Sharp));
466
467 let pitch = Pitch::new(Solfa::F, Octave::Oct1, SharpFlat::Flat);
468 assert_eq!(pitch.apply_key(Key::SHARP_1).unwrap(), Pitch::new(Solfa::F, Octave::Oct1, SharpFlat::Flat));
469
470 let pitch = Pitch::new(Solfa::E, Octave::Oct1, SharpFlat::Null);
471 assert_eq!(pitch.apply_key(Key::FLAT_2).unwrap(), Pitch::new(Solfa::E, Octave::Oct1, SharpFlat::Flat));
472
473 let pitch = Pitch::new(Solfa::E, Octave::Oct1, SharpFlat::Sharp);
474 assert_eq!(pitch.apply_key(Key::FLAT_2).unwrap(), Pitch::new(Solfa::E, Octave::Oct1, SharpFlat::Sharp));
475
476 let pitch = Pitch::new(Solfa::F, Octave::Oct1, SharpFlat::Null);
477 assert_eq!(pitch.apply_key(Key::FLAT_2).unwrap(), Pitch::new(Solfa::F, Octave::Oct1, SharpFlat::Null));
478 }
479}