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