Skip to main content

music21_rs/pitch/
mod.rs

1pub(crate) mod accidental;
2pub(crate) mod microtone;
3pub(crate) mod pitchclass;
4pub(crate) mod pitchclassstring;
5
6use crate::defaults::FloatType;
7use crate::defaults::IntegerType;
8use crate::defaults::Octave;
9use crate::defaults::PITCH_OCTAVE;
10use crate::defaults::PITCH_SPACE_SIGNIFICANT_DIGITS;
11use crate::defaults::PITCH_STEP;
12use crate::defaults::UnsignedIntegerType;
13use crate::error::Error;
14use crate::error::Result;
15use crate::interval::Interval;
16use crate::interval::IntervalArgument;
17use crate::interval::PitchOrNote;
18use crate::interval::intervalstring::IntervalString;
19use crate::key::keysignature::KeySignature;
20use crate::note::Note;
21use crate::stepname::StepName;
22use crate::tuningsystem::OCTAVE_SIZE;
23use crate::tuningsystem::TuningSystem;
24
25use accidental::IntoAccidental;
26pub use accidental::{Accidental, AccidentalSpecifier};
27use microtone::IntoCentShift;
28pub use microtone::{Microtone, MicrotoneSpecifier};
29use pitchclass::convert_ps_to_oct;
30pub use pitchclass::{PitchClass, PitchClassSpecifier};
31
32use itertools::Itertools;
33use num::Num;
34use num_traits::ToPrimitive;
35use ordered_float::OrderedFloat;
36use std::cmp::Ordering;
37use std::collections::HashMap;
38use std::fmt::{Display, Formatter};
39use std::str::FromStr;
40use std::sync::Arc;
41use std::sync::LazyLock;
42use std::sync::Mutex;
43
44/// Canonical pitch names for chromatic pitch classes.
45pub const CHROMATIC_PITCH_CLASS_NAMES: [&str; 12] = [
46    "C", "D-", "D", "E-", "E", "F", "F#", "G", "A-", "A", "B-", "B",
47];
48
49/// Returns a canonical pitch name for a chromatic pitch class.
50pub fn pitch_class_name(pitch_class: u8) -> &'static str {
51    CHROMATIC_PITCH_CLASS_NAMES[pitch_class as usize % 12]
52}
53
54// TODO: rework this, don't use a HashMap for two possible inputs, either figure
55// out what the -d2 and d2 intervals are beforehand or caculate them and store
56// them each in a static
57static TRANSPOSITIONAL_INTERVALS: LazyLock<Mutex<HashMap<IntervalString, Interval>>> =
58    LazyLock::new(|| Mutex::new(HashMap::new()));
59
60#[derive(Clone, Debug, PartialEq)]
61#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
62/// Input accepted as a pitch name or pitch-space number.
63pub enum PitchName {
64    /// A written pitch name such as `"C#4"` or `"E-"`.
65    Name(String),
66    /// A pitch-space number, where 60 corresponds to middle C.
67    Number(FloatType),
68}
69
70impl From<&str> for PitchName {
71    fn from(value: &str) -> Self {
72        Self::Name(value.to_string())
73    }
74}
75
76impl From<String> for PitchName {
77    fn from(value: String) -> Self {
78        Self::Name(value)
79    }
80}
81
82impl From<IntegerType> for PitchName {
83    fn from(value: IntegerType) -> Self {
84        Self::Number(value as FloatType)
85    }
86}
87
88impl From<FloatType> for PitchName {
89    fn from(value: FloatType) -> Self {
90        Self::Number(value)
91    }
92}
93
94#[derive(Clone, Debug, Default, PartialEq)]
95#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
96/// Builder options for constructing a [`Pitch`].
97pub struct PitchOptions {
98    /// Pitch name or pitch-space number.
99    pub name: Option<PitchName>,
100    /// Diatonic step name.
101    pub step: Option<char>,
102    /// Octave number.
103    pub octave: Octave,
104    /// Accidental name or alteration.
105    pub accidental: Option<AccidentalSpecifier>,
106    /// Microtone cent offset.
107    pub microtone: Option<MicrotoneSpecifier>,
108    /// Pitch class to realize as a pitch.
109    pub pitch_class: Option<PitchClassSpecifier>,
110    /// MIDI note number.
111    pub midi: Option<IntegerType>,
112    /// Pitch-space value.
113    pub ps: Option<FloatType>,
114    /// Fundamental pitch used for harmonic construction.
115    pub fundamental: Option<Pitch>,
116}
117
118impl PitchOptions {
119    /// Creates an empty pitch builder.
120    pub fn new() -> Self {
121        Self::default()
122    }
123
124    /// Sets the pitch name or pitch-space number.
125    pub fn name(mut self, name: impl Into<PitchName>) -> Self {
126        self.name = Some(name.into());
127        self
128    }
129
130    /// Sets the diatonic step.
131    pub fn step(mut self, step: char) -> Self {
132        self.step = Some(step);
133        self
134    }
135
136    /// Sets the octave.
137    pub fn octave(mut self, octave: IntegerType) -> Self {
138        self.octave = Some(octave);
139        self
140    }
141
142    /// Sets the accidental.
143    pub fn accidental(mut self, accidental: impl Into<AccidentalSpecifier>) -> Self {
144        self.accidental = Some(accidental.into());
145        self
146    }
147
148    /// Sets the microtone.
149    pub fn microtone(mut self, microtone: impl Into<MicrotoneSpecifier>) -> Self {
150        self.microtone = Some(microtone.into());
151        self
152    }
153
154    /// Sets the pitch class.
155    pub fn pitch_class(mut self, pitch_class: impl Into<PitchClassSpecifier>) -> Self {
156        self.pitch_class = Some(pitch_class.into());
157        self
158    }
159
160    /// Sets the MIDI note number.
161    pub fn midi(mut self, midi: IntegerType) -> Self {
162        self.midi = Some(midi);
163        self
164    }
165
166    /// Sets the pitch-space value.
167    pub fn ps(mut self, ps: FloatType) -> Self {
168        self.ps = Some(ps);
169        self
170    }
171
172    /// Sets the pitch-space value.
173    pub fn pitch_space(mut self, pitch_space: FloatType) -> Self {
174        self.ps = Some(pitch_space);
175        self
176    }
177
178    /// Sets the fundamental pitch.
179    pub fn fundamental(mut self, fundamental: Pitch) -> Self {
180        self.fundamental = Some(fundamental);
181        self
182    }
183
184    /// Builds a [`Pitch`] from the collected options.
185    pub fn build(self) -> Result<Pitch> {
186        Pitch::from_options(self)
187    }
188}
189
190#[derive(Clone, Debug)]
191#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
192/// A musical pitch with spelling, octave, accidental and optional microtone.
193pub struct Pitch {
194    _step: StepName,
195    _octave: Octave,
196    _overriden_freq440: Option<FloatType>,
197    _accidental: Accidental,
198    _microtone: Option<Microtone>,
199    #[cfg_attr(feature = "serde", serde(skip))]
200    _client: Option<Arc<Note>>,
201    spelling_is_infered: bool,
202    #[cfg_attr(feature = "serde", serde(skip))]
203    fundamental: Option<Arc<Pitch>>,
204}
205
206impl PartialEq for Pitch {
207    fn eq(&self, other: &Self) -> bool {
208        self._step == other._step
209            && self._octave == other._octave
210            && self._accidental == other._accidental
211            && self._microtone == other._microtone
212    }
213}
214
215impl FromStr for Pitch {
216    type Err = Error;
217
218    fn from_str(value: &str) -> Result<Self> {
219        Self::from_name(value)
220    }
221}
222
223impl TryFrom<&str> for Pitch {
224    type Error = Error;
225
226    fn try_from(value: &str) -> Result<Self> {
227        Self::from_name(value)
228    }
229}
230
231impl TryFrom<String> for Pitch {
232    type Error = Error;
233
234    fn try_from(value: String) -> Result<Self> {
235        Self::from_name(value)
236    }
237}
238
239impl TryFrom<&Pitch> for Pitch {
240    type Error = Error;
241
242    fn try_from(value: &Pitch) -> Result<Self> {
243        Ok(value.clone())
244    }
245}
246
247impl TryFrom<IntegerType> for Pitch {
248    type Error = Error;
249
250    fn try_from(value: IntegerType) -> Result<Self> {
251        Self::from_midi(value)
252    }
253}
254
255impl TryFrom<FloatType> for Pitch {
256    type Error = Error;
257
258    fn try_from(value: FloatType) -> Result<Self> {
259        Self::from_pitch_space(value)
260    }
261}
262
263impl Display for Pitch {
264    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
265        write!(f, "{}", self.name_with_octave())
266    }
267}
268
269impl Pitch {
270    /// Builds a pitch from [`PitchOptions`].
271    pub fn from_options(options: PitchOptions) -> Result<Self> {
272        let step = options.step.map(StepName::try_from).transpose()?;
273
274        Self::new(
275            options.name,
276            step,
277            options.octave,
278            options.accidental,
279            options.microtone,
280            options.pitch_class,
281            options.midi,
282            options.ps,
283            options.fundamental,
284        )
285    }
286
287    /// Creates a [`PitchOptions`] builder.
288    pub fn builder() -> PitchOptions {
289        PitchOptions::new()
290    }
291
292    /// Builds a pitch from a name such as `"C#4"` or `"E-"`.
293    pub fn from_name(name: impl Into<String>) -> Result<Self> {
294        Self::new(
295            Some(name.into()),
296            None,
297            None,
298            Option::<IntegerType>::None,
299            Option::<IntegerType>::None,
300            None,
301            None,
302            None,
303            None,
304        )
305    }
306
307    /// Builds a pitch from a pitch-space number.
308    pub fn from_number(number: FloatType) -> Result<Self> {
309        Self::new(
310            Some(PitchName::Number(number)),
311            None,
312            None,
313            Option::<IntegerType>::None,
314            Option::<IntegerType>::None,
315            None,
316            None,
317            None,
318            None,
319        )
320    }
321
322    /// Builds a pitch from a diatonic step.
323    pub fn from_step(step: char) -> Result<Self> {
324        Self::new(
325            Option::<String>::None,
326            Some(StepName::try_from(step)?),
327            None,
328            Option::<IntegerType>::None,
329            Option::<IntegerType>::None,
330            None,
331            None,
332            None,
333            None,
334        )
335    }
336
337    /// Builds a pitch from a pitch name and explicit octave.
338    pub fn from_name_and_octave(name: impl Into<String>, octave: IntegerType) -> Result<Self> {
339        PitchOptions::new().name(name.into()).octave(octave).build()
340    }
341
342    /// Builds a pitch from a pitch class.
343    pub fn from_pitch_class(pitch_class: impl Into<PitchClassSpecifier>) -> Result<Self> {
344        PitchOptions::new().pitch_class(pitch_class).build()
345    }
346
347    /// Builds a pitch from a MIDI note number.
348    pub fn from_midi(midi: IntegerType) -> Result<Self> {
349        Self::new(
350            Option::<String>::None,
351            None,
352            None,
353            Option::<IntegerType>::None,
354            Option::<IntegerType>::None,
355            None,
356            Some(midi),
357            None,
358            None,
359        )
360    }
361
362    /// Builds a pitch from a pitch-space value.
363    pub fn from_pitch_space(ps: FloatType) -> Result<Self> {
364        Self::new(
365            Option::<String>::None,
366            None,
367            None,
368            Option::<IntegerType>::None,
369            Option::<IntegerType>::None,
370            None,
371            None,
372            Some(ps),
373            None,
374        )
375    }
376
377    #[allow(clippy::too_many_arguments)]
378    pub(crate) fn new<T, U, V>(
379        name: Option<T>,
380        step: Option<StepName>,
381        octave: Octave,
382        accidental: Option<U>,
383        microtone: Option<V>,
384        pitch_class: Option<PitchClassSpecifier>,
385        midi: Option<IntegerType>,
386        ps: Option<FloatType>,
387        fundamental: Option<Pitch>,
388    ) -> Result<Self>
389    where
390        T: IntoPitchName,
391        U: IntoAccidental,
392        V: IntoCentShift,
393    {
394        let has_explicit_step = step.is_some();
395        let has_explicit_octave = octave.is_some();
396        let has_explicit_accidental = accidental.is_some();
397        let has_explicit_microtone = microtone.is_some();
398
399        // --- Step 1: Parse parameters ---
400        let mut self_name = None;
401        let mut self_step = PITCH_STEP;
402        let mut self_accidental: Option<Accidental> = None;
403        let mut self_microtone: Option<Microtone> = None;
404        let mut self_spelling_is_inferred = false;
405        let mut self_octave = None;
406        let self_pitch_class = pitch_class;
407        let self_fundamental = fundamental;
408        let self_midi = midi;
409        let self_ps = ps;
410
411        if let Some(name) = name {
412            let x = name.into_name();
413            self_name = x.name;
414            if let Some(step) = x.step {
415                self_step = step;
416            }
417            if let Some(accidental) = x.accidental {
418                self_accidental = Some(accidental);
419            }
420            if let Some(inferred) = x.spelling_is_inferred {
421                self_spelling_is_inferred = inferred;
422            }
423            self_octave = x.octave;
424        } else if let Some(s) = step {
425            self_step = s;
426        }
427
428        if let Some(oct) = octave {
429            self_octave = Some(oct);
430        }
431
432        if let Some(acc) = accidental {
433            self_accidental = Some(if acc.is_accidental() {
434                acc.accidental()
435            } else {
436                acc.into_accidental()?
437            });
438        } else if self_accidental.is_none() {
439            self_accidental = Some(Accidental::new("natural")?);
440        }
441
442        if let Some(mt) = microtone {
443            self_microtone = Some(if mt.is_microtone() {
444                mt.microtone()
445            } else {
446                mt.into_microtone()?
447            });
448        }
449
450        // --- Step 2: Construct Pitch with initial values ---
451        let mut pitch = Pitch {
452            _step: self_step,
453            _overriden_freq440: None,
454            _accidental: self_accidental.clone().unwrap(),
455            _microtone: self_microtone,
456            _octave: self_octave,
457            _client: None,
458            spelling_is_infered: self_spelling_is_inferred,
459            fundamental: None,
460        };
461
462        // --- Step 3: Call setters in proper order ---
463        if let Some(ref n) = self_name {
464            pitch.name_setter(n)?;
465        }
466
467        if has_explicit_step || self_name.is_none() {
468            pitch.step_setter(self_step);
469        }
470
471        if has_explicit_octave || self_name.is_none() {
472            pitch.octave_setter(self_octave);
473        }
474
475        if has_explicit_accidental || self_name.is_none() {
476            pitch.accidental_setter(pitch._accidental.clone());
477        }
478        if has_explicit_microtone {
479            let Some(mt) = pitch._microtone.clone() else {
480                return Err(Error::Pitch(
481                    "microtone was expected but missing".to_string(),
482                ));
483            };
484            pitch.microtone_setter(mt.clone());
485        }
486        if let Some(pc) = self_pitch_class {
487            pitch.pitch_class_setter(pc)?;
488        }
489        if let Some(f) = self_fundamental {
490            pitch.fundamental_setter(f);
491        }
492        if let Some(m) = self_midi {
493            pitch.midi_setter(m);
494        }
495        if let Some(p) = self_ps {
496            pitch.ps_setter(p);
497        }
498
499        Ok(pitch)
500    }
501
502    /// Returns the pitch name with the octave suffix when one is set.
503    pub fn name_with_octave(&self) -> String {
504        match self._octave {
505            Some(octave) => format!("{}{}", self.name(), octave),
506            None => self.name(),
507        }
508    }
509
510    /// Returns the pitch name without octave, such as `"F#"` or `"B-"`.
511    pub fn name(&self) -> String {
512        format!("{:?}{}", self._step, self._accidental.modifier())
513    }
514
515    fn name_setter(&mut self, usr_str: &str) -> Result<()> {
516        let usr_str = usr_str.trim();
517
518        let digit_index = usr_str
519            .char_indices()
520            .find(|&(_, c)| c.is_ascii_digit())
521            .map(|(i, _)| i);
522
523        let (pitch_part, octave_part) = if let Some(i) = digit_index {
524            if i == 0 {
525                return Err(Error::Pitch(format!(
526                    "Cannot have octave given before pitch name in {usr_str:?}"
527                )));
528            }
529            (&usr_str[..i], &usr_str[i..])
530        } else {
531            (usr_str, "")
532        };
533
534        // Process the pitch part.
535        let mut pitch_chars = pitch_part.chars();
536        let step = pitch_chars.next().ok_or(Error::Pitch(format!(
537            "Cannot make a name out of {pitch_part:?}"
538        )))?;
539        self.step_setter(StepName::try_from(step)?);
540
541        let accidental_str: String = pitch_chars.collect();
542        if accidental_str.is_empty() {
543            self.accidental_setter(Accidental::natural());
544        } else {
545            self.accidental_setter(Accidental::new(accidental_str)?);
546        }
547
548        if !octave_part.is_empty() {
549            let octave = octave_part
550                .parse::<IntegerType>()
551                .map_err(|_| Error::Pitch(format!("Cannot parse {octave_part:?} to octave")))?;
552            self.octave_setter(Some(octave));
553        }
554
555        Ok(())
556    }
557
558    /// Returns the total semitone alteration from the natural step.
559    pub fn alter(&self) -> FloatType {
560        let mut post = 0.0;
561
562        post += self._accidental._alter;
563
564        if let Some(microtone) = &self._microtone {
565            post += microtone.alter();
566        }
567
568        post
569    }
570
571    /// Returns this pitch's accidental object.
572    ///
573    /// Unlike Python music21, this crate stores an explicit natural accidental
574    /// for natural pitches.
575    pub fn accidental(&self) -> &Accidental {
576        &self._accidental
577    }
578
579    /// Returns this pitch's microtone adjustment, when present.
580    pub fn microtone(&self) -> Option<&Microtone> {
581        self._microtone.as_ref()
582    }
583
584    /// Returns this pitch's normalized pitch class.
585    pub fn pitch_class(&self) -> PitchClass {
586        PitchClass::from_number(self.ps()).unwrap_or_else(|err| {
587            panic!("pitch-space value should always map to pitch class: {err}")
588        })
589    }
590
591    pub(crate) fn octave_setter(&mut self, octave: Octave) {
592        self._octave = octave;
593        self.inform_client()
594    }
595
596    fn get_all_common_enharmonics(&mut self, alter_limit: FloatType) -> Result<Vec<Pitch>> {
597        let mut post = Vec::new();
598
599        let simplified = self.clone().simplify_enharmonic(false)?;
600        if simplified.name() != self.name() {
601            post.push(simplified);
602        }
603
604        let mut higher = self.clone();
605        while let Ok(next) = higher.get_higher_enharmonic() {
606            if next._accidental._alter.abs() > alter_limit {
607                break;
608            }
609            if post.contains(&next) {
610                break;
611            }
612            post.push(next.clone());
613            higher = next;
614        }
615
616        let mut lower = self.clone();
617        while let Ok(next) = lower.get_lower_enharmonic() {
618            if next._accidental._alter.abs() > alter_limit {
619                break;
620            }
621            if post.contains(&next) {
622                break;
623            }
624            post.push(next.clone());
625            lower = next;
626        }
627
628        Ok(post)
629    }
630
631    fn inform_client(&self) {
632        if let Some(ref client) = self._client {
633            client.pitch_changed();
634        }
635    }
636
637    pub(crate) fn transpose(&self, clone: Interval) -> Pitch {
638        let mut p = clone
639            .clone()
640            .transpose_pitch_with_options(self, false, Some(4))
641            .unwrap_or_else(|_| self.clone());
642
643        if !clone.implicit_diatonic {
644            p.spelling_is_infered = self.spelling_is_infered;
645        }
646        if p.spelling_is_infered {
647            let _ = p.simplify_enharmonic_in_place(true);
648        }
649
650        p
651    }
652
653    /// Returns the pitch-space value for this pitch.
654    pub fn ps(&self) -> FloatType {
655        self.pitch_space()
656    }
657
658    /// Returns the pitch-space value for this pitch.
659    pub fn pitch_space(&self) -> FloatType {
660        let octave = self._octave.unwrap_or(PITCH_OCTAVE as IntegerType);
661        ((octave + 1) * 12) as FloatType + self._step.step_ref() as FloatType + self.alter()
662    }
663
664    /// Returns the nearest MIDI note number for this pitch.
665    pub fn midi(&self) -> IntegerType {
666        self.pitch_space().round() as IntegerType
667    }
668
669    /// Returns this pitch's twelve-tone equal-temperament frequency in hertz.
670    pub fn frequency_hz(&self) -> FloatType {
671        self.frequency_hz_in(TuningSystem::EqualTemperament {
672            octave_size: OCTAVE_SIZE,
673        })
674    }
675
676    /// Returns this pitch's frequency in hertz for a supported tuning system.
677    ///
678    /// The pitch-space value is used as the tuning-system degree index, so this
679    /// is most musically meaningful for twelve-tone systems.
680    pub fn frequency_hz_in(&self, tuning_system: TuningSystem) -> FloatType {
681        tuning_system.frequency_at(self.pitch_space())
682    }
683
684    fn step_setter(&mut self, step_name: StepName) {
685        self._step = step_name;
686        self.spelling_is_infered = true;
687        self.inform_client();
688    }
689
690    fn accidental_setter(&mut self, value: Accidental) {
691        self._accidental = value;
692        self.inform_client();
693    }
694
695    fn microtone_setter(&mut self, mt: Microtone) {
696        self._microtone = Some(mt);
697        self.inform_client();
698    }
699
700    fn pitch_class_setter(&mut self, pc: PitchClassSpecifier) -> Result<()> {
701        self.pitch_class_value_setter(PitchClass::new(pc)?.number());
702        Ok(())
703    }
704
705    fn pitch_class_value_setter(&mut self, pc: FloatType) {
706        let (step, accidental, _microtone, _harmonic_shift) = convert_ps_to_step(pc);
707        self._step = step;
708        self._accidental = accidental;
709        self.spelling_is_infered = true;
710        self.inform_client();
711    }
712
713    fn fundamental_setter(&mut self, f: Pitch) {
714        self.fundamental = Some(Arc::new(f));
715        self.inform_client();
716    }
717
718    fn midi_setter(&mut self, m: IntegerType) {
719        self.ps_setter(normalize_midi(m) as FloatType);
720    }
721
722    fn ps_setter(&mut self, p: FloatType) {
723        let (step, accidental, microtone, octave_shift) = convert_ps_to_step(p);
724        self._step = step;
725        self._accidental = accidental;
726        if microtone.alter() == 0.0 {
727            self._microtone = None;
728        } else {
729            self._microtone = Some(microtone);
730        }
731
732        let octave = convert_ps_to_oct(p) + octave_shift;
733        self._octave = Some(octave);
734        self.spelling_is_infered = true;
735        self.inform_client();
736    }
737
738    /// Returns a simpler enharmonic spelling of this pitch.
739    ///
740    /// When `most_common` is true, common spellings such as `E-` are preferred
741    /// over less common equivalents such as `D#`, following music21's
742    /// `Pitch.simplifyEnharmonic` behavior.
743    pub fn simplify_enharmonic(&self, most_common: bool) -> Result<Pitch> {
744        let mut pitch = self.clone();
745        pitch.simplify_enharmonic_in_place(most_common)?;
746        Ok(pitch)
747    }
748
749    /// Simplifies this pitch's enharmonic spelling in place.
750    pub fn simplify_enharmonic_in_place(&mut self, most_common: bool) -> Result<()> {
751        const EXCLUDED_NAMES: [&str; 4] = ["E#", "B#", "C-", "F-"];
752        if self._accidental._alter.abs().partial_cmp(&2.0) != Some(Ordering::Less)
753            || EXCLUDED_NAMES.contains(&self.name().as_str())
754        {
755            // by resetting the pitch space value, we get a simpler enharmonic spelling
756            let save_octave = self._octave;
757            self.ps_setter(self.ps());
758            if save_octave.is_none() {
759                self.octave_setter(None);
760            }
761        }
762
763        if most_common {
764            match self.name().as_str() {
765                "D#" => {
766                    self.step_setter(StepName::E);
767                    self.accidental_setter(Accidental::new("flat")?);
768                }
769                "A#" => {
770                    self.step_setter(StepName::B);
771                    self.accidental_setter(Accidental::new("flat")?);
772                }
773                "G-" => {
774                    self.step_setter(StepName::F);
775                    self.accidental_setter(Accidental::new("sharp")?);
776                }
777                "D-" => {
778                    self.step_setter(StepName::C);
779                    self.accidental_setter(Accidental::new("sharp")?);
780                }
781                _ => {}
782            }
783        }
784
785        Ok(())
786    }
787
788    /// Returns the next higher enharmonic spelling.
789    pub fn get_higher_enharmonic(&self) -> Result<Pitch> {
790        self._get_enharmonic_helper(true)
791    }
792
793    /// Replaces this pitch with its next higher enharmonic spelling.
794    pub fn get_higher_enharmonic_in_place(&mut self) -> Result<()> {
795        self._get_enharmonic_helper_in_place(true)
796    }
797
798    /// Returns the next lower enharmonic spelling.
799    pub fn get_lower_enharmonic(&self) -> Result<Pitch> {
800        self._get_enharmonic_helper(false)
801    }
802
803    /// Replaces this pitch with its next lower enharmonic spelling.
804    pub fn get_lower_enharmonic_in_place(&mut self) -> Result<()> {
805        self._get_enharmonic_helper_in_place(false)
806    }
807
808    fn _get_enharmonic_helper(&self, up: bool) -> Result<Pitch> {
809        let interval_string = match up {
810            true => IntervalString::Up,
811            false => IntervalString::Down,
812        };
813
814        let mut dict = match TRANSPOSITIONAL_INTERVALS.lock() {
815            Ok(dict) => dict,
816            Err(err) => err.into_inner(),
817        };
818
819        let interval: Interval = match dict.get(&interval_string) {
820            None => {
821                let interval =
822                    Interval::new(IntervalArgument::Str(interval_string.clone().string()))?;
823                dict.insert(interval_string.clone(), interval.clone());
824                interval
825            }
826            Some(interval) => interval.to_owned(),
827        };
828
829        let octave_stored = self._octave;
830
831        let mut p = interval.transpose_pitch_with_options(self, false, None)?;
832        if octave_stored.is_none() {
833            p.octave_setter(None);
834        }
835        Ok(p)
836    }
837
838    fn _get_enharmonic_helper_in_place(&mut self, up: bool) -> Result<()> {
839        *self = self._get_enharmonic_helper(up)?;
840        Ok(())
841    }
842
843    /// Returns the stored octave.
844    ///
845    /// Returns `None` when the pitch was created without an explicit octave,
846    /// such as `Pitch::from_name("C")`. In calculations, octave-less pitches
847    /// use the library default octave.
848    pub fn octave(&self) -> Octave {
849        self._octave
850    }
851
852    pub(crate) fn step(&self) -> StepName {
853        self._step
854    }
855
856    pub(crate) fn set_ps(&mut self, p: FloatType) {
857        self.ps_setter(p);
858    }
859}
860
861impl Default for Pitch {
862    fn default() -> Self {
863        Self::from_options(PitchOptions::default())
864            .expect("default Pitch construction should never fail")
865    }
866}
867
868pub(crate) struct PitchParameteres {
869    pub(crate) name: Option<String>,
870    pub(crate) step: Option<StepName>,
871    pub(crate) accidental: Option<Accidental>,
872    pub(crate) spelling_is_inferred: Option<bool>,
873    pub(crate) octave: Octave,
874}
875
876pub(crate) trait IntoPitchName {
877    fn into_name(self) -> PitchParameteres;
878}
879
880impl IntoPitchName for Pitch {
881    fn into_name(self) -> PitchParameteres {
882        self.name_with_octave().into_name()
883    }
884}
885
886impl IntoPitchName for PitchName {
887    fn into_name(self) -> PitchParameteres {
888        match self {
889            PitchName::Name(name) => name.into_name(),
890            PitchName::Number(number) => number.into_name(),
891        }
892    }
893}
894
895impl IntoPitchName for IntegerType {
896    fn into_name(self) -> PitchParameteres {
897        let (step_name, accidental, _, _) = convert_ps_to_step(self);
898
899        let octave = if self >= 12 {
900            Some(self / 12 - 1)
901        } else {
902            None
903        };
904
905        PitchParameteres {
906            name: None,
907            step: Some(step_name),
908            accidental: Some(accidental),
909            spelling_is_inferred: Some(true),
910            octave,
911        }
912    }
913}
914
915impl IntoPitchName for FloatType {
916    fn into_name(self) -> PitchParameteres {
917        let (step_name, accidental, _, _) = convert_ps_to_step(self);
918
919        let octave = if self >= 12.0 {
920            Some((self / 12.0) as IntegerType - 1)
921        } else {
922            None
923        };
924
925        PitchParameteres {
926            name: None,
927            step: Some(step_name),
928            accidental: Some(accidental),
929            spelling_is_inferred: Some(true),
930            octave,
931        }
932    }
933}
934
935impl IntoPitchName for String {
936    fn into_name(self) -> PitchParameteres {
937        PitchParameteres {
938            name: Some(self),
939            step: None,
940            accidental: None,
941            spelling_is_inferred: None,
942            octave: None,
943        }
944    }
945}
946
947impl IntoPitchName for &str {
948    fn into_name(self) -> PitchParameteres {
949        PitchParameteres {
950            name: Some(self.to_string()),
951            step: None,
952            accidental: None,
953            spelling_is_inferred: None,
954            octave: None,
955        }
956    }
957}
958
959fn convert_ps_to_step<T: Num + ToPrimitive>(
960    ps: T,
961) -> (StepName, Accidental, Microtone, IntegerType) {
962    const NATURAL_PCS: [IntegerType; 7] = [0, 2, 4, 5, 7, 9, 11];
963
964    let ps = ps.to_f64().unwrap_or(0.0);
965    let (pc, alter, micro) = if ps.fract() == 0.0 {
966        ((ps as IntegerType).rem_euclid(12), 0.0, 0.0)
967    } else {
968        let ps = round_to_digits(ps, PITCH_SPACE_SIGNIFICANT_DIGITS);
969        let pc_real = ps.rem_euclid(12.0);
970        let pc = pc_real.floor() as IntegerType;
971        let mut micro = pc_real - pc as FloatType;
972
973        let alter = if round_to_digits(micro, 1) == 0.5 || (0.25..0.75).contains(&micro) {
974            micro -= 0.5;
975            0.5
976        } else if (0.75..1.0).contains(&micro) {
977            micro -= 1.0;
978            1.0
979        } else if micro > 0.0 {
980            0.0
981        } else {
982            micro = 0.0;
983            0.0
984        };
985
986        (pc, alter, micro)
987    };
988
989    let mut octave_shift = 0;
990    let (pc_name, accidental_alter) = if alter == 1.0 && matches!(pc, 4 | 11) {
991        if pc == 11 {
992            octave_shift = 1;
993        }
994        ((pc + 1).rem_euclid(12), 0.0)
995    } else if NATURAL_PCS.contains(&pc) {
996        (pc, alter)
997    } else if [0, 5, 7].contains(&(pc - 1)) && alter >= 1.0 {
998        (pc + 1, alter - 1.0)
999    } else if [0, 5, 7].contains(&(pc - 1)) || ([11, 4].contains(&(pc + 1)) && alter <= -1.0) {
1000        (pc - 1, 1.0 + alter)
1001    } else if [11, 4].contains(&(pc + 1)) {
1002        (pc + 1, -1.0 + alter)
1003    } else {
1004        panic!("cannot match condition for pitch class: {pc}");
1005    };
1006
1007    let step = StepName::ref_to_step(pc_name.rem_euclid(12))
1008        .unwrap_or_else(|err| panic!("pitch class should map to a step: {err}"));
1009    let accidental = Accidental::new(accidental_alter)
1010        .unwrap_or_else(|err| panic!("accidental conversion should not fail: {err}"));
1011    let microtone = Microtone::from_cent_shift(Some(micro * 100.0), None)
1012        .unwrap_or_else(|err| panic!("microtone conversion should not fail: {err}"));
1013
1014    (step, accidental, microtone, octave_shift)
1015}
1016
1017fn round_to_digits(value: FloatType, digits: UnsignedIntegerType) -> FloatType {
1018    let factor = (10 as FloatType).powi(digits as IntegerType);
1019    (value * factor).round() / factor
1020}
1021
1022fn normalize_midi(midi: IntegerType) -> IntegerType {
1023    if midi > 127 {
1024        let mut value = (12 * 9) + midi.rem_euclid(12);
1025        if value < (127 - 12) {
1026            value += 12;
1027        }
1028        value
1029    } else if midi < 0 {
1030        midi.rem_euclid(12)
1031    } else {
1032        midi
1033    }
1034}
1035
1036type CriterionFunction = fn(&[Pitch]) -> Result<FloatType>;
1037
1038pub(crate) fn simplify_multiple_enharmonics(
1039    pitches: &[Pitch],
1040    criterion: Option<CriterionFunction>,
1041    key_context: Option<KeySignature>,
1042) -> Result<Vec<Pitch>> {
1043    let mut old_pitches: Vec<Pitch> = pitches.to_vec();
1044    if old_pitches.is_empty() {
1045        return Ok(Vec::new());
1046    }
1047
1048    let criterion: CriterionFunction = criterion.unwrap_or(default_dissonance_score);
1049
1050    let remove_first: bool = match key_context {
1051        Some(key) => {
1052            old_pitches.insert(0, key.as_key("major").tonic());
1053            true
1054        }
1055        None => false,
1056    };
1057
1058    let mut simplified_pitches = match old_pitches.len() < 5 {
1059        true => brute_force_enharmonics_search(&mut old_pitches, criterion)?,
1060        false => greedy_enharmonics_search(&mut old_pitches, criterion)?,
1061    };
1062
1063    for (new_p, old_p) in simplified_pitches.iter_mut().zip(old_pitches) {
1064        new_p.spelling_is_infered = old_p.spelling_is_infered;
1065    }
1066
1067    if remove_first {
1068        simplified_pitches.remove(0);
1069    }
1070
1071    Ok(simplified_pitches)
1072}
1073
1074fn brute_force_enharmonics_search(
1075    old_pitches: &mut [Pitch],
1076    score_func: CriterionFunction,
1077) -> Result<Vec<Pitch>> {
1078    let all_possible_pitches: Result<Vec<Vec<Pitch>>> = old_pitches[1..]
1079        .iter_mut()
1080        .map(|p| -> Result<Vec<Pitch>> {
1081            let mut enharmonics = p.get_all_common_enharmonics(2 as FloatType)?;
1082            enharmonics.insert(0, p.clone());
1083            Ok(enharmonics)
1084        })
1085        .collect();
1086
1087    let all_pitch_combinations = all_possible_pitches?.into_iter().multi_cartesian_product();
1088
1089    let mut min_score = FloatType::MAX;
1090    let mut best_combination: Vec<Pitch> = Vec::new();
1091
1092    for combination in all_pitch_combinations {
1093        let mut pitches: Vec<Pitch> = old_pitches[..1].to_vec();
1094        pitches.extend(combination);
1095        let score = score_func(&pitches)?;
1096        if score < min_score {
1097            min_score = score;
1098            best_combination = pitches;
1099        }
1100    }
1101
1102    Ok(best_combination)
1103}
1104
1105fn greedy_enharmonics_search(
1106    old_pitches: &mut [Pitch],
1107    score_func: CriterionFunction,
1108) -> Result<Vec<Pitch>> {
1109    let mut new_pitches = vec![];
1110
1111    if let Some(first) = old_pitches.first() {
1112        new_pitches.push(first.clone());
1113    } else {
1114        return Err(Error::Pitch(
1115            "can't perform greedy enharmonics search on empty pitches".into(),
1116        ));
1117    }
1118
1119    for old_pitch in old_pitches.iter_mut().skip(1) {
1120        let mut candidates = vec![old_pitch.clone()];
1121        candidates.extend(old_pitch.get_all_common_enharmonics(2 as FloatType)?);
1122
1123        let mut best_candidate = None;
1124        let mut best_score: Option<OrderedFloat<FloatType>> = None;
1125        for candidate in candidates.iter() {
1126            let mut candidate_list = new_pitches.clone();
1127            candidate_list.push(candidate.clone());
1128            let score = score_func(&candidate_list)?;
1129            let score = OrderedFloat(score);
1130            if best_score.is_none() || score < best_score.unwrap() {
1131                best_score = Some(score);
1132                best_candidate = Some(candidate);
1133            }
1134        }
1135        let best_candidate = best_candidate
1136            .ok_or_else(|| Error::Pitch("candidates list is unexpectedly empty".to_string()))?;
1137        new_pitches.push(best_candidate.clone());
1138    }
1139    Ok(new_pitches)
1140}
1141
1142fn default_dissonance_score(pitches: &[Pitch]) -> Result<FloatType> {
1143    dissonance_score(pitches, true, true, true)
1144}
1145
1146fn dissonance_score(
1147    pitches: &[Pitch],
1148    small_pythagorean_ratio: bool,
1149    accidental_penalty: bool,
1150    triad_award: bool,
1151) -> Result<FloatType> {
1152    let mut score_accidentals: FloatType = 0.0;
1153    let mut score_ratio: FloatType = 0.0;
1154    let mut score_triad: FloatType = 0.0;
1155
1156    if pitches.is_empty() {
1157        return Ok(0.0);
1158    }
1159
1160    if accidental_penalty {
1161        let accidentals = pitches
1162            .iter()
1163            .map(|p| p.alter().abs())
1164            .collect::<Vec<FloatType>>();
1165        score_accidentals = accidentals
1166            .iter()
1167            .map(|a| if *a > 1.0 { *a } else { 0.0 })
1168            .sum::<FloatType>()
1169            / pitches.len() as FloatType;
1170    }
1171
1172    let mut intervals: Vec<Interval> = vec![];
1173
1174    if small_pythagorean_ratio | triad_award {
1175        for (index, p1) in pitches.iter().enumerate() {
1176            for p2 in pitches.iter().skip(index + 1) {
1177                let mut p2 = (*p2).clone();
1178                p2.octave_setter(None);
1179                let Ok(interval) = Interval::between(
1180                    PitchOrNote::Pitch(p1.clone()),
1181                    PitchOrNote::Pitch(p2.clone()),
1182                ) else {
1183                    return Ok(FloatType::INFINITY);
1184                };
1185                intervals.push(interval);
1186            }
1187        }
1188
1189        if small_pythagorean_ratio {
1190            for interval in intervals.iter() {
1191                score_ratio += pythagorean_denominator_log(interval)? * 0.075_853_268_88
1192            }
1193            score_ratio /= pitches.len() as FloatType;
1194        }
1195
1196        if triad_award {
1197            intervals.into_iter().for_each(|interval| {
1198                let simple_directed = interval.generic().simple_directed();
1199                let interval_semitones = interval.chromatic.semitones % 12;
1200                if (simple_directed == 3 && (interval_semitones == 3 || interval_semitones == 4))
1201                    || (simple_directed == 6
1202                        && (interval_semitones == 8 || interval_semitones == 9))
1203                {
1204                    score_triad -= 1.0;
1205                }
1206            });
1207            score_triad /= pitches.len() as FloatType;
1208        }
1209    }
1210
1211    Ok((score_accidentals + score_ratio + score_triad)
1212        / (small_pythagorean_ratio as IntegerType
1213            + accidental_penalty as IntegerType
1214            + triad_award as IntegerType) as FloatType)
1215}
1216
1217fn pythagorean_denominator_log(interval: &Interval) -> Result<FloatType> {
1218    let start_pitch = Pitch::new(
1219        Some("C1".to_string()),
1220        None,
1221        None,
1222        Option::<IntegerType>::None,
1223        Option::<IntegerType>::None,
1224        None,
1225        None,
1226        None,
1227        None,
1228    )?;
1229    let end_pitch = interval
1230        .clone()
1231        .transpose_pitch_with_options(&start_pitch, false, Some(4))?;
1232
1233    let natural_fifths = match end_pitch.step() {
1234        StepName::C => 0,
1235        StepName::D => 2,
1236        StepName::E => 4,
1237        StepName::F => -1,
1238        StepName::G => 1,
1239        StepName::A => 3,
1240        StepName::B => 5,
1241    };
1242    let fifth_count = natural_fifths + (end_pitch.alter().round() as IntegerType * 7);
1243    let found_pitch_space = start_pitch.ps() + (7 * fifth_count) as FloatType;
1244    let octave_adjust = ((end_pitch.ps() - found_pitch_space) / 12.0).round() as IntegerType;
1245
1246    let mut denominator_twos = if fifth_count > 0 { fifth_count } else { 0 };
1247    let denominator_threes = if fifth_count < 0 { -fifth_count } else { 0 };
1248    denominator_twos = (denominator_twos - octave_adjust).max(0);
1249
1250    Ok(denominator_twos as FloatType * (2.0 as FloatType).ln()
1251        + denominator_threes as FloatType * (3.0 as FloatType).ln())
1252}
1253
1254fn convert_harmonic_to_cents(_harmonic_shift: IntegerType) -> IntegerType {
1255    let mut value = _harmonic_shift as FloatType;
1256    if value < 0.0 {
1257        value = 1.0 / value.abs();
1258    }
1259    (1200.0 * value.log2()).round() as IntegerType
1260}
1261
1262#[cfg(test)]
1263mod tests {
1264    use crate::defaults::IntegerType;
1265    use crate::interval::{Interval, IntervalArgument};
1266    use crate::tuningsystem::TuningSystem;
1267
1268    use super::{
1269        Accidental, Microtone, Pitch, convert_harmonic_to_cents, simplify_multiple_enharmonics,
1270    };
1271
1272    #[test]
1273    fn simplify_multiple_enharmonics_test() {
1274        let more_than_five = vec![
1275            Pitch::new(
1276                Some(0),
1277                None,
1278                None,
1279                Option::<IntegerType>::None,
1280                Option::<IntegerType>::None,
1281                None,
1282                None,
1283                None,
1284                None,
1285            )
1286            .unwrap(),
1287            Pitch::new(
1288                Some(1),
1289                None,
1290                None,
1291                Option::<IntegerType>::None,
1292                Option::<IntegerType>::None,
1293                None,
1294                None,
1295                None,
1296                None,
1297            )
1298            .unwrap(),
1299            Pitch::new(
1300                Some(2),
1301                None,
1302                None,
1303                Option::<IntegerType>::None,
1304                Option::<IntegerType>::None,
1305                None,
1306                None,
1307                None,
1308                None,
1309            )
1310            .unwrap(),
1311            Pitch::new(
1312                Some(3),
1313                None,
1314                None,
1315                Option::<IntegerType>::None,
1316                Option::<IntegerType>::None,
1317                None,
1318                None,
1319                None,
1320                None,
1321            )
1322            .unwrap(),
1323            Pitch::new(
1324                Some(4),
1325                None,
1326                None,
1327                Option::<IntegerType>::None,
1328                Option::<IntegerType>::None,
1329                None,
1330                None,
1331                None,
1332                None,
1333            )
1334            .unwrap(),
1335            Pitch::new(
1336                Some(5),
1337                None,
1338                None,
1339                Option::<IntegerType>::None,
1340                Option::<IntegerType>::None,
1341                None,
1342                None,
1343                None,
1344                None,
1345            )
1346            .unwrap(),
1347            Pitch::new(
1348                Some(12),
1349                None,
1350                None,
1351                Option::<IntegerType>::None,
1352                Option::<IntegerType>::None,
1353                None,
1354                None,
1355                None,
1356                None,
1357            )
1358            .unwrap(),
1359            Pitch::new(
1360                Some(13),
1361                None,
1362                None,
1363                Option::<IntegerType>::None,
1364                Option::<IntegerType>::None,
1365                None,
1366                None,
1367                None,
1368                None,
1369            )
1370            .unwrap(),
1371        ];
1372
1373        let _x = simplify_multiple_enharmonics(&more_than_five, None, None);
1374        let _less_than_five = [
1375            Pitch::new(
1376                Some(0),
1377                None,
1378                None,
1379                Option::<IntegerType>::None,
1380                Option::<IntegerType>::None,
1381                None,
1382                None,
1383                None,
1384                None,
1385            ),
1386            Pitch::new(
1387                Some(1),
1388                None,
1389                None,
1390                Option::<IntegerType>::None,
1391                Option::<IntegerType>::None,
1392                None,
1393                None,
1394                None,
1395                None,
1396            ),
1397            Pitch::new(
1398                Some(2),
1399                None,
1400                None,
1401                Option::<IntegerType>::None,
1402                Option::<IntegerType>::None,
1403                None,
1404                None,
1405                None,
1406                None,
1407            ),
1408            Pitch::new(
1409                Some(12),
1410                None,
1411                None,
1412                Option::<IntegerType>::None,
1413                Option::<IntegerType>::None,
1414                None,
1415                None,
1416                None,
1417                None,
1418            ),
1419            Pitch::new(
1420                Some(13),
1421                None,
1422                None,
1423                Option::<IntegerType>::None,
1424                Option::<IntegerType>::None,
1425                None,
1426                None,
1427                None,
1428                None,
1429            ),
1430        ];
1431    }
1432
1433    #[test]
1434    fn test_convert_harmonic_to_cents_values() {
1435        assert_eq!(convert_harmonic_to_cents(8), 3600);
1436        assert_eq!(convert_harmonic_to_cents(5), 2786);
1437        assert_eq!(convert_harmonic_to_cents(-2), -1200);
1438    }
1439
1440    #[test]
1441    fn test_pitch_transpose_interval() {
1442        let c4 = Pitch::new(
1443            Some("C4".to_string()),
1444            None,
1445            None,
1446            Option::<IntegerType>::None,
1447            Option::<IntegerType>::None,
1448            None,
1449            None,
1450            None,
1451            None,
1452        )
1453        .unwrap();
1454        let m3 = Interval::new(IntervalArgument::Str("m3".to_string())).unwrap();
1455        let out = c4.transpose(m3);
1456        assert_eq!(out.name_with_octave(), "E-4");
1457    }
1458
1459    #[test]
1460    fn test_pitch_frequency_helpers() {
1461        let a4 = Pitch::from_name("A4").unwrap();
1462        assert!((a4.frequency_hz() - 440.0).abs() < 0.0001);
1463
1464        let e4 = Pitch::from_name("E4").unwrap();
1465        assert!((e4.frequency_hz_in(TuningSystem::FiveLimit) - 327.032).abs() < 0.001);
1466        assert!(e4.frequency_hz_in(TuningSystem::FiveLimit) < e4.frequency_hz());
1467    }
1468
1469    #[test]
1470    fn pitch_exposes_accidental_object() {
1471        let custom_accidental = Accidental::new("half-flat").unwrap();
1472        let pitch = Pitch::builder()
1473            .step('D')
1474            .accidental(custom_accidental.clone())
1475            .octave(4)
1476            .build()
1477            .unwrap();
1478
1479        assert_eq!(pitch.name_with_octave(), "D`4");
1480        assert_eq!(pitch.accidental(), &custom_accidental);
1481        assert_eq!(pitch.accidental().name(), "half-flat");
1482        assert_eq!(pitch.accidental().alter(), -0.5);
1483    }
1484
1485    #[test]
1486    fn pitch_exposes_microtone_object() {
1487        let microtone = Microtone::new(-25.0).unwrap();
1488        let pitch = Pitch::builder()
1489            .name("G#4")
1490            .microtone(microtone.clone())
1491            .build()
1492            .unwrap();
1493
1494        assert_eq!(pitch.microtone(), Some(&microtone));
1495        assert_eq!(pitch.microtone().unwrap().to_string(), "(-25c)");
1496        assert_eq!(pitch.alter(), 0.75);
1497    }
1498
1499    #[test]
1500    fn pitch_supports_rust_conversion_traits() {
1501        let parsed: Pitch = "C#4".parse().unwrap();
1502        assert_eq!(parsed.to_string(), "C#4");
1503
1504        let midi = Pitch::try_from(60 as IntegerType).unwrap();
1505        assert_eq!(midi.name_with_octave(), "C4");
1506        assert_eq!(midi.midi(), 60);
1507
1508        let pitch_space = Pitch::try_from(61.5).unwrap();
1509        assert_eq!(pitch_space.pitch_space(), 61.5);
1510        assert_eq!(pitch_space.midi(), 62);
1511
1512        let built = Pitch::builder().pitch_space(60.0).build().unwrap();
1513        assert_eq!(built.name_with_octave(), "C4");
1514    }
1515
1516    #[test]
1517    fn pitch_exposes_enharmonic_helpers() {
1518        let c_sharp = Pitch::new(
1519            Some("C#3".to_string()),
1520            None,
1521            None,
1522            Option::<IntegerType>::None,
1523            Option::<IntegerType>::None,
1524            None,
1525            None,
1526            None,
1527            None,
1528        )
1529        .unwrap();
1530        let out = c_sharp.get_higher_enharmonic().unwrap();
1531        assert_eq!(out.name_with_octave(), "D-3");
1532
1533        let mut d_flat = out;
1534        d_flat.get_lower_enharmonic_in_place().unwrap();
1535        assert_eq!(d_flat.name_with_octave(), "C#3");
1536
1537        let d_sharp = Pitch::from_name("D#4").unwrap();
1538        assert_eq!(
1539            d_sharp
1540                .simplify_enharmonic(true)
1541                .unwrap()
1542                .name_with_octave(),
1543            "E-4"
1544        );
1545    }
1546}