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::{CHROMATIC_PITCH_CLASS_NAMES, pitch_class_name};
9
10use harmonics::*;
11
12pub(crate) mod accidental;
13pub(crate) mod microtone;
14pub(crate) mod pitchclass;
15
16use crate::defaults::FloatType;
17use crate::defaults::IntegerType;
18use crate::defaults::Octave;
19use crate::defaults::PITCH_OCTAVE;
20use crate::defaults::PITCH_SPACE_SIGNIFICANT_DIGITS;
21use crate::defaults::PITCH_STEP;
22use crate::defaults::UnsignedIntegerType;
23use crate::error::Error;
24use crate::error::Result;
25use crate::interval::Interval;
26use crate::interval::PitchOrNote;
27use crate::key::keysignature::KeySignature;
28use crate::stepname::StepName;
29use crate::tuningsystem::TuningSystem;
30
31pub use accidental::{Accidental, AccidentalAttribute, AccidentalSpecifier};
32pub use microtone::{Microtone, MicrotoneSpecifier};
33use pitchclass::convert_ps_to_oct;
34pub use pitchclass::{PitchClass, PitchClassSpecifier, convert_pitch_class_to_str};
35
36use itertools::Itertools;
37use num::Num;
38use num_traits::ToPrimitive;
39use ordered_float::OrderedFloat;
40use std::cmp::Ordering;
41use std::fmt::{Display, Formatter};
42use std::str::FromStr;
43use std::sync::Arc;
44
45#[derive(Clone, Debug, PartialEq)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47pub enum PitchName {
49 Name(String),
51 Number(FloatType),
53}
54
55impl From<&str> for PitchName {
56 fn from(value: &str) -> Self {
57 Self::Name(value.to_string())
58 }
59}
60
61impl From<String> for PitchName {
62 fn from(value: String) -> Self {
63 Self::Name(value)
64 }
65}
66
67impl From<IntegerType> for PitchName {
68 fn from(value: IntegerType) -> Self {
69 Self::Number(value as FloatType)
70 }
71}
72
73impl From<FloatType> for PitchName {
74 fn from(value: FloatType) -> Self {
75 Self::Number(value)
76 }
77}
78
79#[derive(Clone, Debug, Default, PartialEq)]
80#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
81pub struct PitchOptions {
83 pub name: Option<PitchName>,
85 pub step: Option<char>,
87 pub octave: Octave,
89 pub accidental: Option<AccidentalSpecifier>,
91 pub microtone: Option<MicrotoneSpecifier>,
93 pub pitch_class: Option<PitchClassSpecifier>,
95 pub midi: Option<IntegerType>,
97 pub ps: Option<FloatType>,
99 pub fundamental: Option<Pitch>,
101}
102
103impl PitchOptions {
104 pub fn new() -> Self {
106 Self::default()
107 }
108
109 pub fn name(mut self, name: impl Into<PitchName>) -> Self {
111 self.name = Some(name.into());
112 self
113 }
114
115 pub fn step(mut self, step: char) -> Self {
117 self.step = Some(step);
118 self
119 }
120
121 pub fn octave(mut self, octave: IntegerType) -> Self {
123 self.octave = Some(octave);
124 self
125 }
126
127 pub fn accidental(mut self, accidental: impl Into<AccidentalSpecifier>) -> Self {
129 self.accidental = Some(accidental.into());
130 self
131 }
132
133 pub fn microtone(mut self, microtone: impl Into<MicrotoneSpecifier>) -> Self {
135 self.microtone = Some(microtone.into());
136 self
137 }
138
139 pub fn pitch_class(mut self, pitch_class: impl Into<PitchClassSpecifier>) -> Self {
141 self.pitch_class = Some(pitch_class.into());
142 self
143 }
144
145 pub fn midi(mut self, midi: IntegerType) -> Self {
147 self.midi = Some(midi);
148 self
149 }
150
151 pub fn ps(mut self, ps: FloatType) -> Self {
153 self.ps = Some(ps);
154 self
155 }
156
157 pub fn pitch_space(mut self, pitch_space: FloatType) -> Self {
159 self.ps = Some(pitch_space);
160 self
161 }
162
163 pub fn fundamental(mut self, fundamental: Pitch) -> Self {
165 self.fundamental = Some(fundamental);
166 self
167 }
168
169 pub fn build(self) -> Result<Pitch> {
171 Pitch::from_options(self)
172 }
173}
174
175#[derive(Clone, Debug)]
176#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
177#[must_use]
179pub struct Pitch {
180 step: StepName,
181 octave: Octave,
182 accidental: Accidental,
183 #[cfg_attr(feature = "serde", serde(default))]
184 has_accidental: bool,
185 microtone: Option<Microtone>,
186 spelling_is_inferred: bool,
187 #[cfg_attr(feature = "serde", serde(skip))]
188 fundamental: Option<Arc<Pitch>>,
189}
190
191impl PartialEq for Pitch {
192 fn eq(&self, other: &Self) -> bool {
193 self.step == other.step
194 && self.octave == other.octave
195 && self.accidental == other.accidental
196 && self.has_accidental == other.has_accidental
197 && self.microtone == other.microtone
198 }
199}
200
201impl FromStr for Pitch {
202 type Err = Error;
203
204 fn from_str(value: &str) -> Result<Self> {
205 Self::from_name(value)
206 }
207}
208
209impl TryFrom<&str> for Pitch {
210 type Error = Error;
211
212 fn try_from(value: &str) -> Result<Self> {
213 Self::from_name(value)
214 }
215}
216
217impl TryFrom<String> for Pitch {
218 type Error = Error;
219
220 fn try_from(value: String) -> Result<Self> {
221 Self::from_name(value)
222 }
223}
224
225impl TryFrom<&Pitch> for Pitch {
226 type Error = Error;
227
228 fn try_from(value: &Pitch) -> Result<Self> {
229 Ok(value.clone())
230 }
231}
232
233impl TryFrom<IntegerType> for Pitch {
234 type Error = Error;
235
236 fn try_from(value: IntegerType) -> Result<Self> {
237 Self::from_midi(value)
238 }
239}
240
241impl TryFrom<FloatType> for Pitch {
242 type Error = Error;
243
244 fn try_from(value: FloatType) -> Result<Self> {
245 Self::from_pitch_space(value)
246 }
247}
248
249impl Display for Pitch {
250 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
251 write!(f, "{}", self.name_with_octave())
252 }
253}
254
255impl Pitch {
256 pub fn from_options(options: PitchOptions) -> Result<Self> {
263 let PitchOptions {
264 name,
265 step,
266 octave,
267 accidental,
268 microtone,
269 pitch_class,
270 midi,
271 ps,
272 fundamental,
273 } = options;
274 let explicit_step = step.map(StepName::try_from).transpose()?;
275 let has_explicit_octave = octave.is_some();
276 let has_explicit_accidental = accidental.is_some();
277
278 if let Some(PitchName::Number(number)) = &name
279 && !number.is_finite()
280 {
281 return Err(Error::Pitch(format!(
282 "a pitch-space number must be finite, got {number}"
283 )));
284 }
285 let parsed = name.map(PitchParameters::from).unwrap_or_default();
286 let name = parsed.name;
287 let step = if name.is_some() || parsed.step.is_some() {
288 parsed.step
289 } else {
290 explicit_step
291 }
292 .unwrap_or(PITCH_STEP);
293 let octave = octave.or(parsed.octave);
294 let parsed_has_accidental = parsed.accidental.is_some();
295 let accidental = match accidental {
296 Some(accidental) => Accidental::new(accidental)?,
297 None => parsed.accidental.unwrap_or_default(),
298 };
299 let microtone = match microtone {
300 Some(microtone) => Some(Microtone::new(microtone)?),
301 None => parsed.microtone,
302 };
303
304 let explicit_accidental = has_explicit_accidental.then(|| accidental.clone());
305 let mut pitch = Pitch {
306 step,
307 accidental,
308 has_accidental: has_explicit_accidental || parsed_has_accidental,
309 microtone,
310 octave,
311 spelling_is_inferred: parsed.spelling_is_inferred,
312 fundamental: None,
313 };
314
315 if let Some(name) = &name {
316 pitch.name_setter(name)?;
317 pitch.spelling_is_inferred = parsed.spelling_is_inferred;
318 }
319 if explicit_step.is_some() || name.is_none() {
320 pitch.step_setter(step);
321 pitch.spelling_is_inferred = parsed.spelling_is_inferred;
322 }
323 if has_explicit_octave || name.is_none() {
324 pitch.octave_setter(octave);
325 }
326 if let Some(accidental) = explicit_accidental {
327 pitch.accidental_setter(accidental);
328 } else if pitch.spelling_is_inferred {
329 pitch.has_accidental = pitch.accidental.alter() != 0.0;
330 }
331 if let Some(microtone) = pitch.microtone.clone() {
332 pitch.microtone_setter(microtone);
333 }
334 if let Some(pitch_class) = pitch_class {
335 pitch.pitch_class_setter(pitch_class)?;
336 }
337 if let Some(fundamental) = fundamental {
338 pitch.fundamental_setter(fundamental);
339 }
340 if let Some(midi) = midi {
341 pitch.midi_setter(midi);
342 }
343 if let Some(ps) = ps {
344 if !ps.is_finite() {
345 return Err(Error::Pitch(format!(
346 "a pitch-space number must be finite, got {ps}"
347 )));
348 }
349 pitch.ps_setter(ps);
350 }
351
352 Ok(pitch)
353 }
354
355 pub fn builder() -> PitchOptions {
357 PitchOptions::new()
358 }
359
360 pub fn from_name(name: impl Into<String>) -> Result<Self> {
362 PitchOptions::new().name(name.into()).build()
363 }
364
365 pub fn from_number(number: FloatType) -> Result<Self> {
367 PitchOptions::new().name(PitchName::Number(number)).build()
368 }
369
370 pub fn from_step(step: char) -> Result<Self> {
372 PitchOptions::new().step(step).build()
373 }
374
375 pub fn from_name_and_octave(name: impl Into<String>, octave: IntegerType) -> Result<Self> {
377 PitchOptions::new().name(name.into()).octave(octave).build()
378 }
379
380 pub fn from_pitch_class(pitch_class: impl Into<PitchClassSpecifier>) -> Result<Self> {
382 PitchOptions::new().pitch_class(pitch_class).build()
383 }
384
385 pub fn from_midi(midi: IntegerType) -> Result<Self> {
387 PitchOptions::new().midi(midi).build()
388 }
389
390 pub fn from_pitch_space(ps: FloatType) -> Result<Self> {
392 PitchOptions::new().ps(ps).build()
393 }
394
395 pub fn name_with_octave(&self) -> String {
397 match self.octave {
398 Some(octave) => format!("{}{}", self.name(), octave),
399 None => self.name(),
400 }
401 }
402
403 pub fn name(&self) -> String {
405 format!("{}{}", self.step.as_char(), self.accidental.modifier())
406 }
407
408 fn name_setter(&mut self, usr_str: &str) -> Result<()> {
409 let usr_str = usr_str.trim();
410
411 let mut pitch_part = String::with_capacity(usr_str.len());
412 let mut octave_part = String::new();
413 for character in usr_str.chars() {
414 if character.is_ascii_digit() {
415 if pitch_part.is_empty() {
416 return Err(Error::Value(format!(
417 "Cannot have octave given before pitch name in '{usr_str}'."
418 )));
419 }
420 octave_part.push(character);
421 } else {
422 pitch_part.push(character);
423 }
424 }
425
426 let mut pitch_chars = pitch_part.chars();
427 let step = pitch_chars.next().ok_or(Error::Pitch(format!(
428 "Cannot make a name out of {pitch_part:?}"
429 )))?;
430 self.step_setter(StepName::try_from(step)?);
431
432 let accidental_str: String = pitch_chars.collect();
433 if accidental_str.is_empty() {
434 self.accidental = Accidental::natural();
435 self.has_accidental = false;
436 } else {
437 self.accidental_setter(Accidental::new(accidental_str)?);
438 }
439
440 if !octave_part.is_empty() {
441 let octave = octave_part
442 .parse::<IntegerType>()
443 .map_err(|_| Error::Pitch(format!("Cannot parse {octave_part:?} to octave")))?;
444 self.octave_setter(Some(octave));
445 }
446
447 Ok(())
448 }
449
450 pub fn alter(&self) -> FloatType {
452 let mut post = 0.0;
453
454 post += self.accidental.alter;
455
456 if let Some(microtone) = &self.microtone {
457 post += microtone.alter();
458 }
459
460 post
461 }
462
463 pub fn accidental(&self) -> &Accidental {
468 &self.accidental
469 }
470
471 pub fn microtone(&self) -> Option<&Microtone> {
473 self.microtone.as_ref()
474 }
475
476 pub fn pitch_class(&self) -> PitchClass {
478 PitchClass::from_number(self.ps()).unwrap_or_else(|err| {
479 panic!("pitch-space value should always map to pitch class: {err}")
480 })
481 }
482
483 pub fn set_octave(&mut self, octave: Octave) {
486 self.octave = octave;
487 }
488
489 pub(crate) fn octave_setter(&mut self, octave: Octave) {
490 self.set_octave(octave);
491 }
492
493 pub fn transpose(&self, interval: &Interval) -> Result<Pitch> {
498 let mut p = interval.transpose_pitch_with_options(self, false, Some(4))?;
499
500 if !interval.implicit_diatonic {
501 p.spelling_is_inferred = self.spelling_is_inferred;
502 }
503 if p.spelling_is_inferred {
504 p.simplify_enharmonic_in_place(true)?;
505 }
506 if let Some(fundamental) = &self.fundamental {
507 p.fundamental_setter(fundamental.transpose(interval)?);
508 }
509
510 Ok(p)
511 }
512
513 pub fn ps(&self) -> FloatType {
515 self.pitch_space()
516 }
517
518 pub fn pitch_space(&self) -> FloatType {
520 let octave = self.octave.unwrap_or(PITCH_OCTAVE as IntegerType);
521 ((octave + 1) * 12) as FloatType + self.step.step_ref() as FloatType + self.alter()
522 }
523
524 pub fn midi(&self) -> IntegerType {
529 normalize_midi((self.pitch_space() + 0.5).floor() as IntegerType)
530 }
531
532 pub fn frequency_hz(&self) -> FloatType {
534 440.0 * (2.0 as FloatType).powf((self.pitch_space() - 69.0) / 12.0)
535 }
536
537 pub fn frequency_hz_in(&self, tuning_system: TuningSystem) -> FloatType {
542 tuning_system.frequency_at(self.pitch_space())
543 }
544
545 fn step_setter(&mut self, step_name: StepName) {
546 self.step = step_name;
547 self.spelling_is_inferred = false;
548 }
549
550 pub(crate) fn set_diatonic_note_number(&mut self, dnn: IntegerType) -> Result<()> {
553 let (step, octave) = crate::interval::convert_diatonic_number_to_step(dnn);
554 self.step_setter(StepName::try_from(step)?);
555 self.octave_setter(Some(octave));
556 Ok(())
557 }
558
559 pub(crate) fn set_accidental_or_natural(&mut self, accidental: Option<Accidental>) {
561 self.set_accidental(accidental);
562 }
563
564 pub fn set_accidental(&mut self, accidental: Option<Accidental>) {
568 match accidental {
569 Some(accidental) => self.accidental_setter(accidental),
570 None => {
571 self.accidental = Accidental::natural();
572 self.has_accidental = false;
573 }
574 }
575 }
576
577 pub fn set_accidental_alter(&mut self, alter: FloatType) -> Result<()> {
582 let (alter_shift, cents) = cents_to_alter_and_cents(alter * 100.0);
583 self.accidental_setter(Accidental::new(alter_shift)?);
584 if cents.abs() > 0.01 {
585 self.microtone_setter(Microtone::new(cents)?);
586 }
587 Ok(())
588 }
589
590 pub fn has_accidental(&self) -> bool {
596 self.has_accidental
597 }
598
599 pub fn spelling_is_inferred(&self) -> bool {
604 self.spelling_is_inferred
605 }
606
607 pub fn set_spelling_is_inferred(&mut self, inferred: bool) {
609 self.spelling_is_inferred = inferred;
610 }
611
612 pub fn explicit_accidental(&self) -> Option<&Accidental> {
615 self.has_accidental.then_some(&self.accidental)
616 }
617
618 pub fn explicit_accidental_mut(&mut self) -> Option<&mut Accidental> {
620 self.has_accidental.then_some(&mut self.accidental)
621 }
622
623 fn accidental_setter(&mut self, value: Accidental) {
624 self.accidental = value;
625 self.has_accidental = true;
626 }
627
628 pub fn set_microtone_cents(&mut self, cents: FloatType) -> Result<()> {
631 if cents == 0.0 {
632 self.microtone = None;
633 } else {
634 self.microtone = Some(Microtone::new(cents)?);
635 }
636 Ok(())
637 }
638
639 fn microtone_setter(&mut self, mt: Microtone) {
640 self.microtone = Some(mt);
641 }
642
643 fn pitch_class_setter(&mut self, pc: PitchClassSpecifier) -> Result<()> {
644 self.pitch_class_value_setter(PitchClass::new(pc)?.number());
645 Ok(())
646 }
647
648 fn pitch_class_value_setter(&mut self, pc: FloatType) {
649 let (step, accidental, _, _) = convert_ps_to_step(pc);
650 self.step = step;
651 self.has_accidental = accidental.alter() != 0.0;
652 self.accidental = accidental;
653 self.spelling_is_inferred = true;
654 }
655
656 fn fundamental_setter(&mut self, f: Pitch) {
657 self.fundamental = Some(Arc::new(f));
658 }
659
660 pub fn fundamental(&self) -> Option<&Pitch> {
662 self.fundamental.as_deref()
663 }
664
665 fn midi_setter(&mut self, m: IntegerType) {
666 self.ps_setter(normalize_midi(m) as FloatType);
667 }
668
669 fn ps_setter(&mut self, p: FloatType) {
670 let (step, accidental, microtone, octave_shift) = convert_ps_to_step(p);
671 self.step = step;
672 self.has_accidental = accidental.alter() != 0.0;
673 self.accidental = accidental;
674 if microtone.alter() == 0.0 {
675 self.microtone = None;
676 } else {
677 self.microtone = Some(microtone);
678 }
679
680 let octave = convert_ps_to_oct(p) + octave_shift;
681 self.octave = Some(octave);
682 self.spelling_is_inferred = true;
683 }
684
685 pub fn implicit_octave(&self) -> IntegerType {
692 self.octave.unwrap_or(PITCH_OCTAVE as IntegerType)
693 }
694
695 #[must_use]
701 pub fn octave_is_implicit(&self) -> bool {
702 self.octave.is_none()
703 }
704
705 pub fn set_octave_is_implicit(&mut self, implicit: bool) {
712 if implicit == self.octave.is_none() {
713 return;
714 }
715 self.octave = if implicit {
716 None
717 } else {
718 Some(PITCH_OCTAVE as IntegerType)
719 };
720 }
721
722 pub fn pitch_class_string(&self) -> String {
728 crate::pitch::pitchclass::convert_pitch_class_to_str(
729 self.ps().round_ties_even() as IntegerType
730 )
731 }
732
733 pub fn cent_shift_from_midi(&self) -> IntegerType {
737 let mut distance = self.ps() - FloatType::from(self.midi());
738 while distance < -11.0 {
739 distance += 12.0;
740 }
741 while distance > 11.0 {
742 distance -= 12.0;
743 }
744 (distance * 100.0).round() as IntegerType
745 }
746
747 pub fn from_frequency(hertz: FloatType) -> Result<Self> {
750 if !hertz.is_finite() || hertz <= 0.0 {
751 return Err(Error::Pitch(format!(
752 "frequency must be a finite number greater than zero, got {hertz}"
753 )));
754 }
755 let mut pitch = Pitch::default();
756 pitch.ps_setter(12.0 * (hertz / 440.0).log2() + 69.0);
757 Ok(pitch)
758 }
759
760 pub fn diatonic_note_number(&self) -> IntegerType {
763 let octave = self.octave.unwrap_or(PITCH_OCTAVE as IntegerType);
764 self.step.step_to_dnn_offset() + 7 * octave
765 }
766
767 pub fn is_twelve_tone(&self) -> bool {
770 self.accidental.is_twelve_tone()
771 && self
772 .microtone
773 .as_ref()
774 .is_none_or(|microtone| microtone.cents() == 0.0)
775 }
776
777 fn octave_bearing_copy(&self, operation: &str) -> Result<Pitch> {
778 if self.octave.is_none() {
779 return Err(Error::Pitch(format!(
780 "Cannot call {operation} with an octaveless Pitch."
781 )));
782 }
783 Ok(self.clone())
784 }
785
786 fn shift_octave(&mut self, octaves: IntegerType) {
787 let octave = self.octave.unwrap_or(PITCH_OCTAVE as IntegerType);
788 self.octave_setter(Some(octave + octaves));
789 }
790
791 pub fn octave(&self) -> Octave {
797 self.octave
798 }
799
800 pub(crate) fn step(&self) -> StepName {
801 self.step
802 }
803
804 pub(crate) fn set_ps(&mut self, p: FloatType) {
805 self.ps_setter(p);
806 }
807}
808
809impl Default for Pitch {
810 fn default() -> Self {
811 Self::from_options(PitchOptions::default())
812 .expect("default Pitch construction should never fail")
813 }
814}
815
816#[derive(Default)]
817struct PitchParameters {
818 name: Option<String>,
819 step: Option<StepName>,
820 accidental: Option<Accidental>,
821 microtone: Option<Microtone>,
822 spelling_is_inferred: bool,
823 octave: Octave,
824}
825
826impl From<PitchName> for PitchParameters {
827 fn from(value: PitchName) -> Self {
828 match value {
829 PitchName::Name(name) => Self {
830 name: Some(name),
831 ..Self::default()
832 },
833 PitchName::Number(number) => {
834 let (step, accidental, microtone, octave_shift) = convert_ps_to_step(number);
835 let octave = (number >= 12.0).then(|| convert_ps_to_oct(number) + octave_shift);
836 Self {
837 name: None,
838 step: Some(step),
839 accidental: Some(accidental),
840 microtone: (microtone.cents() != 0.0).then_some(microtone),
841 spelling_is_inferred: true,
842 octave,
843 }
844 }
845 }
846 }
847}
848
849fn convert_ps_to_step<T: Num + ToPrimitive>(
850 ps: T,
851) -> (StepName, Accidental, Microtone, IntegerType) {
852 let ps = ps.to_f64().unwrap_or(0.0);
853 let (pc, alter, micro) = if ps.fract() == 0.0 {
854 ((ps as IntegerType).rem_euclid(12), 0.0, 0.0)
855 } else {
856 let ps = round_to_digits(ps, PITCH_SPACE_SIGNIFICANT_DIGITS);
857 let pc_real = ps.rem_euclid(12.0);
858 let pc = pc_real.floor() as IntegerType;
859 let mut micro = pc_real - pc as FloatType;
860
861 let alter = if round_to_digits(micro, 1) == 0.5 || (0.25 < micro && micro < 0.75) {
865 micro -= 0.5;
866 0.5
867 } else if (0.75..1.0).contains(µ) {
868 micro -= 1.0;
869 1.0
870 } else if micro > 0.0 {
871 0.0
872 } else {
873 micro = 0.0;
874 0.0
875 };
876
877 (pc, alter, micro)
878 };
879
880 let octave_shift = IntegerType::from(pc == 11 && alter == 1.0);
881 let (pc_name, accidental_alter) = match pc {
882 4 | 11 if alter == 1.0 => ((pc + 1).rem_euclid(12), 0.0),
883 1 | 6 | 8 if alter >= 1.0 => (pc + 1, alter - 1.0),
884 1 | 6 | 8 => (pc - 1, 1.0 + alter),
885 3 | 10 if alter <= -1.0 => (pc - 1, 1.0 + alter),
886 3 | 10 => (pc + 1, -1.0 + alter),
887 _ => (pc, alter),
888 };
889
890 let step = StepName::ref_to_step(pc_name.rem_euclid(12))
891 .unwrap_or_else(|err| panic!("pitch class should map to a step: {err}"));
892 let accidental = Accidental::new(accidental_alter)
893 .unwrap_or_else(|err| panic!("accidental conversion should not fail: {err}"));
894 let microtone = Microtone::from_cents(micro * 100.0, 1);
895
896 (step, accidental, microtone, octave_shift)
897}
898
899fn round_to_digits(value: FloatType, digits: UnsignedIntegerType) -> FloatType {
900 let factor = (10 as FloatType).powi(digits as IntegerType);
901 (value * factor).round() / factor
902}
903
904fn normalize_midi(midi: IntegerType) -> IntegerType {
905 if midi > 127 {
906 let mut value = (12 * 9) + midi.rem_euclid(12);
907 if value < (127 - 12) {
908 value += 12;
909 }
910 value
911 } else if midi < 0 {
912 midi.rem_euclid(12)
913 } else {
914 midi
915 }
916}
917
918#[cfg(test)]
919mod tests {
920 #[test]
924 fn a_pitch_space_number_that_is_not_finite_is_refused() {
925 use crate::pitch::Pitch;
926
927 for number in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
928 assert!(Pitch::from_pitch_space(number).is_err());
929 assert!(Pitch::from_number(number).is_err());
930 assert!(Pitch::builder().ps(number).build().is_err());
931 }
932 assert_eq!(Pitch::from_pitch_space(61.0).unwrap().name(), "C#");
933 }
934
935 #[test]
936 fn a_frequency_that_is_not_finite_is_refused() {
937 use crate::pitch::Pitch;
938
939 assert!(Pitch::from_frequency(f64::INFINITY).is_err());
940 assert!(Pitch::from_frequency(f64::NAN).is_err());
941 assert!(Pitch::from_frequency(0.0).is_err());
942 assert_eq!(
943 Pitch::from_frequency(440.0).unwrap().name_with_octave(),
944 "A4"
945 );
946 }
947
948 #[test]
949 fn a_pitch_is_built_from_a_step_a_number_or_an_owned_name() {
950 use crate::pitch::{Pitch, PitchName, pitch_class_name};
951
952 assert_eq!(Pitch::from_step('d').unwrap().name(), "D");
953 assert!(Pitch::from_step('h').is_err());
954 assert_eq!(
955 Pitch::try_from("E-4".to_string())
956 .unwrap()
957 .name_with_octave(),
958 "E-4"
959 );
960 assert!(matches!(PitchName::from(60), PitchName::Number(n) if n == 60.0));
961 assert!(matches!(PitchName::from(61.5), PitchName::Number(n) if n == 61.5));
962 assert_eq!(pitch_class_name(1), "D-");
963 assert_eq!(pitch_class_name(13), "D-");
964 assert_eq!(pitch_class_name(10), "B-");
965 }
966
967 #[test]
968 fn spelling_is_inferred_for_a_pitch_built_from_a_number_and_can_be_unsaid() {
969 use crate::pitch::Pitch;
970
971 let mut pitch = Pitch::from_midi(61).unwrap();
972 assert!(pitch.spelling_is_inferred());
973 pitch.set_spelling_is_inferred(false);
974 assert!(!pitch.spelling_is_inferred());
975 assert!(!Pitch::from_name("C#").unwrap().spelling_is_inferred());
976
977 let mut raised = Pitch::from_name("C#4").unwrap();
978 raised.get_higher_enharmonic_in_place().unwrap();
979 assert_eq!(raised.name_with_octave(), "D-4");
980 }
981
982 #[test]
983 fn a_pitch_knows_whether_a_key_signature_already_writes_it() {
984 use crate::pitch::Pitch;
985
986 let altered = [
987 Pitch::from_name("F#").unwrap(),
988 Pitch::from_name("C#").unwrap(),
989 ];
990 assert!(
991 Pitch::from_name("F#4")
992 .unwrap()
993 .name_in_key_signature(&altered)
994 );
995 assert!(
996 !Pitch::from_name("F4")
997 .unwrap()
998 .name_in_key_signature(&altered)
999 );
1000 assert!(
1001 !Pitch::from_name("F-4")
1002 .unwrap()
1003 .name_in_key_signature(&altered)
1004 );
1005 assert!(
1006 Pitch::from_name("F4")
1007 .unwrap()
1008 .step_in_key_signature(&altered)
1009 );
1010 assert!(
1011 !Pitch::from_name("G4")
1012 .unwrap()
1013 .step_in_key_signature(&altered)
1014 );
1015 }
1016
1017 #[test]
1022 fn accidental_display_follows_the_pitches_before_it() {
1023 use crate::pitch::{AccidentalDisplayOptions, Pitch};
1024
1025 let first = Pitch::from_name("F#4").unwrap();
1026 let mut repeat = Pitch::from_name("F#4").unwrap();
1027 let past = [first.clone()];
1028 repeat.update_accidental_display(&AccidentalDisplayOptions {
1029 pitch_past: &past,
1030 ..AccidentalDisplayOptions::default()
1031 });
1032 assert_eq!(repeat.accidental().display_status(), Some(false));
1033
1034 let mut natural = Pitch::from_name("F4").unwrap();
1035 natural.update_accidental_display(&AccidentalDisplayOptions {
1036 pitch_past: &past,
1037 ..AccidentalDisplayOptions::default()
1038 });
1039 assert!(natural.has_accidental());
1040 assert_eq!(natural.accidental().display_status(), Some(true));
1041
1042 let mut in_key = Pitch::from_name("F#4").unwrap();
1043 in_key.update_accidental_display(&AccidentalDisplayOptions {
1044 altered_pitches: &[Pitch::from_name("F#").unwrap()],
1045 ..AccidentalDisplayOptions::default()
1046 });
1047 assert_eq!(in_key.accidental().display_status(), Some(false));
1048
1049 let mut fresh = Pitch::from_name("B-4").unwrap();
1050 fresh.update_accidental_display(&AccidentalDisplayOptions::default());
1051 assert_eq!(fresh.accidental().display_status(), Some(true));
1052 }
1053
1054 #[test]
1057 fn an_octave_is_implicit_until_one_is_given() {
1058 use crate::pitch::Pitch;
1059
1060 let mut anywhere = Pitch::from_name("G#").unwrap();
1061 assert!(anywhere.octave_is_implicit());
1062 assert_eq!(anywhere.octave(), None);
1063 assert_eq!(anywhere.implicit_octave(), 4);
1064
1065 let somewhere = Pitch::from_name("E-6").unwrap();
1066 assert!(!somewhere.octave_is_implicit());
1067 assert_eq!(somewhere.octave(), Some(6));
1068 assert_eq!(somewhere.implicit_octave(), 6);
1069
1070 anywhere.set_octave_is_implicit(false);
1073 assert!(!anywhere.octave_is_implicit());
1074 assert_eq!(anywhere.octave(), Some(4));
1075 anywhere.set_octave_is_implicit(true);
1076 assert_eq!(anywhere.octave(), None);
1077
1078 let mut high = Pitch::from_name("C7").unwrap();
1080 high.set_octave_is_implicit(false);
1081 assert_eq!(high.octave(), Some(7));
1082 }
1083
1084 #[test]
1085 fn a_pitch_is_respelled_to_agree_with_the_key_signature() {
1086 use crate::key::KeySignature;
1087 let f_sharp = Pitch::from_name("F#4").unwrap();
1090 assert_eq!(
1091 f_sharp.respelled_for(&KeySignature::new(2)).unwrap().name(),
1092 "F#"
1093 );
1094 assert_eq!(
1095 f_sharp
1096 .respelled_for(&KeySignature::new(-5))
1097 .unwrap()
1098 .name(),
1099 "G-"
1100 );
1101 assert_eq!(
1104 Pitch::from_name("C4")
1105 .unwrap()
1106 .respelled_for(&KeySignature::new(-5))
1107 .unwrap()
1108 .name(),
1109 "C"
1110 );
1111 }
1112
1113 #[test]
1114 fn an_octave_digit_may_sit_anywhere_after_the_step() {
1115 for (name, expected) in [
1116 ("e4-", "E-4"),
1117 ("e-4", "E-4"),
1118 ("b3-", "B-3"),
1119 ("g4--", "G--4"),
1120 ("c#4", "C#4"),
1121 ("f##2", "F##2"),
1122 ("d1", "D1"),
1123 ] {
1124 assert_eq!(
1125 Pitch::from_name(name).unwrap().name_with_octave(),
1126 expected,
1127 "parsing {name:?}"
1128 );
1129 }
1130 assert_eq!(
1131 Pitch::from_name("c4#2").unwrap().name_with_octave(),
1132 "C#42",
1133 "the digits concatenate wherever they fall, as music21's do"
1134 );
1135 assert!(Pitch::from_name("4c").is_err());
1136 }
1137
1138 #[test]
1139 fn options_keep_an_explicit_accidental_and_a_numeric_name_stays_inferred() {
1140 let sharp = crate::PitchOptions::new()
1141 .name("C")
1142 .accidental("#")
1143 .octave(7)
1144 .microtone(-30)
1145 .build()
1146 .unwrap();
1147 assert_eq!(sharp.full_name(), "C-sharp in octave 7 (-30c)");
1148 let double_flat = crate::PitchOptions::new()
1149 .name("D")
1150 .accidental(Accidental::new("double-flat").unwrap())
1151 .build()
1152 .unwrap();
1153 assert_eq!(double_flat.name(), "D--");
1154 let inferred = Pitch::from_number(6.0).unwrap();
1155 assert_eq!(
1156 inferred
1157 .transpose(&Interval::from_name("-m2").unwrap())
1158 .unwrap()
1159 .name(),
1160 "F"
1161 );
1162 }
1163
1164 #[test]
1165 fn midi_folds_into_range_like_music21() {
1166 let high = Pitch::from_name("C#10").unwrap();
1167 assert_eq!(high.midi(), 121);
1168 assert_eq!(high.cent_shift_from_midi(), 0);
1169 let half_flat = crate::PitchOptions::new().name("F`10").build().unwrap();
1170 assert_eq!(half_flat.midi(), 125);
1171 assert_eq!(
1172 Pitch::from_name("C#-2")
1173 .unwrap_or_else(|_| Pitch::from_pitch_space(-11.0).unwrap())
1174 .midi(),
1175 1
1176 );
1177 assert_eq!(Pitch::from_name("C~4").unwrap().midi(), 61);
1178 assert_eq!(Pitch::from_name("A4").unwrap().frequency_hz(), 440.0);
1179 }
1180
1181 #[test]
1182 fn harmonics_chain_and_transpose_with_their_fundamental() {
1183 let a4 = Pitch::from_name("A4").unwrap();
1184 let seventh = a4.harmonic(7).unwrap();
1185 assert_eq!(seventh.name_with_octave(), "F#~7");
1186 assert_eq!(seventh.microtone().unwrap().cents().round(), 19.0);
1187 let doubled = seventh.harmonic(2).unwrap();
1188 assert_eq!(doubled.name_with_octave(), "F#~8");
1189 assert_eq!(doubled.microtone().unwrap().cents().round(), 19.0);
1190 assert_eq!(doubled.fundamental().unwrap().name_with_octave(), "F#~7");
1191
1192 let second = a4.harmonic(2).unwrap();
1193 assert_eq!(second.fundamental().unwrap().name_with_octave(), "A4");
1194 let up_a_fifth = second
1195 .transpose(&Interval::from_name("p5").unwrap())
1196 .unwrap();
1197 assert_eq!(up_a_fifth.name_with_octave(), "E6");
1198 assert_eq!(up_a_fifth.fundamental().unwrap().name_with_octave(), "E5");
1199 }
1200
1201 #[test]
1202 fn numbers_and_chromatic_transposition_keep_microtones() {
1203 let sharp_twenty = Pitch::from_number(60.2).unwrap();
1204 assert_eq!(sharp_twenty.name_with_octave(), "C4");
1205 assert_eq!(sharp_twenty.microtone().unwrap().cents().round(), 20.0);
1206 let harmonic = Pitch::from_name("A4")
1207 .unwrap()
1208 .harmonic(7)
1209 .unwrap()
1210 .harmonic(2)
1211 .unwrap();
1212 let down_two_octaves = harmonic
1213 .transpose(&Interval::from_semitones(-24).unwrap())
1214 .unwrap();
1215 assert_eq!(down_two_octaves.name_with_octave(), "F#~6");
1216 assert_eq!(down_two_octaves.microtone().unwrap().cents().round(), 19.0);
1217 }
1218
1219 #[test]
1220 fn transposition_reaches_octave_minus_one() {
1221 let low = Pitch::from_name("D2")
1222 .unwrap()
1223 .transpose(&Interval::from_name("m-23").unwrap())
1224 .unwrap();
1225 assert_eq!(low.name_with_octave(), "C#-1");
1226 assert_eq!(low.ps(), 1.0);
1227 }
1228
1229 #[test]
1230 fn simplify_multiple_enharmonics_matches_music21() {
1231 let names = |names: &[&str]| -> Vec<Pitch> {
1232 names
1233 .iter()
1234 .map(|n| Pitch::from_name(*n).unwrap())
1235 .collect()
1236 };
1237 let spelled = |pitches: Vec<Pitch>| -> Vec<String> {
1238 pitches.iter().map(Pitch::name_with_octave).collect()
1239 };
1240 let cases: [(&[&str], &[&str]); 11] = [
1241 (&["C#", "D-", "E"], &["C#", "C#", "E"]),
1242 (&["A#4", "C#5", "E#5"], &["A#4", "C#5", "E#5"]),
1243 (&["F#", "A#", "C#"], &["F#", "A#", "C#"]),
1244 (&["G-", "B-", "D-"], &["G-", "B-", "D-"]),
1245 (&["C", "E-", "G-", "B--"], &["C", "E-", "F#", "A"]),
1246 (&["B#", "D##", "F##"], &["B#", "E", "G"]),
1247 (&["C", "E", "G#", "B"], &["C", "E", "G#", "B"]),
1248 (&["E#", "G##", "B#"], &["E#", "G##", "B#"]),
1249 (
1250 &["C#", "E#", "G#", "B", "D#"],
1251 &["C#", "E#", "G#", "B", "D#"],
1252 ),
1253 (
1254 &["D-", "F", "A-", "C-", "E-", "G-"],
1255 &["D-", "F", "A-", "C-", "E-", "G-"],
1256 ),
1257 (&["C4", "E-4", "F#4"], &["C4", "E-4", "G-4"]),
1258 ];
1259 for (input, expected) in cases {
1260 let simplified = simplify_multiple_enharmonics(&names(input), None, None).unwrap();
1261 assert_eq!(spelled(simplified), expected, "{input:?}");
1262 }
1263 let three_flats = crate::KeySignature::new(-3);
1264 assert_eq!(
1265 spelled(
1266 simplify_multiple_enharmonics(&names(&["C#", "D-", "E"]), None, Some(three_flats))
1267 .unwrap()
1268 ),
1269 ["D-", "D-", "F-"]
1270 );
1271 let six_sharps = crate::KeySignature::new(6);
1272 assert_eq!(
1273 spelled(
1274 simplify_multiple_enharmonics(&names(&["G-", "B-", "D-"]), None, Some(six_sharps))
1275 .unwrap()
1276 ),
1277 ["F#", "A#", "C#"]
1278 );
1279 assert!(
1280 simplify_multiple_enharmonics(&[], None, None)
1281 .unwrap()
1282 .is_empty()
1283 );
1284 assert_eq!(super::dissonance_score(&[]).unwrap(), 0.0);
1285 assert!(
1286 super::dissonance_score(&names(&["C", "E", "G"])).unwrap()
1287 < super::dissonance_score(&names(&["C", "F-", "G"])).unwrap()
1288 );
1289 }
1290
1291 #[test]
1292 fn get_enharmonic_picks_the_direction_music21_does() {
1293 let cases = [
1294 ("C#4", "D-4"),
1295 ("D-4", "C#4"),
1296 ("C4", "B#3"),
1297 ("D4", "C##4"),
1298 ("G4", "F##4"),
1299 ("E4", "F-4"),
1300 ("F4", "G--4"),
1301 ("B4", "C-5"),
1302 ("A-4", "G#4"),
1303 ("F##4", "G4"),
1304 ("C--4", "B-3"),
1305 ("B#4", "C5"),
1306 ("G##4", "A4"),
1307 ];
1308 for (name, expected) in cases {
1309 let pitch = Pitch::from_name(name).unwrap();
1310 assert_eq!(
1311 pitch.get_enharmonic().unwrap().name_with_octave(),
1312 expected,
1313 "{name}"
1314 );
1315 }
1316 }
1317
1318 #[test]
1319 fn cent_shift_from_midi_matches_music21() {
1320 let cases: [(&str, FloatType, IntegerType, i32, &str); 8] = [
1321 ("C4", 0.0, 60, 0, "0"),
1322 ("C4", 20.0, 60, 20, "0"),
1323 ("C4", -20.0, 60, -20, "0"),
1324 ("C4", 60.0, 61, -40, "1"),
1325 ("C~4", 0.0, 61, -50, "0"),
1326 ("C`4", 0.0, 60, -50, "0"),
1327 ("C4", -60.0, 59, 40, "B"),
1328 ("C4", 130.0, 61, 30, "1"),
1329 ];
1330 for (name, cents, midi, shift, pitch_class) in cases {
1331 let pitch = crate::PitchOptions::new()
1332 .name(name)
1333 .microtone(cents)
1334 .build()
1335 .unwrap();
1336 assert_eq!(pitch.midi(), midi, "{name} {cents}");
1337 assert_eq!(pitch.cent_shift_from_midi(), shift, "{name} {cents}");
1338 assert_eq!(pitch.pitch_class_string(), pitch_class, "{name} {cents}");
1339 assert_eq!(pitch.implicit_octave(), 4);
1340 }
1341 assert_eq!(Pitch::from_name("G").unwrap().implicit_octave(), 4);
1342 assert_eq!(Pitch::from_name("G2").unwrap().implicit_octave(), 2);
1343 }
1344
1345 #[test]
1346 fn quarter_tones_and_microtones_convert_both_ways() {
1347 let build = |name: &str, cents: FloatType| {
1348 crate::PitchOptions::new()
1349 .name(name)
1350 .microtone(cents)
1351 .build()
1352 .unwrap()
1353 };
1354 let describe = |pitch: &Pitch| {
1355 (
1356 pitch.name_with_octave(),
1357 pitch.microtone().map_or(0.0, Microtone::cents),
1358 pitch.accidental().name().to_string(),
1359 )
1360 };
1361
1362 let to_microtones: [(&str, FloatType, &str, FloatType, &str); 6] = [
1363 ("C~4", 0.0, "C4", 50.0, "natural"),
1364 ("C`4", 0.0, "C4", -50.0, "natural"),
1365 ("D#~4", 0.0, "D#4", 50.0, "sharp"),
1366 ("E-`4", 0.0, "E-4", -50.0, "flat"),
1367 ("C~4", 10.0, "C4", 60.0, "natural"),
1368 ("C#4", 0.0, "C#4", 0.0, "sharp"),
1369 ];
1370 for (name, cents, expected_name, expected_cents, accidental) in to_microtones {
1371 let converted = build(name, cents)
1372 .convert_quarter_tones_to_microtones()
1373 .unwrap();
1374 assert_eq!(
1375 describe(&converted),
1376 (
1377 expected_name.to_string(),
1378 expected_cents,
1379 accidental.to_string()
1380 ),
1381 "{name} {cents}"
1382 );
1383 }
1384
1385 let to_quarter_tones: [(&str, FloatType, &str, FloatType, &str); 11] = [
1386 ("C4", 50.0, "C~4", 0.0, "half-sharp"),
1387 ("C4", -50.0, "C`4", 0.0, "half-flat"),
1388 ("C4", 30.0, "C~4", -20.0, "half-sharp"),
1389 ("C4", -30.0, "C`4", 20.0, "half-flat"),
1390 ("C4", 70.0, "C~4", 20.0, "half-sharp"),
1391 ("C#4", 50.0, "C#~4", 0.0, "one-and-a-half-sharp"),
1392 ("C4", 150.0, "C#4", 50.0, "sharp"),
1393 ("C4", -150.0, "C-4", -50.0, "flat"),
1394 ("C4", 120.0, "C#4", 20.0, "sharp"),
1395 ("C-4", -50.0, "C-`4", 0.0, "one-and-a-half-flat"),
1396 ("C4", 0.0, "C4", 0.0, "natural"),
1397 ];
1398 for (name, cents, expected_name, expected_cents, accidental) in to_quarter_tones {
1399 let converted = build(name, cents)
1400 .convert_microtones_to_quarter_tones()
1401 .unwrap();
1402 assert_eq!(
1403 describe(&converted),
1404 (
1405 expected_name.to_string(),
1406 expected_cents,
1407 accidental.to_string()
1408 ),
1409 "{name} {cents}"
1410 );
1411 }
1412 }
1413
1414 #[test]
1415 fn harmonic_and_fundamental_match_music21() {
1416 let cases = [
1417 ("E5", "C2", 10, 14.0, "10thH/C2(+14c)"),
1418 ("G4", "C2", 6, -2.0, "6thH/C2(-2c)"),
1419 ("B-4", "C2", 7, 31.0, "7thH/C2(+31c)"),
1420 ("F#5", "D3", 5, 14.0, "5thH/D3(+14c)"),
1421 ("C5", "C4", 2, 0.0, "2ndH/C4"),
1422 ];
1423 for (name, fundamental, number, cents, text) in cases {
1424 let pitch = Pitch::from_name(name).unwrap();
1425 let fundamental = Pitch::from_name(fundamental).unwrap();
1426 let (harmonic, retuned) = pitch
1427 .harmonic_and_fundamental_from_pitch(&fundamental)
1428 .unwrap();
1429 assert_eq!(harmonic, number, "{name}");
1430 let retuned_cents = retuned.microtone().map_or(0.0, Microtone::cents);
1431 assert!(
1432 (retuned_cents - cents).abs() < 1e-6,
1433 "{name}: {retuned_cents}"
1434 );
1435 assert_eq!(
1436 pitch
1437 .harmonic_and_fundamental_string_from_pitch(&fundamental)
1438 .unwrap(),
1439 text,
1440 "{name}"
1441 );
1442 }
1443 let c4 = Pitch::from_name("C4").unwrap();
1444 assert!(c4.harmonic_and_fundamental_from_pitch(&c4).is_err());
1445 }
1446
1447 #[test]
1448 fn full_name_matches_music21() {
1449 let cases = [
1450 ("C4", "C in octave 4"),
1451 ("E-4", "E-flat in octave 4"),
1452 ("F#", "F-sharp"),
1453 ("B--3", "B-double-flat in octave 3"),
1454 ("G##5", "G-double-sharp in octave 5"),
1455 ("A~4", "A-half-sharp in octave 4"),
1456 ("C`4", "C-half-flat in octave 4"),
1457 ("D#~4", "D-one-and-a-half-sharp in octave 4"),
1458 ];
1459 for (name, expected) in cases {
1460 assert_eq!(
1461 Pitch::from_name(name).unwrap().full_name(),
1462 expected,
1463 "{name}"
1464 );
1465 }
1466 let sharp = crate::PitchOptions::new()
1467 .name("C4")
1468 .microtone(20)
1469 .build()
1470 .unwrap();
1471 assert_eq!(sharp.full_name(), "C in octave 4 (+20c)");
1472 let flat = crate::PitchOptions::new()
1473 .name("E-4")
1474 .microtone(-33)
1475 .build()
1476 .unwrap();
1477 assert_eq!(flat.full_name(), "E-flat in octave 4 (-33c)");
1478 let hair = crate::PitchOptions::new()
1479 .name("E-4")
1480 .microtone(-0.2)
1481 .build()
1482 .unwrap();
1483 assert_eq!(hair.full_name(), "E-flat in octave 4 (-0c)");
1484 }
1485 use crate::defaults::{FloatType, IntegerType};
1486 use crate::interval::Interval;
1487 use crate::tuningsystem::TuningSystem;
1488
1489 use super::{
1490 Accidental, Microtone, Pitch, convert_harmonic_to_cents, simplify_multiple_enharmonics,
1491 };
1492
1493 #[test]
1494 fn harmonics_match_music21() {
1495 let cases = [
1496 ("C2", 1, "C2", None, 65.406),
1497 ("C2", 2, "C3", None, 130.813),
1498 ("C2", 3, "G3", Some("(+2c)"), 196.224),
1499 ("C2", 4, "C4", None, 261.626),
1500 ("C2", 5, "E4", Some("(-14c)"), 326.973),
1501 ("C2", 6, "G4", Some("(+2c)"), 392.449),
1502 ("C2", 7, "A~4", Some("(+19c)"), 457.891),
1503 ("C2", 8, "C5", None, 523.251),
1504 ("C2", 9, "D5", Some("(+4c)"), 588.688),
1505 ("C2", 11, "F~5", Some("(+1c)"), 719.338),
1506 ("C2", 13, "G#~5", Some("(-9c)"), 850.515),
1507 ("A2", 3, "E4", Some("(+2c)"), 330.009),
1508 ("A2", 5, "C#5", Some("(-14c)"), 549.9),
1509 ("C", 3, "G5", Some("(+2c)"), 784.897),
1510 ("E-3", 7, "C~6", Some("(+19c)"), 1089.054),
1511 ];
1512 for (fundamental, number, name, microtone, hertz) in cases {
1513 let harmonic = Pitch::from_name(fundamental)
1514 .unwrap()
1515 .harmonic(number)
1516 .unwrap();
1517 assert_eq!(harmonic.name_with_octave(), name, "{fundamental} {number}");
1518 assert_eq!(
1519 harmonic.microtone().map(ToString::to_string).as_deref(),
1520 microtone,
1521 "{fundamental} {number}"
1522 );
1523 assert!(
1524 (harmonic.frequency_hz() - hertz).abs() < 5e-4,
1525 "{fundamental} {number}: {}",
1526 harmonic.frequency_hz()
1527 );
1528 assert_eq!(
1529 harmonic
1530 .fundamental()
1531 .map(Pitch::name_with_octave)
1532 .as_deref(),
1533 (number > 1).then_some(fundamental),
1534 "{fundamental} {number}"
1535 );
1536 }
1537 }
1538
1539 #[test]
1540 fn harmonic_from_fundamental_matches_music21() {
1541 let cases = [
1542 ("G4", "C2", 6, 2.0, "6thH(-2c)/C2"),
1543 ("E5", "C2", 10, -14.0, "10thH(+14c)/C2"),
1544 ("B-4", "C2", 7, -31.0, "7thH(+31c)/C2"),
1545 ("F#5", "C2", 11, -49.0, "11thH(+49c)/C2"),
1546 ("C4", "C2", 4, 0.0, "4thH/C2"),
1547 ("E4", "A2", 3, 2.0, "3rdH(-2c)/A2"),
1548 ("G#4", "A2", 4, 100.0, "4thH(-100c)/A2"),
1549 ("A4", "A2", 4, 0.0, "4thH/A2"),
1550 ("B3", "C2", 4, 100.0, "4thH(-100c)/C2"),
1551 ("D5", "C2", 9, 4.0, "9thH(-4c)/C2"),
1552 ];
1553 for (target, fundamental, number, cents, text) in cases {
1554 let target_pitch = Pitch::from_name(target).unwrap();
1555 let fundamental_pitch = Pitch::from_name(fundamental).unwrap();
1556 let (found, gap) = target_pitch
1557 .harmonic_from_fundamental(&fundamental_pitch)
1558 .unwrap();
1559 assert_eq!(found, number, "{target} over {fundamental}");
1560 assert!(
1561 (gap - cents).abs() < 1e-6,
1562 "{target} over {fundamental}: {gap}"
1563 );
1564 assert_eq!(
1565 target_pitch
1566 .harmonic_string(Some(&fundamental_pitch))
1567 .unwrap(),
1568 text
1569 );
1570 }
1571 let c2 = Pitch::from_name("C2").unwrap();
1572 assert!(
1573 c2.harmonic_from_fundamental(&Pitch::from_name("C4").unwrap())
1574 .is_err()
1575 );
1576 assert!(c2.harmonic_string(None).is_err());
1577 assert_eq!(
1578 c2.harmonic(5).unwrap().harmonic_string(None).unwrap(),
1579 "5thH/C2"
1580 );
1581 }
1582
1583 #[test]
1584 fn enharmonic_helpers_match_music21() {
1585 let names = |pitches: Vec<Pitch>| {
1586 pitches
1587 .iter()
1588 .map(Pitch::name_with_octave)
1589 .collect::<Vec<_>>()
1590 };
1591 let cases = [
1592 (
1593 "C#4",
1594 vec!["D-4", "B##3"],
1595 vec!["D-4"],
1596 vec!["D-4", "E---4", "B##3"],
1597 ),
1598 (
1599 "E-",
1600 vec!["F--", "D#"],
1601 vec!["D#"],
1602 vec!["F--", "D#", "C###"],
1603 ),
1604 ("B#3", vec!["C4"], vec!["C4"], vec!["C4", "A###3"]),
1605 ("F##4", vec!["G4"], vec!["G4"], vec!["G4", "E###4"]),
1606 (
1607 "C4",
1608 vec!["D--4", "B#3"],
1609 vec!["B#3"],
1610 vec!["D--4", "B#3", "A###3"],
1611 ),
1612 (
1613 "G-2",
1614 vec!["F#2", "E##2"],
1615 vec!["F#2"],
1616 vec!["A---2", "F#2", "E##2"],
1617 ),
1618 (
1619 "A4",
1620 vec!["B--4", "G##4"],
1621 vec![],
1622 vec!["B--4", "C---5", "G##4"],
1623 ),
1624 (
1625 "B-4",
1626 vec!["C--5", "A#4"],
1627 vec!["A#4"],
1628 vec!["C--5", "A#4", "G###4"],
1629 ),
1630 ("E#5", vec!["F5"], vec!["F5"], vec!["F5", "D###5"]),
1631 ("D--4", vec!["C4"], vec!["C4"], vec!["C4"]),
1632 ];
1633 for (name, limit_two, limit_one, limit_three) in cases {
1634 let pitch = Pitch::from_name(name).unwrap();
1635 assert_eq!(names(pitch.all_common_enharmonics(2)), limit_two, "{name}");
1636 assert_eq!(names(pitch.all_common_enharmonics(1)), limit_one, "{name}");
1637 assert_eq!(
1638 names(pitch.all_common_enharmonics(3)),
1639 limit_three,
1640 "{name}"
1641 );
1642 }
1643
1644 let enharmonic = |left: &str, right: &str| {
1645 Pitch::from_name(left)
1646 .unwrap()
1647 .is_enharmonic(&Pitch::from_name(right).unwrap())
1648 };
1649 assert!(enharmonic("C#4", "D-4"));
1650 assert!(!enharmonic("C#4", "D-5"));
1651 assert!(enharmonic("C#", "D-5"));
1652 assert!(!enharmonic("C#4", "C4"));
1653 assert!(enharmonic("B#3", "C4"));
1654 assert!(enharmonic("B#", "C"));
1655 assert!(enharmonic("C", "C5"));
1656 }
1657
1658 #[test]
1659 fn transposing_to_a_target_matches_music21() {
1660 let g3 = Pitch::from_name("G3").unwrap();
1661 let below = [
1662 ("C4", false, "C3"),
1663 ("C4", true, "C3"),
1664 ("C6", false, "C3"),
1665 ("C6", true, "C3"),
1666 ("G3", false, "G3"),
1667 ("C2", false, "C2"),
1668 ("C2", true, "C3"),
1669 ("F4", true, "F3"),
1670 ("G4", true, "G3"),
1671 ];
1672 for (source, minimize, expected) in below {
1673 let moved = Pitch::from_name(source)
1674 .unwrap()
1675 .transpose_below_target(&g3, minimize)
1676 .unwrap();
1677 assert_eq!(
1678 moved.name_with_octave(),
1679 expected,
1680 "{source} below {minimize}"
1681 );
1682 }
1683 let above = [
1684 ("C4", false, "C4"),
1685 ("C1", false, "C4"),
1686 ("C1", true, "C4"),
1687 ("C6", false, "C6"),
1688 ("C6", true, "C4"),
1689 ("G3", false, "G3"),
1690 ("F#3", true, "F#4"),
1691 ];
1692 for (source, minimize, expected) in above {
1693 let moved = Pitch::from_name(source)
1694 .unwrap()
1695 .transpose_above_target(&g3, minimize)
1696 .unwrap();
1697 assert_eq!(
1698 moved.name_with_octave(),
1699 expected,
1700 "{source} above {minimize}"
1701 );
1702 }
1703 assert!(
1704 Pitch::from_name("C")
1705 .unwrap()
1706 .transpose_below_target(&g3, false)
1707 .is_err()
1708 );
1709 }
1710
1711 #[test]
1712 fn twelve_tone_and_frequency_construction() {
1713 assert!(Pitch::from_name("C4").unwrap().is_twelve_tone());
1714 assert!(Pitch::from_name("C#4").unwrap().is_twelve_tone());
1715 assert!(!Pitch::from_name("C~4").unwrap().is_twelve_tone());
1716 assert!(!Pitch::from_name("C`4").unwrap().is_twelve_tone());
1717 let detuned = Pitch::builder().name("A4").microtone(25).build().unwrap();
1718 assert!(!detuned.is_twelve_tone());
1719 assert_eq!(detuned.ps(), 69.25);
1720
1721 let g4 =
1722 Pitch::from_frequency(440.0 * (2.0 as FloatType).powf((67.02 - 69.0) / 12.0)).unwrap();
1723 assert_eq!(g4.name_with_octave(), "G4");
1724 assert_eq!(g4.microtone().unwrap().to_string(), "(+2c)");
1725 assert!((g4.ps() - 67.02).abs() < 1e-6);
1726
1727 let low = Pitch::from_frequency(100.0).unwrap();
1728 assert_eq!(low.name_with_octave(), "G~2");
1729 assert_eq!(low.microtone().unwrap().to_string(), "(-15c)");
1730 assert!(Pitch::from_frequency(0.0).is_err());
1731 assert_eq!(Pitch::from_name("C4").unwrap().diatonic_note_number(), 29);
1732 assert_eq!(Pitch::from_name("B#3").unwrap().diatonic_note_number(), 28);
1733 }
1734
1735 #[test]
1736 fn language_names_match_music21() {
1737 let cases = [
1738 ("C", "C", "do", "do", "do", "C"),
1739 ("C#", "Cis", "do diesis", "do dièse", "do sostenido", "C♯"),
1740 ("D-", "Des", "re bemolle", "ré bémol", "re bemol", "D♭"),
1741 ("B", "H", "si", "si", "si", "B"),
1742 ("B-", "B", "si bemolle", "si bémol", "si bemol", "B♭"),
1743 ("B#", "His", "si diesis", "si dièse", "si sostenido", "B♯"),
1744 ("E-", "Es", "mi bemolle", "mi bémol", "mi bemol", "E♭"),
1745 ("A-", "As", "la bemolle", "la bémol", "la bemol", "A♭"),
1746 (
1747 "F##",
1748 "Fisis",
1749 "fa doppio diesis",
1750 "fa double dièse",
1751 "fa doble sostenido",
1752 "F𝄪",
1753 ),
1754 (
1755 "G--",
1756 "Geses",
1757 "sol doppio bemolle",
1758 "sol double bémol",
1759 "sol doble bemol",
1760 "G𝄫",
1761 ),
1762 (
1763 "B--",
1764 "Heses",
1765 "si doppio bemolle",
1766 "si double bémol",
1767 "si doble bemol",
1768 "B𝄫",
1769 ),
1770 ("F-", "Fes", "fa bemolle", "fa bémol", "fa bemol", "F♭"),
1771 ("E#", "Eis", "mi diesis", "mi dièse", "mi sostenido", "E♯"),
1772 (
1773 "D##",
1774 "Disis",
1775 "re doppio diesis",
1776 "ré double dièse",
1777 "re doble sostenido",
1778 "D𝄪",
1779 ),
1780 (
1781 "A####",
1782 "Aisisisis",
1783 "la quadruplo diesis",
1784 "la quadruple dièse",
1785 "la cuádruple sostenido",
1786 "A𝄪𝄪",
1787 ),
1788 (
1789 "B----",
1790 "Heseseses",
1791 "si quadruplo bemolle",
1792 "si quadruple bémol",
1793 "si cuádruple bemol",
1794 "B𝄫𝄫",
1795 ),
1796 ];
1797 for (name, german, italian, french, spanish, unicode) in cases {
1798 let pitch = Pitch::from_name(name).unwrap();
1799 assert_eq!(pitch.german().unwrap(), german, "{name}");
1800 assert_eq!(pitch.italian().unwrap(), italian, "{name}");
1801 assert_eq!(pitch.french().unwrap(), french, "{name}");
1802 assert_eq!(pitch.spanish().unwrap(), spanish, "{name}");
1803 assert_eq!(pitch.unicode_name(), unicode, "{name}");
1804 }
1805 assert_eq!(
1806 Pitch::from_name("A#4").unwrap().unicode_name_with_octave(),
1807 "A♯4"
1808 );
1809 assert_eq!(Pitch::from_name("E-5").unwrap().german().unwrap(), "Es");
1810 let quarter_sharp = Pitch::from_name("C~").unwrap();
1811 assert!(quarter_sharp.german().is_err());
1812 assert!(quarter_sharp.italian().is_err());
1813 assert!(quarter_sharp.french().is_err());
1814 assert!(quarter_sharp.spanish().is_err());
1815 }
1816
1817 #[test]
1818 fn fundamental_round_trips_through_the_builder() {
1819 let pitch = Pitch::builder()
1820 .name("E4")
1821 .fundamental(Pitch::from_name("C2").unwrap())
1822 .build()
1823 .unwrap();
1824 assert_eq!(
1825 pitch.fundamental().map(Pitch::name_with_octave),
1826 Some("C2".to_string())
1827 );
1828 assert_eq!(Pitch::from_name("E4").unwrap().fundamental(), None);
1829 }
1830
1831 #[test]
1832 fn simplify_multiple_enharmonics_test() {
1833 let more_than_five = vec![
1834 Pitch::from_number(0.0).unwrap(),
1835 Pitch::from_number(1.0).unwrap(),
1836 Pitch::from_number(2.0).unwrap(),
1837 Pitch::from_number(3.0).unwrap(),
1838 Pitch::from_number(4.0).unwrap(),
1839 Pitch::from_number(5.0).unwrap(),
1840 Pitch::from_number(12.0).unwrap(),
1841 Pitch::from_number(13.0).unwrap(),
1842 ];
1843
1844 let _x = simplify_multiple_enharmonics(&more_than_five, None, None);
1845 let _less_than_five = [
1846 Pitch::from_number(0.0),
1847 Pitch::from_number(1.0),
1848 Pitch::from_number(2.0),
1849 Pitch::from_number(12.0),
1850 Pitch::from_number(13.0),
1851 ];
1852 }
1853
1854 #[test]
1855 fn test_convert_harmonic_to_cents_values() {
1856 assert_eq!(convert_harmonic_to_cents(8), 3600);
1857 assert_eq!(convert_harmonic_to_cents(5), 2786);
1858 assert_eq!(convert_harmonic_to_cents(-2), -1200);
1859 }
1860
1861 #[test]
1862 fn test_pitch_transpose_interval() {
1863 let c4 = Pitch::from_name("C4").unwrap();
1864 let m3 = Interval::from_name("m3").unwrap();
1865 let out = c4.transpose(&m3).unwrap();
1866 assert_eq!(out.name_with_octave(), "E-4");
1867 }
1868
1869 #[test]
1870 fn test_pitch_frequency_helpers() {
1871 let a4 = Pitch::from_name("A4").unwrap();
1872 assert!((a4.frequency_hz() - 440.0).abs() < 0.0001);
1873
1874 let e4 = Pitch::from_name("E4").unwrap();
1875 assert!((e4.frequency_hz_in(TuningSystem::FiveLimit) - 327.032).abs() < 0.001);
1876 assert!(e4.frequency_hz_in(TuningSystem::FiveLimit) < e4.frequency_hz());
1877 }
1878
1879 #[test]
1880 fn pitch_exposes_accidental_object() {
1881 let custom_accidental = Accidental::new("half-flat").unwrap();
1882 let pitch = Pitch::builder()
1883 .step('D')
1884 .accidental(custom_accidental.clone())
1885 .octave(4)
1886 .build()
1887 .unwrap();
1888
1889 assert_eq!(pitch.name_with_octave(), "D`4");
1890 assert_eq!(pitch.accidental(), &custom_accidental);
1891 assert_eq!(pitch.accidental().name(), "half-flat");
1892 assert_eq!(pitch.accidental().alter(), -0.5);
1893 }
1894
1895 #[test]
1896 fn pitch_exposes_microtone_object() {
1897 let microtone = Microtone::new(-25.0).unwrap();
1898 let pitch = Pitch::builder()
1899 .name("G#4")
1900 .microtone(microtone.clone())
1901 .build()
1902 .unwrap();
1903
1904 assert_eq!(pitch.microtone(), Some(µtone));
1905 assert_eq!(pitch.microtone().unwrap().to_string(), "(-25c)");
1906 assert_eq!(pitch.alter(), 0.75);
1907 }
1908
1909 #[test]
1910 fn pitch_supports_rust_conversion_traits() {
1911 let parsed: Pitch = "C#4".parse().unwrap();
1912 assert_eq!(parsed.to_string(), "C#4");
1913
1914 let midi = Pitch::try_from(60 as IntegerType).unwrap();
1915 assert_eq!(midi.name_with_octave(), "C4");
1916 assert_eq!(midi.midi(), 60);
1917
1918 let pitch_space = Pitch::try_from(61.5).unwrap();
1919 assert_eq!(pitch_space.pitch_space(), 61.5);
1920 assert_eq!(pitch_space.midi(), 62);
1921
1922 let built = Pitch::builder().pitch_space(60.0).build().unwrap();
1923 assert_eq!(built.name_with_octave(), "C4");
1924 }
1925
1926 #[test]
1927 fn pitch_exposes_enharmonic_helpers() {
1928 let c_sharp = Pitch::from_name("C#3").unwrap();
1929 let out = c_sharp.get_higher_enharmonic().unwrap();
1930 assert_eq!(out.name_with_octave(), "D-3");
1931
1932 let mut d_flat = out;
1933 d_flat.get_lower_enharmonic_in_place().unwrap();
1934 assert_eq!(d_flat.name_with_octave(), "C#3");
1935
1936 let d_sharp = Pitch::from_name("D#4").unwrap();
1937 assert_eq!(
1938 d_sharp
1939 .simplify_enharmonic(true)
1940 .unwrap()
1941 .name_with_octave(),
1942 "E-4"
1943 );
1944 }
1945}