Skip to main content

music21_rs/pitch/
mod.rs

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))]
47/// Input accepted as a pitch name or pitch-space number.
48pub enum PitchName {
49    /// A written pitch name such as `"C#4"` or `"E-"`.
50    Name(String),
51    /// A pitch-space number, where 60 corresponds to middle C.
52    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))]
81/// Builder options for constructing a [`Pitch`].
82pub struct PitchOptions {
83    /// Pitch name or pitch-space number.
84    pub name: Option<PitchName>,
85    /// Diatonic step name.
86    pub step: Option<char>,
87    /// Octave number.
88    pub octave: Octave,
89    /// Accidental name or alteration.
90    pub accidental: Option<AccidentalSpecifier>,
91    /// Microtone cent offset.
92    pub microtone: Option<MicrotoneSpecifier>,
93    /// Pitch class to realize as a pitch.
94    pub pitch_class: Option<PitchClassSpecifier>,
95    /// MIDI note number.
96    pub midi: Option<IntegerType>,
97    /// Pitch-space value.
98    pub ps: Option<FloatType>,
99    /// Fundamental pitch used for harmonic construction.
100    pub fundamental: Option<Pitch>,
101}
102
103impl PitchOptions {
104    /// Creates an empty pitch builder.
105    pub fn new() -> Self {
106        Self::default()
107    }
108
109    /// Sets the pitch name or pitch-space number.
110    pub fn name(mut self, name: impl Into<PitchName>) -> Self {
111        self.name = Some(name.into());
112        self
113    }
114
115    /// Sets the diatonic step.
116    pub fn step(mut self, step: char) -> Self {
117        self.step = Some(step);
118        self
119    }
120
121    /// Sets the octave.
122    pub fn octave(mut self, octave: IntegerType) -> Self {
123        self.octave = Some(octave);
124        self
125    }
126
127    /// Sets the accidental.
128    pub fn accidental(mut self, accidental: impl Into<AccidentalSpecifier>) -> Self {
129        self.accidental = Some(accidental.into());
130        self
131    }
132
133    /// Sets the microtone.
134    pub fn microtone(mut self, microtone: impl Into<MicrotoneSpecifier>) -> Self {
135        self.microtone = Some(microtone.into());
136        self
137    }
138
139    /// Sets the pitch class.
140    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    /// Sets the MIDI note number.
146    pub fn midi(mut self, midi: IntegerType) -> Self {
147        self.midi = Some(midi);
148        self
149    }
150
151    /// Sets the pitch-space value.
152    pub fn ps(mut self, ps: FloatType) -> Self {
153        self.ps = Some(ps);
154        self
155    }
156
157    /// Sets the pitch-space value.
158    pub fn pitch_space(mut self, pitch_space: FloatType) -> Self {
159        self.ps = Some(pitch_space);
160        self
161    }
162
163    /// Sets the fundamental pitch.
164    pub fn fundamental(mut self, fundamental: Pitch) -> Self {
165        self.fundamental = Some(fundamental);
166        self
167    }
168
169    /// Builds a [`Pitch`] from the collected options.
170    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/// A musical pitch with spelling, octave, accidental and optional microtone.
178#[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    /// Builds a pitch from [`PitchOptions`].
257    ///
258    /// This is the port of music21's keyword-argument `Pitch.__init__`: a
259    /// `name` wins over an explicit `step`, and `octave`, `accidental`,
260    /// `microtone`, `pitch_class`, `midi` and `ps` are applied afterwards
261    /// in that order.
262    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    /// Creates a [`PitchOptions`] builder.
356    pub fn builder() -> PitchOptions {
357        PitchOptions::new()
358    }
359
360    /// Builds a pitch from a name such as `"C#4"` or `"E-"`.
361    pub fn from_name(name: impl Into<String>) -> Result<Self> {
362        PitchOptions::new().name(name.into()).build()
363    }
364
365    /// Builds a pitch from a pitch-space number.
366    pub fn from_number(number: FloatType) -> Result<Self> {
367        PitchOptions::new().name(PitchName::Number(number)).build()
368    }
369
370    /// Builds a pitch from a diatonic step.
371    pub fn from_step(step: char) -> Result<Self> {
372        PitchOptions::new().step(step).build()
373    }
374
375    /// Builds a pitch from a pitch name and explicit octave.
376    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    /// Builds a pitch from a pitch class.
381    pub fn from_pitch_class(pitch_class: impl Into<PitchClassSpecifier>) -> Result<Self> {
382        PitchOptions::new().pitch_class(pitch_class).build()
383    }
384
385    /// Builds a pitch from a MIDI note number.
386    pub fn from_midi(midi: IntegerType) -> Result<Self> {
387        PitchOptions::new().midi(midi).build()
388    }
389
390    /// Builds a pitch from a pitch-space value.
391    pub fn from_pitch_space(ps: FloatType) -> Result<Self> {
392        PitchOptions::new().ps(ps).build()
393    }
394
395    /// Returns the pitch name with the octave suffix when one is set.
396    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    /// Returns the pitch name without octave, such as `"F#"` or `"B-"`.
404    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    /// Returns the total semitone alteration from the natural step.
451    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    /// Returns this pitch's accidental object.
464    ///
465    /// Unlike Python music21, this crate stores an explicit natural accidental
466    /// for natural pitches.
467    pub fn accidental(&self) -> &Accidental {
468        &self.accidental
469    }
470
471    /// Returns this pitch's microtone adjustment, when present.
472    pub fn microtone(&self) -> Option<&Microtone> {
473        self.microtone.as_ref()
474    }
475
476    /// Returns this pitch's normalized pitch class.
477    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    /// Puts this pitch in an octave, or in none at all: music21's settable
484    /// `octave`, which only moves the pitch and does not respell it.
485    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    /// Returns this pitch transposed by the interval, as music21's
494    /// `Pitch.transpose` does: a pitch whose spelling was inferred from a
495    /// number is respelled to its most common enharmonic afterwards, one
496    /// spelled explicitly keeps its accidentals.
497    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    /// Returns the pitch-space value for this pitch.
514    pub fn ps(&self) -> FloatType {
515        self.pitch_space()
516    }
517
518    /// Returns the pitch-space value for this pitch.
519    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    /// Returns the MIDI note number the way music21's `midi` reports it: the
525    /// pitch space rounded half up, then folded into 0 to 127 by octaves, so
526    /// a pitch above the MIDI range reports its highest in-range octave and
527    /// one below it its lowest.
528    pub fn midi(&self) -> IntegerType {
529        normalize_midi((self.pitch_space() + 0.5).floor() as IntegerType)
530    }
531
532    /// Returns this pitch's twelve-tone equal-temperament frequency in hertz.
533    pub fn frequency_hz(&self) -> FloatType {
534        440.0 * (2.0 as FloatType).powf((self.pitch_space() - 69.0) / 12.0)
535    }
536
537    /// Returns this pitch's frequency in hertz for a supported tuning system.
538    ///
539    /// The pitch-space value is used as the tuning-system degree index, so this
540    /// is most musically meaningful for twelve-tone systems.
541    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    /// Moves the pitch to a staff position by diatonic note number, keeping
551    /// its accidental and microtone: music21's `diatonicNoteNum` setter.
552    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    /// Replaces the accidental, a natural when `None` is given.
560    pub(crate) fn set_accidental_or_natural(&mut self, accidental: Option<Accidental>) {
561        self.set_accidental(accidental);
562    }
563
564    /// Sets or removes the accidental the way music21's `accidental` setter
565    /// does: `None` leaves the pitch with no accidental object, which
566    /// [`Self::accidental`] still reports as a natural.
567    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    /// Sets the accidental from a semitone alteration the way music21's
578    /// setter does with a float: the part that is a quarter-tone or larger
579    /// becomes the accidental and the rest a microtone, so `-1.5` is a flat
580    /// with fifty cents down.
581    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    /// Whether the pitch carries an accidental object at all. music21 keeps
591    /// none on a pitch spelled with a bare letter (`D`) or built from a
592    /// number that needs none, and an explicit natural (`Dn`) is one, so
593    /// `D` and `Dn` differ here while [`Self::accidental`] answers a natural
594    /// for both.
595    pub fn has_accidental(&self) -> bool {
596        self.has_accidental
597    }
598
599    /// music21's `spellingIsInferred`: whether the crate chose the spelling
600    /// rather than being told it. A pitch built from a number, a MIDI value,
601    /// a pitch class or a frequency has an inferred spelling, and only such
602    /// a pitch is respelled by a transposition.
603    pub fn spelling_is_inferred(&self) -> bool {
604        self.spelling_is_inferred
605    }
606
607    /// Says whether the spelling was chosen or given.
608    pub fn set_spelling_is_inferred(&mut self, inferred: bool) {
609        self.spelling_is_inferred = inferred;
610    }
611
612    /// The accidental object, if the pitch carries one; see
613    /// [`Self::has_accidental`].
614    pub fn explicit_accidental(&self) -> Option<&Accidental> {
615        self.has_accidental.then_some(&self.accidental)
616    }
617
618    /// The accidental object for editing in place, if the pitch carries one.
619    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    /// Sets the microtone from a cent shift, removing it when the shift is
629    /// zero.
630    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    /// Returns the fundamental this pitch was built against, when one was set.
661    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    /// Returns the octave, or music21's default of 4 when none is set.
686    ///
687    /// This is what music21's `.octave` answers, which is always an `int`.
688    /// [`Self::octave`] keeps the `Option`, which is music21's own `_octave`
689    /// and says strictly more; [`Self::octave_is_implicit`] tells the two
690    /// apart.
691    pub fn implicit_octave(&self) -> IntegerType {
692        self.octave.unwrap_or(PITCH_OCTAVE as IntegerType)
693    }
694
695    /// Whether this pitch was never given an octave, so it stands for its
696    /// pitch class in any octave: music21's `octaveIsImplicit`.
697    ///
698    /// Such a pitch prints without an octave number and reports the default
699    /// octave from [`Self::implicit_octave`].
700    #[must_use]
701    pub fn octave_is_implicit(&self) -> bool {
702        self.octave.is_none()
703    }
704
705    /// Makes the octave implicit or explicit: the setter of music21's
706    /// `octaveIsImplicit`.
707    ///
708    /// Making it explicit puts the pitch in the default octave, as music21
709    /// does; making it implicit takes the octave away. Setting it to what it
710    /// already is does nothing.
711    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    /// Returns the pitch class as music21's `pitchClassString`, one
723    /// character with `A` and `B` for ten and eleven. Like music21's integer
724    /// `pitchClass` it rounds a microtone away with Python's round-half-to-even,
725    /// so `C` with `+20c` is `0` where [`Self::pitch_class`] would say `0.2`,
726    /// and a half-sharp C is `0` even though its MIDI number rounds up to 61.
727    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    /// Returns how many cents the pitch sits from the nearest MIDI note,
734    /// rounded to a whole cent: music21's `getCentShiftFromMidi`, so a
735    /// half-sharp C reads `-50` because it rounds up to C-sharp.
736    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    /// Builds a pitch from a frequency in hertz, spelled in twelve-tone equal
748    /// temperament at A4 = 440 with any remainder as a microtone.
749    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    /// Returns music21's `diatonicNoteNum`: the staff position counting `C0`
761    /// as `1`, with the implicit octave standing in when none is set.
762    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    /// Returns whether this pitch lies on the twelve-tone grid: no quarter
768    /// tone accidental and no microtone.
769    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    /// Returns the stored octave.
792    ///
793    /// Returns `None` when the pitch was created without an explicit octave,
794    /// such as `Pitch::from_name("C")`. In calculations, octave-less pitches
795    /// use the library default octave.
796    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        // A quarter of a semitone exactly is *not* a quarter tone: music21
862        // writes `F` plus twenty-five cents rather than an F half-sharp
863        // twenty-five cents flat, and the bound it draws is exclusive.
864        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(&micro) {
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    /// A number that is not a number spells no pitch. Left to the conversion
921    /// it became a C, because the cast that reads a step off pitch space
922    /// answers nought for anything it cannot represent.
923    #[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    /// music21's `updateAccidentalDisplay` on its simplest shapes: a repeat
1018    /// of a written accidental in the bar is not written again, a natural
1019    /// after an accidental in the bar is a caution, and a note the key
1020    /// signature already alters needs nothing.
1021    #[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    /// music21's `.octave` always answers a number, and `.octaveIsImplicit`
1055    /// says whether one was ever given.
1056    #[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        // Making it explicit puts the pitch in the default octave; making it
1071        // implicit again takes the octave away.
1072        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        // Setting it to what it already is leaves the octave alone.
1079        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        // music21's own example: a semitone above F is F# in D major and
1088        // G- in B-flat minor, because the signature spells that note.
1089        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        // A pitch the signature says nothing about is left alone, and so is
1102        // one carrying no accidental at all.
1103        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(&microtone));
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}