Skip to main content

music21_rs/pitch/
harmonics.rs

1//! A pitch as a harmonic of a fundamental, and the microtone that
2//! separates the two readings of a quarter tone.
3
4use super::*;
5
6impl Pitch {
7    /// Returns the pitch with any quarter-tone accidental folded into the
8    /// microtone: music21's `convertQuarterTonesToMicrotones`, so a half-sharp
9    /// C becomes C with `+50c` and a one-and-a-half-sharp D becomes D-sharp
10    /// with `+50c`. Other accidentals are untouched.
11    pub fn convert_quarter_tones_to_microtones(&self) -> Result<Pitch> {
12        let (alter, shift) = match self.accidental.name() {
13            "half-flat" => (0.0, -50.0),
14            "half-sharp" => (0.0, 50.0),
15            "one-and-a-half-sharp" => (1.0, 50.0),
16            "one-and-a-half-flat" => (-1.0, -50.0),
17            _ => return Ok(self.clone()),
18        };
19        let cents = self.microtone.as_ref().map_or(0.0, Microtone::cents);
20        let mut pitch = self.clone();
21        pitch.accidental_setter(Accidental::new(alter)?);
22        pitch.microtone_setter(Microtone::new(cents + shift)?);
23        Ok(pitch)
24    }
25
26    /// Returns the pitch with its microtone rounded into the nearest
27    /// quarter-tone accidental and the remainder kept as a microtone:
28    /// music21's `convertMicrotonesToQuarterTones`, so C with `+30c` becomes a
29    /// half-sharp C with `-20c` and C with `+150c` becomes C-sharp with `+50c`.
30    pub fn convert_microtones_to_quarter_tones(&self) -> Result<Pitch> {
31        let cents = self.microtone.as_ref().map_or(0.0, Microtone::cents);
32        let (shift, remainder) = cents_to_alter_and_cents(cents);
33        let mut pitch = self.clone();
34        pitch.accidental_setter(Accidental::new(self.accidental.alter() + shift)?);
35        pitch.microtone_setter(Microtone::new(remainder)?);
36        Ok(pitch)
37    }
38
39    /// Returns which harmonic of `fundamental` this pitch is closest to, and
40    /// the fundamental retuned by the difference so that the harmonic lands
41    /// exactly here: music21's `harmonicAndFundamentalFromPitch`, so `E5`
42    /// over `C2` is the tenth harmonic of `C2` raised 14 cents.
43    pub fn harmonic_and_fundamental_from_pitch(&self, fundamental: &Pitch) -> Result<(u32, Pitch)> {
44        let (number, cents) = self.harmonic_from_fundamental(fundamental)?;
45        let cents = -cents;
46        let mut retuned = fundamental.clone();
47        match retuned.microtone.as_ref().map(Microtone::cents) {
48            Some(existing) => retuned.microtone_setter(Microtone::new(existing + cents)?),
49            None if cents != 0.0 => retuned.microtone_setter(Microtone::new(cents)?),
50            None => {}
51        }
52        Ok((number, retuned))
53    }
54
55    /// Returns [`Self::harmonic_and_fundamental_from_pitch`] in music21's
56    /// notation, `10thH/C2(+14c)`.
57    pub fn harmonic_and_fundamental_string_from_pitch(
58        &self,
59        fundamental: &Pitch,
60    ) -> Result<String> {
61        let (number, retuned) = self.harmonic_and_fundamental_from_pitch(fundamental)?;
62        let suffix = crate::pitch::microtone::ordinal_suffix(number as IntegerType);
63        Ok(format!(
64            "{number}{suffix}H/{}",
65            retuned.name_with_octave_and_microtone()
66        ))
67    }
68
69    /// Returns the `number`th harmonic of this pitch as a fundamental, spelled
70    /// to the nearest twelve-tone pitch with the remainder as a microtone and
71    /// this pitch recorded as its fundamental. The first harmonic is the
72    /// pitch itself.
73    pub fn harmonic(&self, number: u32) -> Result<Pitch> {
74        let cent_shift = convert_harmonic_to_cents(number as IntegerType);
75        if cent_shift == 0 {
76            return Ok(self.clone());
77        }
78        let mut shifted = self.clone();
79        let existing = self.microtone.as_ref().map_or(0.0, Microtone::cents);
80        shifted.microtone_setter(Microtone::new(existing + cent_shift as FloatType)?);
81        let mut harmonic = Pitch::from_frequency(shifted.frequency_hz())?;
82        harmonic.fundamental_setter(self.clone());
83        Ok(harmonic)
84    }
85
86    /// Returns which harmonic of `fundamental` this pitch is closest to, and
87    /// the distance to that harmonic in cents: negative when this pitch lies
88    /// above the harmonic, positive when below.
89    pub fn harmonic_from_fundamental(&self, fundamental: &Pitch) -> Result<(u32, FloatType)> {
90        if self.ps() <= fundamental.ps() {
91            return Err(Error::Pitch(format!(
92                "cannot find an equivalent harmonic for a fundamental ({fundamental}) that is not above this Pitch ({self})"
93            )));
94        }
95        let mut found = Vec::new();
96        for number in 1..32 {
97            let candidate = fundamental.harmonic(number)?;
98            let above = candidate.ps() > self.ps();
99            found.push((number, candidate));
100            if above {
101                break;
102            }
103        }
104        let (number, gap) = match found.as_slice() {
105            [(number, only)] => (*number, only.ps() - self.ps()),
106            [.., (lower_number, lower), (higher_number, higher)] => {
107                let below = self.ps() - lower.ps();
108                let above = higher.ps() - self.ps();
109                if below <= above {
110                    (*lower_number, -below.abs())
111                } else {
112                    (*higher_number, above.abs())
113                }
114            }
115            [] => unreachable!("the first harmonic is always collected"),
116        };
117        Ok((
118            number,
119            round_to_digits(gap, PITCH_SPACE_SIGNIFICANT_DIGITS) * 100.0,
120        ))
121    }
122
123    /// Describes this pitch as a harmonic of `fundamental`, or of its own
124    /// fundamental when none is given, in music21's notation: `"3rdH(-2c)/C2"`.
125    pub fn harmonic_string(&self, fundamental: Option<&Pitch>) -> Result<String> {
126        let fundamental = fundamental.or(self.fundamental()).ok_or_else(|| {
127            Error::Pitch("no fundamental is defined for this Pitch: provide one".to_string())
128        })?;
129        let (number, cents) = self.harmonic_from_fundamental(fundamental)?;
130        let suffix = crate::pitch::microtone::ordinal_suffix(number as IntegerType);
131        let fundamental = fundamental.name_with_octave_and_microtone();
132        if cents == 0.0 {
133            Ok(format!("{number}{suffix}H/{fundamental}"))
134        } else {
135            let microtone = Microtone::new(-cents)?;
136            Ok(format!("{number}{suffix}H{microtone}/{fundamental}"))
137        }
138    }
139}
140
141pub(super) fn convert_harmonic_to_cents(harmonic_shift: IntegerType) -> IntegerType {
142    let mut value = harmonic_shift as FloatType;
143    if value < 0.0 {
144        value = 1.0 / value.abs();
145    }
146    (1200.0 * value.log2()).round() as IntegerType
147}
148
149/// music21's `_convertCentsToAlterAndCents`: how much of a cent shift becomes
150/// an accidental, in quarter-tone steps, and what is left as a microtone.
151/// Ported as written, including the loop upstream runs for shifts below
152/// -150 cents, which adds whole tones until the value passes +100 rather than
153/// stopping at zero; nothing here relies on that range.
154pub(super) fn cents_to_alter_and_cents(shift: FloatType) -> (FloatType, FloatType) {
155    let mut value = shift;
156    let mut alter_add = 0.0;
157    if value > 150.0 {
158        let increment = (value / 100.0).floor();
159        value -= increment * 100.0;
160        alter_add += increment;
161    } else if value < -150.0 {
162        while value < 100.0 {
163            value += 100.0;
164            alter_add -= 1.0;
165        }
166    }
167    let (alter_shift, cents) = if value < -75.0 {
168        (-1.0, value + 100.0)
169    } else if value < -25.0 {
170        (-0.5, value + 50.0)
171    } else if value <= 25.0 {
172        (0.0, value)
173    } else if value <= 75.0 {
174        (0.5, value - 50.0)
175    } else {
176        (1.0, value - 100.0)
177    };
178    (alter_shift + alter_add, cents)
179}