1mod display;
2mod enharmonic;
3mod harmonics;
4mod names;
5
6pub use display::AccidentalDisplayOptions;
7pub use enharmonic::{CriterionFunction, dissonance_score, simplify_multiple_enharmonics};
8pub use names::{
9 CHROMATIC_PITCH_CLASS_NAMES, Language, from_language_name, language_dictionary,
10 pitch_class_name,
11};
12
13use harmonics::*;
14
15pub(crate) mod accidental;
16pub(crate) mod microtone;
17pub(crate) mod pitchclass;
18
19use crate::defaults::FloatType;
20use crate::defaults::IntegerType;
21use crate::defaults::Octave;
22use crate::defaults::PITCH_OCTAVE;
23use crate::defaults::PITCH_SPACE_SIGNIFICANT_DIGITS;
24use crate::defaults::PITCH_STEP;
25use crate::defaults::UnsignedIntegerType;
26use crate::error::Error;
27use crate::error::Result;
28use crate::interval::Interval;
29use crate::key::keysignature::KeySignature;
30use crate::stepname::StepName;
31use crate::tuningsystem::TuningSystem;
32
33pub use accidental::{Accidental, AccidentalAttribute, AccidentalSpecifier};
34pub use microtone::{Microtone, MicrotoneSpecifier};
35use pitchclass::convert_ps_to_oct;
36pub use pitchclass::{PitchClass, PitchClassSpecifier, convert_pitch_class_to_str};
37
38use itertools::Itertools;
39use num::Num;
40use num_traits::ToPrimitive;
41use ordered_float::OrderedFloat;
42use std::cmp::Ordering;
43use std::fmt::{Display, Formatter};
44use std::hash::{Hash, Hasher};
45use std::str::FromStr;
46use std::sync::{Arc, LazyLock};
47
48#[derive(Clone, Debug, PartialEq)]
49#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
50pub enum PitchName {
52 Name(String),
54 Number(FloatType),
56}
57
58impl From<&str> for PitchName {
59 fn from(value: &str) -> Self {
60 Self::Name(value.to_string())
61 }
62}
63
64impl From<String> for PitchName {
65 fn from(value: String) -> Self {
66 Self::Name(value)
67 }
68}
69
70impl From<IntegerType> for PitchName {
71 fn from(value: IntegerType) -> Self {
72 Self::Number(value as FloatType)
73 }
74}
75
76impl From<FloatType> for PitchName {
77 fn from(value: FloatType) -> Self {
78 Self::Number(value)
79 }
80}
81
82#[derive(Clone, Debug, Default, PartialEq)]
83#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
84#[non_exhaustive]
86pub struct PitchOptions {
87 pub name: Option<PitchName>,
89 pub step: Option<char>,
91 pub octave: Octave,
93 pub accidental: Option<AccidentalSpecifier>,
95 pub microtone: Option<MicrotoneSpecifier>,
97 pub pitch_class: Option<PitchClassSpecifier>,
99 pub midi: Option<IntegerType>,
101 pub ps: Option<FloatType>,
103 pub fundamental: Option<Pitch>,
105}
106
107impl PitchOptions {
108 pub fn new() -> Self {
110 Self::default()
111 }
112
113 pub fn name(mut self, name: impl Into<PitchName>) -> Self {
115 self.name = Some(name.into());
116 self
117 }
118
119 pub fn step(mut self, step: char) -> Self {
121 self.step = Some(step);
122 self
123 }
124
125 pub fn octave(mut self, octave: IntegerType) -> Self {
127 self.octave = Some(octave);
128 self
129 }
130
131 pub fn accidental(mut self, accidental: impl Into<AccidentalSpecifier>) -> Self {
133 self.accidental = Some(accidental.into());
134 self
135 }
136
137 pub fn microtone(mut self, microtone: impl Into<MicrotoneSpecifier>) -> Self {
139 self.microtone = Some(microtone.into());
140 self
141 }
142
143 pub fn pitch_class(mut self, pitch_class: impl Into<PitchClassSpecifier>) -> Self {
145 self.pitch_class = Some(pitch_class.into());
146 self
147 }
148
149 pub fn midi(mut self, midi: IntegerType) -> Self {
151 self.midi = Some(midi);
152 self
153 }
154
155 pub fn ps(mut self, ps: FloatType) -> Self {
157 self.ps = Some(ps);
158 self
159 }
160
161 pub fn pitch_space(mut self, pitch_space: FloatType) -> Self {
163 self.ps = Some(pitch_space);
164 self
165 }
166
167 pub fn fundamental(mut self, fundamental: Pitch) -> Self {
169 self.fundamental = Some(fundamental);
170 self
171 }
172
173 pub fn build(self) -> Result<Pitch> {
175 Pitch::from_options(self)
176 }
177}
178
179#[derive(Clone, Debug)]
180#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
181#[must_use]
183pub struct Pitch {
184 step: StepName,
185 octave: Octave,
186 accidental: Option<Accidental>,
189 microtone: Option<Microtone>,
190 spelling_is_inferred: bool,
191 #[cfg_attr(feature = "serde", serde(skip))]
192 fundamental: Option<Arc<Pitch>>,
193}
194
195impl PartialEq for Pitch {
196 fn eq(&self, other: &Self) -> bool {
197 self.step == other.step
198 && self.octave == other.octave
199 && self.accidental == other.accidental
200 && self.microtone == other.microtone
201 }
202}
203
204impl Eq for Pitch {}
205
206impl Hash for Pitch {
209 fn hash<H: Hasher>(&self, state: &mut H) {
210 self.step.hash(state);
211 self.octave.hash(state);
212 self.accidental.hash(state);
213 self.microtone.hash(state);
214 }
215}
216
217impl FromStr for Pitch {
218 type Err = Error;
219
220 fn from_str(value: &str) -> Result<Self> {
221 Self::from_name(value)
222 }
223}
224
225impl TryFrom<&str> for Pitch {
226 type Error = Error;
227
228 fn try_from(value: &str) -> Result<Self> {
229 Self::from_name(value)
230 }
231}
232
233impl TryFrom<String> for Pitch {
234 type Error = Error;
235
236 fn try_from(value: String) -> Result<Self> {
237 Self::from_name(value)
238 }
239}
240
241impl TryFrom<&Pitch> for Pitch {
242 type Error = Error;
243
244 fn try_from(value: &Pitch) -> Result<Self> {
245 Ok(value.clone())
246 }
247}
248
249impl TryFrom<IntegerType> for Pitch {
250 type Error = Error;
251
252 fn try_from(value: IntegerType) -> Result<Self> {
253 Self::from_midi(value)
254 }
255}
256
257impl TryFrom<FloatType> for Pitch {
258 type Error = Error;
259
260 fn try_from(value: FloatType) -> Result<Self> {
261 Self::from_pitch_space(value)
262 }
263}
264
265impl Display for Pitch {
266 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
267 write!(f, "{}", self.name_with_octave())
268 }
269}
270
271impl Pitch {
272 pub fn from_options(options: PitchOptions) -> Result<Self> {
279 let PitchOptions {
280 name,
281 step,
282 octave,
283 accidental,
284 microtone,
285 pitch_class,
286 midi,
287 ps,
288 fundamental,
289 } = options;
290 let explicit_step = step.map(StepName::try_from).transpose()?;
291 let has_explicit_octave = octave.is_some();
292 let has_explicit_accidental = accidental.is_some();
293
294 if let Some(PitchName::Number(number)) = &name
295 && !number.is_finite()
296 {
297 return Err(Error::Pitch(format!(
298 "a pitch-space number must be finite, got {number}"
299 )));
300 }
301 let parsed = name.map(PitchParameters::from).unwrap_or_default();
302 let name = parsed.name;
303 let step = if name.is_some() || parsed.step.is_some() {
304 parsed.step
305 } else {
306 explicit_step
307 }
308 .unwrap_or(PITCH_STEP);
309 let octave = octave.or(parsed.octave);
310 let accidental = match accidental {
311 Some(accidental) => Some(Accidental::new(accidental)?),
312 None => parsed.accidental,
313 };
314 let microtone = match microtone {
315 Some(microtone) => Some(Microtone::new(microtone)?),
316 None => parsed.microtone,
317 };
318
319 let explicit_accidental = accidental.clone().filter(|_| has_explicit_accidental);
320 let mut pitch = Pitch {
321 step,
322 accidental,
323 microtone,
324 octave,
325 spelling_is_inferred: parsed.spelling_is_inferred,
326 fundamental: None,
327 };
328
329 if let Some(name) = &name {
330 pitch.name_setter(name)?;
331 pitch.spelling_is_inferred = parsed.spelling_is_inferred;
332 }
333 if explicit_step.is_some() || name.is_none() {
334 pitch.step_setter(step);
335 pitch.spelling_is_inferred = parsed.spelling_is_inferred;
336 }
337 if has_explicit_octave || name.is_none() {
338 pitch.octave_setter(octave);
339 }
340 if let Some(accidental) = explicit_accidental {
341 pitch.accidental_setter(accidental);
342 }
343 if let Some(microtone) = pitch.microtone.clone() {
344 pitch.microtone_setter(microtone);
345 }
346 if let Some(pitch_class) = pitch_class {
347 pitch.pitch_class_setter(pitch_class)?;
348 }
349 if let Some(fundamental) = fundamental {
350 pitch.fundamental_setter(fundamental);
351 }
352 if let Some(midi) = midi {
353 pitch.midi_setter(midi);
354 }
355 if let Some(ps) = ps {
356 if !ps.is_finite() {
357 return Err(Error::Pitch(format!(
358 "a pitch-space number must be finite, got {ps}"
359 )));
360 }
361 pitch.ps_setter(ps);
362 }
363
364 Ok(pitch)
365 }
366
367 pub fn builder() -> PitchOptions {
369 PitchOptions::new()
370 }
371
372 pub fn from_name(name: impl AsRef<str>) -> Result<Self> {
374 let mut pitch = Self {
378 step: PITCH_STEP,
379 accidental: None,
380 microtone: None,
381 octave: None,
382 spelling_is_inferred: false,
383 fundamental: None,
384 };
385 pitch.name_setter(name.as_ref())?;
386 pitch.spelling_is_inferred = false;
389 Ok(pitch)
390 }
391
392 pub fn from_number(number: FloatType) -> Result<Self> {
394 PitchOptions::new().name(PitchName::Number(number)).build()
395 }
396
397 pub fn from_step(step: char) -> Result<Self> {
399 PitchOptions::new().step(step).build()
400 }
401
402 pub fn from_name_and_octave(name: impl Into<String>, octave: IntegerType) -> Result<Self> {
404 PitchOptions::new().name(name.into()).octave(octave).build()
405 }
406
407 pub fn from_pitch_class(pitch_class: impl Into<PitchClassSpecifier>) -> Result<Self> {
409 PitchOptions::new().pitch_class(pitch_class).build()
410 }
411
412 pub fn from_midi(midi: IntegerType) -> Result<Self> {
414 PitchOptions::new().midi(midi).build()
415 }
416
417 pub fn from_pitch_space(ps: FloatType) -> Result<Self> {
419 PitchOptions::new().ps(ps).build()
420 }
421
422 pub fn name_with_octave(&self) -> String {
424 let mut name = self.name();
425 if let Some(octave) = self.octave {
426 use std::fmt::Write;
427 let _ = write!(name, "{octave}");
428 }
429 name
430 }
431
432 pub fn name(&self) -> String {
434 let modifier = self.accidental().modifier();
438 let mut name = String::with_capacity(1 + modifier.len());
439 name.push(self.step.as_char());
440 name.push_str(modifier);
441 name
442 }
443
444 pub(crate) fn is_named(&self, name: &str) -> bool {
447 let mut letters = name.chars();
448 letters.next() == Some(self.step.as_char())
449 && Accidental::from_standard(letters.as_str())
450 .is_some_and(|accidental| accidental == *self.accidental())
451 }
452
453 fn name_setter(&mut self, usr_str: &str) -> Result<()> {
454 let usr_str = usr_str.trim();
455
456 let digits_at = usr_str.find(|character: char| character.is_ascii_digit());
461 let written_plainly = digits_at
462 .is_none_or(|at| at > 0 && usr_str[at..].bytes().all(|byte| byte.is_ascii_digit()));
463 if written_plainly {
464 let (name_part, octave_part) = usr_str.split_at(digits_at.unwrap_or(usr_str.len()));
465 let mut characters = name_part.chars();
466 let step = characters.next().ok_or(Error::Pitch(format!(
467 "Cannot make a name out of {name_part:?}"
468 )))?;
469 self.step_setter(StepName::try_from(step)?);
470 let accidental_str = characters.as_str();
471 if accidental_str.is_empty() {
472 self.accidental = None;
473 } else if let Some(accidental) = Accidental::from_standard(accidental_str) {
474 self.accidental_setter(accidental);
475 } else {
476 self.accidental_setter(Accidental::new(accidental_str.to_string())?);
479 }
480 if !octave_part.is_empty() {
481 let octave = octave_part
482 .parse::<IntegerType>()
483 .map_err(|_| Error::Pitch(format!("Cannot parse {octave_part:?} to octave")))?;
484 self.octave_setter(Some(octave));
485 }
486 return Ok(());
487 }
488
489 let mut pitch_part = String::with_capacity(usr_str.len());
490 let mut octave_part = String::new();
491 for character in usr_str.chars() {
492 if character.is_ascii_digit() {
493 if pitch_part.is_empty() {
494 return Err(Error::Value(format!(
495 "Cannot have octave given before pitch name in '{usr_str}'."
496 )));
497 }
498 octave_part.push(character);
499 } else {
500 pitch_part.push(character);
501 }
502 }
503
504 let mut pitch_chars = pitch_part.chars();
505 let step = pitch_chars.next().ok_or(Error::Pitch(format!(
506 "Cannot make a name out of {pitch_part:?}"
507 )))?;
508 self.step_setter(StepName::try_from(step)?);
509
510 let accidental_str: String = pitch_chars.collect();
511 if accidental_str.is_empty() {
512 self.accidental = None;
513 } else {
514 self.accidental_setter(Accidental::new(accidental_str)?);
515 }
516
517 if !octave_part.is_empty() {
518 let octave = octave_part
519 .parse::<IntegerType>()
520 .map_err(|_| Error::Pitch(format!("Cannot parse {octave_part:?} to octave")))?;
521 self.octave_setter(Some(octave));
522 }
523
524 Ok(())
525 }
526
527 pub fn alter(&self) -> FloatType {
529 let mut post = 0.0;
530
531 post += self
532 .accidental
533 .as_ref()
534 .map_or(0.0, |accidental| accidental.alter);
535
536 if let Some(microtone) = &self.microtone {
537 post += microtone.alter();
538 }
539
540 post
541 }
542
543 pub fn accidental(&self) -> &Accidental {
547 static NATURAL: LazyLock<Accidental> = LazyLock::new(Accidental::natural);
548 self.accidental.as_ref().unwrap_or(&NATURAL)
549 }
550
551 pub fn written_accidental(&self) -> Option<&Accidental> {
555 self.accidental.as_ref()
556 }
557
558 pub fn written_accidental_mut(&mut self) -> Option<&mut Accidental> {
560 self.accidental.as_mut()
561 }
562
563 pub fn microtone(&self) -> Option<&Microtone> {
565 self.microtone.as_ref()
566 }
567
568 pub fn pitch_class(&self) -> PitchClass {
570 PitchClass::from_number(self.ps()).unwrap_or_else(|err| {
571 panic!("pitch-space value should always map to pitch class: {err}")
572 })
573 }
574
575 pub fn set_octave(&mut self, octave: Octave) {
578 self.octave = octave;
579 }
580
581 pub(crate) fn octave_setter(&mut self, octave: Octave) {
582 self.set_octave(octave);
583 }
584
585 pub(crate) fn take_name_of(&mut self, other: &Pitch) -> Result<()> {
589 self.step_setter(other.step);
590 self.accidental = match other.written_accidental() {
591 Some(accidental) if !accidental.modifier().is_empty() => {
592 Some(Accidental::new(accidental.modifier())?)
593 }
594 _ => None,
595 };
596 Ok(())
597 }
598
599 pub fn transpose(&self, interval: &Interval) -> Result<Pitch> {
604 let mut p = interval.transpose_pitch_with_options(self, false, Some(4))?;
605
606 if !interval.implicit_diatonic {
607 p.spelling_is_inferred = self.spelling_is_inferred;
608 }
609 if p.spelling_is_inferred {
610 p.simplify_enharmonic_in_place(true)?;
611 }
612 if let Some(fundamental) = &self.fundamental {
613 p.fundamental_setter(fundamental.transpose(interval)?);
614 }
615
616 Ok(p)
617 }
618
619 pub fn ps(&self) -> FloatType {
621 self.pitch_space()
622 }
623
624 pub fn pitch_space(&self) -> FloatType {
626 let octave = self.octave.unwrap_or(PITCH_OCTAVE as IntegerType);
627 (FloatType::from(octave) + 1.0) * 12.0 + self.step.step_ref() as FloatType + self.alter()
629 }
630
631 pub fn midi(&self) -> IntegerType {
636 normalize_midi((self.pitch_space() + 0.5).floor() as IntegerType)
637 }
638
639 pub fn frequency_hz(&self) -> FloatType {
641 let twelfth_root_of_two = (2.0 as FloatType).powf(1.0 / 12.0);
644 crate::tuningsystem::A4 * twelfth_root_of_two.powf(self.pitch_space() - 69.0)
645 }
646
647 pub fn frequency_hz_tuned(
671 &self,
672 tuning: &impl crate::tuningsystem::Tuning,
673 reference: &crate::tuningsystem::Reference,
674 ) -> Result<FloatType> {
675 let (steps, semitones, cents) = self.spelled_place();
676 let (reference_steps, reference_semitones, reference_cents) =
677 reference.pitch().spelled_place();
678 let interval = crate::tuningsystem::Monzo::pythagorean(
679 steps - reference_steps,
680 semitones - reference_semitones,
681 );
682 let cents = tuning.cents_of(&interval)? + cents - reference_cents;
683 Ok(reference.hertz() * (2.0 as FloatType).powf(cents / 1200.0))
684 }
685
686 fn spelled_place(&self) -> (IntegerType, IntegerType, FloatType) {
689 let accidental = self.accidental.as_ref().map_or(0.0, Accidental::alter);
690 let whole = accidental.trunc();
691 let natural = self.pitch_space() - self.alter();
692 let microtone = self.microtone.as_ref().map_or(0.0, Microtone::cents);
693 (
694 self.diatonic_note_number(),
695 (natural + whole).round() as IntegerType,
696 (accidental - whole) * 100.0 + microtone,
697 )
698 }
699
700 pub fn frequency_hz_in(&self, tuning_system: TuningSystem) -> FloatType {
707 if tuning_system.is_keyboard() {
708 return tuning_system.frequency_at(self.pitch_space());
709 }
710 let degree = tuning_system
711 .nearest_degree(self.frequency_hz())
712 .expect("an equal-tempered frequency is finite and above nought");
713 tuning_system.frequency_at(degree.into())
714 }
715
716 pub fn from_frequency_in(hertz: FloatType, tuning_system: TuningSystem) -> Result<Self> {
731 tuning_system.pitch_at(tuning_system.nearest_degree(hertz)?)
732 }
733
734 fn step_setter(&mut self, step_name: StepName) {
735 self.step = step_name;
736 self.spelling_is_inferred = false;
737 }
738
739 pub(crate) fn set_diatonic_note_number(&mut self, dnn: IntegerType) -> Result<()> {
742 let (step, octave) = crate::interval::convert_diatonic_number_to_step(dnn);
743 self.step_setter(StepName::try_from(step)?);
744 self.octave_setter(Some(octave));
745 Ok(())
746 }
747
748 pub fn set_accidental(&mut self, accidental: Accidental) {
750 self.accidental_setter(accidental);
751 }
752
753 pub fn set_written_accidental(&mut self, accidental: Option<Accidental>) {
757 self.accidental = accidental;
758 }
759
760 pub fn set_accidental_alter(&mut self, alter: FloatType) -> Result<()> {
765 let (alter_shift, cents) = cents_to_alter_and_cents(alter * 100.0);
766 self.accidental_setter(Accidental::new(alter_shift)?);
767 if cents.abs() > 0.01 {
768 self.microtone_setter(Microtone::new(cents)?);
769 }
770 Ok(())
771 }
772
773 pub fn spelling_is_inferred(&self) -> bool {
778 self.spelling_is_inferred
779 }
780
781 pub fn set_spelling_is_inferred(&mut self, inferred: bool) {
783 self.spelling_is_inferred = inferred;
784 }
785
786 fn accidental_setter(&mut self, value: Accidental) {
787 self.accidental = Some(value);
788 }
789
790 pub fn set_microtone_cents(&mut self, cents: FloatType) -> Result<()> {
793 if cents == 0.0 {
794 self.microtone = None;
795 } else {
796 self.microtone = Some(Microtone::new(cents)?);
797 }
798 Ok(())
799 }
800
801 fn microtone_setter(&mut self, mt: Microtone) {
802 self.microtone = Some(mt);
803 }
804
805 fn pitch_class_setter(&mut self, pc: PitchClassSpecifier) -> Result<()> {
806 self.pitch_class_value_setter(PitchClass::new(pc)?.number());
807 Ok(())
808 }
809
810 fn pitch_class_value_setter(&mut self, pc: FloatType) {
811 let (step, accidental, _, _) = convert_ps_to_step(pc);
812 self.step = step;
813 self.accidental = (accidental.alter() != 0.0).then_some(accidental);
814 self.spelling_is_inferred = true;
815 }
816
817 pub(crate) fn fundamental_setter(&mut self, f: Pitch) {
818 self.fundamental = Some(Arc::new(f));
819 }
820
821 pub fn fundamental(&self) -> Option<&Pitch> {
823 self.fundamental.as_deref()
824 }
825
826 fn midi_setter(&mut self, m: IntegerType) {
827 self.ps_setter(normalize_midi(m) as FloatType);
828 }
829
830 fn ps_setter(&mut self, p: FloatType) {
831 let (step, accidental, microtone, octave_shift) = convert_ps_to_step(p);
832 self.step = step;
833 self.accidental = (accidental.alter() != 0.0).then_some(accidental);
834 if microtone.alter() == 0.0 {
835 self.microtone = None;
836 } else {
837 self.microtone = Some(microtone);
838 }
839
840 let octave = convert_ps_to_oct(p) + octave_shift;
841 self.octave = Some(octave);
842 self.spelling_is_inferred = true;
843 }
844
845 pub fn implicit_octave(&self) -> IntegerType {
852 self.octave.unwrap_or(PITCH_OCTAVE as IntegerType)
853 }
854
855 #[must_use]
861 pub fn octave_is_implicit(&self) -> bool {
862 self.octave.is_none()
863 }
864
865 pub fn set_octave_is_implicit(&mut self, implicit: bool) {
872 if implicit == self.octave.is_none() {
873 return;
874 }
875 self.octave = if implicit {
876 None
877 } else {
878 Some(PITCH_OCTAVE as IntegerType)
879 };
880 }
881
882 pub fn pitch_class_string(&self) -> String {
888 crate::pitch::pitchclass::convert_pitch_class_to_str(
889 self.ps().round_ties_even() as IntegerType
890 )
891 }
892
893 pub fn cent_shift_from_midi(&self) -> IntegerType {
897 let mut distance = self.ps() - FloatType::from(self.midi());
898 while distance < -11.0 {
899 distance += 12.0;
900 }
901 while distance > 11.0 {
902 distance -= 12.0;
903 }
904 (distance * 100.0).round() as IntegerType
905 }
906
907 pub fn from_frequency(hertz: FloatType) -> Result<Self> {
910 if !hertz.is_finite() || hertz <= 0.0 {
911 return Err(Error::Pitch(format!(
912 "frequency must be a finite number greater than zero, got {hertz}"
913 )));
914 }
915 let mut pitch = Pitch::default();
916 pitch.ps_setter(12.0 * (hertz / crate::tuningsystem::A4).log2() + 69.0);
917 Ok(pitch)
918 }
919
920 pub fn diatonic_note_number(&self) -> IntegerType {
923 let octave = self.octave.unwrap_or(PITCH_OCTAVE as IntegerType);
924 self.step.step_to_dnn_offset() + 7 * octave
925 }
926
927 pub(crate) fn checked_diatonic_note_number(&self) -> Option<IntegerType> {
930 let octave = self.octave.unwrap_or(PITCH_OCTAVE as IntegerType);
931 octave
932 .checked_mul(7)?
933 .checked_add(self.step.step_to_dnn_offset())
934 }
935
936 pub(crate) fn step_index(&self) -> IntegerType {
938 self.step as IntegerType - 1
939 }
940
941 pub fn is_twelve_tone(&self) -> bool {
944 self.accidental().is_twelve_tone()
945 && self
946 .microtone
947 .as_ref()
948 .is_none_or(|microtone| microtone.cents() == 0.0)
949 }
950
951 fn octave_bearing_copy(&self, operation: &str) -> Result<Pitch> {
952 if self.octave.is_none() {
953 return Err(Error::Pitch(format!(
954 "Cannot call {operation} with an octaveless Pitch."
955 )));
956 }
957 Ok(self.clone())
958 }
959
960 fn shift_octave(&mut self, octaves: IntegerType) {
961 let octave = self.octave.unwrap_or(PITCH_OCTAVE as IntegerType);
962 self.octave_setter(Some(octave + octaves));
963 }
964
965 pub fn octave(&self) -> Octave {
971 self.octave
972 }
973
974 pub(crate) fn step(&self) -> StepName {
975 self.step
976 }
977
978 pub(crate) fn set_ps(&mut self, p: FloatType) {
979 self.ps_setter(p);
980 }
981}
982
983impl Default for Pitch {
984 fn default() -> Self {
985 Self::from_options(PitchOptions::default())
986 .expect("default Pitch construction should never fail")
987 }
988}
989
990#[derive(Default)]
991struct PitchParameters {
992 name: Option<String>,
993 step: Option<StepName>,
994 accidental: Option<Accidental>,
995 microtone: Option<Microtone>,
996 spelling_is_inferred: bool,
997 octave: Octave,
998}
999
1000impl From<PitchName> for PitchParameters {
1001 fn from(value: PitchName) -> Self {
1002 match value {
1003 PitchName::Name(name) => Self {
1004 name: Some(name),
1005 ..Self::default()
1006 },
1007 PitchName::Number(number) => {
1008 let (step, accidental, microtone, octave_shift) = convert_ps_to_step(number);
1009 let octave = (number >= 12.0).then(|| convert_ps_to_oct(number) + octave_shift);
1010 Self {
1011 name: None,
1012 step: Some(step),
1013 accidental: Some(accidental),
1014 microtone: (microtone.cents() != 0.0).then_some(microtone),
1015 spelling_is_inferred: true,
1016 octave,
1017 }
1018 }
1019 }
1020 }
1021}
1022
1023fn convert_ps_to_step<T: Num + ToPrimitive>(
1024 ps: T,
1025) -> (StepName, Accidental, Microtone, IntegerType) {
1026 let ps = ps.to_f64().unwrap_or(0.0);
1027 let (pc, alter, micro) = if ps.fract() == 0.0 {
1028 ((ps as IntegerType).rem_euclid(12), 0.0, 0.0)
1029 } else {
1030 let ps = round_to_digits(ps, PITCH_SPACE_SIGNIFICANT_DIGITS);
1031 let pc_real = ps.rem_euclid(12.0);
1032 let pc = pc_real.floor() as IntegerType;
1033 let mut micro = pc_real - pc as FloatType;
1034
1035 let alter = if round_to_digits(micro, 1) == 0.5 || (0.25 < micro && micro < 0.75) {
1039 micro -= 0.5;
1040 0.5
1041 } else if (0.75..1.0).contains(µ) {
1042 micro -= 1.0;
1043 1.0
1044 } else if micro > 0.0 {
1045 0.0
1046 } else {
1047 micro = 0.0;
1048 0.0
1049 };
1050
1051 (pc, alter, micro)
1052 };
1053
1054 let octave_shift = IntegerType::from(pc == 11 && alter == 1.0);
1055 let (pc_name, accidental_alter) = match pc {
1056 4 | 11 if alter == 1.0 => ((pc + 1).rem_euclid(12), 0.0),
1057 1 | 6 | 8 if alter >= 1.0 => (pc + 1, alter - 1.0),
1058 1 | 6 | 8 => (pc - 1, 1.0 + alter),
1059 3 | 10 if alter <= -1.0 => (pc - 1, 1.0 + alter),
1060 3 | 10 => (pc + 1, -1.0 + alter),
1061 _ => (pc, alter),
1062 };
1063
1064 let step = StepName::ref_to_step(pc_name.rem_euclid(12))
1065 .unwrap_or_else(|err| panic!("pitch class should map to a step: {err}"));
1066 let accidental = Accidental::new(accidental_alter)
1067 .unwrap_or_else(|err| panic!("accidental conversion should not fail: {err}"));
1068 let microtone = Microtone::from_cents(micro * 100.0, 1);
1069
1070 (step, accidental, microtone, octave_shift)
1071}
1072
1073fn round_to_digits(value: FloatType, digits: UnsignedIntegerType) -> FloatType {
1074 let factor = (10 as FloatType).powi(digits as IntegerType);
1075 (value * factor).round() / factor
1076}
1077
1078fn normalize_midi(midi: IntegerType) -> IntegerType {
1079 if midi > 127 {
1080 let mut value = (12 * 9) + midi.rem_euclid(12);
1081 if value < (127 - 12) {
1082 value += 12;
1083 }
1084 value
1085 } else if midi < 0 {
1086 midi.rem_euclid(12)
1087 } else {
1088 midi
1089 }
1090}
1091
1092#[cfg(test)]
1093mod tests {
1094 #[test]
1098 fn a_pitch_space_number_that_is_not_finite_is_refused() {
1099 use crate::pitch::Pitch;
1100
1101 for number in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1102 assert!(Pitch::from_pitch_space(number).is_err());
1103 assert!(Pitch::from_number(number).is_err());
1104 assert!(Pitch::builder().ps(number).build().is_err());
1105 }
1106 assert_eq!(Pitch::from_pitch_space(61.0).unwrap().name(), "C#");
1107 }
1108
1109 #[test]
1112 fn a_pitch_in_the_last_octave_has_a_pitch_space() {
1113 use crate::IntegerType;
1114 use crate::pitch::Pitch;
1115
1116 let mut pitch = Pitch::from_name("C").unwrap();
1117 for octave in [IntegerType::MAX, IntegerType::MIN] {
1118 pitch.set_octave(Some(octave));
1119 let expected = (f64::from(octave) + 1.0) * 12.0;
1120 assert_eq!(pitch.ps(), expected, "{octave}");
1121 }
1122 }
1123
1124 #[test]
1126 fn equal_pitches_are_one_set_entry() {
1127 use crate::pitch::Pitch;
1128 use std::collections::HashSet;
1129
1130 let pitches: HashSet<Pitch> = ["B-4", "B-4", "A#4", "Bb3"]
1131 .into_iter()
1132 .map(|name| Pitch::from_name(name).unwrap())
1133 .collect();
1134 assert_eq!(pitches.len(), 3);
1135 }
1136
1137 #[test]
1139 fn a_pitch_is_named_either_flat_spelling() {
1140 use crate::pitch::Pitch;
1141
1142 let a_flat = Pitch::from_name("Ab2").unwrap();
1143 assert!(a_flat.is_named("Ab"));
1144 assert!(a_flat.is_named("A-"));
1145 assert!(!a_flat.is_named("A"));
1146 }
1147
1148 #[test]
1150 fn every_pitch_has_an_accidental() {
1151 use crate::pitch::Pitch;
1152
1153 let bare = Pitch::from_name("D").unwrap();
1154 let natural = Pitch::from_name("Dn").unwrap();
1155 assert_eq!(bare.accidental().name(), "natural");
1156 assert_eq!(natural.accidental().name(), "natural");
1157 assert!(bare.written_accidental().is_none());
1158 assert!(natural.written_accidental().is_some());
1159 }
1160
1161 #[test]
1162 fn a_frequency_that_is_not_finite_is_refused() {
1163 use crate::pitch::Pitch;
1164
1165 assert!(Pitch::from_frequency(f64::INFINITY).is_err());
1166 assert!(Pitch::from_frequency(f64::NAN).is_err());
1167 assert!(Pitch::from_frequency(0.0).is_err());
1168 assert_eq!(
1169 Pitch::from_frequency(440.0).unwrap().name_with_octave(),
1170 "A4"
1171 );
1172 }
1173
1174 #[test]
1175 fn a_pitch_is_built_from_a_step_a_number_or_an_owned_name() {
1176 use crate::pitch::{Pitch, PitchName, pitch_class_name};
1177
1178 assert_eq!(Pitch::from_step('d').unwrap().name(), "D");
1179 assert!(Pitch::from_step('h').is_err());
1180 assert_eq!(
1181 Pitch::try_from("Eb4".to_string())
1182 .unwrap()
1183 .name_with_octave(),
1184 "Eb4"
1185 );
1186 assert!(matches!(PitchName::from(60), PitchName::Number(n) if n == 60.0));
1187 assert!(matches!(PitchName::from(61.5), PitchName::Number(n) if n == 61.5));
1188 assert_eq!(pitch_class_name(1), "Db");
1189 assert_eq!(pitch_class_name(13), "Db");
1190 assert_eq!(pitch_class_name(10), "Bb");
1191 }
1192
1193 #[test]
1194 fn spelling_is_inferred_for_a_pitch_built_from_a_number_and_can_be_unsaid() {
1195 use crate::pitch::Pitch;
1196
1197 let mut pitch = Pitch::from_midi(61).unwrap();
1198 assert!(pitch.spelling_is_inferred());
1199 pitch.set_spelling_is_inferred(false);
1200 assert!(!pitch.spelling_is_inferred());
1201 assert!(!Pitch::from_name("C#").unwrap().spelling_is_inferred());
1202
1203 let mut raised = Pitch::from_name("C#4").unwrap();
1204 raised.get_higher_enharmonic_in_place().unwrap();
1205 assert_eq!(raised.name_with_octave(), "Db4");
1206 }
1207
1208 #[test]
1209 fn a_pitch_knows_whether_a_key_signature_already_writes_it() {
1210 use crate::pitch::Pitch;
1211
1212 let altered = [
1213 Pitch::from_name("F#").unwrap(),
1214 Pitch::from_name("C#").unwrap(),
1215 ];
1216 assert!(
1217 Pitch::from_name("F#4")
1218 .unwrap()
1219 .name_in_key_signature(&altered)
1220 );
1221 assert!(
1222 !Pitch::from_name("F4")
1223 .unwrap()
1224 .name_in_key_signature(&altered)
1225 );
1226 assert!(
1227 !Pitch::from_name("Fb4")
1228 .unwrap()
1229 .name_in_key_signature(&altered)
1230 );
1231 assert!(
1232 Pitch::from_name("F4")
1233 .unwrap()
1234 .step_in_key_signature(&altered)
1235 );
1236 assert!(
1237 !Pitch::from_name("G4")
1238 .unwrap()
1239 .step_in_key_signature(&altered)
1240 );
1241 }
1242
1243 #[test]
1248 fn accidental_display_follows_the_pitches_before_it() {
1249 use crate::pitch::{AccidentalDisplayOptions, Pitch};
1250
1251 let first = Pitch::from_name("F#4").unwrap();
1252 let mut repeat = Pitch::from_name("F#4").unwrap();
1253 let past = [first.clone()];
1254 repeat.update_accidental_display(&AccidentalDisplayOptions {
1255 pitch_past: &past,
1256 ..AccidentalDisplayOptions::default()
1257 });
1258 assert_eq!(repeat.accidental().display_status(), Some(false));
1259
1260 let mut natural = Pitch::from_name("F4").unwrap();
1261 natural.update_accidental_display(&AccidentalDisplayOptions {
1262 pitch_past: &past,
1263 ..AccidentalDisplayOptions::default()
1264 });
1265 assert!(natural.written_accidental().is_some());
1266 assert_eq!(natural.accidental().display_status(), Some(true));
1267
1268 let mut in_key = Pitch::from_name("F#4").unwrap();
1269 in_key.update_accidental_display(&AccidentalDisplayOptions {
1270 altered_pitches: &[Pitch::from_name("F#").unwrap()],
1271 ..AccidentalDisplayOptions::default()
1272 });
1273 assert_eq!(in_key.accidental().display_status(), Some(false));
1274
1275 let mut fresh = Pitch::from_name("B-4").unwrap();
1276 fresh.update_accidental_display(&AccidentalDisplayOptions::default());
1277 assert_eq!(fresh.accidental().display_status(), Some(true));
1278 }
1279
1280 #[test]
1283 fn an_octave_is_implicit_until_one_is_given() {
1284 use crate::pitch::Pitch;
1285
1286 let mut anywhere = Pitch::from_name("G#").unwrap();
1287 assert!(anywhere.octave_is_implicit());
1288 assert_eq!(anywhere.octave(), None);
1289 assert_eq!(anywhere.implicit_octave(), 4);
1290
1291 let somewhere = Pitch::from_name("E-6").unwrap();
1292 assert!(!somewhere.octave_is_implicit());
1293 assert_eq!(somewhere.octave(), Some(6));
1294 assert_eq!(somewhere.implicit_octave(), 6);
1295
1296 anywhere.set_octave_is_implicit(false);
1299 assert!(!anywhere.octave_is_implicit());
1300 assert_eq!(anywhere.octave(), Some(4));
1301 anywhere.set_octave_is_implicit(true);
1302 assert_eq!(anywhere.octave(), None);
1303
1304 let mut high = Pitch::from_name("C7").unwrap();
1306 high.set_octave_is_implicit(false);
1307 assert_eq!(high.octave(), Some(7));
1308 }
1309
1310 #[test]
1311 fn a_pitch_is_respelled_to_agree_with_the_key_signature() {
1312 use crate::key::KeySignature;
1313 let f_sharp = Pitch::from_name("F#4").unwrap();
1316 assert_eq!(
1317 f_sharp.respelled_for(&KeySignature::new(2)).unwrap().name(),
1318 "F#"
1319 );
1320 assert_eq!(
1321 f_sharp
1322 .respelled_for(&KeySignature::new(-5))
1323 .unwrap()
1324 .name(),
1325 "Gb"
1326 );
1327 assert_eq!(
1330 Pitch::from_name("C4")
1331 .unwrap()
1332 .respelled_for(&KeySignature::new(-5))
1333 .unwrap()
1334 .name(),
1335 "C"
1336 );
1337 }
1338
1339 #[test]
1340 fn an_octave_digit_may_sit_anywhere_after_the_step() {
1341 for (name, expected) in [
1342 ("e4-", "Eb4"),
1343 ("e-4", "Eb4"),
1344 ("b3-", "Bb3"),
1345 ("g4--", "Gbb4"),
1346 ("c#4", "C#4"),
1347 ("f##2", "F##2"),
1348 ("d1", "D1"),
1349 ] {
1350 assert_eq!(
1351 Pitch::from_name(name).unwrap().name_with_octave(),
1352 expected,
1353 "parsing {name:?}"
1354 );
1355 }
1356 assert_eq!(
1357 Pitch::from_name("c4#2").unwrap().name_with_octave(),
1358 "C#42",
1359 "the digits concatenate wherever they fall, as music21's do"
1360 );
1361 assert!(Pitch::from_name("4c").is_err());
1362 }
1363
1364 #[test]
1365 fn options_keep_an_explicit_accidental_and_a_numeric_name_stays_inferred() {
1366 let sharp = crate::PitchOptions::new()
1367 .name("C")
1368 .accidental("#")
1369 .octave(7)
1370 .microtone(-30)
1371 .build()
1372 .unwrap();
1373 assert_eq!(sharp.full_name(), "C-sharp in octave 7 (-30c)");
1374 let double_flat = crate::PitchOptions::new()
1375 .name("D")
1376 .accidental(Accidental::new("double-flat").unwrap())
1377 .build()
1378 .unwrap();
1379 assert_eq!(double_flat.name(), "Dbb");
1380 let inferred = Pitch::from_number(6.0).unwrap();
1381 assert_eq!(
1382 inferred
1383 .transpose(&Interval::from_name("-m2").unwrap())
1384 .unwrap()
1385 .name(),
1386 "F"
1387 );
1388 }
1389
1390 #[test]
1391 fn midi_folds_into_range_like_music21() {
1392 let high = Pitch::from_name("C#10").unwrap();
1393 assert_eq!(high.midi(), 121);
1394 assert_eq!(high.cent_shift_from_midi(), 0);
1395 let half_flat = crate::PitchOptions::new().name("F`10").build().unwrap();
1396 assert_eq!(half_flat.midi(), 125);
1397 assert_eq!(
1398 Pitch::from_name("C#-2")
1399 .unwrap_or_else(|_| Pitch::from_pitch_space(-11.0).unwrap())
1400 .midi(),
1401 1
1402 );
1403 assert_eq!(Pitch::from_name("C~4").unwrap().midi(), 61);
1404 assert_eq!(Pitch::from_name("A4").unwrap().frequency_hz(), 440.0);
1405 }
1406
1407 #[test]
1408 fn harmonics_chain_and_transpose_with_their_fundamental() {
1409 let a4 = Pitch::from_name("A4").unwrap();
1410 let seventh = a4.harmonic(7).unwrap();
1411 assert_eq!(seventh.name_with_octave(), "F#~7");
1412 assert_eq!(seventh.microtone().unwrap().cents().round(), 19.0);
1413 let doubled = seventh.harmonic(2).unwrap();
1414 assert_eq!(doubled.name_with_octave(), "F#~8");
1415 assert_eq!(doubled.microtone().unwrap().cents().round(), 19.0);
1416 assert_eq!(doubled.fundamental().unwrap().name_with_octave(), "F#~7");
1417
1418 let second = a4.harmonic(2).unwrap();
1419 assert_eq!(second.fundamental().unwrap().name_with_octave(), "A4");
1420 let up_a_fifth = second
1421 .transpose(&Interval::from_name("p5").unwrap())
1422 .unwrap();
1423 assert_eq!(up_a_fifth.name_with_octave(), "E6");
1424 assert_eq!(up_a_fifth.fundamental().unwrap().name_with_octave(), "E5");
1425 }
1426
1427 #[test]
1428 fn numbers_and_chromatic_transposition_keep_microtones() {
1429 let sharp_twenty = Pitch::from_number(60.2).unwrap();
1430 assert_eq!(sharp_twenty.name_with_octave(), "C4");
1431 assert_eq!(sharp_twenty.microtone().unwrap().cents().round(), 20.0);
1432 let harmonic = Pitch::from_name("A4")
1433 .unwrap()
1434 .harmonic(7)
1435 .unwrap()
1436 .harmonic(2)
1437 .unwrap();
1438 let down_two_octaves = harmonic
1439 .transpose(&Interval::from_semitones(-24).unwrap())
1440 .unwrap();
1441 assert_eq!(down_two_octaves.name_with_octave(), "F#~6");
1442 assert_eq!(down_two_octaves.microtone().unwrap().cents().round(), 19.0);
1443 }
1444
1445 #[test]
1446 fn transposition_reaches_octave_minus_one() {
1447 let low = Pitch::from_name("D2")
1448 .unwrap()
1449 .transpose(&Interval::from_name("m-23").unwrap())
1450 .unwrap();
1451 assert_eq!(low.name_with_octave(), "C#-1");
1452 assert_eq!(low.ps(), 1.0);
1453 }
1454
1455 #[test]
1456 fn simplify_multiple_enharmonics_matches_music21() {
1457 let names = |names: &[&str]| -> Vec<Pitch> {
1458 names
1459 .iter()
1460 .map(|n| Pitch::from_name(*n).unwrap())
1461 .collect()
1462 };
1463 let spelled = |pitches: Vec<Pitch>| -> Vec<String> {
1464 pitches.iter().map(Pitch::name_with_octave).collect()
1465 };
1466 let cases: [(&[&str], &[&str]); 11] = [
1467 (&["C#", "Db", "E"], &["C#", "C#", "E"]),
1468 (&["A#4", "C#5", "E#5"], &["A#4", "C#5", "E#5"]),
1469 (&["F#", "A#", "C#"], &["F#", "A#", "C#"]),
1470 (&["Gb", "Bb", "Db"], &["Gb", "Bb", "Db"]),
1471 (&["C", "Eb", "Gb", "B--"], &["C", "Eb", "F#", "A"]),
1472 (&["B#", "D##", "F##"], &["B#", "E", "G"]),
1473 (&["C", "E", "G#", "B"], &["C", "E", "G#", "B"]),
1474 (&["E#", "G##", "B#"], &["E#", "G##", "B#"]),
1475 (
1476 &["C#", "E#", "G#", "B", "D#"],
1477 &["C#", "E#", "G#", "B", "D#"],
1478 ),
1479 (
1480 &["Db", "F", "Ab", "Cb", "Eb", "Gb"],
1481 &["Db", "F", "Ab", "Cb", "Eb", "Gb"],
1482 ),
1483 (&["C4", "Eb4", "F#4"], &["C4", "Eb4", "Gb4"]),
1484 ];
1485 for (input, expected) in cases {
1486 let simplified = simplify_multiple_enharmonics(&names(input), None, None).unwrap();
1487 assert_eq!(spelled(simplified), expected, "{input:?}");
1488 }
1489 let three_flats = crate::KeySignature::new(-3);
1490 assert_eq!(
1491 spelled(
1492 simplify_multiple_enharmonics(&names(&["C#", "Db", "E"]), None, Some(three_flats))
1493 .unwrap()
1494 ),
1495 ["Db", "Db", "Fb"]
1496 );
1497 let six_sharps = crate::KeySignature::new(6);
1498 assert_eq!(
1499 spelled(
1500 simplify_multiple_enharmonics(&names(&["Gb", "B-", "Db"]), None, Some(six_sharps))
1501 .unwrap()
1502 ),
1503 ["F#", "A#", "C#"]
1504 );
1505 assert!(
1506 simplify_multiple_enharmonics(&[], None, None)
1507 .unwrap()
1508 .is_empty()
1509 );
1510 assert_eq!(super::dissonance_score(&[]).unwrap(), 0.0);
1511 assert!(
1512 super::dissonance_score(&names(&["C", "E", "G"])).unwrap()
1513 < super::dissonance_score(&names(&["C", "F-", "G"])).unwrap()
1514 );
1515 }
1516
1517 #[test]
1518 fn frequency_rounds_as_music21_rounds() {
1519 let cases = [
1520 ("F#6", 1_479.977_690_846_539),
1521 ("D3", 146.832_383_958_703_64),
1522 ("F1", 43.653_528_929_125_41),
1523 ("C4", 261.625_565_300_598_5),
1524 ("A~4", 452.892_984_123_136_5),
1525 ("B-7", 3_729.310_092_144_725),
1526 ];
1527 for (name, hertz) in cases {
1528 let pitch: Pitch = name.parse().unwrap();
1529 assert_eq!(pitch.frequency_hz(), hertz, "{name}");
1530 }
1531 }
1532
1533 #[test]
1534 fn dissonance_rounds_as_music21_rounds() {
1535 let score = |spelled: &[&str]| {
1538 let pitches: Vec<Pitch> = spelled.iter().map(|name| name.parse().unwrap()).collect();
1539 super::dissonance_score(&pitches).unwrap()
1540 };
1541 let flat = score(&["Eb", "Gb", "A"]);
1542 let sharp = score(&["Eb", "F#", "A"]);
1543 assert_eq!(flat, 0.051_031_336_396_830_44);
1544 assert_eq!(sharp, 0.051_031_336_396_830_475);
1545
1546 let chord = crate::Chord::new([3, 6, 9].as_slice()).unwrap();
1548 assert_eq!(chord.pitch_names(), ["Eb", "Gb", "A"]);
1549
1550 assert_eq!(score(&["D#6", "B#5", "E#5", "D5"]), 0.235_696_022_824_957_3);
1552 assert_eq!(score(&["D#6", "C6", "F5", "D5"]), 0.235_696_022_824_957_36);
1553 let respelled = simplify_multiple_enharmonics(
1554 &["D#6", "C6", "F5", "C##5"].map(|name| name.parse().unwrap()),
1555 None,
1556 None,
1557 )
1558 .unwrap();
1559 let respelled: Vec<String> = respelled.iter().map(Pitch::name_with_octave).collect();
1560 assert_eq!(respelled, ["D#6", "B#5", "E#5", "D5"]);
1561 }
1562
1563 #[test]
1564 fn get_enharmonic_picks_the_direction_music21_does() {
1565 let cases = [
1566 ("C#4", "Db4"),
1567 ("Db4", "C#4"),
1568 ("C4", "B#3"),
1569 ("D4", "C##4"),
1570 ("G4", "F##4"),
1571 ("E4", "Fb4"),
1572 ("F4", "Gbb4"),
1573 ("B4", "Cb5"),
1574 ("A-4", "G#4"),
1575 ("F##4", "G4"),
1576 ("C--4", "Bb3"),
1577 ("B#4", "C5"),
1578 ("G##4", "A4"),
1579 ];
1580 for (name, expected) in cases {
1581 let pitch = Pitch::from_name(name).unwrap();
1582 assert_eq!(
1583 pitch.get_enharmonic().unwrap().name_with_octave(),
1584 expected,
1585 "{name}"
1586 );
1587 }
1588 }
1589
1590 #[test]
1591 fn cent_shift_from_midi_matches_music21() {
1592 let cases: [(&str, FloatType, IntegerType, i32, &str); 8] = [
1593 ("C4", 0.0, 60, 0, "0"),
1594 ("C4", 20.0, 60, 20, "0"),
1595 ("C4", -20.0, 60, -20, "0"),
1596 ("C4", 60.0, 61, -40, "1"),
1597 ("C~4", 0.0, 61, -50, "0"),
1598 ("C`4", 0.0, 60, -50, "0"),
1599 ("C4", -60.0, 59, 40, "B"),
1600 ("C4", 130.0, 61, 30, "1"),
1601 ];
1602 for (name, cents, midi, shift, pitch_class) in cases {
1603 let pitch = crate::PitchOptions::new()
1604 .name(name)
1605 .microtone(cents)
1606 .build()
1607 .unwrap();
1608 assert_eq!(pitch.midi(), midi, "{name} {cents}");
1609 assert_eq!(pitch.cent_shift_from_midi(), shift, "{name} {cents}");
1610 assert_eq!(pitch.pitch_class_string(), pitch_class, "{name} {cents}");
1611 assert_eq!(pitch.implicit_octave(), 4);
1612 }
1613 assert_eq!(Pitch::from_name("G").unwrap().implicit_octave(), 4);
1614 assert_eq!(Pitch::from_name("G2").unwrap().implicit_octave(), 2);
1615 }
1616
1617 #[test]
1618 fn quarter_tones_and_microtones_convert_both_ways() {
1619 let build = |name: &str, cents: FloatType| {
1620 crate::PitchOptions::new()
1621 .name(name)
1622 .microtone(cents)
1623 .build()
1624 .unwrap()
1625 };
1626 let describe = |pitch: &Pitch| {
1627 (
1628 pitch.name_with_octave(),
1629 pitch.microtone().map_or(0.0, Microtone::cents),
1630 pitch.accidental().name().to_string(),
1631 )
1632 };
1633
1634 let to_microtones: [(&str, FloatType, &str, FloatType, &str); 6] = [
1635 ("C~4", 0.0, "C4", 50.0, "natural"),
1636 ("C`4", 0.0, "C4", -50.0, "natural"),
1637 ("D#~4", 0.0, "D#4", 50.0, "sharp"),
1638 ("E-`4", 0.0, "Eb4", -50.0, "flat"),
1639 ("C~4", 10.0, "C4", 60.0, "natural"),
1640 ("C#4", 0.0, "C#4", 0.0, "sharp"),
1641 ];
1642 for (name, cents, expected_name, expected_cents, accidental) in to_microtones {
1643 let converted = build(name, cents)
1644 .convert_quarter_tones_to_microtones()
1645 .unwrap();
1646 assert_eq!(
1647 describe(&converted),
1648 (
1649 expected_name.to_string(),
1650 expected_cents,
1651 accidental.to_string()
1652 ),
1653 "{name} {cents}"
1654 );
1655 }
1656
1657 let to_quarter_tones: [(&str, FloatType, &str, FloatType, &str); 11] = [
1658 ("C4", 50.0, "C~4", 0.0, "half-sharp"),
1659 ("C4", -50.0, "C`4", 0.0, "half-flat"),
1660 ("C4", 30.0, "C~4", -20.0, "half-sharp"),
1661 ("C4", -30.0, "C`4", 20.0, "half-flat"),
1662 ("C4", 70.0, "C~4", 20.0, "half-sharp"),
1663 ("C#4", 50.0, "C#~4", 0.0, "one-and-a-half-sharp"),
1664 ("C4", 150.0, "C#4", 50.0, "sharp"),
1665 ("C4", -150.0, "Cb4", -50.0, "flat"),
1666 ("C4", 120.0, "C#4", 20.0, "sharp"),
1667 ("Cb4", -50.0, "Cb`4", 0.0, "one-and-a-half-flat"),
1668 ("C4", 0.0, "C4", 0.0, "natural"),
1669 ];
1670 for (name, cents, expected_name, expected_cents, accidental) in to_quarter_tones {
1671 let converted = build(name, cents)
1672 .convert_microtones_to_quarter_tones()
1673 .unwrap();
1674 assert_eq!(
1675 describe(&converted),
1676 (
1677 expected_name.to_string(),
1678 expected_cents,
1679 accidental.to_string()
1680 ),
1681 "{name} {cents}"
1682 );
1683 }
1684 }
1685
1686 #[test]
1687 fn harmonic_and_fundamental_match_music21() {
1688 let cases = [
1689 ("E5", "C2", 10, 14.0, "10thH/C2(+14c)"),
1690 ("G4", "C2", 6, -2.0, "6thH/C2(-2c)"),
1691 ("B-4", "C2", 7, 31.0, "7thH/C2(+31c)"),
1692 ("F#5", "D3", 5, 14.0, "5thH/D3(+14c)"),
1693 ("C5", "C4", 2, 0.0, "2ndH/C4"),
1694 ];
1695 for (name, fundamental, number, cents, text) in cases {
1696 let pitch = Pitch::from_name(name).unwrap();
1697 let fundamental = Pitch::from_name(fundamental).unwrap();
1698 let (harmonic, retuned) = pitch
1699 .harmonic_and_fundamental_from_pitch(&fundamental)
1700 .unwrap();
1701 assert_eq!(harmonic, number, "{name}");
1702 let retuned_cents = retuned.microtone().map_or(0.0, Microtone::cents);
1703 assert!(
1704 (retuned_cents - cents).abs() < 1e-6,
1705 "{name}: {retuned_cents}"
1706 );
1707 assert_eq!(
1708 pitch
1709 .harmonic_and_fundamental_string_from_pitch(&fundamental)
1710 .unwrap(),
1711 text,
1712 "{name}"
1713 );
1714 }
1715 let c4 = Pitch::from_name("C4").unwrap();
1716 assert!(c4.harmonic_and_fundamental_from_pitch(&c4).is_err());
1717 }
1718
1719 #[test]
1720 fn full_name_matches_music21() {
1721 let cases = [
1722 ("C4", "C in octave 4"),
1723 ("E-4", "E-flat in octave 4"),
1724 ("F#", "F-sharp"),
1725 ("B--3", "B-double-flat in octave 3"),
1726 ("G##5", "G-double-sharp in octave 5"),
1727 ("A~4", "A-half-sharp in octave 4"),
1728 ("C`4", "C-half-flat in octave 4"),
1729 ("D#~4", "D-one-and-a-half-sharp in octave 4"),
1730 ];
1731 for (name, expected) in cases {
1732 assert_eq!(
1733 Pitch::from_name(name).unwrap().full_name(),
1734 expected,
1735 "{name}"
1736 );
1737 }
1738 let sharp = crate::PitchOptions::new()
1739 .name("C4")
1740 .microtone(20)
1741 .build()
1742 .unwrap();
1743 assert_eq!(sharp.full_name(), "C in octave 4 (+20c)");
1744 let flat = crate::PitchOptions::new()
1745 .name("E-4")
1746 .microtone(-33)
1747 .build()
1748 .unwrap();
1749 assert_eq!(flat.full_name(), "E-flat in octave 4 (-33c)");
1750 let hair = crate::PitchOptions::new()
1751 .name("E-4")
1752 .microtone(-0.2)
1753 .build()
1754 .unwrap();
1755 assert_eq!(hair.full_name(), "E-flat in octave 4 (-0c)");
1756 }
1757 use crate::defaults::{FloatType, IntegerType};
1758 use crate::interval::Interval;
1759 use crate::tuningsystem::TuningSystem;
1760
1761 use super::{
1762 Accidental, Microtone, Pitch, convert_harmonic_to_cents, simplify_multiple_enharmonics,
1763 };
1764
1765 #[test]
1766 fn harmonics_match_music21() {
1767 let cases = [
1768 ("C2", 1, "C2", None, 65.406),
1769 ("C2", 2, "C3", None, 130.813),
1770 ("C2", 3, "G3", Some("(+2c)"), 196.224),
1771 ("C2", 4, "C4", None, 261.626),
1772 ("C2", 5, "E4", Some("(-14c)"), 326.973),
1773 ("C2", 6, "G4", Some("(+2c)"), 392.449),
1774 ("C2", 7, "A~4", Some("(+19c)"), 457.891),
1775 ("C2", 8, "C5", None, 523.251),
1776 ("C2", 9, "D5", Some("(+4c)"), 588.688),
1777 ("C2", 11, "F~5", Some("(+1c)"), 719.338),
1778 ("C2", 13, "G#~5", Some("(-9c)"), 850.515),
1779 ("A2", 3, "E4", Some("(+2c)"), 330.009),
1780 ("A2", 5, "C#5", Some("(-14c)"), 549.9),
1781 ("C", 3, "G5", Some("(+2c)"), 784.897),
1782 ("Eb3", 7, "C~6", Some("(+19c)"), 1089.054),
1783 ];
1784 for (fundamental, number, name, microtone, hertz) in cases {
1785 let harmonic = Pitch::from_name(fundamental)
1786 .unwrap()
1787 .harmonic(number)
1788 .unwrap();
1789 assert_eq!(harmonic.name_with_octave(), name, "{fundamental} {number}");
1790 assert_eq!(
1791 harmonic.microtone().map(ToString::to_string).as_deref(),
1792 microtone,
1793 "{fundamental} {number}"
1794 );
1795 assert!(
1796 (harmonic.frequency_hz() - hertz).abs() < 5e-4,
1797 "{fundamental} {number}: {}",
1798 harmonic.frequency_hz()
1799 );
1800 assert_eq!(
1801 harmonic
1802 .fundamental()
1803 .map(Pitch::name_with_octave)
1804 .as_deref(),
1805 (number > 1).then_some(fundamental),
1806 "{fundamental} {number}"
1807 );
1808 }
1809 }
1810
1811 #[test]
1812 fn harmonic_from_fundamental_matches_music21() {
1813 let cases = [
1814 ("G4", "C2", 6, 2.0, "6thH(-2c)/C2"),
1815 ("E5", "C2", 10, -14.0, "10thH(+14c)/C2"),
1816 ("B-4", "C2", 7, -31.0, "7thH(+31c)/C2"),
1817 ("F#5", "C2", 11, -49.0, "11thH(+49c)/C2"),
1818 ("C4", "C2", 4, 0.0, "4thH/C2"),
1819 ("E4", "A2", 3, 2.0, "3rdH(-2c)/A2"),
1820 ("G#4", "A2", 4, 100.0, "4thH(-100c)/A2"),
1821 ("A4", "A2", 4, 0.0, "4thH/A2"),
1822 ("B3", "C2", 4, 100.0, "4thH(-100c)/C2"),
1823 ("D5", "C2", 9, 4.0, "9thH(-4c)/C2"),
1824 ];
1825 for (target, fundamental, number, cents, text) in cases {
1826 let target_pitch = Pitch::from_name(target).unwrap();
1827 let fundamental_pitch = Pitch::from_name(fundamental).unwrap();
1828 let (found, gap) = target_pitch
1829 .harmonic_from_fundamental(&fundamental_pitch)
1830 .unwrap();
1831 assert_eq!(found, number, "{target} over {fundamental}");
1832 assert!(
1833 (gap - cents).abs() < 1e-6,
1834 "{target} over {fundamental}: {gap}"
1835 );
1836 assert_eq!(
1837 target_pitch
1838 .harmonic_string(Some(&fundamental_pitch))
1839 .unwrap(),
1840 text
1841 );
1842 }
1843 let c2 = Pitch::from_name("C2").unwrap();
1844 assert!(
1845 c2.harmonic_from_fundamental(&Pitch::from_name("C4").unwrap())
1846 .is_err()
1847 );
1848 assert!(c2.harmonic_string(None).is_err());
1849 assert_eq!(
1850 c2.harmonic(5).unwrap().harmonic_string(None).unwrap(),
1851 "5thH/C2"
1852 );
1853 }
1854
1855 #[test]
1856 fn enharmonic_helpers_match_music21() {
1857 let names = |pitches: Vec<Pitch>| {
1858 pitches
1859 .iter()
1860 .map(Pitch::name_with_octave)
1861 .collect::<Vec<_>>()
1862 };
1863 let cases = [
1864 (
1865 "C#4",
1866 vec!["Db4", "B##3"],
1867 vec!["Db4"],
1868 vec!["Db4", "Ebbb4", "B##3"],
1869 ),
1870 (
1871 "E-",
1872 vec!["Fbb", "D#"],
1873 vec!["D#"],
1874 vec!["Fbb", "D#", "C###"],
1875 ),
1876 ("B#3", vec!["C4"], vec!["C4"], vec!["C4", "A###3"]),
1877 ("F##4", vec!["G4"], vec!["G4"], vec!["G4", "E###4"]),
1878 (
1879 "C4",
1880 vec!["Dbb4", "B#3"],
1881 vec!["B#3"],
1882 vec!["Dbb4", "B#3", "A###3"],
1883 ),
1884 (
1885 "G-2",
1886 vec!["F#2", "E##2"],
1887 vec!["F#2"],
1888 vec!["Abbb2", "F#2", "E##2"],
1889 ),
1890 (
1891 "A4",
1892 vec!["Bbb4", "G##4"],
1893 vec![],
1894 vec!["Bbb4", "Cbbb5", "G##4"],
1895 ),
1896 (
1897 "B-4",
1898 vec!["Cbb5", "A#4"],
1899 vec!["A#4"],
1900 vec!["Cbb5", "A#4", "G###4"],
1901 ),
1902 ("E#5", vec!["F5"], vec!["F5"], vec!["F5", "D###5"]),
1903 ("Dbb4", vec!["C4"], vec!["C4"], vec!["C4"]),
1904 ];
1905 for (name, limit_two, limit_one, limit_three) in cases {
1906 let pitch = Pitch::from_name(name).unwrap();
1907 assert_eq!(names(pitch.all_common_enharmonics(2)), limit_two, "{name}");
1908 assert_eq!(names(pitch.all_common_enharmonics(1)), limit_one, "{name}");
1909 assert_eq!(
1910 names(pitch.all_common_enharmonics(3)),
1911 limit_three,
1912 "{name}"
1913 );
1914 }
1915
1916 let enharmonic = |left: &str, right: &str| {
1917 Pitch::from_name(left)
1918 .unwrap()
1919 .is_enharmonic(&Pitch::from_name(right).unwrap())
1920 };
1921 assert!(enharmonic("C#4", "D-4"));
1922 assert!(!enharmonic("C#4", "D-5"));
1923 assert!(enharmonic("C#", "D-5"));
1924 assert!(!enharmonic("C#4", "C4"));
1925 assert!(enharmonic("B#3", "C4"));
1926 assert!(enharmonic("B#", "C"));
1927 assert!(enharmonic("C", "C5"));
1928 }
1929
1930 #[test]
1931 fn transposing_to_a_target_matches_music21() {
1932 let g3 = Pitch::from_name("G3").unwrap();
1933 let below = [
1934 ("C4", false, "C3"),
1935 ("C4", true, "C3"),
1936 ("C6", false, "C3"),
1937 ("C6", true, "C3"),
1938 ("G3", false, "G3"),
1939 ("C2", false, "C2"),
1940 ("C2", true, "C3"),
1941 ("F4", true, "F3"),
1942 ("G4", true, "G3"),
1943 ];
1944 for (source, minimize, expected) in below {
1945 let moved = Pitch::from_name(source)
1946 .unwrap()
1947 .transpose_below_target(&g3, minimize)
1948 .unwrap();
1949 assert_eq!(
1950 moved.name_with_octave(),
1951 expected,
1952 "{source} below {minimize}"
1953 );
1954 }
1955 let above = [
1956 ("C4", false, "C4"),
1957 ("C1", false, "C4"),
1958 ("C1", true, "C4"),
1959 ("C6", false, "C6"),
1960 ("C6", true, "C4"),
1961 ("G3", false, "G3"),
1962 ("F#3", true, "F#4"),
1963 ];
1964 for (source, minimize, expected) in above {
1965 let moved = Pitch::from_name(source)
1966 .unwrap()
1967 .transpose_above_target(&g3, minimize)
1968 .unwrap();
1969 assert_eq!(
1970 moved.name_with_octave(),
1971 expected,
1972 "{source} above {minimize}"
1973 );
1974 }
1975 assert!(
1976 Pitch::from_name("C")
1977 .unwrap()
1978 .transpose_below_target(&g3, false)
1979 .is_err()
1980 );
1981 }
1982
1983 #[test]
1984 fn twelve_tone_and_frequency_construction() {
1985 assert!(Pitch::from_name("C4").unwrap().is_twelve_tone());
1986 assert!(Pitch::from_name("C#4").unwrap().is_twelve_tone());
1987 assert!(!Pitch::from_name("C~4").unwrap().is_twelve_tone());
1988 assert!(!Pitch::from_name("C`4").unwrap().is_twelve_tone());
1989 let detuned = Pitch::builder().name("A4").microtone(25).build().unwrap();
1990 assert!(!detuned.is_twelve_tone());
1991 assert_eq!(detuned.ps(), 69.25);
1992
1993 let g4 =
1994 Pitch::from_frequency(440.0 * (2.0 as FloatType).powf((67.02 - 69.0) / 12.0)).unwrap();
1995 assert_eq!(g4.name_with_octave(), "G4");
1996 assert_eq!(g4.microtone().unwrap().to_string(), "(+2c)");
1997 assert!((g4.ps() - 67.02).abs() < 1e-6);
1998
1999 let low = Pitch::from_frequency(100.0).unwrap();
2000 assert_eq!(low.name_with_octave(), "G~2");
2001 assert_eq!(low.microtone().unwrap().to_string(), "(-15c)");
2002 assert!(Pitch::from_frequency(0.0).is_err());
2003 assert_eq!(Pitch::from_name("C4").unwrap().diatonic_note_number(), 29);
2004 assert_eq!(Pitch::from_name("B#3").unwrap().diatonic_note_number(), 28);
2005 }
2006
2007 #[test]
2008 fn language_names_match_music21() {
2009 let cases = [
2010 ("C", "C", "do", "do", "do", "C"),
2011 ("C#", "Cis", "do diesis", "do dièse", "do sostenido", "C♯"),
2012 ("D-", "Des", "re bemolle", "ré bémol", "re bemol", "D♭"),
2013 ("B", "H", "si", "si", "si", "B"),
2014 ("B-", "B", "si bemolle", "si bémol", "si bemol", "B♭"),
2015 ("B#", "His", "si diesis", "si dièse", "si sostenido", "B♯"),
2016 ("E-", "Es", "mi bemolle", "mi bémol", "mi bemol", "E♭"),
2017 ("A-", "As", "la bemolle", "la bémol", "la bemol", "A♭"),
2018 (
2019 "F##",
2020 "Fisis",
2021 "fa doppio diesis",
2022 "fa double dièse",
2023 "fa doble sostenido",
2024 "F𝄪",
2025 ),
2026 (
2027 "G--",
2028 "Geses",
2029 "sol doppio bemolle",
2030 "sol double bémol",
2031 "sol doble bemol",
2032 "G𝄫",
2033 ),
2034 (
2035 "B--",
2036 "Heses",
2037 "si doppio bemolle",
2038 "si double bémol",
2039 "si doble bemol",
2040 "B𝄫",
2041 ),
2042 ("F-", "Fes", "fa bemolle", "fa bémol", "fa bemol", "F♭"),
2043 ("E#", "Eis", "mi diesis", "mi dièse", "mi sostenido", "E♯"),
2044 (
2045 "D##",
2046 "Disis",
2047 "re doppio diesis",
2048 "ré double dièse",
2049 "re doble sostenido",
2050 "D𝄪",
2051 ),
2052 (
2053 "A####",
2054 "Aisisisis",
2055 "la quadruplo diesis",
2056 "la quadruple dièse",
2057 "la cuádruple sostenido",
2058 "A𝄪𝄪",
2059 ),
2060 (
2061 "B----",
2062 "Heseseses",
2063 "si quadruplo bemolle",
2064 "si quadruple bémol",
2065 "si cuádruple bemol",
2066 "B𝄫𝄫",
2067 ),
2068 ];
2069 for (name, german, italian, french, spanish, unicode) in cases {
2070 let pitch = Pitch::from_name(name).unwrap();
2071 assert_eq!(pitch.german().unwrap(), german, "{name}");
2072 assert_eq!(pitch.italian().unwrap(), italian, "{name}");
2073 assert_eq!(pitch.french().unwrap(), french, "{name}");
2074 assert_eq!(pitch.spanish().unwrap(), spanish, "{name}");
2075 assert_eq!(pitch.unicode_name(), unicode, "{name}");
2076 }
2077 assert_eq!(
2078 Pitch::from_name("A#4").unwrap().unicode_name_with_octave(),
2079 "A♯4"
2080 );
2081 assert_eq!(Pitch::from_name("E-5").unwrap().german().unwrap(), "Es");
2082 let quarter_sharp = Pitch::from_name("C~").unwrap();
2083 assert!(quarter_sharp.german().is_err());
2084 assert!(quarter_sharp.italian().is_err());
2085 assert!(quarter_sharp.french().is_err());
2086 assert!(quarter_sharp.spanish().is_err());
2087 }
2088
2089 #[test]
2090 fn fundamental_round_trips_through_the_builder() {
2091 let pitch = Pitch::builder()
2092 .name("E4")
2093 .fundamental(Pitch::from_name("C2").unwrap())
2094 .build()
2095 .unwrap();
2096 assert_eq!(
2097 pitch.fundamental().map(Pitch::name_with_octave),
2098 Some("C2".to_string())
2099 );
2100 assert_eq!(Pitch::from_name("E4").unwrap().fundamental(), None);
2101 }
2102
2103 #[test]
2104 fn simplify_multiple_enharmonics_test() {
2105 let more_than_five = vec![
2106 Pitch::from_number(0.0).unwrap(),
2107 Pitch::from_number(1.0).unwrap(),
2108 Pitch::from_number(2.0).unwrap(),
2109 Pitch::from_number(3.0).unwrap(),
2110 Pitch::from_number(4.0).unwrap(),
2111 Pitch::from_number(5.0).unwrap(),
2112 Pitch::from_number(12.0).unwrap(),
2113 Pitch::from_number(13.0).unwrap(),
2114 ];
2115
2116 let _x = simplify_multiple_enharmonics(&more_than_five, None, None);
2117 let _less_than_five = [
2118 Pitch::from_number(0.0),
2119 Pitch::from_number(1.0),
2120 Pitch::from_number(2.0),
2121 Pitch::from_number(12.0),
2122 Pitch::from_number(13.0),
2123 ];
2124 }
2125
2126 #[test]
2127 fn test_convert_harmonic_to_cents_values() {
2128 assert_eq!(convert_harmonic_to_cents(8), 3600);
2129 assert_eq!(convert_harmonic_to_cents(5), 2786);
2130 assert_eq!(convert_harmonic_to_cents(-2), -1200);
2131 }
2132
2133 #[test]
2134 fn test_pitch_transpose_interval() {
2135 let c4 = Pitch::from_name("C4").unwrap();
2136 let m3 = Interval::from_name("m3").unwrap();
2137 let out = c4.transpose(&m3).unwrap();
2138 assert_eq!(out.name_with_octave(), "Eb4");
2139 }
2140
2141 #[test]
2142 fn test_pitch_frequency_helpers() {
2143 let a4 = Pitch::from_name("A4").unwrap();
2144 assert!((a4.frequency_hz() - 440.0).abs() < 0.0001);
2145
2146 let e4 = Pitch::from_name("E4").unwrap();
2147 assert!((e4.frequency_hz_in(TuningSystem::FiveLimit) - 327.032).abs() < 0.001);
2148 assert!(e4.frequency_hz_in(TuningSystem::FiveLimit) < e4.frequency_hz());
2149 }
2150
2151 #[test]
2152 fn pitch_exposes_accidental_object() {
2153 let custom_accidental = Accidental::new("half-flat").unwrap();
2154 let pitch = Pitch::builder()
2155 .step('D')
2156 .accidental(custom_accidental.clone())
2157 .octave(4)
2158 .build()
2159 .unwrap();
2160
2161 assert_eq!(pitch.name_with_octave(), "D`4");
2162 assert_eq!(pitch.accidental(), &custom_accidental);
2163 assert_eq!(pitch.accidental().name(), "half-flat");
2164 assert_eq!(pitch.accidental().alter(), -0.5);
2165 }
2166
2167 #[test]
2168 fn pitch_exposes_microtone_object() {
2169 let microtone = Microtone::new(-25.0).unwrap();
2170 let pitch = Pitch::builder()
2171 .name("G#4")
2172 .microtone(microtone.clone())
2173 .build()
2174 .unwrap();
2175
2176 assert_eq!(pitch.microtone(), Some(µtone));
2177 assert_eq!(pitch.microtone().unwrap().to_string(), "(-25c)");
2178 assert_eq!(pitch.alter(), 0.75);
2179 }
2180
2181 #[test]
2182 fn pitch_supports_rust_conversion_traits() {
2183 let parsed: Pitch = "C#4".parse().unwrap();
2184 assert_eq!(parsed.to_string(), "C#4");
2185
2186 let midi = Pitch::try_from(60 as IntegerType).unwrap();
2187 assert_eq!(midi.name_with_octave(), "C4");
2188 assert_eq!(midi.midi(), 60);
2189
2190 let pitch_space = Pitch::try_from(61.5).unwrap();
2191 assert_eq!(pitch_space.pitch_space(), 61.5);
2192 assert_eq!(pitch_space.midi(), 62);
2193
2194 let built = Pitch::builder().pitch_space(60.0).build().unwrap();
2195 assert_eq!(built.name_with_octave(), "C4");
2196 }
2197
2198 #[test]
2199 fn pitch_exposes_enharmonic_helpers() {
2200 let c_sharp = Pitch::from_name("C#3").unwrap();
2201 let out = c_sharp.get_higher_enharmonic().unwrap();
2202 assert_eq!(out.name_with_octave(), "Db3");
2203
2204 let mut d_flat = out;
2205 d_flat.get_lower_enharmonic_in_place().unwrap();
2206 assert_eq!(d_flat.name_with_octave(), "C#3");
2207
2208 let d_sharp = Pitch::from_name("D#4").unwrap();
2209 assert_eq!(
2210 d_sharp
2211 .simplify_enharmonic(true)
2212 .unwrap()
2213 .name_with_octave(),
2214 "Eb4"
2215 );
2216 }
2217}