Skip to main content

music21_rs/pitch/
enharmonic.rs

1//! Respelling a pitch: its enharmonics, the simplest of them, and the
2//! spelling a set of pitches reads best in together.
3
4use super::*;
5
6impl Pitch {
7    pub(super) fn get_all_common_enharmonics(
8        &mut self,
9        alter_limit: FloatType,
10    ) -> Result<Vec<Pitch>> {
11        let mut post = Vec::new();
12
13        let simplified = self.clone().simplify_enharmonic(false)?;
14        if simplified.name() != self.name() {
15            post.push(simplified);
16        }
17
18        let mut higher = self.clone();
19        while let Ok(next) = higher.get_higher_enharmonic() {
20            if next.accidental().alter.abs() > alter_limit {
21                break;
22            }
23            if post.contains(&next) {
24                break;
25            }
26            post.push(next.clone());
27            higher = next;
28        }
29
30        let mut lower = self.clone();
31        while let Ok(next) = lower.get_lower_enharmonic() {
32            if next.accidental().alter.abs() > alter_limit {
33                break;
34            }
35            if post.contains(&next) {
36                break;
37            }
38            post.push(next.clone());
39            lower = next;
40        }
41
42        Ok(post)
43    }
44
45    /// Returns a simpler enharmonic spelling of this pitch.
46    ///
47    /// When `most_common` is true, common spellings such as `E-` are preferred
48    /// over less common equivalents such as `D#`, following music21's
49    /// `Pitch.simplifyEnharmonic` behavior.
50    pub fn simplify_enharmonic(&self, most_common: bool) -> Result<Pitch> {
51        let mut pitch = self.clone();
52        pitch.simplify_enharmonic_in_place(most_common)?;
53        Ok(pitch)
54    }
55
56    /// Simplifies this pitch's enharmonic spelling in place.
57    pub fn simplify_enharmonic_in_place(&mut self, most_common: bool) -> Result<()> {
58        const EXCLUDED_NAMES: [&str; 4] = ["E#", "B#", "Cb", "Fb"];
59        if self.accidental().alter.abs().partial_cmp(&2.0) != Some(Ordering::Less)
60            || EXCLUDED_NAMES.contains(&self.name().as_str())
61        {
62            // by resetting the pitch space value, we get a simpler enharmonic spelling
63            let save_octave = self.octave;
64            self.ps_setter(self.ps());
65            if save_octave.is_none() {
66                self.octave_setter(None);
67            }
68        }
69
70        if most_common {
71            match self.name().as_str() {
72                "D#" => {
73                    self.step_setter(StepName::E);
74                    self.accidental_setter(Accidental::new("flat")?);
75                }
76                "A#" => {
77                    self.step_setter(StepName::B);
78                    self.accidental_setter(Accidental::new("flat")?);
79                }
80                "Gb" => {
81                    self.step_setter(StepName::F);
82                    self.accidental_setter(Accidental::new("sharp")?);
83                }
84                "Db" => {
85                    self.step_setter(StepName::C);
86                    self.accidental_setter(Accidental::new("sharp")?);
87                }
88                _ => {}
89            }
90        }
91
92        Ok(())
93    }
94
95    /// Returns the next higher enharmonic spelling.
96    pub fn get_higher_enharmonic(&self) -> Result<Pitch> {
97        self.enharmonic_neighbour(true)
98    }
99
100    /// Replaces this pitch with its next higher enharmonic spelling.
101    pub fn get_higher_enharmonic_in_place(&mut self) -> Result<()> {
102        self.enharmonic_neighbour_in_place(true)
103    }
104
105    /// Returns the next lower enharmonic spelling.
106    pub fn get_lower_enharmonic(&self) -> Result<Pitch> {
107        self.enharmonic_neighbour(false)
108    }
109
110    /// Replaces this pitch with its next lower enharmonic spelling.
111    pub fn get_lower_enharmonic_in_place(&mut self) -> Result<()> {
112        self.enharmonic_neighbour_in_place(false)
113    }
114
115    pub(super) fn enharmonic_neighbour(&self, up: bool) -> Result<Pitch> {
116        let interval: &Interval = if up {
117            &DIMINISHED_SECOND_UP
118        } else {
119            &DIMINISHED_SECOND_DOWN
120        };
121
122        let octave_stored = self.octave;
123
124        let mut p = interval.transpose_pitch_with_options(self, false, None)?;
125        if octave_stored.is_none() {
126            p.octave_setter(None);
127        }
128        Ok(p)
129    }
130
131    pub(super) fn enharmonic_neighbour_in_place(&mut self, up: bool) -> Result<()> {
132        *self = self.enharmonic_neighbour(up)?;
133        Ok(())
134    }
135
136    /// Returns the enharmonic music21's `getEnharmonic` picks: sharps respell
137    /// upward and flats downward, and a natural goes down for C, D and G and
138    /// up for the rest, so C is B-sharp and E is F-flat.
139    /// This pitch respelled to agree with a key signature, when the two
140    /// disagree about the same sounding note.
141    ///
142    /// This is the rule music21 applies after transposing by a number of
143    /// semitones: a `G-` in a key that writes an `F#` is written `F#`, and
144    /// the other way round, because a chromatic step says how far to move
145    /// and not how to spell what it lands on. A pitch with no accidental,
146    /// or one the signature does not alter, is left as it is.
147    pub fn respelled_for(&self, signature: &crate::key::KeySignature) -> Result<Pitch> {
148        if self.written_accidental().is_none() {
149            return Ok(self.clone());
150        }
151        let alter = self.accidental().alter();
152        for altered in signature.altered_pitches()? {
153            if altered.pitch_class() == self.pitch_class() && altered.accidental().alter() != alter
154            {
155                return self.get_enharmonic();
156            }
157        }
158        Ok(self.clone())
159    }
160
161    /// The next enharmonic spelling of this pitch: music21's `getEnharmonic`.
162    ///
163    /// A sharpened pitch respells on the letter above and a flattened one on
164    /// the letter below; a natural takes whichever direction its letter has
165    /// room for, so `C` answers `B#`.
166    pub fn get_enharmonic(&self) -> Result<Pitch> {
167        let alter = self.accidental().alter();
168        let downward = if alter > 0.0 {
169            false
170        } else if alter < 0.0 {
171            true
172        } else {
173            matches!(self.step.as_char(), 'C' | 'D' | 'G')
174        };
175        if downward {
176            self.get_lower_enharmonic()
177        } else {
178            self.get_higher_enharmonic()
179        }
180    }
181
182    /// Returns whether the two pitches sound the same. Without an octave on
183    /// either side only the pitch class is compared.
184    pub fn is_enharmonic(&self, other: &Pitch) -> bool {
185        if self.octave.is_none() || other.octave.is_none() {
186            (other.ps() - self.ps()).rem_euclid(12.0) == 0.0
187        } else {
188            other.ps() == self.ps()
189        }
190    }
191
192    /// Returns the other spellings of this pitch with at most `alter_limit`
193    /// sharps or flats, simplest first, as music21's `getAllCommonEnharmonics`
194    /// lists them.
195    pub fn all_common_enharmonics(&self, alter_limit: IntegerType) -> Vec<Pitch> {
196        let mut found = Vec::new();
197        if let Ok(simplified) = self.simplify_enharmonic(false)
198            && simplified.name() != self.name()
199        {
200            found.push(simplified);
201        }
202        for upward in [true, false] {
203            let mut current = self.clone();
204            while let Ok(next) = current.enharmonic_neighbour(upward) {
205                if next.accidental().alter().abs() > alter_limit as FloatType
206                    || found.contains(&next)
207                {
208                    break;
209                }
210                found.push(next.clone());
211                current = next;
212            }
213        }
214        found
215    }
216
217    /// Returns this pitch moved down by octaves until it is at or below
218    /// `target`. With `minimize` it is then raised back to within an octave.
219    pub fn transpose_below_target(&self, target: &Pitch, minimize: bool) -> Result<Pitch> {
220        let mut pitch = self.octave_bearing_copy("transposeBelowTarget")?;
221        while pitch.ps() > target.ps() {
222            pitch.shift_octave(-1);
223        }
224        if minimize {
225            while target.ps() - pitch.ps() >= 12.0 {
226                pitch.shift_octave(1);
227            }
228        }
229        Ok(pitch)
230    }
231
232    /// Returns this pitch moved up by octaves until it is at or above
233    /// `target`. With `minimize` it is then lowered back to within an octave.
234    pub fn transpose_above_target(&self, target: &Pitch, minimize: bool) -> Result<Pitch> {
235        let mut pitch = self.octave_bearing_copy("transposeAboveTarget")?;
236        while pitch.ps() < target.ps() {
237            pitch.shift_octave(1);
238        }
239        if minimize {
240            while pitch.ps() - target.ps() >= 12.0 {
241                pitch.shift_octave(-1);
242            }
243        }
244        Ok(pitch)
245    }
246}
247
248pub(super) use crate::interval::constants::{DIMINISHED_SECOND_DOWN, DIMINISHED_SECOND_UP};
249
250/// A scoring function for [`simplify_multiple_enharmonics`]: lower is a
251/// simpler spelling.
252pub type CriterionFunction = fn(&[Pitch]) -> Result<FloatType>;
253
254/// Respells a set of pitches so that they read as simply as possible
255/// together: music21's `simplifyMultipleEnharmonics`. The first pitch is kept
256/// as written and each of the others may be swapped for a common enharmonic;
257/// the spelling chosen is the one the criterion scores lowest, by default
258/// [`dissonance_score`]. Up to four pitches are searched exhaustively, more
259/// are settled greedily one at a time, as upstream does. With a key
260/// signature the tonic of its major key is placed first as an anchor and
261/// removed again afterwards.
262pub fn simplify_multiple_enharmonics(
263    pitches: &[Pitch],
264    criterion: Option<CriterionFunction>,
265    key_context: Option<KeySignature>,
266) -> Result<Vec<Pitch>> {
267    let mut old_pitches: Vec<Pitch> = pitches.to_vec();
268    if old_pitches.is_empty() {
269        return Ok(Vec::new());
270    }
271
272    let criterion: CriterionFunction = criterion.unwrap_or(dissonance_score);
273
274    let remove_first: bool = match key_context {
275        Some(key) => {
276            old_pitches.insert(0, key.as_key("major").tonic());
277            true
278        }
279        None => false,
280    };
281
282    let mut simplified_pitches = match old_pitches.len() < 5 {
283        true => brute_force_enharmonics_search(&mut old_pitches, criterion)?,
284        false => greedy_enharmonics_search(&mut old_pitches, criterion)?,
285    };
286
287    for (new_p, old_p) in simplified_pitches.iter_mut().zip(old_pitches) {
288        new_p.spelling_is_inferred = old_p.spelling_is_inferred;
289    }
290
291    if remove_first {
292        let _ = simplified_pitches.remove(0);
293    }
294
295    Ok(simplified_pitches)
296}
297
298pub(super) fn brute_force_enharmonics_search(
299    old_pitches: &mut [Pitch],
300    score_func: CriterionFunction,
301) -> Result<Vec<Pitch>> {
302    let all_possible_pitches: Result<Vec<Vec<Pitch>>> = old_pitches[1..]
303        .iter_mut()
304        .map(|p| -> Result<Vec<Pitch>> {
305            let mut enharmonics = p.get_all_common_enharmonics(2 as FloatType)?;
306            enharmonics.insert(0, p.clone());
307            Ok(enharmonics)
308        })
309        .collect();
310
311    let all_pitch_combinations = all_possible_pitches?.into_iter().multi_cartesian_product();
312
313    let mut min_score = FloatType::MAX;
314    let mut best_combination: Vec<Pitch> = Vec::new();
315
316    for combination in all_pitch_combinations {
317        let mut pitches: Vec<Pitch> = old_pitches[..1].to_vec();
318        pitches.extend(combination);
319        let score = score_func(&pitches)?;
320        // Two spellings that score the same keep the first, as music21's
321        // `min` does. The scores are music21's to the last bit, so a
322        // spelling one ulp lower wins here as it wins there: pitch classes
323        // 3, 6 and 9 spell `E- G- A`.
324        if score < min_score {
325            min_score = score;
326            best_combination = pitches;
327        }
328    }
329
330    Ok(best_combination)
331}
332
333pub(super) fn greedy_enharmonics_search(
334    old_pitches: &mut [Pitch],
335    score_func: CriterionFunction,
336) -> Result<Vec<Pitch>> {
337    let mut new_pitches = vec![];
338
339    if let Some(first) = old_pitches.first() {
340        new_pitches.push(first.clone());
341    } else {
342        return Err(Error::Pitch(
343            "can't perform greedy enharmonics search on empty pitches".into(),
344        ));
345    }
346
347    for old_pitch in old_pitches.iter_mut().skip(1) {
348        let mut candidates = vec![old_pitch.clone()];
349        candidates.extend(old_pitch.get_all_common_enharmonics(2 as FloatType)?);
350
351        let mut best_candidate = None;
352        let mut best_score: Option<OrderedFloat<FloatType>> = None;
353        for candidate in candidates.iter() {
354            let mut candidate_list = new_pitches.clone();
355            candidate_list.push(candidate.clone());
356            let score = score_func(&candidate_list)?;
357            let score = OrderedFloat(score);
358            if best_score.is_none_or(|best| score < best) {
359                best_score = Some(score);
360                best_candidate = Some(candidate);
361            }
362        }
363        let best_candidate = best_candidate
364            .ok_or_else(|| Error::Pitch("candidates list is unexpectedly empty".to_string()))?;
365        new_pitches.push(best_candidate.clone());
366    }
367    Ok(new_pitches)
368}
369
370/// How awkward a set of pitches reads together: music21's
371/// `_dissonanceScore` with all three of its terms on. It averages a penalty
372/// for accidentals beyond one sharp or flat, a penalty growing with the
373/// denominator of each pair's Pythagorean ratio, and a reward for every
374/// third and sixth, so that `C E G` scores below `C F- G`.
375pub fn dissonance_score(pitches: &[Pitch]) -> Result<FloatType> {
376    weighted_dissonance_score(pitches, true, true, true)
377}
378
379pub(super) fn weighted_dissonance_score(
380    pitches: &[Pitch],
381    small_pythagorean_ratio: bool,
382    accidental_penalty: bool,
383    triad_award: bool,
384) -> Result<FloatType> {
385    let mut score_accidentals: FloatType = 0.0;
386    let mut score_ratio: FloatType = 0.0;
387    let mut score_triad: FloatType = 0.0;
388
389    if pitches.is_empty() {
390        return Ok(0.0);
391    }
392
393    if accidental_penalty {
394        let accidentals = pitches
395            .iter()
396            .map(|p| p.alter().abs())
397            .collect::<Vec<FloatType>>();
398        score_accidentals = accidentals
399            .iter()
400            .map(|a| if *a > 1.0 { *a } else { 0.0 })
401            .sum::<FloatType>()
402            / pitches.len() as FloatType;
403    }
404
405    let mut intervals: Vec<Interval> = vec![];
406
407    if small_pythagorean_ratio | triad_award {
408        for (index, p1) in pitches.iter().enumerate() {
409            for p2 in pitches.iter().skip(index + 1) {
410                let mut p2 = (*p2).clone();
411                p2.octave_setter(None);
412                let Ok(interval) = Interval::between(p1.clone(), p2.clone()) else {
413                    return Ok(FloatType::INFINITY);
414                };
415                intervals.push(interval);
416            }
417        }
418
419        if small_pythagorean_ratio {
420            for interval in intervals.iter() {
421                score_ratio += pythagorean_denominator_log(interval)? * 0.075_853_268_88
422            }
423            score_ratio /= pitches.len() as FloatType;
424        }
425
426        if triad_award {
427            intervals.into_iter().for_each(|interval| {
428                let simple_directed = interval.generic().simple_directed();
429                let interval_semitones = interval.chromatic.whole_semitones() % 12;
430                if (simple_directed == 3 && (interval_semitones == 3 || interval_semitones == 4))
431                    || (simple_directed == 6
432                        && (interval_semitones == 8 || interval_semitones == 9))
433                {
434                    score_triad -= 1.0;
435                }
436            });
437            score_triad /= pitches.len() as FloatType;
438        }
439    }
440
441    Ok((score_accidentals + score_ratio + score_triad)
442        / (small_pythagorean_ratio as IntegerType
443            + accidental_penalty as IntegerType
444            + triad_award as IntegerType) as FloatType)
445}
446
447pub(super) fn pythagorean_denominator_log(interval: &Interval) -> Result<FloatType> {
448    let start_pitch = Pitch::from_name("C1")?;
449    let end_pitch = interval.transpose_pitch_with_options(&start_pitch, false, Some(4))?;
450
451    let natural_fifths = match end_pitch.step() {
452        StepName::C => 0,
453        StepName::D => 2,
454        StepName::E => 4,
455        StepName::F => -1,
456        StepName::G => 1,
457        StepName::A => 3,
458        StepName::B => 5,
459    };
460    let fifth_count = natural_fifths + (end_pitch.alter().round() as IntegerType * 7);
461    let found_pitch_space = start_pitch.ps() + (7 * fifth_count) as FloatType;
462    let octave_adjust = ((end_pitch.ps() - found_pitch_space) / 12.0).round() as IntegerType;
463
464    // The ratio is 3^f * 2^(octaves - f) for f fifths, down or up: a fifth
465    // down is 2/3, whose 2 an octave's 1/2 cancels. So a twelfth down is
466    // 1/3, not 1/6.
467    let denominator_twos = (fifth_count - octave_adjust).max(0);
468    let denominator_threes = (-fifth_count).max(0);
469
470    // One log of the whole denominator, as music21 takes it: the sum of the
471    // two logs rounds differently, and a tie between spellings falls on
472    // those last bits.
473    // Past u128 the denominator is rounded to a float first, as Python
474    // rounds an int too large to be exact.
475    let twos = denominator_twos as u32;
476    let threes = denominator_threes as u32;
477    let denominator = 2_u128
478        .checked_pow(twos)
479        .zip(3_u128.checked_pow(threes))
480        .and_then(|(twos, threes)| twos.checked_mul(threes))
481        .map_or_else(
482            || (2.0 as FloatType).powi(twos as i32) * (3.0 as FloatType).powi(threes as i32),
483            |exact| exact as FloatType,
484        );
485    Ok(denominator.ln())
486}