Skip to main content

music21_rs/chordsymbol/
realize.rs

1//! Sounding a chord symbol the way music21 sounds one: its
2//! `ChordSymbol._updatePitches`, octaves and all.
3//!
4//! The order of the steps is music21's and it matters. The kind's notation
5//! is laid out in the octave above the root, taken from octave three; the
6//! upper notes of a ninth, eleventh or thirteenth are lifted an octave; a
7//! bass the chord does not invert onto is added below it; the chord-step
8//! modifications are applied to that list as it then stands; the notes below
9//! an inverted bass go up; and the whole is moved into the middle of a piano.
10//! Every one of those steps reads the list the one before it left, so a
11//! subtraction pairs notes with degrees in whatever order the notes are in
12//! by then — which is music21's, and is kept.
13
14use crate::{
15    defaults::IntegerType,
16    error::{Error, Result},
17    interval::Interval,
18    pitch::Pitch,
19    scale::{Scale, ScaleType},
20};
21
22use super::tables::notation_intervals;
23
24/// What a [`ChordStepModification`] does to the chord: music21's `modType`.
25#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27pub enum ChordStepModificationType {
28    /// A degree sounded beside those the kind has, or in place of one of
29    /// them with the same number.
30    Add,
31    /// A degree the kind has, taken out.
32    Subtract,
33    /// A degree the kind has, raised or lowered.
34    Alter,
35}
36
37impl ChordStepModificationType {
38    /// music21's name for it: `add`, `subtract` or `alter`.
39    #[must_use]
40    pub fn music21_name(self) -> &'static str {
41        match self {
42            Self::Add => "add",
43            Self::Subtract => "subtract",
44            Self::Alter => "alter",
45        }
46    }
47
48    /// Reads music21's name, in any case.
49    pub fn from_music21_name(name: &str) -> Result<Self> {
50        match name.to_ascii_lowercase().as_str() {
51            "add" => Ok(Self::Add),
52            "subtract" => Ok(Self::Subtract),
53            "alter" => Ok(Self::Alter),
54            _ => Err(Error::Chord(format!(
55                "not a valid degree modification type: {name}"
56            ))),
57        }
58    }
59}
60
61/// One degree added to, taken from or altered in a chord symbol: music21's
62/// `ChordStepModification`, and MusicXML's `<degree>`.
63///
64/// The interval is how far the degree is moved, usually an augmented unison
65/// one way or the other. An alteration given as a number of semitones is
66/// spelled as music21 spells it — up to three as that many augmentations of
67/// the unison, beyond that as the interval that many semitones make.
68#[derive(Clone, Debug, PartialEq)]
69#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
70pub struct ChordStepModification {
71    modification_type: ChordStepModificationType,
72    degree: u8,
73    interval: Interval,
74}
75
76impl ChordStepModification {
77    /// A modification of a degree by a number of semitones.
78    pub fn new(
79        modification_type: ChordStepModificationType,
80        degree: u8,
81        alter: IntegerType,
82    ) -> Result<Self> {
83        Ok(Self {
84            modification_type,
85            degree,
86            interval: interval_of_alteration(alter)?,
87        })
88    }
89
90    /// A modification of a degree by an interval already in hand.
91    pub fn with_interval(
92        modification_type: ChordStepModificationType,
93        degree: u8,
94        interval: Interval,
95    ) -> Self {
96        Self {
97            modification_type,
98            degree,
99            interval,
100        }
101    }
102
103    /// What the modification does.
104    #[must_use]
105    pub fn modification_type(&self) -> ChordStepModificationType {
106        self.modification_type
107    }
108
109    /// The degree it does it to, counted from the root as one.
110    #[must_use]
111    pub fn degree(&self) -> u8 {
112        self.degree
113    }
114
115    /// How far the degree is moved.
116    pub fn interval(&self) -> &Interval {
117        &self.interval
118    }
119}
120
121/// music21's reading of an alteration in semitones as an interval: a
122/// perfect unison for none, augmented unisons up to three, and the interval
123/// the semitones make past that.
124fn interval_of_alteration(alter: IntegerType) -> Result<Interval> {
125    let size = alter.unsigned_abs();
126    if size > 3 {
127        return Interval::from_semitones(alter);
128    }
129    let mut name = if size == 0 {
130        "P".to_string()
131    } else {
132        "a".repeat(size as usize)
133    };
134    name.push('1');
135    if alter < 0 {
136        name.push('-');
137    }
138    Interval::from_name(&name)
139}
140
141/// A chord symbol sounded: its pitches with their octaves, lowest first, and
142/// the root and bass among them.
143#[derive(Clone, Debug)]
144pub(super) struct Realized {
145    pub(super) pitches: Vec<Pitch>,
146    root: Pitch,
147    bass: Pitch,
148}
149
150/// A chord of one of music21's kinds as music21 voices it: its pitches, and
151/// the root and bass music21 holds as objects of their own. Those move with
152/// the chord where it holds them and not where it does not, so they need
153/// not be the first note of their name, nor a note of the chord at all:
154/// `Ab10/F#` sounds `F#2 Ab3 C#4 E4` over a root of A3.
155#[derive(Clone, Debug, PartialEq)]
156pub struct ChordVoicing {
157    pitches: Vec<Pitch>,
158    root: Pitch,
159    bass: Pitch,
160}
161
162impl ChordVoicing {
163    /// The pitches, lowest written first.
164    pub fn pitches(&self) -> &[Pitch] {
165        &self.pitches
166    }
167
168    /// The root, where music21's own root object ends up.
169    pub fn root(&self) -> &Pitch {
170        &self.root
171    }
172
173    /// The bass, where music21's own bass object ends up.
174    pub fn bass(&self) -> &Pitch {
175        &self.bass
176    }
177}
178
179/// The inversion a bass stands the chord in, read off the interval from the
180/// bass up to the root as music21's `Chord._findInversion` reads it: a sixth
181/// is the first inversion, a fourth the second, a second the third, and a
182/// seventh, fifth and third the fourth to the sixth. Anything else is no
183/// inversion at all.
184pub(super) fn inversion_between(root: &Pitch, bass: &Pitch) -> Result<Option<u8>> {
185    // The size of the step up from the bass to the root, whichever octave
186    // either was written in.
187    let steps = (root.diatonic_note_number() - bass.diatonic_note_number()).rem_euclid(7) + 1;
188    Ok(match steps {
189        1 => Some(0),
190        6 => Some(1),
191        4 => Some(2),
192        2 => Some(3),
193        7 => Some(4),
194        5 => Some(5),
195        3 => Some(6),
196        _ => None,
197    })
198}
199
200/// Whether a chord of this kind can stand in this inversion: music21's
201/// `ChordSymbol.inversionIsValid`, which reads the kind and not the notes —
202/// so a dominant seventh with a ninth added still inverts as a seventh.
203pub fn inversion_is_valid_for_kind(kind: &str, inversion: u8) -> bool {
204    const SEVENTHS: [&str; 12] = [
205        "French",
206        "German",
207        "Italian",
208        "Neapolitan",
209        "Tristan",
210        "augmented-seventh",
211        "diminished-seventh",
212        "dominant-seventh",
213        "half-diminished",
214        "major-minor",
215        "major-seventh",
216        "minor-seventh",
217    ];
218    const NINTHS: [&str; 3] = ["dominant-ninth", "major-ninth", "minor-ninth"];
219    const ELEVENTHS: [&str; 3] = ["dominant-11th", "major-11th", "minor-11th"];
220    const THIRTEENTHS: [&str; 3] = ["dominant-13th", "major-13th", "minor-13th"];
221    let within = |list: &[&str]| list.contains(&kind);
222    match inversion {
223        5 => within(&THIRTEENTHS) || within(&ELEVENTHS),
224        4 => within(&ELEVENTHS) || within(&THIRTEENTHS) || within(&NINTHS),
225        3 => within(&SEVENTHS) || within(&NINTHS) || within(&ELEVENTHS) || within(&THIRTEENTHS),
226        1 | 2 => kind != "pedal",
227        _ => false,
228    }
229}
230
231/// The pitches a chord of one of music21's kinds sounds on a root, with a
232/// bass and chord-step modifications: music21's `ChordSymbol._updatePitches`
233/// run over the state MusicXML hands it, rather than over a figure.
234///
235/// A kind music21's table has not got — or none at all — sounds its root,
236/// and its bass beside it, as music21's does.
237pub fn sound_chord_kind(
238    root: &Pitch,
239    kind: &str,
240    bass: Option<&Pitch>,
241    modifications: &[ChordStepModification],
242) -> Result<Vec<Pitch>> {
243    // The alias is read only to find the notation: music21 compares the
244    // kind as written everywhere else, which is why its sevenths include
245    // `half-diminished` and `major-minor`.
246    let notation = super::tables::notation_for_kind(super::tables::resolve_kind_alias(kind));
247    sound_chord_notation(root, kind, notation, bass, modifications)
248}
249
250/// The same with the kind's notation handed over rather than looked up:
251/// music21's table can be added to while a program runs
252/// (`addNewChordSymbol`), and a kind added there is sounded from the
253/// notation it was added with.
254pub fn sound_chord_notation(
255    root: &Pitch,
256    kind: &str,
257    notation: Option<&str>,
258    bass: Option<&Pitch>,
259    modifications: &[ChordStepModification],
260) -> Result<Vec<Pitch>> {
261    Ok(realize(root, bass, Some(kind), notation, None, modifications)?.pitches)
262}
263
264/// The same, with the root and bass as music21 is left holding them: its
265/// `_updatePitches` in full.
266pub fn voice_chord_notation(
267    root: &Pitch,
268    kind: &str,
269    notation: Option<&str>,
270    bass: Option<&Pitch>,
271    modifications: &[ChordStepModification],
272) -> Result<ChordVoicing> {
273    let realized = realize(root, bass, Some(kind), notation, None, modifications)?;
274    Ok(ChordVoicing {
275        pitches: realized.pitches,
276        root: realized.root,
277        bass: realized.bass,
278    })
279}
280
281/// A pitch named `name` in `octave`.
282fn named(name: &str, octave: IntegerType) -> Result<Pitch> {
283    let mut pitch = Pitch::from_name(name)?;
284    pitch.set_octave(Some(octave));
285    Ok(pitch)
286}
287
288fn octave_of(pitch: &Pitch) -> IntegerType {
289    pitch
290        .octave()
291        .unwrap_or(crate::defaults::PITCH_OCTAVE as IntegerType)
292}
293
294fn shift_octave(pitch: &mut Pitch, by: IntegerType) {
295    let octave = octave_of(pitch);
296    pitch.set_octave(Some(octave + by));
297}
298
299/// music21's `sortDiatonicAscending`: by staff position, then by pitch
300/// space, so `B#3` comes before `C4`.
301fn sort_diatonic(pitches: &mut [Pitch]) {
302    pitches.sort_by(|left, right| {
303        left.diatonic_note_number()
304            .cmp(&right.diatonic_note_number())
305            .then(left.ps().total_cmp(&right.ps()))
306    });
307}
308
309/// music21's `FiguredBassScale.getSamplePitches` over the kind's notation,
310/// with the duplicated root taken off the front: every note the notation
311/// names, placed from the root upward to a diminished octave above it.
312///
313/// The names are the bass's and then the notation's own, last first, which
314/// is the order music21 builds them in; the sort that follows is stable, so
315/// notes that sound alike keep it.
316fn sample_pitches(root: &Pitch, names: &[String]) -> Result<Vec<Pitch>> {
317    let top = root.transpose(&Interval::from_name("d8")?)?;
318    let highest_octave = octave_of(&top);
319    let mut order = vec![root.name()];
320    order.extend(names.iter().rev().cloned());
321    let mut placed = Vec::new();
322    for name in &order {
323        for octave in 0..=highest_octave {
324            let candidate = named(name, octave)?;
325            if candidate.ps() >= root.ps() && candidate.ps() <= top.ps() {
326                placed.push(candidate);
327            }
328        }
329    }
330    placed.sort_by(|left, right| left.ps().total_cmp(&right.ps()));
331    if !placed.is_empty() {
332        let _ = placed.remove(0);
333    }
334    Ok(placed)
335}
336
337/// Sounds a chord of a music21 kind on a root, with a bass and the
338/// modifications read off its figure or handed over with it.
339///
340/// `names` are the notation's pitch names over the root and `degrees` the
341/// notation's own tokens (`1`, `-3`, `5`), which the modifications are
342/// matched against. `kind` decides the octave lifts and which inversions
343/// the bass may make.
344/// What one place in music21's list of pitches holds. music21 puts its
345/// root and bass objects themselves in the list where it puts them there
346/// at all, so an octave pass over the list moves them with it, and once for
347/// every place they hold: a bass added twice is one note moved twice.
348#[derive(Clone, Copy, PartialEq)]
349enum Slot {
350    Root,
351    Bass,
352    Own(usize),
353}
354
355/// music21's list of pitches with the identity of each place kept.
356///
357/// ```text
358///   slots:  [Root, Bass, Own(0), Bass]
359///   root    C3 <- one object, read and moved through its slot
360///   bass    E2 <- one object, held twice, moved twice a pass
361///   own     [G3]
362/// ```
363struct Voicing {
364    root: Pitch,
365    bass: Pitch,
366    own: Vec<Pitch>,
367    slots: Vec<Slot>,
368}
369
370impl Voicing {
371    /// A list of pitches of their own.
372    fn owning(root: Pitch, bass: Pitch, pitches: Vec<Pitch>) -> Self {
373        let slots = (0..pitches.len()).map(Slot::Own).collect();
374        Self {
375            root,
376            bass,
377            own: pitches,
378            slots,
379        }
380    }
381
382    fn pitch(&self, slot: Slot) -> &Pitch {
383        match slot {
384            Slot::Root => &self.root,
385            Slot::Bass => &self.bass,
386            Slot::Own(index) => &self.own[index],
387        }
388    }
389
390    fn pitch_mut(&mut self, slot: Slot) -> &mut Pitch {
391        match slot {
392            Slot::Root => &mut self.root,
393            Slot::Bass => &mut self.bass,
394            Slot::Own(index) => &mut self.own[index],
395        }
396    }
397
398    /// The pitches the places hold, in order.
399    fn values(&self) -> Vec<Pitch> {
400        self.slots
401            .iter()
402            .map(|slot| self.pitch(*slot).clone())
403            .collect()
404    }
405
406    /// Moves the pitch in the place at `index` by `by` octaves.
407    fn shift(&mut self, index: usize, by: IntegerType) {
408        let slot = self.slots[index];
409        shift_octave(self.pitch_mut(slot), by);
410    }
411
412    /// Moves every place by `by` octaves, as music21's loop over the list
413    /// does: a pitch held twice moves twice.
414    fn shift_all(&mut self, by: IntegerType) {
415        for index in 0..self.slots.len() {
416            self.shift(index, by);
417        }
418    }
419
420    /// Takes `pitches` as the list after a step that worked on its values:
421    /// music21's modifications take pitches out and add new ones at the
422    /// end, so the places that remain keep what they held, in order, and
423    /// what follows them is new.
424    fn replace(&mut self, pitches: Vec<Pitch>) {
425        let old = std::mem::take(&mut self.slots);
426        let mut remaining = old.into_iter().peekable();
427        for pitch in pitches {
428            // Skip the places taken out, up to one that holds this pitch.
429            while remaining
430                .peek()
431                .is_some_and(|slot| *self.pitch(*slot) != pitch)
432            {
433                remaining.next();
434            }
435            match remaining.next() {
436                Some(slot) => self.slots.push(slot),
437                None => {
438                    self.slots.push(Slot::Own(self.own.len()));
439                    self.own.push(pitch);
440                }
441            }
442        }
443    }
444}
445
446pub(super) fn realize(
447    root: &Pitch,
448    bass: Option<&Pitch>,
449    kind: Option<&str>,
450    notation: Option<&str>,
451    names: Option<Vec<String>>,
452    modifications: &[ChordStepModification],
453) -> Result<Realized> {
454    let root = named(&root.name(), 3)?;
455    let bass = named(&bass.unwrap_or(&root).name(), 3)?;
456    let kind = kind.unwrap_or("");
457
458    let mut degrees: Vec<String> = notation
459        .map(|notation| notation.split(',').map(str::to_string).collect())
460        .unwrap_or_default();
461    let mut voicing = match (&names, notation) {
462        (Some(names), _) => {
463            Voicing::owning(root.clone(), bass.clone(), sample_pitches(&root, names)?)
464        }
465        (None, Some(notation)) => {
466            let names = notation_intervals(notation)?
467                .into_iter()
468                .map(|(_, name)| Ok(Interval::from_name(&name)?.transpose_pitch(&root)?.name()))
469                .collect::<Result<Vec<_>>>()?;
470            Voicing::owning(root.clone(), bass.clone(), sample_pitches(&root, &names)?)
471        }
472        // A kind music21 has no notation for sounds its root, and its bass
473        // beside it: the objects themselves.
474        (None, None) => {
475            let mut slots = vec![Slot::Root];
476            if bass != root {
477                slots.push(Slot::Bass);
478            }
479            Voicing {
480                root: root.clone(),
481                bass: bass.clone(),
482                own: Vec::new(),
483                slots,
484            }
485        }
486    };
487
488    // music21's `_adjustOctaves`: the upper notes of the stacked kinds go up
489    // an octave, which is what spaces a ninth as a ninth. It builds new
490    // pitches as it does.
491    let lifted: &[usize] = match kind {
492        "dominant-ninth" | "major-ninth" | "minor-ninth" => &[1],
493        "dominant-11th" | "major-11th" | "minor-11th" => &[1, 3],
494        "dominant-13th" | "major-13th" | "minor-13th" => &[1, 3, 5],
495        _ => &[],
496    };
497    if !lifted.is_empty() {
498        let mut pitches = voicing.values();
499        for &index in lifted {
500            if let Some(pitch) = pitches.get_mut(index) {
501                shift_octave(pitch, 1);
502            }
503        }
504        sort_diatonic(&mut pitches);
505        voicing = Voicing::owning(voicing.root, voicing.bass, pitches);
506    }
507
508    let mut inversion = None;
509    if voicing.root.name() != voicing.bass.name() {
510        let found = inversion_between(&voicing.root, &voicing.bass)?;
511        match found {
512            Some(number) if inversion_is_valid_for_kind(kind, number) => inversion = Some(number),
513            // A bass the chord does not invert onto is a note added under
514            // it, in the octave below the root; the object moves, wherever
515            // the list already holds it.
516            _ => {
517                voicing.bass.set_octave(Some(2));
518                voicing.slots.push(Slot::Bass);
519            }
520        }
521    }
522
523    let mut pitches = voicing.values();
524    let current_root = voicing.root.clone();
525    apply_modifications(
526        &current_root,
527        kind,
528        &mut pitches,
529        &mut degrees,
530        modifications,
531    )?;
532    voicing.replace(pitches);
533
534    if let Some(number) = inversion.filter(|number| *number != 0) {
535        let number = usize::from(number);
536        for index in 0..number.min(voicing.slots.len()) {
537            voicing.shift(index, 1);
538            if number > 3 {
539                voicing.shift(index, 1);
540            }
541        }
542        // The bass is read as it stands now, for it may be in the list.
543        for index in 0..voicing.slots.len() {
544            let slot = voicing.slots[index];
545            if voicing.pitch(slot).diatonic_note_number() < voicing.bass.diatonic_note_number() {
546                voicing.shift(index, 1);
547            }
548        }
549    }
550
551    // Down while anything is above middle C's D, then back up while
552    // anything is below the piano's lowest A: music21's `_hasPitchAboveC4`
553    // and `_hasPitchBelowA1`.
554    let highest = |voicing: &Voicing| {
555        voicing
556            .values()
557            .iter()
558            .map(Pitch::diatonic_note_number)
559            .max()
560    };
561    while highest(&voicing).is_some_and(|number| number > 30) {
562        voicing.shift_all(-1);
563    }
564    let lowest = |voicing: &Voicing| {
565        voicing
566            .values()
567            .iter()
568            .map(Pitch::diatonic_note_number)
569            .min()
570    };
571    while lowest(&voicing).is_some_and(|number| number < 13) {
572        voicing.shift_all(1);
573    }
574
575    let mut pitches = voicing.values();
576    sort_diatonic(&mut pitches);
577    // music21 then sets the bass on the chord it has built, and a bass the
578    // chord has no note of by that name is put in front of it where it was
579    // written: `C+/G` sounds a G under an augmented triad that has a G#.
580    let bass = voicing.bass;
581    if !pitches.iter().any(|pitch| pitch.name() == bass.name()) {
582        pitches.insert(0, bass.clone());
583    }
584    Ok(Realized {
585        pitches,
586        root: voicing.root,
587        bass,
588    })
589}
590
591/// A degree token of the notation read as its number: `-7` and `#5` are the
592/// seventh and the fifth.
593fn degree_number(token: &str) -> Option<u8> {
594    token.replace(['-', '#', 'A'], "").parse().ok()
595}
596
597/// music21's `_adjustPitchesForChordStepModifications`, over the list as the
598/// steps before it left it.
599///
600/// Its loops remove from the lists they are walking, which in Python skips
601/// the element that moves into the removed one's place; the walks here are
602/// written to do the same, since which note a subtraction takes depends on
603/// it.
604fn apply_modifications(
605    root: &Pitch,
606    kind: &str,
607    pitches: &mut Vec<Pitch>,
608    degrees: &mut Vec<String>,
609    modifications: &[ChordStepModification],
610) -> Result<()> {
611    let scale = Scale::new(ScaleType::Major, root.clone());
612    for modification in modifications {
613        match modification.modification_type {
614            ChordStepModificationType::Add => {
615                add_degree(&scale, root, kind, pitches, degrees, modification)?;
616            }
617            ChordStepModificationType::Subtract => {
618                if degrees.is_empty() {
619                    continue;
620                }
621                let mut found = false;
622                let mut index = 0;
623                while index < pitches.len() && index < degrees.len() {
624                    if degree_number(&degrees[index]) == Some(modification.degree) {
625                        let taken = pitches[index].clone();
626                        if let Some(at) = pitches.iter().position(|pitch| *pitch == taken) {
627                            let _ = pitches.remove(at);
628                        }
629                        found = true;
630                        let written = modification.degree.to_string();
631                        if let Some(at) = degrees.iter().position(|token| token.contains(&written))
632                        {
633                            let _ = degrees.remove(at);
634                        }
635                    }
636                    index += 1;
637                }
638                if !found {
639                    return Err(Error::Chord(format!(
640                        "Degree not in specified chord: {}",
641                        modification.degree
642                    )));
643                }
644            }
645            ChordStepModificationType::Alter => {
646                let mut found = false;
647                for index in 0..pitches.len().min(degrees.len()) {
648                    if degree_number(&degrees[index]) == Some(modification.degree) {
649                        pitches[index] = pitches[index].transpose(&modification.interval)?;
650                        found = true;
651                    }
652                }
653                // A degree the chord has not got is added instead, as
654                // music21 turns the modification into an addition.
655                if !found {
656                    add_degree(&scale, root, kind, pitches, degrees, modification)?;
657                }
658            }
659        }
660    }
661    Ok(())
662}
663
664/// music21's `typeAdd`: the degree of the major scale on the root, placed at
665/// or above the root, moved by the modification's interval and put an
666/// octave higher from the seventh up; in place of a note on the same degree
667/// where the kind has one, and beside the others where it has not.
668fn add_degree(
669    scale: &Scale,
670    root: &Pitch,
671    kind: &str,
672    pitches: &mut Vec<Pitch>,
673    degrees: &[String],
674    modification: &ChordStepModification,
675) -> Result<()> {
676    let folded = IntegerType::from((modification.degree.max(1) - 1) % 7 + 1);
677    let mut added = scale.pitch_at_degree(folded)?;
678    added.set_octave(Some(octave_of(root)));
679    if added.ps() < root.ps() {
680        shift_octave(&mut added, 1);
681    }
682    let semitones = modification.interval.semitones();
683    if semitones != 0.0 {
684        // Added degrees are measured from a dominant chord, whose seventh is
685        // minor, so a raised seventh is raised from there: a semitone down
686        // first, spelled from its number as music21's integer transposition
687        // spells it.
688        if modification.degree == 7 && semitones > 0.0 && !kind.is_empty() {
689            let mut lowered = Pitch::from_number(added.ps() - 1.0)?;
690            lowered.set_octave(Some(octave_of(&lowered)));
691            added = lowered;
692        }
693        added = added.transpose(&modification.interval)?;
694    }
695    if modification.degree >= 7 {
696        shift_octave(&mut added, 1);
697    }
698    if degrees.contains(&modification.degree.to_string()) {
699        let mut index = 0;
700        while index < pitches.len() {
701            let on_degree = scale.degree_of(&pitches[index])?;
702            if on_degree == Some(usize::from(modification.degree)) {
703                let _ = pitches.remove(index);
704                pitches.push(added.clone());
705            }
706            index += 1;
707        }
708    } else {
709        pitches.push(added);
710    }
711    Ok(())
712}