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#", "C-", "F-"];
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                "G-" => {
81                    self.step_setter(StepName::F);
82                    self.accidental_setter(Accidental::new("sharp")?);
83                }
84                "D-" => {
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.has_accidental() {
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/// How far apart two dissonance scores must be to count as different: a
255/// spelling within this of the best so far is a tie, and the first wins.
256const SCORE_TOLERANCE: FloatType = 1e-9;
257
258/// Respells a set of pitches so that they read as simply as possible
259/// together: music21's `simplifyMultipleEnharmonics`. The first pitch is kept
260/// as written and each of the others may be swapped for a common enharmonic;
261/// the spelling chosen is the one the criterion scores lowest, by default
262/// [`dissonance_score`]. Up to four pitches are searched exhaustively, more
263/// are settled greedily one at a time, as upstream does. With a key
264/// signature the tonic of its major key is placed first as an anchor and
265/// removed again afterwards.
266pub fn simplify_multiple_enharmonics(
267    pitches: &[Pitch],
268    criterion: Option<CriterionFunction>,
269    key_context: Option<KeySignature>,
270) -> Result<Vec<Pitch>> {
271    let mut old_pitches: Vec<Pitch> = pitches.to_vec();
272    if old_pitches.is_empty() {
273        return Ok(Vec::new());
274    }
275
276    let criterion: CriterionFunction = criterion.unwrap_or(dissonance_score);
277
278    let remove_first: bool = match key_context {
279        Some(key) => {
280            old_pitches.insert(0, key.as_key("major").tonic());
281            true
282        }
283        None => false,
284    };
285
286    let mut simplified_pitches = match old_pitches.len() < 5 {
287        true => brute_force_enharmonics_search(&mut old_pitches, criterion)?,
288        false => greedy_enharmonics_search(&mut old_pitches, criterion)?,
289    };
290
291    for (new_p, old_p) in simplified_pitches.iter_mut().zip(old_pitches) {
292        new_p.spelling_is_inferred = old_p.spelling_is_inferred;
293    }
294
295    if remove_first {
296        let _ = simplified_pitches.remove(0);
297    }
298
299    Ok(simplified_pitches)
300}
301
302pub(super) fn brute_force_enharmonics_search(
303    old_pitches: &mut [Pitch],
304    score_func: CriterionFunction,
305) -> Result<Vec<Pitch>> {
306    let all_possible_pitches: Result<Vec<Vec<Pitch>>> = old_pitches[1..]
307        .iter_mut()
308        .map(|p| -> Result<Vec<Pitch>> {
309            let mut enharmonics = p.get_all_common_enharmonics(2 as FloatType)?;
310            enharmonics.insert(0, p.clone());
311            Ok(enharmonics)
312        })
313        .collect();
314
315    let all_pitch_combinations = all_possible_pitches?.into_iter().multi_cartesian_product();
316
317    let mut min_score = FloatType::MAX;
318    let mut best_combination: Vec<Pitch> = Vec::new();
319
320    for combination in all_pitch_combinations {
321        let mut pitches: Vec<Pitch> = old_pitches[..1].to_vec();
322        pitches.extend(combination);
323        let score = score_func(&pitches)?;
324        // Two spellings that score the same keep the first, as music21's
325        // `min` does; a difference in the last bits of a logarithm is not a
326        // difference in dissonance.
327        if score < min_score - SCORE_TOLERANCE {
328            min_score = score;
329            best_combination = pitches;
330        }
331    }
332
333    Ok(best_combination)
334}
335
336pub(super) fn greedy_enharmonics_search(
337    old_pitches: &mut [Pitch],
338    score_func: CriterionFunction,
339) -> Result<Vec<Pitch>> {
340    let mut new_pitches = vec![];
341
342    if let Some(first) = old_pitches.first() {
343        new_pitches.push(first.clone());
344    } else {
345        return Err(Error::Pitch(
346            "can't perform greedy enharmonics search on empty pitches".into(),
347        ));
348    }
349
350    for old_pitch in old_pitches.iter_mut().skip(1) {
351        let mut candidates = vec![old_pitch.clone()];
352        candidates.extend(old_pitch.get_all_common_enharmonics(2 as FloatType)?);
353
354        let mut best_candidate = None;
355        let mut best_score: Option<OrderedFloat<FloatType>> = None;
356        for candidate in candidates.iter() {
357            let mut candidate_list = new_pitches.clone();
358            candidate_list.push(candidate.clone());
359            let score = score_func(&candidate_list)?;
360            let score = OrderedFloat(score);
361            if best_score.is_none_or(|best| score < best - SCORE_TOLERANCE) {
362                best_score = Some(score);
363                best_candidate = Some(candidate);
364            }
365        }
366        let best_candidate = best_candidate
367            .ok_or_else(|| Error::Pitch("candidates list is unexpectedly empty".to_string()))?;
368        new_pitches.push(best_candidate.clone());
369    }
370    Ok(new_pitches)
371}
372
373/// How awkward a set of pitches reads together: music21's
374/// `_dissonanceScore` with all three of its terms on. It averages a penalty
375/// for accidentals beyond one sharp or flat, a penalty growing with the
376/// denominator of each pair's Pythagorean ratio, and a reward for every
377/// third and sixth, so that `C E G` scores below `C F- G`.
378pub fn dissonance_score(pitches: &[Pitch]) -> Result<FloatType> {
379    weighted_dissonance_score(pitches, true, true, true)
380}
381
382pub(super) fn weighted_dissonance_score(
383    pitches: &[Pitch],
384    small_pythagorean_ratio: bool,
385    accidental_penalty: bool,
386    triad_award: bool,
387) -> Result<FloatType> {
388    let mut score_accidentals: FloatType = 0.0;
389    let mut score_ratio: FloatType = 0.0;
390    let mut score_triad: FloatType = 0.0;
391
392    if pitches.is_empty() {
393        return Ok(0.0);
394    }
395
396    if accidental_penalty {
397        let accidentals = pitches
398            .iter()
399            .map(|p| p.alter().abs())
400            .collect::<Vec<FloatType>>();
401        score_accidentals = accidentals
402            .iter()
403            .map(|a| if *a > 1.0 { *a } else { 0.0 })
404            .sum::<FloatType>()
405            / pitches.len() as FloatType;
406    }
407
408    let mut intervals: Vec<Interval> = vec![];
409
410    if small_pythagorean_ratio | triad_award {
411        for (index, p1) in pitches.iter().enumerate() {
412            for p2 in pitches.iter().skip(index + 1) {
413                let mut p2 = (*p2).clone();
414                p2.octave_setter(None);
415                let Ok(interval) = Interval::between(
416                    PitchOrNote::Pitch(p1.clone()),
417                    PitchOrNote::Pitch(p2.clone()),
418                ) else {
419                    return Ok(FloatType::INFINITY);
420                };
421                intervals.push(interval);
422            }
423        }
424
425        if small_pythagorean_ratio {
426            for interval in intervals.iter() {
427                score_ratio += pythagorean_denominator_log(interval)? * 0.075_853_268_88
428            }
429            score_ratio /= pitches.len() as FloatType;
430        }
431
432        if triad_award {
433            intervals.into_iter().for_each(|interval| {
434                let simple_directed = interval.generic().simple_directed();
435                let interval_semitones = interval.chromatic.whole_semitones() % 12;
436                if (simple_directed == 3 && (interval_semitones == 3 || interval_semitones == 4))
437                    || (simple_directed == 6
438                        && (interval_semitones == 8 || interval_semitones == 9))
439                {
440                    score_triad -= 1.0;
441                }
442            });
443            score_triad /= pitches.len() as FloatType;
444        }
445    }
446
447    Ok((score_accidentals + score_ratio + score_triad)
448        / (small_pythagorean_ratio as IntegerType
449            + accidental_penalty as IntegerType
450            + triad_award as IntegerType) as FloatType)
451}
452
453pub(super) fn pythagorean_denominator_log(interval: &Interval) -> Result<FloatType> {
454    let start_pitch = Pitch::from_name("C1")?;
455    let end_pitch = interval.transpose_pitch_with_options(&start_pitch, false, Some(4))?;
456
457    let natural_fifths = match end_pitch.step() {
458        StepName::C => 0,
459        StepName::D => 2,
460        StepName::E => 4,
461        StepName::F => -1,
462        StepName::G => 1,
463        StepName::A => 3,
464        StepName::B => 5,
465    };
466    let fifth_count = natural_fifths + (end_pitch.alter().round() as IntegerType * 7);
467    let found_pitch_space = start_pitch.ps() + (7 * fifth_count) as FloatType;
468    let octave_adjust = ((end_pitch.ps() - found_pitch_space) / 12.0).round() as IntegerType;
469
470    let mut denominator_twos = if fifth_count > 0 { fifth_count } else { 0 };
471    let denominator_threes = if fifth_count < 0 { -fifth_count } else { 0 };
472    denominator_twos = (denominator_twos - octave_adjust).max(0);
473
474    Ok(denominator_twos as FloatType * (2.0 as FloatType).ln()
475        + denominator_threes as FloatType * (3.0 as FloatType).ln())
476}