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