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::key::keysignature::KeySignature;
27use crate::stepname::StepName;
28use crate::tuningsystem::TuningSystem;
29
30pub use accidental::{Accidental, AccidentalAttribute, AccidentalSpecifier};
31pub use microtone::{Microtone, MicrotoneSpecifier};
32use pitchclass::convert_ps_to_oct;
33pub use pitchclass::{PitchClass, PitchClassSpecifier, convert_pitch_class_to_str};
34
35use itertools::Itertools;
36use num::Num;
37use num_traits::ToPrimitive;
38use ordered_float::OrderedFloat;
39use std::cmp::Ordering;
40use std::fmt::{Display, Formatter};
41use std::hash::{Hash, Hasher};
42use std::str::FromStr;
43use std::sync::{Arc, LazyLock};
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    /// The accidental written, if any: a bare `D` has none and `Dn` a
183    /// natural, as music21 keeps them.
184    accidental: Option<Accidental>,
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.microtone == other.microtone
197    }
198}
199
200impl Eq for Pitch {}
201
202/// Hashes what equality compares, so pitches can key a map or fill a set:
203/// `Bb4` twice is one entry, `Bb4` and `A#4` two.
204impl Hash for Pitch {
205    fn hash<H: Hasher>(&self, state: &mut H) {
206        self.step.hash(state);
207        self.octave.hash(state);
208        self.accidental.hash(state);
209        self.microtone.hash(state);
210    }
211}
212
213impl FromStr for Pitch {
214    type Err = Error;
215
216    fn from_str(value: &str) -> Result<Self> {
217        Self::from_name(value)
218    }
219}
220
221impl TryFrom<&str> for Pitch {
222    type Error = Error;
223
224    fn try_from(value: &str) -> Result<Self> {
225        Self::from_name(value)
226    }
227}
228
229impl TryFrom<String> for Pitch {
230    type Error = Error;
231
232    fn try_from(value: String) -> Result<Self> {
233        Self::from_name(value)
234    }
235}
236
237impl TryFrom<&Pitch> for Pitch {
238    type Error = Error;
239
240    fn try_from(value: &Pitch) -> Result<Self> {
241        Ok(value.clone())
242    }
243}
244
245impl TryFrom<IntegerType> for Pitch {
246    type Error = Error;
247
248    fn try_from(value: IntegerType) -> Result<Self> {
249        Self::from_midi(value)
250    }
251}
252
253impl TryFrom<FloatType> for Pitch {
254    type Error = Error;
255
256    fn try_from(value: FloatType) -> Result<Self> {
257        Self::from_pitch_space(value)
258    }
259}
260
261impl Display for Pitch {
262    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
263        write!(f, "{}", self.name_with_octave())
264    }
265}
266
267impl Pitch {
268    /// Builds a pitch from [`PitchOptions`].
269    ///
270    /// This is the port of music21's keyword-argument `Pitch.__init__`: a
271    /// `name` wins over an explicit `step`, and `octave`, `accidental`,
272    /// `microtone`, `pitch_class`, `midi` and `ps` are applied afterwards
273    /// in that order.
274    pub fn from_options(options: PitchOptions) -> Result<Self> {
275        let PitchOptions {
276            name,
277            step,
278            octave,
279            accidental,
280            microtone,
281            pitch_class,
282            midi,
283            ps,
284            fundamental,
285        } = options;
286        let explicit_step = step.map(StepName::try_from).transpose()?;
287        let has_explicit_octave = octave.is_some();
288        let has_explicit_accidental = accidental.is_some();
289
290        if let Some(PitchName::Number(number)) = &name
291            && !number.is_finite()
292        {
293            return Err(Error::Pitch(format!(
294                "a pitch-space number must be finite, got {number}"
295            )));
296        }
297        let parsed = name.map(PitchParameters::from).unwrap_or_default();
298        let name = parsed.name;
299        let step = if name.is_some() || parsed.step.is_some() {
300            parsed.step
301        } else {
302            explicit_step
303        }
304        .unwrap_or(PITCH_STEP);
305        let octave = octave.or(parsed.octave);
306        let accidental = match accidental {
307            Some(accidental) => Some(Accidental::new(accidental)?),
308            None => parsed.accidental,
309        };
310        let microtone = match microtone {
311            Some(microtone) => Some(Microtone::new(microtone)?),
312            None => parsed.microtone,
313        };
314
315        let explicit_accidental = accidental.clone().filter(|_| has_explicit_accidental);
316        let mut pitch = Pitch {
317            step,
318            accidental,
319            microtone,
320            octave,
321            spelling_is_inferred: parsed.spelling_is_inferred,
322            fundamental: None,
323        };
324
325        if let Some(name) = &name {
326            pitch.name_setter(name)?;
327            pitch.spelling_is_inferred = parsed.spelling_is_inferred;
328        }
329        if explicit_step.is_some() || name.is_none() {
330            pitch.step_setter(step);
331            pitch.spelling_is_inferred = parsed.spelling_is_inferred;
332        }
333        if has_explicit_octave || name.is_none() {
334            pitch.octave_setter(octave);
335        }
336        if let Some(accidental) = explicit_accidental {
337            pitch.accidental_setter(accidental);
338        }
339        if let Some(microtone) = pitch.microtone.clone() {
340            pitch.microtone_setter(microtone);
341        }
342        if let Some(pitch_class) = pitch_class {
343            pitch.pitch_class_setter(pitch_class)?;
344        }
345        if let Some(fundamental) = fundamental {
346            pitch.fundamental_setter(fundamental);
347        }
348        if let Some(midi) = midi {
349            pitch.midi_setter(midi);
350        }
351        if let Some(ps) = ps {
352            if !ps.is_finite() {
353                return Err(Error::Pitch(format!(
354                    "a pitch-space number must be finite, got {ps}"
355                )));
356            }
357            pitch.ps_setter(ps);
358        }
359
360        Ok(pitch)
361    }
362
363    /// Creates a [`PitchOptions`] builder.
364    pub fn builder() -> PitchOptions {
365        PitchOptions::new()
366    }
367
368    /// Builds a pitch from a name such as `"C#4"` or `"E-"`.
369    pub fn from_name(name: impl AsRef<str>) -> Result<Self> {
370        // A name says the whole pitch, so it is read straight in. Going
371        // through the options leaves every step after the name with nothing
372        // to do, and a pitch is built from a name everywhere.
373        let mut pitch = Self {
374            step: PITCH_STEP,
375            accidental: None,
376            microtone: None,
377            octave: None,
378            spelling_is_inferred: false,
379            fundamental: None,
380        };
381        pitch.name_setter(name.as_ref())?;
382        // A name is a spelling somebody wrote, whatever the step setter made
383        // of it on the way through.
384        pitch.spelling_is_inferred = false;
385        Ok(pitch)
386    }
387
388    /// Builds a pitch from a pitch-space number.
389    pub fn from_number(number: FloatType) -> Result<Self> {
390        PitchOptions::new().name(PitchName::Number(number)).build()
391    }
392
393    /// Builds a pitch from a diatonic step.
394    pub fn from_step(step: char) -> Result<Self> {
395        PitchOptions::new().step(step).build()
396    }
397
398    /// Builds a pitch from a pitch name and explicit octave.
399    pub fn from_name_and_octave(name: impl Into<String>, octave: IntegerType) -> Result<Self> {
400        PitchOptions::new().name(name.into()).octave(octave).build()
401    }
402
403    /// Builds a pitch from a pitch class.
404    pub fn from_pitch_class(pitch_class: impl Into<PitchClassSpecifier>) -> Result<Self> {
405        PitchOptions::new().pitch_class(pitch_class).build()
406    }
407
408    /// Builds a pitch from a MIDI note number.
409    pub fn from_midi(midi: IntegerType) -> Result<Self> {
410        PitchOptions::new().midi(midi).build()
411    }
412
413    /// Builds a pitch from a pitch-space value.
414    pub fn from_pitch_space(ps: FloatType) -> Result<Self> {
415        PitchOptions::new().ps(ps).build()
416    }
417
418    /// Returns the pitch name with the octave suffix when one is set.
419    pub fn name_with_octave(&self) -> String {
420        let mut name = self.name();
421        if let Some(octave) = self.octave {
422            use std::fmt::Write;
423            let _ = write!(name, "{octave}");
424        }
425        name
426    }
427
428    /// Returns the pitch name without octave, such as `"F#"` or `"B-"`.
429    pub fn name(&self) -> String {
430        // Written out rather than formatted: a name is a letter and a
431        // modifier, and `format!` costs more than the string it builds. Every
432        // comparison of two pitches by name goes through here.
433        let modifier = self.accidental().modifier();
434        let mut name = String::with_capacity(1 + modifier.len());
435        name.push(self.step.as_char());
436        name.push_str(modifier);
437        name
438    }
439
440    /// Whether this pitch is spelled `name` -- a letter and a modifier,
441    /// the flat written `b` or music21's `-` -- without writing it out.
442    pub(crate) fn is_named(&self, name: &str) -> bool {
443        let mut letters = name.chars();
444        letters.next() == Some(self.step.as_char())
445            && Accidental::from_standard(letters.as_str())
446                .is_some_and(|accidental| accidental == *self.accidental())
447    }
448
449    fn name_setter(&mut self, usr_str: &str) -> Result<()> {
450        let usr_str = usr_str.trim();
451
452        // A name written the ordinary way -- a letter, an accidental, an
453        // octave -- is read in place. Every pitch built from a string comes
454        // through here, and the general reading below builds three of them
455        // to take one apart.
456        let digits_at = usr_str.find(|character: char| character.is_ascii_digit());
457        let written_plainly = digits_at
458            .is_none_or(|at| at > 0 && usr_str[at..].bytes().all(|byte| byte.is_ascii_digit()));
459        if written_plainly {
460            let (name_part, octave_part) = usr_str.split_at(digits_at.unwrap_or(usr_str.len()));
461            let mut characters = name_part.chars();
462            let step = characters.next().ok_or(Error::Pitch(format!(
463                "Cannot make a name out of {name_part:?}"
464            )))?;
465            self.step_setter(StepName::try_from(step)?);
466            let accidental_str = characters.as_str();
467            if accidental_str.is_empty() {
468                self.accidental = None;
469            } else if let Some(accidental) = Accidental::from_standard(accidental_str) {
470                self.accidental_setter(accidental);
471            } else {
472                // Not one music21 has a name for, which `Accidental::new`
473                // says in the words music21 says it in.
474                self.accidental_setter(Accidental::new(accidental_str.to_string())?);
475            }
476            if !octave_part.is_empty() {
477                let octave = octave_part
478                    .parse::<IntegerType>()
479                    .map_err(|_| Error::Pitch(format!("Cannot parse {octave_part:?} to octave")))?;
480                self.octave_setter(Some(octave));
481            }
482            return Ok(());
483        }
484
485        let mut pitch_part = String::with_capacity(usr_str.len());
486        let mut octave_part = String::new();
487        for character in usr_str.chars() {
488            if character.is_ascii_digit() {
489                if pitch_part.is_empty() {
490                    return Err(Error::Value(format!(
491                        "Cannot have octave given before pitch name in '{usr_str}'."
492                    )));
493                }
494                octave_part.push(character);
495            } else {
496                pitch_part.push(character);
497            }
498        }
499
500        let mut pitch_chars = pitch_part.chars();
501        let step = pitch_chars.next().ok_or(Error::Pitch(format!(
502            "Cannot make a name out of {pitch_part:?}"
503        )))?;
504        self.step_setter(StepName::try_from(step)?);
505
506        let accidental_str: String = pitch_chars.collect();
507        if accidental_str.is_empty() {
508            self.accidental = None;
509        } else {
510            self.accidental_setter(Accidental::new(accidental_str)?);
511        }
512
513        if !octave_part.is_empty() {
514            let octave = octave_part
515                .parse::<IntegerType>()
516                .map_err(|_| Error::Pitch(format!("Cannot parse {octave_part:?} to octave")))?;
517            self.octave_setter(Some(octave));
518        }
519
520        Ok(())
521    }
522
523    /// Returns the total semitone alteration from the natural step.
524    pub fn alter(&self) -> FloatType {
525        let mut post = 0.0;
526
527        post += self
528            .accidental
529            .as_ref()
530            .map_or(0.0, |accidental| accidental.alter);
531
532        if let Some(microtone) = &self.microtone {
533            post += microtone.alter();
534        }
535
536        post
537    }
538
539    /// The pitch's accidental, a natural where none is written: every pitch
540    /// has one, so `D` and `Dn` both answer a natural. Whether one was
541    /// written is [`Self::written_accidental`].
542    pub fn accidental(&self) -> &Accidental {
543        static NATURAL: LazyLock<Accidental> = LazyLock::new(Accidental::natural);
544        self.accidental.as_ref().unwrap_or(&NATURAL)
545    }
546
547    /// The accidental written on the pitch, if any: music21's `accidental`.
548    /// A bare `D` has none and `Dn` a natural, so the two are different
549    /// pitches as they are upstream.
550    pub fn written_accidental(&self) -> Option<&Accidental> {
551        self.accidental.as_ref()
552    }
553
554    /// The written accidental, for editing in place, if the pitch has one.
555    pub fn written_accidental_mut(&mut self) -> Option<&mut Accidental> {
556        self.accidental.as_mut()
557    }
558
559    /// Returns this pitch's microtone adjustment, when present.
560    pub fn microtone(&self) -> Option<&Microtone> {
561        self.microtone.as_ref()
562    }
563
564    /// Returns this pitch's normalized pitch class.
565    pub fn pitch_class(&self) -> PitchClass {
566        PitchClass::from_number(self.ps()).unwrap_or_else(|err| {
567            panic!("pitch-space value should always map to pitch class: {err}")
568        })
569    }
570
571    /// Puts this pitch in an octave, or in none at all: music21's settable
572    /// `octave`, which only moves the pitch and does not respell it.
573    pub fn set_octave(&mut self, octave: Octave) {
574        self.octave = octave;
575    }
576
577    pub(crate) fn octave_setter(&mut self, octave: Octave) {
578        self.set_octave(octave);
579    }
580
581    /// Spells the pitch as `other` is named, keeping its own octave and
582    /// microtone: music21's `name` setter given `other.name`, which writes
583    /// a fresh accidental from the modifier and none for a natural.
584    pub(crate) fn take_name_of(&mut self, other: &Pitch) -> Result<()> {
585        self.step_setter(other.step);
586        self.accidental = match other.written_accidental() {
587            Some(accidental) if !accidental.modifier().is_empty() => {
588                Some(Accidental::new(accidental.modifier())?)
589            }
590            _ => None,
591        };
592        Ok(())
593    }
594
595    /// Returns this pitch transposed by the interval, as music21's
596    /// `Pitch.transpose` does: a pitch whose spelling was inferred from a
597    /// number is respelled to its most common enharmonic afterwards, one
598    /// spelled explicitly keeps its accidentals.
599    pub fn transpose(&self, interval: &Interval) -> Result<Pitch> {
600        let mut p = interval.transpose_pitch_with_options(self, false, Some(4))?;
601
602        if !interval.implicit_diatonic {
603            p.spelling_is_inferred = self.spelling_is_inferred;
604        }
605        if p.spelling_is_inferred {
606            p.simplify_enharmonic_in_place(true)?;
607        }
608        if let Some(fundamental) = &self.fundamental {
609            p.fundamental_setter(fundamental.transpose(interval)?);
610        }
611
612        Ok(p)
613    }
614
615    /// Returns the pitch-space value for this pitch.
616    pub fn ps(&self) -> FloatType {
617        self.pitch_space()
618    }
619
620    /// Returns the pitch-space value for this pitch.
621    pub fn pitch_space(&self) -> FloatType {
622        let octave = self.octave.unwrap_or(PITCH_OCTAVE as IntegerType);
623        ((octave + 1) * 12) as FloatType + self.step.step_ref() as FloatType + self.alter()
624    }
625
626    /// Returns the MIDI note number the way music21's `midi` reports it: the
627    /// pitch space rounded half up, then folded into 0 to 127 by octaves, so
628    /// a pitch above the MIDI range reports its highest in-range octave and
629    /// one below it its lowest.
630    pub fn midi(&self) -> IntegerType {
631        normalize_midi((self.pitch_space() + 0.5).floor() as IntegerType)
632    }
633
634    /// Returns this pitch's twelve-tone equal-temperament frequency in hertz.
635    pub fn frequency_hz(&self) -> FloatType {
636        // music21 raises its stored twelfth root of two to the distance
637        // from A4, which rounds otherwise than 2^(distance / 12).
638        let twelfth_root_of_two = (2.0 as FloatType).powf(1.0 / 12.0);
639        crate::tuningsystem::A4 * twelfth_root_of_two.powf(self.pitch_space() - 69.0)
640    }
641
642    /// Returns this pitch's frequency in hertz in a tuning that sizes
643    /// intervals by how they are spelled, measured from `reference`.
644    ///
645    /// The interval from the reference to this pitch is read as octaves and
646    /// fifths ([`Interval::pythagorean_monzo`]) and `tuning` says how wide it
647    /// sounds, so `C#` and `Db` differ in a meantone or in 19-EDO and agree in
648    /// 12-EDO. A microtone, and the part of a quarter-tone accidental past a
649    /// whole semitone, are added on top in cents.
650    ///
651    /// ```
652    /// use music21_rs::Pitch;
653    /// use music21_rs::tuningsystem::{EqualDivision, Just, Reference};
654    ///
655    /// let a440 = Reference::a440();
656    /// let e5 = Pitch::from_name("E5")?.frequency_hz_tuned(&Just, &a440)?;
657    /// assert!((e5 - 660.0).abs() < 1e-9);
658    ///
659    /// let nineteen = EqualDivision::octave(19)?;
660    /// let sharp = Pitch::from_name("C#4")?.frequency_hz_tuned(&nineteen, &a440)?;
661    /// let flat = Pitch::from_name("Db4")?.frequency_hz_tuned(&nineteen, &a440)?;
662    /// assert!(sharp < flat);
663    /// # Ok::<(), music21_rs::Error>(())
664    /// ```
665    pub fn frequency_hz_tuned(
666        &self,
667        tuning: &impl crate::tuningsystem::Tuning,
668        reference: &crate::tuningsystem::Reference,
669    ) -> Result<FloatType> {
670        let (steps, semitones, cents) = self.spelled_place();
671        let (reference_steps, reference_semitones, reference_cents) =
672            reference.pitch().spelled_place();
673        let interval = crate::tuningsystem::Monzo::pythagorean(
674            steps - reference_steps,
675            semitones - reference_semitones,
676        );
677        let cents = tuning.cents_of(&interval)? + cents - reference_cents;
678        Ok(reference.hertz() * (2.0 as FloatType).powf(cents / 1200.0))
679    }
680
681    /// Where the pitch is spelled: its staff position, the semitones of its
682    /// whole-semitone spelling, and the cents past that spelling.
683    fn spelled_place(&self) -> (IntegerType, IntegerType, FloatType) {
684        let accidental = self.accidental.as_ref().map_or(0.0, Accidental::alter);
685        let whole = accidental.trunc();
686        let natural = self.pitch_space() - self.alter();
687        let microtone = self.microtone.as_ref().map_or(0.0, Microtone::cents);
688        (
689            self.diatonic_note_number(),
690            (natural + whole).round() as IntegerType,
691            (accidental - whole) * 100.0 + microtone,
692        )
693    }
694
695    /// Returns this pitch's frequency in hertz in a tuning system, reading
696    /// the pitch as a key on a keyboard: `C#` and `Db` sound alike.
697    ///
698    /// A system of twelve degrees to the octave gives each key its own
699    /// degree; any other sounds the degree nearest the key's equal-tempered
700    /// frequency. [`Pitch::frequency_hz_tuned`] tunes the spelling instead.
701    pub fn frequency_hz_in(&self, tuning_system: TuningSystem) -> FloatType {
702        if tuning_system.is_keyboard() {
703            return tuning_system.frequency_at(self.pitch_space());
704        }
705        let degree = tuning_system
706            .nearest_degree(self.frequency_hz())
707            .expect("an equal-tempered frequency is finite and above nought");
708        tuning_system.frequency_at(degree.into())
709    }
710
711    /// The pitch a tuning system sounds nearest `hertz`, spelled as
712    /// [`TuningSystem::pitch_at`] spells a degree. Errors on a frequency that
713    /// is not a finite number above nought.
714    ///
715    /// ```
716    /// use music21_rs::Pitch;
717    /// use music21_rs::tuningsystem::TuningSystem;
718    ///
719    /// // 330 Hz is a hair flat of E4; the five-limit E is flatter still.
720    /// let e = Pitch::from_frequency_in(330.0, TuningSystem::FiveLimit)?;
721    /// assert_eq!(e.name_with_octave(), "E4");
722    /// assert!(e.frequency_hz() < 330.0);
723    /// # Ok::<(), music21_rs::Error>(())
724    /// ```
725    pub fn from_frequency_in(hertz: FloatType, tuning_system: TuningSystem) -> Result<Self> {
726        tuning_system.pitch_at(tuning_system.nearest_degree(hertz)?)
727    }
728
729    fn step_setter(&mut self, step_name: StepName) {
730        self.step = step_name;
731        self.spelling_is_inferred = false;
732    }
733
734    /// Moves the pitch to a staff position by diatonic note number, keeping
735    /// its accidental and microtone: music21's `diatonicNoteNum` setter.
736    pub(crate) fn set_diatonic_note_number(&mut self, dnn: IntegerType) -> Result<()> {
737        let (step, octave) = crate::interval::convert_diatonic_number_to_step(dnn);
738        self.step_setter(StepName::try_from(step)?);
739        self.octave_setter(Some(octave));
740        Ok(())
741    }
742
743    /// Writes an accidental on the pitch, a natural included.
744    pub fn set_accidental(&mut self, accidental: Accidental) {
745        self.accidental_setter(accidental);
746    }
747
748    /// Sets or removes the written accidental the way music21's
749    /// `accidental` setter does: `None` leaves none written, which reads as
750    /// a natural.
751    pub fn set_written_accidental(&mut self, accidental: Option<Accidental>) {
752        self.accidental = accidental;
753    }
754
755    /// Sets the accidental from a semitone alteration the way music21's
756    /// setter does with a float: the part that is a quarter-tone or larger
757    /// becomes the accidental and the rest a microtone, so `-1.5` is a flat
758    /// with fifty cents down.
759    pub fn set_accidental_alter(&mut self, alter: FloatType) -> Result<()> {
760        let (alter_shift, cents) = cents_to_alter_and_cents(alter * 100.0);
761        self.accidental_setter(Accidental::new(alter_shift)?);
762        if cents.abs() > 0.01 {
763            self.microtone_setter(Microtone::new(cents)?);
764        }
765        Ok(())
766    }
767
768    /// music21's `spellingIsInferred`: whether the crate chose the spelling
769    /// rather than being told it. A pitch built from a number, a MIDI value,
770    /// a pitch class or a frequency has an inferred spelling, and only such
771    /// a pitch is respelled by a transposition.
772    pub fn spelling_is_inferred(&self) -> bool {
773        self.spelling_is_inferred
774    }
775
776    /// Says whether the spelling was chosen or given.
777    pub fn set_spelling_is_inferred(&mut self, inferred: bool) {
778        self.spelling_is_inferred = inferred;
779    }
780
781    fn accidental_setter(&mut self, value: Accidental) {
782        self.accidental = Some(value);
783    }
784
785    /// Sets the microtone from a cent shift, removing it when the shift is
786    /// zero.
787    pub fn set_microtone_cents(&mut self, cents: FloatType) -> Result<()> {
788        if cents == 0.0 {
789            self.microtone = None;
790        } else {
791            self.microtone = Some(Microtone::new(cents)?);
792        }
793        Ok(())
794    }
795
796    fn microtone_setter(&mut self, mt: Microtone) {
797        self.microtone = Some(mt);
798    }
799
800    fn pitch_class_setter(&mut self, pc: PitchClassSpecifier) -> Result<()> {
801        self.pitch_class_value_setter(PitchClass::new(pc)?.number());
802        Ok(())
803    }
804
805    fn pitch_class_value_setter(&mut self, pc: FloatType) {
806        let (step, accidental, _, _) = convert_ps_to_step(pc);
807        self.step = step;
808        self.accidental = (accidental.alter() != 0.0).then_some(accidental);
809        self.spelling_is_inferred = true;
810    }
811
812    pub(crate) fn fundamental_setter(&mut self, f: Pitch) {
813        self.fundamental = Some(Arc::new(f));
814    }
815
816    /// Returns the fundamental this pitch was built against, when one was set.
817    pub fn fundamental(&self) -> Option<&Pitch> {
818        self.fundamental.as_deref()
819    }
820
821    fn midi_setter(&mut self, m: IntegerType) {
822        self.ps_setter(normalize_midi(m) as FloatType);
823    }
824
825    fn ps_setter(&mut self, p: FloatType) {
826        let (step, accidental, microtone, octave_shift) = convert_ps_to_step(p);
827        self.step = step;
828        self.accidental = (accidental.alter() != 0.0).then_some(accidental);
829        if microtone.alter() == 0.0 {
830            self.microtone = None;
831        } else {
832            self.microtone = Some(microtone);
833        }
834
835        let octave = convert_ps_to_oct(p) + octave_shift;
836        self.octave = Some(octave);
837        self.spelling_is_inferred = true;
838    }
839
840    /// Returns the octave, or music21's default of 4 when none is set.
841    ///
842    /// This is what music21's `.octave` answers, which is always an `int`.
843    /// [`Self::octave`] keeps the `Option`, which is music21's own `_octave`
844    /// and says strictly more; [`Self::octave_is_implicit`] tells the two
845    /// apart.
846    pub fn implicit_octave(&self) -> IntegerType {
847        self.octave.unwrap_or(PITCH_OCTAVE as IntegerType)
848    }
849
850    /// Whether this pitch was never given an octave, so it stands for its
851    /// pitch class in any octave: music21's `octaveIsImplicit`.
852    ///
853    /// Such a pitch prints without an octave number and reports the default
854    /// octave from [`Self::implicit_octave`].
855    #[must_use]
856    pub fn octave_is_implicit(&self) -> bool {
857        self.octave.is_none()
858    }
859
860    /// Makes the octave implicit or explicit: the setter of music21's
861    /// `octaveIsImplicit`.
862    ///
863    /// Making it explicit puts the pitch in the default octave, as music21
864    /// does; making it implicit takes the octave away. Setting it to what it
865    /// already is does nothing.
866    pub fn set_octave_is_implicit(&mut self, implicit: bool) {
867        if implicit == self.octave.is_none() {
868            return;
869        }
870        self.octave = if implicit {
871            None
872        } else {
873            Some(PITCH_OCTAVE as IntegerType)
874        };
875    }
876
877    /// Returns the pitch class as music21's `pitchClassString`, one
878    /// character with `A` and `B` for ten and eleven. Like music21's integer
879    /// `pitchClass` it rounds a microtone away with Python's round-half-to-even,
880    /// so `C` with `+20c` is `0` where [`Self::pitch_class`] would say `0.2`,
881    /// and a half-sharp C is `0` even though its MIDI number rounds up to 61.
882    pub fn pitch_class_string(&self) -> String {
883        crate::pitch::pitchclass::convert_pitch_class_to_str(
884            self.ps().round_ties_even() as IntegerType
885        )
886    }
887
888    /// Returns how many cents the pitch sits from the nearest MIDI note,
889    /// rounded to a whole cent: music21's `getCentShiftFromMidi`, so a
890    /// half-sharp C reads `-50` because it rounds up to C-sharp.
891    pub fn cent_shift_from_midi(&self) -> IntegerType {
892        let mut distance = self.ps() - FloatType::from(self.midi());
893        while distance < -11.0 {
894            distance += 12.0;
895        }
896        while distance > 11.0 {
897            distance -= 12.0;
898        }
899        (distance * 100.0).round() as IntegerType
900    }
901
902    /// Builds a pitch from a frequency in hertz, spelled in twelve-tone equal
903    /// temperament at A4 = 440 with any remainder as a microtone.
904    pub fn from_frequency(hertz: FloatType) -> Result<Self> {
905        if !hertz.is_finite() || hertz <= 0.0 {
906            return Err(Error::Pitch(format!(
907                "frequency must be a finite number greater than zero, got {hertz}"
908            )));
909        }
910        let mut pitch = Pitch::default();
911        pitch.ps_setter(12.0 * (hertz / crate::tuningsystem::A4).log2() + 69.0);
912        Ok(pitch)
913    }
914
915    /// Returns music21's `diatonicNoteNum`: the staff position counting `C0`
916    /// as `1`, with the implicit octave standing in when none is set.
917    pub fn diatonic_note_number(&self) -> IntegerType {
918        let octave = self.octave.unwrap_or(PITCH_OCTAVE as IntegerType);
919        self.step.step_to_dnn_offset() + 7 * octave
920    }
921
922    /// The letter as a number of steps above C, nought to six.
923    pub(crate) fn step_index(&self) -> IntegerType {
924        self.step as IntegerType - 1
925    }
926
927    /// Returns whether this pitch lies on the twelve-tone grid: no quarter
928    /// tone accidental and no microtone.
929    pub fn is_twelve_tone(&self) -> bool {
930        self.accidental().is_twelve_tone()
931            && self
932                .microtone
933                .as_ref()
934                .is_none_or(|microtone| microtone.cents() == 0.0)
935    }
936
937    fn octave_bearing_copy(&self, operation: &str) -> Result<Pitch> {
938        if self.octave.is_none() {
939            return Err(Error::Pitch(format!(
940                "Cannot call {operation} with an octaveless Pitch."
941            )));
942        }
943        Ok(self.clone())
944    }
945
946    fn shift_octave(&mut self, octaves: IntegerType) {
947        let octave = self.octave.unwrap_or(PITCH_OCTAVE as IntegerType);
948        self.octave_setter(Some(octave + octaves));
949    }
950
951    /// Returns the stored octave.
952    ///
953    /// Returns `None` when the pitch was created without an explicit octave,
954    /// such as `Pitch::from_name("C")`. In calculations, octave-less pitches
955    /// use the library default octave.
956    pub fn octave(&self) -> Octave {
957        self.octave
958    }
959
960    pub(crate) fn step(&self) -> StepName {
961        self.step
962    }
963
964    pub(crate) fn set_ps(&mut self, p: FloatType) {
965        self.ps_setter(p);
966    }
967}
968
969impl Default for Pitch {
970    fn default() -> Self {
971        Self::from_options(PitchOptions::default())
972            .expect("default Pitch construction should never fail")
973    }
974}
975
976#[derive(Default)]
977struct PitchParameters {
978    name: Option<String>,
979    step: Option<StepName>,
980    accidental: Option<Accidental>,
981    microtone: Option<Microtone>,
982    spelling_is_inferred: bool,
983    octave: Octave,
984}
985
986impl From<PitchName> for PitchParameters {
987    fn from(value: PitchName) -> Self {
988        match value {
989            PitchName::Name(name) => Self {
990                name: Some(name),
991                ..Self::default()
992            },
993            PitchName::Number(number) => {
994                let (step, accidental, microtone, octave_shift) = convert_ps_to_step(number);
995                let octave = (number >= 12.0).then(|| convert_ps_to_oct(number) + octave_shift);
996                Self {
997                    name: None,
998                    step: Some(step),
999                    accidental: Some(accidental),
1000                    microtone: (microtone.cents() != 0.0).then_some(microtone),
1001                    spelling_is_inferred: true,
1002                    octave,
1003                }
1004            }
1005        }
1006    }
1007}
1008
1009fn convert_ps_to_step<T: Num + ToPrimitive>(
1010    ps: T,
1011) -> (StepName, Accidental, Microtone, IntegerType) {
1012    let ps = ps.to_f64().unwrap_or(0.0);
1013    let (pc, alter, micro) = if ps.fract() == 0.0 {
1014        ((ps as IntegerType).rem_euclid(12), 0.0, 0.0)
1015    } else {
1016        let ps = round_to_digits(ps, PITCH_SPACE_SIGNIFICANT_DIGITS);
1017        let pc_real = ps.rem_euclid(12.0);
1018        let pc = pc_real.floor() as IntegerType;
1019        let mut micro = pc_real - pc as FloatType;
1020
1021        // A quarter of a semitone exactly is *not* a quarter tone: music21
1022        // writes `F` plus twenty-five cents rather than an F half-sharp
1023        // twenty-five cents flat, and the bound it draws is exclusive.
1024        let alter = if round_to_digits(micro, 1) == 0.5 || (0.25 < micro && micro < 0.75) {
1025            micro -= 0.5;
1026            0.5
1027        } else if (0.75..1.0).contains(&micro) {
1028            micro -= 1.0;
1029            1.0
1030        } else if micro > 0.0 {
1031            0.0
1032        } else {
1033            micro = 0.0;
1034            0.0
1035        };
1036
1037        (pc, alter, micro)
1038    };
1039
1040    let octave_shift = IntegerType::from(pc == 11 && alter == 1.0);
1041    let (pc_name, accidental_alter) = match pc {
1042        4 | 11 if alter == 1.0 => ((pc + 1).rem_euclid(12), 0.0),
1043        1 | 6 | 8 if alter >= 1.0 => (pc + 1, alter - 1.0),
1044        1 | 6 | 8 => (pc - 1, 1.0 + alter),
1045        3 | 10 if alter <= -1.0 => (pc - 1, 1.0 + alter),
1046        3 | 10 => (pc + 1, -1.0 + alter),
1047        _ => (pc, alter),
1048    };
1049
1050    let step = StepName::ref_to_step(pc_name.rem_euclid(12))
1051        .unwrap_or_else(|err| panic!("pitch class should map to a step: {err}"));
1052    let accidental = Accidental::new(accidental_alter)
1053        .unwrap_or_else(|err| panic!("accidental conversion should not fail: {err}"));
1054    let microtone = Microtone::from_cents(micro * 100.0, 1);
1055
1056    (step, accidental, microtone, octave_shift)
1057}
1058
1059fn round_to_digits(value: FloatType, digits: UnsignedIntegerType) -> FloatType {
1060    let factor = (10 as FloatType).powi(digits as IntegerType);
1061    (value * factor).round() / factor
1062}
1063
1064fn normalize_midi(midi: IntegerType) -> IntegerType {
1065    if midi > 127 {
1066        let mut value = (12 * 9) + midi.rem_euclid(12);
1067        if value < (127 - 12) {
1068            value += 12;
1069        }
1070        value
1071    } else if midi < 0 {
1072        midi.rem_euclid(12)
1073    } else {
1074        midi
1075    }
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080    /// A number that is not a number spells no pitch. Left to the conversion
1081    /// it became a C, because the cast that reads a step off pitch space
1082    /// answers nought for anything it cannot represent.
1083    #[test]
1084    fn a_pitch_space_number_that_is_not_finite_is_refused() {
1085        use crate::pitch::Pitch;
1086
1087        for number in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1088            assert!(Pitch::from_pitch_space(number).is_err());
1089            assert!(Pitch::from_number(number).is_err());
1090            assert!(Pitch::builder().ps(number).build().is_err());
1091        }
1092        assert_eq!(Pitch::from_pitch_space(61.0).unwrap().name(), "C#");
1093    }
1094
1095    /// Pitches compare and hash by value, so a set holds each spelling once.
1096    #[test]
1097    fn equal_pitches_are_one_set_entry() {
1098        use crate::pitch::Pitch;
1099        use std::collections::HashSet;
1100
1101        let pitches: HashSet<Pitch> = ["B-4", "B-4", "A#4", "Bb3"]
1102            .into_iter()
1103            .map(|name| Pitch::from_name(name).unwrap())
1104            .collect();
1105        assert_eq!(pitches.len(), 3);
1106    }
1107
1108    /// A name is the pitch's whichever way its flat is written.
1109    #[test]
1110    fn a_pitch_is_named_either_flat_spelling() {
1111        use crate::pitch::Pitch;
1112
1113        let a_flat = Pitch::from_name("Ab2").unwrap();
1114        assert!(a_flat.is_named("Ab"));
1115        assert!(a_flat.is_named("A-"));
1116        assert!(!a_flat.is_named("A"));
1117    }
1118
1119    /// Every pitch has an accidental; only a written one tells `D` from `Dn`.
1120    #[test]
1121    fn every_pitch_has_an_accidental() {
1122        use crate::pitch::Pitch;
1123
1124        let bare = Pitch::from_name("D").unwrap();
1125        let natural = Pitch::from_name("Dn").unwrap();
1126        assert_eq!(bare.accidental().name(), "natural");
1127        assert_eq!(natural.accidental().name(), "natural");
1128        assert!(bare.written_accidental().is_none());
1129        assert!(natural.written_accidental().is_some());
1130    }
1131
1132    #[test]
1133    fn a_frequency_that_is_not_finite_is_refused() {
1134        use crate::pitch::Pitch;
1135
1136        assert!(Pitch::from_frequency(f64::INFINITY).is_err());
1137        assert!(Pitch::from_frequency(f64::NAN).is_err());
1138        assert!(Pitch::from_frequency(0.0).is_err());
1139        assert_eq!(
1140            Pitch::from_frequency(440.0).unwrap().name_with_octave(),
1141            "A4"
1142        );
1143    }
1144
1145    #[test]
1146    fn a_pitch_is_built_from_a_step_a_number_or_an_owned_name() {
1147        use crate::pitch::{Pitch, PitchName, pitch_class_name};
1148
1149        assert_eq!(Pitch::from_step('d').unwrap().name(), "D");
1150        assert!(Pitch::from_step('h').is_err());
1151        assert_eq!(
1152            Pitch::try_from("Eb4".to_string())
1153                .unwrap()
1154                .name_with_octave(),
1155            "Eb4"
1156        );
1157        assert!(matches!(PitchName::from(60), PitchName::Number(n) if n == 60.0));
1158        assert!(matches!(PitchName::from(61.5), PitchName::Number(n) if n == 61.5));
1159        assert_eq!(pitch_class_name(1), "Db");
1160        assert_eq!(pitch_class_name(13), "Db");
1161        assert_eq!(pitch_class_name(10), "Bb");
1162    }
1163
1164    #[test]
1165    fn spelling_is_inferred_for_a_pitch_built_from_a_number_and_can_be_unsaid() {
1166        use crate::pitch::Pitch;
1167
1168        let mut pitch = Pitch::from_midi(61).unwrap();
1169        assert!(pitch.spelling_is_inferred());
1170        pitch.set_spelling_is_inferred(false);
1171        assert!(!pitch.spelling_is_inferred());
1172        assert!(!Pitch::from_name("C#").unwrap().spelling_is_inferred());
1173
1174        let mut raised = Pitch::from_name("C#4").unwrap();
1175        raised.get_higher_enharmonic_in_place().unwrap();
1176        assert_eq!(raised.name_with_octave(), "Db4");
1177    }
1178
1179    #[test]
1180    fn a_pitch_knows_whether_a_key_signature_already_writes_it() {
1181        use crate::pitch::Pitch;
1182
1183        let altered = [
1184            Pitch::from_name("F#").unwrap(),
1185            Pitch::from_name("C#").unwrap(),
1186        ];
1187        assert!(
1188            Pitch::from_name("F#4")
1189                .unwrap()
1190                .name_in_key_signature(&altered)
1191        );
1192        assert!(
1193            !Pitch::from_name("F4")
1194                .unwrap()
1195                .name_in_key_signature(&altered)
1196        );
1197        assert!(
1198            !Pitch::from_name("Fb4")
1199                .unwrap()
1200                .name_in_key_signature(&altered)
1201        );
1202        assert!(
1203            Pitch::from_name("F4")
1204                .unwrap()
1205                .step_in_key_signature(&altered)
1206        );
1207        assert!(
1208            !Pitch::from_name("G4")
1209                .unwrap()
1210                .step_in_key_signature(&altered)
1211        );
1212    }
1213
1214    /// music21's `updateAccidentalDisplay` on its simplest shapes: a repeat
1215    /// of a written accidental in the bar is not written again, a natural
1216    /// after an accidental in the bar is a caution, and a note the key
1217    /// signature already alters needs nothing.
1218    #[test]
1219    fn accidental_display_follows_the_pitches_before_it() {
1220        use crate::pitch::{AccidentalDisplayOptions, Pitch};
1221
1222        let first = Pitch::from_name("F#4").unwrap();
1223        let mut repeat = Pitch::from_name("F#4").unwrap();
1224        let past = [first.clone()];
1225        repeat.update_accidental_display(&AccidentalDisplayOptions {
1226            pitch_past: &past,
1227            ..AccidentalDisplayOptions::default()
1228        });
1229        assert_eq!(repeat.accidental().display_status(), Some(false));
1230
1231        let mut natural = Pitch::from_name("F4").unwrap();
1232        natural.update_accidental_display(&AccidentalDisplayOptions {
1233            pitch_past: &past,
1234            ..AccidentalDisplayOptions::default()
1235        });
1236        assert!(natural.written_accidental().is_some());
1237        assert_eq!(natural.accidental().display_status(), Some(true));
1238
1239        let mut in_key = Pitch::from_name("F#4").unwrap();
1240        in_key.update_accidental_display(&AccidentalDisplayOptions {
1241            altered_pitches: &[Pitch::from_name("F#").unwrap()],
1242            ..AccidentalDisplayOptions::default()
1243        });
1244        assert_eq!(in_key.accidental().display_status(), Some(false));
1245
1246        let mut fresh = Pitch::from_name("B-4").unwrap();
1247        fresh.update_accidental_display(&AccidentalDisplayOptions::default());
1248        assert_eq!(fresh.accidental().display_status(), Some(true));
1249    }
1250
1251    /// music21's `.octave` always answers a number, and `.octaveIsImplicit`
1252    /// says whether one was ever given.
1253    #[test]
1254    fn an_octave_is_implicit_until_one_is_given() {
1255        use crate::pitch::Pitch;
1256
1257        let mut anywhere = Pitch::from_name("G#").unwrap();
1258        assert!(anywhere.octave_is_implicit());
1259        assert_eq!(anywhere.octave(), None);
1260        assert_eq!(anywhere.implicit_octave(), 4);
1261
1262        let somewhere = Pitch::from_name("E-6").unwrap();
1263        assert!(!somewhere.octave_is_implicit());
1264        assert_eq!(somewhere.octave(), Some(6));
1265        assert_eq!(somewhere.implicit_octave(), 6);
1266
1267        // Making it explicit puts the pitch in the default octave; making it
1268        // implicit again takes the octave away.
1269        anywhere.set_octave_is_implicit(false);
1270        assert!(!anywhere.octave_is_implicit());
1271        assert_eq!(anywhere.octave(), Some(4));
1272        anywhere.set_octave_is_implicit(true);
1273        assert_eq!(anywhere.octave(), None);
1274
1275        // Setting it to what it already is leaves the octave alone.
1276        let mut high = Pitch::from_name("C7").unwrap();
1277        high.set_octave_is_implicit(false);
1278        assert_eq!(high.octave(), Some(7));
1279    }
1280
1281    #[test]
1282    fn a_pitch_is_respelled_to_agree_with_the_key_signature() {
1283        use crate::key::KeySignature;
1284        // music21's own example: a semitone above F is F# in D major and
1285        // G- in B-flat minor, because the signature spells that note.
1286        let f_sharp = Pitch::from_name("F#4").unwrap();
1287        assert_eq!(
1288            f_sharp.respelled_for(&KeySignature::new(2)).unwrap().name(),
1289            "F#"
1290        );
1291        assert_eq!(
1292            f_sharp
1293                .respelled_for(&KeySignature::new(-5))
1294                .unwrap()
1295                .name(),
1296            "Gb"
1297        );
1298        // A pitch the signature says nothing about is left alone, and so is
1299        // one carrying no accidental at all.
1300        assert_eq!(
1301            Pitch::from_name("C4")
1302                .unwrap()
1303                .respelled_for(&KeySignature::new(-5))
1304                .unwrap()
1305                .name(),
1306            "C"
1307        );
1308    }
1309
1310    #[test]
1311    fn an_octave_digit_may_sit_anywhere_after_the_step() {
1312        for (name, expected) in [
1313            ("e4-", "Eb4"),
1314            ("e-4", "Eb4"),
1315            ("b3-", "Bb3"),
1316            ("g4--", "Gbb4"),
1317            ("c#4", "C#4"),
1318            ("f##2", "F##2"),
1319            ("d1", "D1"),
1320        ] {
1321            assert_eq!(
1322                Pitch::from_name(name).unwrap().name_with_octave(),
1323                expected,
1324                "parsing {name:?}"
1325            );
1326        }
1327        assert_eq!(
1328            Pitch::from_name("c4#2").unwrap().name_with_octave(),
1329            "C#42",
1330            "the digits concatenate wherever they fall, as music21's do"
1331        );
1332        assert!(Pitch::from_name("4c").is_err());
1333    }
1334
1335    #[test]
1336    fn options_keep_an_explicit_accidental_and_a_numeric_name_stays_inferred() {
1337        let sharp = crate::PitchOptions::new()
1338            .name("C")
1339            .accidental("#")
1340            .octave(7)
1341            .microtone(-30)
1342            .build()
1343            .unwrap();
1344        assert_eq!(sharp.full_name(), "C-sharp in octave 7 (-30c)");
1345        let double_flat = crate::PitchOptions::new()
1346            .name("D")
1347            .accidental(Accidental::new("double-flat").unwrap())
1348            .build()
1349            .unwrap();
1350        assert_eq!(double_flat.name(), "Dbb");
1351        let inferred = Pitch::from_number(6.0).unwrap();
1352        assert_eq!(
1353            inferred
1354                .transpose(&Interval::from_name("-m2").unwrap())
1355                .unwrap()
1356                .name(),
1357            "F"
1358        );
1359    }
1360
1361    #[test]
1362    fn midi_folds_into_range_like_music21() {
1363        let high = Pitch::from_name("C#10").unwrap();
1364        assert_eq!(high.midi(), 121);
1365        assert_eq!(high.cent_shift_from_midi(), 0);
1366        let half_flat = crate::PitchOptions::new().name("F`10").build().unwrap();
1367        assert_eq!(half_flat.midi(), 125);
1368        assert_eq!(
1369            Pitch::from_name("C#-2")
1370                .unwrap_or_else(|_| Pitch::from_pitch_space(-11.0).unwrap())
1371                .midi(),
1372            1
1373        );
1374        assert_eq!(Pitch::from_name("C~4").unwrap().midi(), 61);
1375        assert_eq!(Pitch::from_name("A4").unwrap().frequency_hz(), 440.0);
1376    }
1377
1378    #[test]
1379    fn harmonics_chain_and_transpose_with_their_fundamental() {
1380        let a4 = Pitch::from_name("A4").unwrap();
1381        let seventh = a4.harmonic(7).unwrap();
1382        assert_eq!(seventh.name_with_octave(), "F#~7");
1383        assert_eq!(seventh.microtone().unwrap().cents().round(), 19.0);
1384        let doubled = seventh.harmonic(2).unwrap();
1385        assert_eq!(doubled.name_with_octave(), "F#~8");
1386        assert_eq!(doubled.microtone().unwrap().cents().round(), 19.0);
1387        assert_eq!(doubled.fundamental().unwrap().name_with_octave(), "F#~7");
1388
1389        let second = a4.harmonic(2).unwrap();
1390        assert_eq!(second.fundamental().unwrap().name_with_octave(), "A4");
1391        let up_a_fifth = second
1392            .transpose(&Interval::from_name("p5").unwrap())
1393            .unwrap();
1394        assert_eq!(up_a_fifth.name_with_octave(), "E6");
1395        assert_eq!(up_a_fifth.fundamental().unwrap().name_with_octave(), "E5");
1396    }
1397
1398    #[test]
1399    fn numbers_and_chromatic_transposition_keep_microtones() {
1400        let sharp_twenty = Pitch::from_number(60.2).unwrap();
1401        assert_eq!(sharp_twenty.name_with_octave(), "C4");
1402        assert_eq!(sharp_twenty.microtone().unwrap().cents().round(), 20.0);
1403        let harmonic = Pitch::from_name("A4")
1404            .unwrap()
1405            .harmonic(7)
1406            .unwrap()
1407            .harmonic(2)
1408            .unwrap();
1409        let down_two_octaves = harmonic
1410            .transpose(&Interval::from_semitones(-24).unwrap())
1411            .unwrap();
1412        assert_eq!(down_two_octaves.name_with_octave(), "F#~6");
1413        assert_eq!(down_two_octaves.microtone().unwrap().cents().round(), 19.0);
1414    }
1415
1416    #[test]
1417    fn transposition_reaches_octave_minus_one() {
1418        let low = Pitch::from_name("D2")
1419            .unwrap()
1420            .transpose(&Interval::from_name("m-23").unwrap())
1421            .unwrap();
1422        assert_eq!(low.name_with_octave(), "C#-1");
1423        assert_eq!(low.ps(), 1.0);
1424    }
1425
1426    #[test]
1427    fn simplify_multiple_enharmonics_matches_music21() {
1428        let names = |names: &[&str]| -> Vec<Pitch> {
1429            names
1430                .iter()
1431                .map(|n| Pitch::from_name(*n).unwrap())
1432                .collect()
1433        };
1434        let spelled = |pitches: Vec<Pitch>| -> Vec<String> {
1435            pitches.iter().map(Pitch::name_with_octave).collect()
1436        };
1437        let cases: [(&[&str], &[&str]); 11] = [
1438            (&["C#", "Db", "E"], &["C#", "C#", "E"]),
1439            (&["A#4", "C#5", "E#5"], &["A#4", "C#5", "E#5"]),
1440            (&["F#", "A#", "C#"], &["F#", "A#", "C#"]),
1441            (&["Gb", "Bb", "Db"], &["Gb", "Bb", "Db"]),
1442            (&["C", "Eb", "Gb", "B--"], &["C", "Eb", "F#", "A"]),
1443            (&["B#", "D##", "F##"], &["B#", "E", "G"]),
1444            (&["C", "E", "G#", "B"], &["C", "E", "G#", "B"]),
1445            (&["E#", "G##", "B#"], &["E#", "G##", "B#"]),
1446            (
1447                &["C#", "E#", "G#", "B", "D#"],
1448                &["C#", "E#", "G#", "B", "D#"],
1449            ),
1450            (
1451                &["Db", "F", "Ab", "Cb", "Eb", "Gb"],
1452                &["Db", "F", "Ab", "Cb", "Eb", "Gb"],
1453            ),
1454            (&["C4", "Eb4", "F#4"], &["C4", "Eb4", "Gb4"]),
1455        ];
1456        for (input, expected) in cases {
1457            let simplified = simplify_multiple_enharmonics(&names(input), None, None).unwrap();
1458            assert_eq!(spelled(simplified), expected, "{input:?}");
1459        }
1460        let three_flats = crate::KeySignature::new(-3);
1461        assert_eq!(
1462            spelled(
1463                simplify_multiple_enharmonics(&names(&["C#", "Db", "E"]), None, Some(three_flats))
1464                    .unwrap()
1465            ),
1466            ["Db", "Db", "Fb"]
1467        );
1468        let six_sharps = crate::KeySignature::new(6);
1469        assert_eq!(
1470            spelled(
1471                simplify_multiple_enharmonics(&names(&["Gb", "B-", "Db"]), None, Some(six_sharps))
1472                    .unwrap()
1473            ),
1474            ["F#", "A#", "C#"]
1475        );
1476        assert!(
1477            simplify_multiple_enharmonics(&[], None, None)
1478                .unwrap()
1479                .is_empty()
1480        );
1481        assert_eq!(super::dissonance_score(&[]).unwrap(), 0.0);
1482        assert!(
1483            super::dissonance_score(&names(&["C", "E", "G"])).unwrap()
1484                < super::dissonance_score(&names(&["C", "F-", "G"])).unwrap()
1485        );
1486    }
1487
1488    #[test]
1489    fn frequency_rounds_as_music21_rounds() {
1490        let cases = [
1491            ("F#6", 1_479.977_690_846_539),
1492            ("D3", 146.832_383_958_703_64),
1493            ("F1", 43.653_528_929_125_41),
1494            ("C4", 261.625_565_300_598_5),
1495            ("A~4", 452.892_984_123_136_5),
1496            ("B-7", 3_729.310_092_144_725),
1497        ];
1498        for (name, hertz) in cases {
1499            let pitch: Pitch = name.parse().unwrap();
1500            assert_eq!(pitch.frequency_hz(), hertz, "{name}");
1501        }
1502    }
1503
1504    #[test]
1505    fn dissonance_rounds_as_music21_rounds() {
1506        // Two spellings that tie in exact arithmetic and differ by one ulp
1507        // in music21's, which takes one log of each whole denominator.
1508        let score = |spelled: &[&str]| {
1509            let pitches: Vec<Pitch> = spelled.iter().map(|name| name.parse().unwrap()).collect();
1510            super::dissonance_score(&pitches).unwrap()
1511        };
1512        let flat = score(&["Eb", "Gb", "A"]);
1513        let sharp = score(&["Eb", "F#", "A"]);
1514        assert_eq!(flat, 0.051_031_336_396_830_44);
1515        assert_eq!(sharp, 0.051_031_336_396_830_475);
1516
1517        // So pitch classes 3, 6 and 9 spell with G-, as music21 spells them.
1518        let chord = crate::Chord::new([3, 6, 9].as_slice()).unwrap();
1519        assert_eq!(chord.pitch_names(), ["Eb", "Gb", "A"]);
1520
1521        // A twelfth down is 1/3: its fifth's 2 cancels the octave's.
1522        assert_eq!(score(&["D#6", "B#5", "E#5", "D5"]), 0.235_696_022_824_957_3);
1523        assert_eq!(score(&["D#6", "C6", "F5", "D5"]), 0.235_696_022_824_957_36);
1524        let respelled = simplify_multiple_enharmonics(
1525            &["D#6", "C6", "F5", "C##5"].map(|name| name.parse().unwrap()),
1526            None,
1527            None,
1528        )
1529        .unwrap();
1530        let respelled: Vec<String> = respelled.iter().map(Pitch::name_with_octave).collect();
1531        assert_eq!(respelled, ["D#6", "B#5", "E#5", "D5"]);
1532    }
1533
1534    #[test]
1535    fn get_enharmonic_picks_the_direction_music21_does() {
1536        let cases = [
1537            ("C#4", "Db4"),
1538            ("Db4", "C#4"),
1539            ("C4", "B#3"),
1540            ("D4", "C##4"),
1541            ("G4", "F##4"),
1542            ("E4", "Fb4"),
1543            ("F4", "Gbb4"),
1544            ("B4", "Cb5"),
1545            ("A-4", "G#4"),
1546            ("F##4", "G4"),
1547            ("C--4", "Bb3"),
1548            ("B#4", "C5"),
1549            ("G##4", "A4"),
1550        ];
1551        for (name, expected) in cases {
1552            let pitch = Pitch::from_name(name).unwrap();
1553            assert_eq!(
1554                pitch.get_enharmonic().unwrap().name_with_octave(),
1555                expected,
1556                "{name}"
1557            );
1558        }
1559    }
1560
1561    #[test]
1562    fn cent_shift_from_midi_matches_music21() {
1563        let cases: [(&str, FloatType, IntegerType, i32, &str); 8] = [
1564            ("C4", 0.0, 60, 0, "0"),
1565            ("C4", 20.0, 60, 20, "0"),
1566            ("C4", -20.0, 60, -20, "0"),
1567            ("C4", 60.0, 61, -40, "1"),
1568            ("C~4", 0.0, 61, -50, "0"),
1569            ("C`4", 0.0, 60, -50, "0"),
1570            ("C4", -60.0, 59, 40, "B"),
1571            ("C4", 130.0, 61, 30, "1"),
1572        ];
1573        for (name, cents, midi, shift, pitch_class) in cases {
1574            let pitch = crate::PitchOptions::new()
1575                .name(name)
1576                .microtone(cents)
1577                .build()
1578                .unwrap();
1579            assert_eq!(pitch.midi(), midi, "{name} {cents}");
1580            assert_eq!(pitch.cent_shift_from_midi(), shift, "{name} {cents}");
1581            assert_eq!(pitch.pitch_class_string(), pitch_class, "{name} {cents}");
1582            assert_eq!(pitch.implicit_octave(), 4);
1583        }
1584        assert_eq!(Pitch::from_name("G").unwrap().implicit_octave(), 4);
1585        assert_eq!(Pitch::from_name("G2").unwrap().implicit_octave(), 2);
1586    }
1587
1588    #[test]
1589    fn quarter_tones_and_microtones_convert_both_ways() {
1590        let build = |name: &str, cents: FloatType| {
1591            crate::PitchOptions::new()
1592                .name(name)
1593                .microtone(cents)
1594                .build()
1595                .unwrap()
1596        };
1597        let describe = |pitch: &Pitch| {
1598            (
1599                pitch.name_with_octave(),
1600                pitch.microtone().map_or(0.0, Microtone::cents),
1601                pitch.accidental().name().to_string(),
1602            )
1603        };
1604
1605        let to_microtones: [(&str, FloatType, &str, FloatType, &str); 6] = [
1606            ("C~4", 0.0, "C4", 50.0, "natural"),
1607            ("C`4", 0.0, "C4", -50.0, "natural"),
1608            ("D#~4", 0.0, "D#4", 50.0, "sharp"),
1609            ("E-`4", 0.0, "Eb4", -50.0, "flat"),
1610            ("C~4", 10.0, "C4", 60.0, "natural"),
1611            ("C#4", 0.0, "C#4", 0.0, "sharp"),
1612        ];
1613        for (name, cents, expected_name, expected_cents, accidental) in to_microtones {
1614            let converted = build(name, cents)
1615                .convert_quarter_tones_to_microtones()
1616                .unwrap();
1617            assert_eq!(
1618                describe(&converted),
1619                (
1620                    expected_name.to_string(),
1621                    expected_cents,
1622                    accidental.to_string()
1623                ),
1624                "{name} {cents}"
1625            );
1626        }
1627
1628        let to_quarter_tones: [(&str, FloatType, &str, FloatType, &str); 11] = [
1629            ("C4", 50.0, "C~4", 0.0, "half-sharp"),
1630            ("C4", -50.0, "C`4", 0.0, "half-flat"),
1631            ("C4", 30.0, "C~4", -20.0, "half-sharp"),
1632            ("C4", -30.0, "C`4", 20.0, "half-flat"),
1633            ("C4", 70.0, "C~4", 20.0, "half-sharp"),
1634            ("C#4", 50.0, "C#~4", 0.0, "one-and-a-half-sharp"),
1635            ("C4", 150.0, "C#4", 50.0, "sharp"),
1636            ("C4", -150.0, "Cb4", -50.0, "flat"),
1637            ("C4", 120.0, "C#4", 20.0, "sharp"),
1638            ("Cb4", -50.0, "Cb`4", 0.0, "one-and-a-half-flat"),
1639            ("C4", 0.0, "C4", 0.0, "natural"),
1640        ];
1641        for (name, cents, expected_name, expected_cents, accidental) in to_quarter_tones {
1642            let converted = build(name, cents)
1643                .convert_microtones_to_quarter_tones()
1644                .unwrap();
1645            assert_eq!(
1646                describe(&converted),
1647                (
1648                    expected_name.to_string(),
1649                    expected_cents,
1650                    accidental.to_string()
1651                ),
1652                "{name} {cents}"
1653            );
1654        }
1655    }
1656
1657    #[test]
1658    fn harmonic_and_fundamental_match_music21() {
1659        let cases = [
1660            ("E5", "C2", 10, 14.0, "10thH/C2(+14c)"),
1661            ("G4", "C2", 6, -2.0, "6thH/C2(-2c)"),
1662            ("B-4", "C2", 7, 31.0, "7thH/C2(+31c)"),
1663            ("F#5", "D3", 5, 14.0, "5thH/D3(+14c)"),
1664            ("C5", "C4", 2, 0.0, "2ndH/C4"),
1665        ];
1666        for (name, fundamental, number, cents, text) in cases {
1667            let pitch = Pitch::from_name(name).unwrap();
1668            let fundamental = Pitch::from_name(fundamental).unwrap();
1669            let (harmonic, retuned) = pitch
1670                .harmonic_and_fundamental_from_pitch(&fundamental)
1671                .unwrap();
1672            assert_eq!(harmonic, number, "{name}");
1673            let retuned_cents = retuned.microtone().map_or(0.0, Microtone::cents);
1674            assert!(
1675                (retuned_cents - cents).abs() < 1e-6,
1676                "{name}: {retuned_cents}"
1677            );
1678            assert_eq!(
1679                pitch
1680                    .harmonic_and_fundamental_string_from_pitch(&fundamental)
1681                    .unwrap(),
1682                text,
1683                "{name}"
1684            );
1685        }
1686        let c4 = Pitch::from_name("C4").unwrap();
1687        assert!(c4.harmonic_and_fundamental_from_pitch(&c4).is_err());
1688    }
1689
1690    #[test]
1691    fn full_name_matches_music21() {
1692        let cases = [
1693            ("C4", "C in octave 4"),
1694            ("E-4", "E-flat in octave 4"),
1695            ("F#", "F-sharp"),
1696            ("B--3", "B-double-flat in octave 3"),
1697            ("G##5", "G-double-sharp in octave 5"),
1698            ("A~4", "A-half-sharp in octave 4"),
1699            ("C`4", "C-half-flat in octave 4"),
1700            ("D#~4", "D-one-and-a-half-sharp in octave 4"),
1701        ];
1702        for (name, expected) in cases {
1703            assert_eq!(
1704                Pitch::from_name(name).unwrap().full_name(),
1705                expected,
1706                "{name}"
1707            );
1708        }
1709        let sharp = crate::PitchOptions::new()
1710            .name("C4")
1711            .microtone(20)
1712            .build()
1713            .unwrap();
1714        assert_eq!(sharp.full_name(), "C in octave 4 (+20c)");
1715        let flat = crate::PitchOptions::new()
1716            .name("E-4")
1717            .microtone(-33)
1718            .build()
1719            .unwrap();
1720        assert_eq!(flat.full_name(), "E-flat in octave 4 (-33c)");
1721        let hair = crate::PitchOptions::new()
1722            .name("E-4")
1723            .microtone(-0.2)
1724            .build()
1725            .unwrap();
1726        assert_eq!(hair.full_name(), "E-flat in octave 4 (-0c)");
1727    }
1728    use crate::defaults::{FloatType, IntegerType};
1729    use crate::interval::Interval;
1730    use crate::tuningsystem::TuningSystem;
1731
1732    use super::{
1733        Accidental, Microtone, Pitch, convert_harmonic_to_cents, simplify_multiple_enharmonics,
1734    };
1735
1736    #[test]
1737    fn harmonics_match_music21() {
1738        let cases = [
1739            ("C2", 1, "C2", None, 65.406),
1740            ("C2", 2, "C3", None, 130.813),
1741            ("C2", 3, "G3", Some("(+2c)"), 196.224),
1742            ("C2", 4, "C4", None, 261.626),
1743            ("C2", 5, "E4", Some("(-14c)"), 326.973),
1744            ("C2", 6, "G4", Some("(+2c)"), 392.449),
1745            ("C2", 7, "A~4", Some("(+19c)"), 457.891),
1746            ("C2", 8, "C5", None, 523.251),
1747            ("C2", 9, "D5", Some("(+4c)"), 588.688),
1748            ("C2", 11, "F~5", Some("(+1c)"), 719.338),
1749            ("C2", 13, "G#~5", Some("(-9c)"), 850.515),
1750            ("A2", 3, "E4", Some("(+2c)"), 330.009),
1751            ("A2", 5, "C#5", Some("(-14c)"), 549.9),
1752            ("C", 3, "G5", Some("(+2c)"), 784.897),
1753            ("Eb3", 7, "C~6", Some("(+19c)"), 1089.054),
1754        ];
1755        for (fundamental, number, name, microtone, hertz) in cases {
1756            let harmonic = Pitch::from_name(fundamental)
1757                .unwrap()
1758                .harmonic(number)
1759                .unwrap();
1760            assert_eq!(harmonic.name_with_octave(), name, "{fundamental} {number}");
1761            assert_eq!(
1762                harmonic.microtone().map(ToString::to_string).as_deref(),
1763                microtone,
1764                "{fundamental} {number}"
1765            );
1766            assert!(
1767                (harmonic.frequency_hz() - hertz).abs() < 5e-4,
1768                "{fundamental} {number}: {}",
1769                harmonic.frequency_hz()
1770            );
1771            assert_eq!(
1772                harmonic
1773                    .fundamental()
1774                    .map(Pitch::name_with_octave)
1775                    .as_deref(),
1776                (number > 1).then_some(fundamental),
1777                "{fundamental} {number}"
1778            );
1779        }
1780    }
1781
1782    #[test]
1783    fn harmonic_from_fundamental_matches_music21() {
1784        let cases = [
1785            ("G4", "C2", 6, 2.0, "6thH(-2c)/C2"),
1786            ("E5", "C2", 10, -14.0, "10thH(+14c)/C2"),
1787            ("B-4", "C2", 7, -31.0, "7thH(+31c)/C2"),
1788            ("F#5", "C2", 11, -49.0, "11thH(+49c)/C2"),
1789            ("C4", "C2", 4, 0.0, "4thH/C2"),
1790            ("E4", "A2", 3, 2.0, "3rdH(-2c)/A2"),
1791            ("G#4", "A2", 4, 100.0, "4thH(-100c)/A2"),
1792            ("A4", "A2", 4, 0.0, "4thH/A2"),
1793            ("B3", "C2", 4, 100.0, "4thH(-100c)/C2"),
1794            ("D5", "C2", 9, 4.0, "9thH(-4c)/C2"),
1795        ];
1796        for (target, fundamental, number, cents, text) in cases {
1797            let target_pitch = Pitch::from_name(target).unwrap();
1798            let fundamental_pitch = Pitch::from_name(fundamental).unwrap();
1799            let (found, gap) = target_pitch
1800                .harmonic_from_fundamental(&fundamental_pitch)
1801                .unwrap();
1802            assert_eq!(found, number, "{target} over {fundamental}");
1803            assert!(
1804                (gap - cents).abs() < 1e-6,
1805                "{target} over {fundamental}: {gap}"
1806            );
1807            assert_eq!(
1808                target_pitch
1809                    .harmonic_string(Some(&fundamental_pitch))
1810                    .unwrap(),
1811                text
1812            );
1813        }
1814        let c2 = Pitch::from_name("C2").unwrap();
1815        assert!(
1816            c2.harmonic_from_fundamental(&Pitch::from_name("C4").unwrap())
1817                .is_err()
1818        );
1819        assert!(c2.harmonic_string(None).is_err());
1820        assert_eq!(
1821            c2.harmonic(5).unwrap().harmonic_string(None).unwrap(),
1822            "5thH/C2"
1823        );
1824    }
1825
1826    #[test]
1827    fn enharmonic_helpers_match_music21() {
1828        let names = |pitches: Vec<Pitch>| {
1829            pitches
1830                .iter()
1831                .map(Pitch::name_with_octave)
1832                .collect::<Vec<_>>()
1833        };
1834        let cases = [
1835            (
1836                "C#4",
1837                vec!["Db4", "B##3"],
1838                vec!["Db4"],
1839                vec!["Db4", "Ebbb4", "B##3"],
1840            ),
1841            (
1842                "E-",
1843                vec!["Fbb", "D#"],
1844                vec!["D#"],
1845                vec!["Fbb", "D#", "C###"],
1846            ),
1847            ("B#3", vec!["C4"], vec!["C4"], vec!["C4", "A###3"]),
1848            ("F##4", vec!["G4"], vec!["G4"], vec!["G4", "E###4"]),
1849            (
1850                "C4",
1851                vec!["Dbb4", "B#3"],
1852                vec!["B#3"],
1853                vec!["Dbb4", "B#3", "A###3"],
1854            ),
1855            (
1856                "G-2",
1857                vec!["F#2", "E##2"],
1858                vec!["F#2"],
1859                vec!["Abbb2", "F#2", "E##2"],
1860            ),
1861            (
1862                "A4",
1863                vec!["Bbb4", "G##4"],
1864                vec![],
1865                vec!["Bbb4", "Cbbb5", "G##4"],
1866            ),
1867            (
1868                "B-4",
1869                vec!["Cbb5", "A#4"],
1870                vec!["A#4"],
1871                vec!["Cbb5", "A#4", "G###4"],
1872            ),
1873            ("E#5", vec!["F5"], vec!["F5"], vec!["F5", "D###5"]),
1874            ("Dbb4", vec!["C4"], vec!["C4"], vec!["C4"]),
1875        ];
1876        for (name, limit_two, limit_one, limit_three) in cases {
1877            let pitch = Pitch::from_name(name).unwrap();
1878            assert_eq!(names(pitch.all_common_enharmonics(2)), limit_two, "{name}");
1879            assert_eq!(names(pitch.all_common_enharmonics(1)), limit_one, "{name}");
1880            assert_eq!(
1881                names(pitch.all_common_enharmonics(3)),
1882                limit_three,
1883                "{name}"
1884            );
1885        }
1886
1887        let enharmonic = |left: &str, right: &str| {
1888            Pitch::from_name(left)
1889                .unwrap()
1890                .is_enharmonic(&Pitch::from_name(right).unwrap())
1891        };
1892        assert!(enharmonic("C#4", "D-4"));
1893        assert!(!enharmonic("C#4", "D-5"));
1894        assert!(enharmonic("C#", "D-5"));
1895        assert!(!enharmonic("C#4", "C4"));
1896        assert!(enharmonic("B#3", "C4"));
1897        assert!(enharmonic("B#", "C"));
1898        assert!(enharmonic("C", "C5"));
1899    }
1900
1901    #[test]
1902    fn transposing_to_a_target_matches_music21() {
1903        let g3 = Pitch::from_name("G3").unwrap();
1904        let below = [
1905            ("C4", false, "C3"),
1906            ("C4", true, "C3"),
1907            ("C6", false, "C3"),
1908            ("C6", true, "C3"),
1909            ("G3", false, "G3"),
1910            ("C2", false, "C2"),
1911            ("C2", true, "C3"),
1912            ("F4", true, "F3"),
1913            ("G4", true, "G3"),
1914        ];
1915        for (source, minimize, expected) in below {
1916            let moved = Pitch::from_name(source)
1917                .unwrap()
1918                .transpose_below_target(&g3, minimize)
1919                .unwrap();
1920            assert_eq!(
1921                moved.name_with_octave(),
1922                expected,
1923                "{source} below {minimize}"
1924            );
1925        }
1926        let above = [
1927            ("C4", false, "C4"),
1928            ("C1", false, "C4"),
1929            ("C1", true, "C4"),
1930            ("C6", false, "C6"),
1931            ("C6", true, "C4"),
1932            ("G3", false, "G3"),
1933            ("F#3", true, "F#4"),
1934        ];
1935        for (source, minimize, expected) in above {
1936            let moved = Pitch::from_name(source)
1937                .unwrap()
1938                .transpose_above_target(&g3, minimize)
1939                .unwrap();
1940            assert_eq!(
1941                moved.name_with_octave(),
1942                expected,
1943                "{source} above {minimize}"
1944            );
1945        }
1946        assert!(
1947            Pitch::from_name("C")
1948                .unwrap()
1949                .transpose_below_target(&g3, false)
1950                .is_err()
1951        );
1952    }
1953
1954    #[test]
1955    fn twelve_tone_and_frequency_construction() {
1956        assert!(Pitch::from_name("C4").unwrap().is_twelve_tone());
1957        assert!(Pitch::from_name("C#4").unwrap().is_twelve_tone());
1958        assert!(!Pitch::from_name("C~4").unwrap().is_twelve_tone());
1959        assert!(!Pitch::from_name("C`4").unwrap().is_twelve_tone());
1960        let detuned = Pitch::builder().name("A4").microtone(25).build().unwrap();
1961        assert!(!detuned.is_twelve_tone());
1962        assert_eq!(detuned.ps(), 69.25);
1963
1964        let g4 =
1965            Pitch::from_frequency(440.0 * (2.0 as FloatType).powf((67.02 - 69.0) / 12.0)).unwrap();
1966        assert_eq!(g4.name_with_octave(), "G4");
1967        assert_eq!(g4.microtone().unwrap().to_string(), "(+2c)");
1968        assert!((g4.ps() - 67.02).abs() < 1e-6);
1969
1970        let low = Pitch::from_frequency(100.0).unwrap();
1971        assert_eq!(low.name_with_octave(), "G~2");
1972        assert_eq!(low.microtone().unwrap().to_string(), "(-15c)");
1973        assert!(Pitch::from_frequency(0.0).is_err());
1974        assert_eq!(Pitch::from_name("C4").unwrap().diatonic_note_number(), 29);
1975        assert_eq!(Pitch::from_name("B#3").unwrap().diatonic_note_number(), 28);
1976    }
1977
1978    #[test]
1979    fn language_names_match_music21() {
1980        let cases = [
1981            ("C", "C", "do", "do", "do", "C"),
1982            ("C#", "Cis", "do diesis", "do dièse", "do sostenido", "C♯"),
1983            ("D-", "Des", "re bemolle", "ré bémol", "re bemol", "D♭"),
1984            ("B", "H", "si", "si", "si", "B"),
1985            ("B-", "B", "si bemolle", "si bémol", "si bemol", "B♭"),
1986            ("B#", "His", "si diesis", "si dièse", "si sostenido", "B♯"),
1987            ("E-", "Es", "mi bemolle", "mi bémol", "mi bemol", "E♭"),
1988            ("A-", "As", "la bemolle", "la bémol", "la bemol", "A♭"),
1989            (
1990                "F##",
1991                "Fisis",
1992                "fa doppio diesis",
1993                "fa double dièse",
1994                "fa doble sostenido",
1995                "F𝄪",
1996            ),
1997            (
1998                "G--",
1999                "Geses",
2000                "sol doppio bemolle",
2001                "sol double bémol",
2002                "sol doble bemol",
2003                "G𝄫",
2004            ),
2005            (
2006                "B--",
2007                "Heses",
2008                "si doppio bemolle",
2009                "si double bémol",
2010                "si doble bemol",
2011                "B𝄫",
2012            ),
2013            ("F-", "Fes", "fa bemolle", "fa bémol", "fa bemol", "F♭"),
2014            ("E#", "Eis", "mi diesis", "mi dièse", "mi sostenido", "E♯"),
2015            (
2016                "D##",
2017                "Disis",
2018                "re doppio diesis",
2019                "ré double dièse",
2020                "re doble sostenido",
2021                "D𝄪",
2022            ),
2023            (
2024                "A####",
2025                "Aisisisis",
2026                "la quadruplo diesis",
2027                "la quadruple dièse",
2028                "la cuádruple sostenido",
2029                "A𝄪𝄪",
2030            ),
2031            (
2032                "B----",
2033                "Heseseses",
2034                "si quadruplo bemolle",
2035                "si quadruple bémol",
2036                "si cuádruple bemol",
2037                "B𝄫𝄫",
2038            ),
2039        ];
2040        for (name, german, italian, french, spanish, unicode) in cases {
2041            let pitch = Pitch::from_name(name).unwrap();
2042            assert_eq!(pitch.german().unwrap(), german, "{name}");
2043            assert_eq!(pitch.italian().unwrap(), italian, "{name}");
2044            assert_eq!(pitch.french().unwrap(), french, "{name}");
2045            assert_eq!(pitch.spanish().unwrap(), spanish, "{name}");
2046            assert_eq!(pitch.unicode_name(), unicode, "{name}");
2047        }
2048        assert_eq!(
2049            Pitch::from_name("A#4").unwrap().unicode_name_with_octave(),
2050            "A♯4"
2051        );
2052        assert_eq!(Pitch::from_name("E-5").unwrap().german().unwrap(), "Es");
2053        let quarter_sharp = Pitch::from_name("C~").unwrap();
2054        assert!(quarter_sharp.german().is_err());
2055        assert!(quarter_sharp.italian().is_err());
2056        assert!(quarter_sharp.french().is_err());
2057        assert!(quarter_sharp.spanish().is_err());
2058    }
2059
2060    #[test]
2061    fn fundamental_round_trips_through_the_builder() {
2062        let pitch = Pitch::builder()
2063            .name("E4")
2064            .fundamental(Pitch::from_name("C2").unwrap())
2065            .build()
2066            .unwrap();
2067        assert_eq!(
2068            pitch.fundamental().map(Pitch::name_with_octave),
2069            Some("C2".to_string())
2070        );
2071        assert_eq!(Pitch::from_name("E4").unwrap().fundamental(), None);
2072    }
2073
2074    #[test]
2075    fn simplify_multiple_enharmonics_test() {
2076        let more_than_five = vec![
2077            Pitch::from_number(0.0).unwrap(),
2078            Pitch::from_number(1.0).unwrap(),
2079            Pitch::from_number(2.0).unwrap(),
2080            Pitch::from_number(3.0).unwrap(),
2081            Pitch::from_number(4.0).unwrap(),
2082            Pitch::from_number(5.0).unwrap(),
2083            Pitch::from_number(12.0).unwrap(),
2084            Pitch::from_number(13.0).unwrap(),
2085        ];
2086
2087        let _x = simplify_multiple_enharmonics(&more_than_five, None, None);
2088        let _less_than_five = [
2089            Pitch::from_number(0.0),
2090            Pitch::from_number(1.0),
2091            Pitch::from_number(2.0),
2092            Pitch::from_number(12.0),
2093            Pitch::from_number(13.0),
2094        ];
2095    }
2096
2097    #[test]
2098    fn test_convert_harmonic_to_cents_values() {
2099        assert_eq!(convert_harmonic_to_cents(8), 3600);
2100        assert_eq!(convert_harmonic_to_cents(5), 2786);
2101        assert_eq!(convert_harmonic_to_cents(-2), -1200);
2102    }
2103
2104    #[test]
2105    fn test_pitch_transpose_interval() {
2106        let c4 = Pitch::from_name("C4").unwrap();
2107        let m3 = Interval::from_name("m3").unwrap();
2108        let out = c4.transpose(&m3).unwrap();
2109        assert_eq!(out.name_with_octave(), "Eb4");
2110    }
2111
2112    #[test]
2113    fn test_pitch_frequency_helpers() {
2114        let a4 = Pitch::from_name("A4").unwrap();
2115        assert!((a4.frequency_hz() - 440.0).abs() < 0.0001);
2116
2117        let e4 = Pitch::from_name("E4").unwrap();
2118        assert!((e4.frequency_hz_in(TuningSystem::FiveLimit) - 327.032).abs() < 0.001);
2119        assert!(e4.frequency_hz_in(TuningSystem::FiveLimit) < e4.frequency_hz());
2120    }
2121
2122    #[test]
2123    fn pitch_exposes_accidental_object() {
2124        let custom_accidental = Accidental::new("half-flat").unwrap();
2125        let pitch = Pitch::builder()
2126            .step('D')
2127            .accidental(custom_accidental.clone())
2128            .octave(4)
2129            .build()
2130            .unwrap();
2131
2132        assert_eq!(pitch.name_with_octave(), "D`4");
2133        assert_eq!(pitch.accidental(), &custom_accidental);
2134        assert_eq!(pitch.accidental().name(), "half-flat");
2135        assert_eq!(pitch.accidental().alter(), -0.5);
2136    }
2137
2138    #[test]
2139    fn pitch_exposes_microtone_object() {
2140        let microtone = Microtone::new(-25.0).unwrap();
2141        let pitch = Pitch::builder()
2142            .name("G#4")
2143            .microtone(microtone.clone())
2144            .build()
2145            .unwrap();
2146
2147        assert_eq!(pitch.microtone(), Some(&microtone));
2148        assert_eq!(pitch.microtone().unwrap().to_string(), "(-25c)");
2149        assert_eq!(pitch.alter(), 0.75);
2150    }
2151
2152    #[test]
2153    fn pitch_supports_rust_conversion_traits() {
2154        let parsed: Pitch = "C#4".parse().unwrap();
2155        assert_eq!(parsed.to_string(), "C#4");
2156
2157        let midi = Pitch::try_from(60 as IntegerType).unwrap();
2158        assert_eq!(midi.name_with_octave(), "C4");
2159        assert_eq!(midi.midi(), 60);
2160
2161        let pitch_space = Pitch::try_from(61.5).unwrap();
2162        assert_eq!(pitch_space.pitch_space(), 61.5);
2163        assert_eq!(pitch_space.midi(), 62);
2164
2165        let built = Pitch::builder().pitch_space(60.0).build().unwrap();
2166        assert_eq!(built.name_with_octave(), "C4");
2167    }
2168
2169    #[test]
2170    fn pitch_exposes_enharmonic_helpers() {
2171        let c_sharp = Pitch::from_name("C#3").unwrap();
2172        let out = c_sharp.get_higher_enharmonic().unwrap();
2173        assert_eq!(out.name_with_octave(), "Db3");
2174
2175        let mut d_flat = out;
2176        d_flat.get_lower_enharmonic_in_place().unwrap();
2177        assert_eq!(d_flat.name_with_octave(), "C#3");
2178
2179        let d_sharp = Pitch::from_name("D#4").unwrap();
2180        assert_eq!(
2181            d_sharp
2182                .simplify_enharmonic(true)
2183                .unwrap()
2184                .name_with_octave(),
2185            "Eb4"
2186        );
2187    }
2188}