Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Scales, modes and keys

Files: crates/lineup-core/src/harmony/scale.rs, crates/lineup-core/src/key_map.rs, crates/lineup-core/src/tools.rs and crates/lineup-core/src/project.rs

A key is what most of the app looks at before it shows anything: the piano roll lights up the key’s rows, the harmony keys play its chords, the chord sheet names chords in it, the band plays in it, and every note’s name depends on it. This chapter is about the Scale type that holds a key, how a key writes each of its notes, and what happens when the song changes key, all at once or partway through.

The music, briefly

A scale is a choice of some of the twelve pitch classes, counted up from a root. The major scale is the white keys from C, C D E F G A B: as semitones above the root, 0 2 4 5 7 9 11. The same pattern started on another note gives the major scale of that note: from D, it is D E F♯ G A B C♯. A scale on a particular root, used as the home of a piece, is a key: D major.

The scales Ferrous Lineup knows, as semitones above the root:

ScaleSemitones
major0 2 4 5 7 9 11
minor (natural)0 2 3 5 7 8 10
harmonic minor0 2 3 5 7 8 11
melodic minor0 2 3 5 7 9 11
Dorian0 2 3 5 7 9 10
Phrygian0 1 3 5 7 8 10
Lydian0 2 4 6 7 9 11
Mixolydian0 2 4 5 7 9 10
Locrian0 1 3 5 6 8 10
major pentatonic0 2 4 7 9
minor pentatonic0 3 5 7 10
blues0 3 5 6 7 10
chromaticall twelve

Harmonic minor raises the seventh note of the minor scale so that it leans on the root a semitone above it; melodic minor raises the sixth too. (Classical melodic minor falls back to the natural minor on the way down; the app uses the rising form both ways, as jazz musicians do.) The modes, Dorian to Locrian, are the major scale started on another of its notes: D Dorian is the white keys from D. So each mode shares its notes, and its key signature, with one major key: its relative major. A minor’s relative major is C major, three semitones up.

The key signature is the sharps or flats written at the start of every line of a score. Each major key has its own, and going up a fifth adds a sharp: C major has none, G major one, D major two. Going down a fifth adds a flat: F major has one, B♭ major two. That circle of keys a fifth apart is the circle of fifths. The manual’s Keys and modes tells all of this with sounds to try.

Kinds of scale: an enum with data

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ScaleKind {
    Major,
    NaturalMinor,
    HarmonicMinor,
    MelodicMinor,
    Dorian,
    Phrygian,
    Lydian,
    Mixolydian,
    Locrian,
    MajorPentatonic,
    MinorPentatonic,
    Blues,
    Chromatic,
    Row(ToneRow),
    Set(PitchSet),
}
}

The thirteen scales of tonal music are plain variants. The last two carry their notes with them: a twelve-tone row and a pitch-class set can be the “key” of a song too, for music that has no key in the usual sense. They have chapters of their own (Twelve-tone rows and their rules and Pitch-class sets, and one harmony with four names); here they mostly show up as the cases that are different. One method sorts them out:

#![allow(unused)]
fn main() {
pub fn is_tonal(self) -> bool {
    !matches!(self, ScaleKind::Row(_) | ScaleKind::Set(_))
}
}

Row(_) matches a Row whatever row it holds.

The notes of a scale are listed as semitones above the root. For the tonal scales those lists never change, so they are written into the program and borrowed as &'static [u8], slices that can have any length, five for a pentatonic scale, seven for the major scale:

#![allow(unused)]
fn main() {
fn tonal_intervals(self) -> &'static [u8] {
    match self {
        ScaleKind::Major => &[0, 2, 4, 5, 7, 9, 11],
        ScaleKind::NaturalMinor => &[0, 2, 3, 5, 7, 8, 10],
        // ...
        ScaleKind::Chromatic | ScaleKind::Row(_) | ScaleKind::Set(_) => {
            &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
        }
    }
}
}

A row or a set has its own notes, which exist only inside the value, so nothing 'static can point at them. The public intervals therefore returns an owned Vec<u8>, made from the row, the set or the table:

#![allow(unused)]
fn main() {
pub fn intervals(self) -> Vec<u8> {
    match self {
        ScaleKind::Row(row) => row.semitones().to_vec(),
        ScaleKind::Set(set) => set.semitones(),
        kind => kind.tonal_intervals().to_vec(),
    }
}
}

The last arm’s pattern is a plain name, kind, which matches anything and binds it, so every tonal scale goes there.

A key: a root and a kind

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Scale {
    pub root: PitchClass,
    pub kind: ScaleKind,
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    respelled: bool,
}
}

respelled is for the few keys that can be written two ways, below. Most of the methods on Scale are short iterator chains. The pitch classes of the scale are the root moved up by each interval:

#![allow(unused)]
fn main() {
pub fn pitch_classes(self) -> impl Iterator<Item = PitchClass> {
    self.kind
        .intervals()
        .into_iter()
        .map(move |interval| self.root.transpose(interval as i32))
}
}

impl Iterator<Item = PitchClass> says “some iterator of pitch classes” without naming its real type, which involves a closure, and closures have types nobody can write. into_iter() on the Vec gives its numbers by value, and move gives the closure its own copy of self (a Scale is Copy), since the iterator outlives the call.

The degree of a note is its place in the scale. position finds it, counting from 0, where musicians count from 1:

#![allow(unused)]
fn main() {
pub fn degree_of(self, pitch_class: PitchClass) -> Option<usize> {
    self.pitch_classes().position(|p| p == pitch_class)
}
}

In A minor, E is degree 4 (musicians say “the fifth”), and F♯ has no degree. pitch_set collects the same pitch classes into a PitchSet, so the test that every mode shares its notes with its relative major is a comparison of three numbers:

#![allow(unused)]
fn main() {
let c_major = Scale::default().pitch_set();
assert_eq!(Scale::new(D, ScaleKind::Dorian).pitch_set(), c_major);
assert_eq!(Scale::new(G, ScaleKind::Mixolydian).pitch_set(), c_major);
assert_eq!(Scale::new(A, ScaleKind::NaturalMinor).pitch_set(), c_major);
}

Scale::default() is C major, and is_heptatonic says whether a scale has seven notes. Chords stacked in thirds and Roman numerals only make sense in those, so many methods start by asking it (Chords: building, naming and inversions).

Sharps or flats: how a key is written

Every minor key and mode shares its key signature with its relative major, so the question “sharps or flats?” is answered by that major key. relative_major says how far up it is:

#![allow(unused)]
fn main() {
pub(crate) fn relative_major(self) -> u8 {
    match self {
        ScaleKind::Major
        | ScaleKind::MajorPentatonic
        | ScaleKind::Chromatic
        | ScaleKind::Row(_)
        | ScaleKind::Set(_) => 0,
        ScaleKind::NaturalMinor
        | ScaleKind::HarmonicMinor
        | ScaleKind::MelodicMinor
        | ScaleKind::MinorPentatonic
        | ScaleKind::Blues => 3,
        ScaleKind::Dorian => 10,
        ScaleKind::Phrygian => 8,
        ScaleKind::Lydian => 7,
        ScaleKind::Mixolydian => 5,
        ScaleKind::Locrian => 1,
    }
}
}

D Dorian is the white keys from D, and C is ten semitones above D; E Phrygian’s C is eight above E, and so on. pub(crate) makes the method visible inside lineup-core only: the rest of the program asks the questions built on it.

The usual spelling then depends on that major key. The keys whose signature has flats are F, B♭, E♭, A♭ and D♭ major, and their minors and modes:

#![allow(unused)]
fn main() {
fn usual_spelling(self) -> Spelling {
    use PitchClass::*;
    match self.root.transpose(self.kind.relative_major() as i32) {
        F | ASharp | DSharp | GSharp | CSharp => Spelling::Flats,
        // F# major and Gb major are the same keys of the piano, and
        // the tonic's name decides between them. Their modes are
        // named with sharps (G# Dorian, B Lydian), except F Locrian,
        // whose notes are those of Gb major: F Gb Ab Bb Cb Db Eb.
        FSharp if self.root == F => Spelling::Flats,
        _ => Spelling::Sharps,
    }
}
}

The middle arm has a match guard, if self.root == F: the arm is taken only if the pattern matches and the condition holds. F Locrian is the one mode on those keys whose root is a white key with its own name; written with sharps it would be E♯ Locrian, so it takes the flats of G♭ major.

The key signature, round the circle of fifths

A key signature is a number: how many sharps, or how many flats as a negative number. Going round the circle of fifths, the major key n fifths above C has n sharps. key_signature finds n by trying every step round the circle until it lands on the relative major:

#![allow(unused)]
fn main() {
pub fn key_signature(self) -> Option<(i8, bool)> {
    if !self.kind.is_tonal() || self.kind == ScaleKind::Chromatic {
        return None;
    }
    let major = self.root.transpose(self.kind.relative_major() as i32);
    // Going round the circle of fifths: each fifth up adds a sharp.
    let fifths = (0..12i8)
        .find(|&n| PitchClass::C.transpose(7 * i32::from(n)) == major)
        .unwrap_or(0);
    let sharps = match self.spelling() {
        Spelling::Flats => fifths - 12,
        Spelling::Sharps => fifths,
    };
    let minor = self.kind.relative_major() == 3;
    Some((sharps, minor))
}
}

Twelve fifths up come back to C, so a key with flats is the same number of fifths counted the other way: F major is eleven fifths up, which is one fifth down, so 11 - 12 = -1, one flat. D♭ major is seven fifths up and five down: five flats. The second value says whether the key is minor, which a MIDI file’s key signature event stores too (MIDI: messages, devices, recording and files). A row, a set or the chromatic scale has no key signature, hence the Option.

The 0..12i8 range has its type written on the number, 12i8, so n is an i8 like the result. find stops at the first n that fits; one always does, and unwrap_or(0) only keeps the compiler from asking what happens if not.

Keys that can be written two ways

The keys with six sharps or six flats are the same keys of the piano: F♯ major and G♭ major. Around them, C♯ major (seven sharps) is D♭ major (five flats), and B major (five sharps) is C♭ major (seven flats). Composers choose the name that fits the music around it; Debussy writes the middle of Golliwogg’s Cakewalk in G♭ major, a key of flats like the E♭ major it comes from. Any other key would need eight signs or more the other way round: D♯ major needs nine sharps, so it is always E♭ major.

So a Scale can be respelled: the same notes, the other name. other_spelling gives that key when there is one:

#![allow(unused)]
fn main() {
pub fn other_spelling(self) -> Option<Scale> {
    use PitchClass::*;
    let key = self.as_key();
    let major = key.root.transpose(key.kind.relative_major() as i32);
    let either_way = key.kind.is_tonal()
        && key.kind != ScaleKind::Chromatic
        && matches!(major, FSharp | CSharp | B);
    either_way.then_some(Scale {
        respelled: !self.respelled,
        ..self
    })
}
}

Scale { respelled: !self.respelled, ..self } is struct update syntax: a copy of self with one field changed. spelling() turns the usual answer round when the key is respelled, and the tonic takes the next letter: F♯ becomes G♭, B becomes C♭.

The field is private, so the only ways to set it are the methods, which check that the key can be written the other way. Scale::written(tonic, kind) builds a key from how its tonic is written, which is what the Key menus and the key line of a typed chord sheet use: G♭ with “major” gives the respelled F♯ major, and G♭ with “minor”, a key that would need a double flat in its signature, gives the usual F♯ minor.

In a file, the attribute #[serde(default, skip_serializing_if = "std::ops::Not::not")] leaves the field out when it is false, which is almost always, and reads a missing one as false. skip_serializing_if takes the path of a function that receives &bool and returns true to skip; std::ops::Not::not is the ! operator as a function, so a field is skipped when !respelled. A file saved with a respelled key and opened by a version of the app that does not know the field gets the key under its usual name, with the same notes.

A letter for every note

spelling() only says how the black keys of the tonic are written. A key’s notes need more: D harmonic minor has a B♭ and a C♯, and F♯ major has an E♯, which is neither the sharp nor the flat name of any black key. The rule is the one from Pitch classes, notes and spelling: a seven-note scale uses each letter once. spell gives a pitch class the letter of its place in the scale, counted up from the tonic’s letter, and lets NoteName::with_letter find the sharps or flats:

#![allow(unused)]
fn main() {
pub fn spell(self, pitch_class: PitchClass) -> NoteName {
    if let Some(scale) = self.set_as_scale() {
        return scale.spell(pitch_class);
    }
    if !self.kind.is_tonal() {
        return NoteName::chromatic(pitch_class);
    }
    let name = NoteName::with_letter(
        pitch_class,
        self.tonic_name()
            .letter
            .up(i32::from(self.letters_above_tonic(pitch_class))),
    );
    if self.has_letter_for(pitch_class) {
        name
    } else {
        name.simplified()
    }
}
}

How many letters above the tonic a note goes is its place in the scale for a seven-note scale. The pentatonic and blues scales leave letters out, so they have a table of their own:

#![allow(unused)]
fn main() {
fn letter_steps(self) -> &'static [u8] {
    match self {
        ScaleKind::MajorPentatonic => &[0, 1, 2, 4, 5],
        ScaleKind::MinorPentatonic => &[0, 2, 3, 4, 6],
        ScaleKind::Blues => &[0, 2, 3, 4, 4, 6],
        ScaleKind::Chromatic | ScaleKind::Row(_) | ScaleKind::Set(_) => &CHROMATIC_LETTERS,
        _ => &[0, 1, 2, 3, 4, 5, 6],
    }
}
}

The blues scale’s two fourth-letter entries are its “blue note”, a flat fifth next to the fifth: A blues is A C D E♭ E G, not A C D D♯ E G.

Notes from outside the key

A note outside the scale has no place in it. It is written the way musicians most often meet it, counted from the tonic: a flat second, third, sixth or seventh, a sharp fourth, and the major sixth and seventh, which in a minor key are the raised notes of melodic and harmonic minor. That is a table of letters by semitones above the tonic:

#![allow(unused)]
fn main() {
const CHROMATIC_LETTERS: [u8; 12] = [0, 1, 1, 2, 2, 3, 3, 4, 5, 5, 6, 6];
}

One semitone above the tonic is one letter up (a flat second), six semitones is three letters up (a sharp fourth), eleven is six letters up (the major seventh). So in C major the black keys outside the key are D♭, E♭, F♯, A♭ and B♭, and in D minor C♯ is C♯.

letters_above_tonic chooses between the scale’s place and this table:

#![allow(unused)]
fn main() {
fn letters_above_tonic(self, pitch_class: PitchClass) -> u8 {
    let semitones = self.root.semitones_up_to(pitch_class);
    if self.has_letter_for(pitch_class)
        && let Some(place) = self
            .kind
            .tonal_intervals()
            .iter()
            .position(|&interval| interval == semitones)
    {
        return self.kind.letter_steps()[place];
    }
    CHROMATIC_LETTERS[semitones as usize]
}
}

The if joins a condition and a let with &&. This is a let chain: the block runs only if the condition is true and the pattern matches, and place is bound inside it. Without it, the code would need an if inside an if, or a match on a tuple.

The table can ask for two signs. In D♭ major the note a semitone above the tonic, D, is a flat second, the letter E: E𝄫. No musician writes that for a note outside the key, so spell takes the simpler name, D, for any note the scale has no letter for. Inside the scale two signs stay, because the key needs them: G♯ harmonic minor’s seventh note is F𝄪.

A pitch-class set whose notes are exactly a tonal scale on its root is spelled as that scale (set_as_scale), so E major shown as a set keeps its G♯, C♯ and D♯. A row or any other set has no letters, so its notes take the chromatic names.

Names with octaves

note_name puts the spelled name together with the right octave number, through Note::name_as:

#![allow(unused)]
fn main() {
pub fn note_name(self, note: Note) -> String {
    note.name_as(self.spell(note.pitch_class()))
}
}

C♯ harmonic minor raises its seventh, B, to B♯, so in that key MIDI note 60, middle C, is “B♯3”. The voice-leading check and the part motion view name the notes they talk about this way (Voice leading and part motion).

Changing key: moving the song

The Key menus, left of the harmony keys and at the top of the piano roll, set the key of the whole song, the one the harmony keys, the hints and the band use. So they move the song too, the way a band plays the same song in another key when asked: pick D and the chords, the bass and the melody all go up a tone; pick minor and every note moves to the same place in the minor scale. The drums stay as they are, and Ctrl+Z puts everything back in one go.

As with every editing tool, the musical part is a plain function that changes nothing itself. It answers one question: where does this note go when the key changes from from to to?

#![allow(unused)]
fn main() {
pub fn change_key(note: Note, from: Scale, to: Scale) -> Note {
    let from = from_key(from, to);
    if !from.kind.notes_follow(to.kind) {
        return note;
    }
    let up = from.root.semitones_up_to(to.root) as i32;
    let shift = if up > 6 { up - 12 } else { up };
    let Some(moved) = note.transpose(shift) else {
        return note;
    };
    // The chromatic scale holds every note, so it has no degrees to
    // match with a key's: into it or out of it, notes only take the new
    // root. (Out of it, every note would otherwise be "of the old scale"
    // and be pulled into the new key, losing every sharp and flat.)
    if from.kind == ScaleKind::Chromatic || to.kind == ScaleKind::Chromatic {
        return moved;
    }
    // The old scale moved to the new root, so `moved` can be compared
    // with it degree by degree.
    let old = Scale::new(to.root, from.kind);
    let Some(degree) = old.degree_of(moved.pitch_class()) else {
        return moved;
    };
    let (old_steps, new_steps) = (from.kind.intervals(), to.kind.intervals());
    if old_steps.len() == new_steps.len() {
        let offset = new_steps[degree] as i32 - old_steps[degree] as i32;
        moved.transpose(offset).unwrap_or(moved)
    } else {
        nearest_in_key(moved, to)
    }
}
}

It works in two moves.

  1. A new root moves every note by the same distance. C to D is two semitones up. C to G could be seven up or five down; shift takes the nearer one, so the song stays roughly where it was on the keyboard instead of jumping up a fifth.

  2. A new mode moves each note to the same degree. After the first move the note is compared with the old kind of scale on the new root. If it is the third note of D major (F♯), it becomes the third note of D minor (F). The difference between the two interval lists at the same place is how far that degree moves:

    degree1234567
    major02457911
    natural minor02357810
    moves by00-100-1-1

    So C major to C minor lowers E, A and B, and leaves C, D, F and G.

Some notes need a rule of their own:

  • A note outside the old scale, such as a note of a borrowed chord or of a part written in another key, has no degree, so the let ... else returns it after the first move only. It keeps its distance from the root, and a modulation stays a modulation.
  • Scales of different sizes, like major (seven notes) and major pentatonic (five), have no matching degrees. A note that is not in the new scale goes to the nearest one that is, with nearest_in_key (Editing tools as plain functions).
  • The chromatic scale has every note, so going into it or out of it only moves the root.
  • Between a key and a row or a set, there is nothing to move by, and notes_follow says so: only what the piano roll highlights changes. A set whose notes are exactly a key, such as C major shown as the set 7-35, moves on as that key (from_key).

let Some(moved) = ... else { return note; }; is a let-else: if the pattern does not match, the else block must leave the function. Here it covers “the note would leave the MIDI range” and “the note is not in the scale”.

The whole project

Project::change_key runs that function over every note that should move. Its core loop:

#![allow(unused)]
fn main() {
let pitched: Vec<bool> = self.channels.iter().map(Channel::is_pitched).collect();
for (index, pattern) in self.patterns.iter_mut().enumerate() {
    let (old, new) = (before[index], after[index]);
    for (channel, _) in pitched.iter().enumerate().filter(|(_, p)| **p) {
        if pattern.is_step_row(channel) {
            continue;
        }
        let notes: Vec<NoteEvent> = pattern
            .notes(channel)
            .iter()
            .map(|note| NoteEvent {
                pitch: tools::change_key(note.pitch, old, new),
                ..*note
            })
            .collect();
        pattern.set_notes(channel, notes);
    }
}
}
  • pitched is collected before the loop because the loop borrows self.patterns mutably, and Rust will not let the code look at self.channels through self at the same time. A small Vec<bool> read first ends that borrow early.
  • Channel::is_pitched is false for the built-in drums and for a MIDI Out channel on the General MIDI drum channel: drums do not move.
  • A row of step-sequencer steps (every note a one-step C4) is a sound played on the beat, not a tune, so it stays as it is.
  • before[index] and after[index] are the keys each pattern is played in before and after the change, which are not always the song’s key, as the next section shows.

The app calls it through edit, which keeps the project as it was before (Undo and redo), so one Ctrl+Z restores the key and every note:

#![allow(unused)]
fn main() {
self.edit("change key", |project| project.change_key(scale));
}

The chord sheet and the band’s parts move with the notes; they are in The chord sheet: model, layout and text and How the band plays a chord sheet.

Key changes partway through: key markers

Many pieces do not stay in one key. Mozart’s Rondo alla Turca starts in A minor, turns to A major at bar 25, where the key signature gets three sharps, and goes back to A minor at bar 65. Moving to a new key is a modulation. Three sharps are also the signature of F♯ minor, A major’s relative minor, and the A major part passes through F♯ minor without changing its signature; you hear which key the music is in by where it comes home, not by the signature.

A key change is one more marker on the playlist, next to the time signature and tempo markers:

#![allow(unused)]
fn main() {
pub struct KeyMarker {
    /// The step it starts on, counted from the start of the song.
    pub start: usize,
    pub scale: Scale,
}
}

A key marker never moves a note. It changes what the key is called, which rows the piano roll lights up, which chords the harmony keys offer and which chords count as “in the key”. That is the difference from the Key menus, which move the song.

KeyMap answers “what key is step 300 in?”: the song’s own key until the first marker, then each marker’s key in turn. Building one puts the markers in order and keeps one per step:

#![allow(unused)]
fn main() {
pub fn new(first: Scale, markers: impl IntoIterator<Item = KeyMarker>) -> Self {
    let mut markers: Vec<KeyMarker> = markers
        .into_iter()
        .filter(|marker| marker.start > 0)
        .collect();
    markers.sort_by_key(|marker| marker.start);
    markers.dedup_by_key(|marker| marker.start);
    KeyMap { first, markers }
}
}

dedup_by_key removes an item when it has the same key as the one before it, so after sorting it keeps the first marker on each step. Looking a step up takes the last marker that starts at or before it:

#![allow(unused)]
fn main() {
pub fn marker_at(&self, step: usize) -> Option<&KeyMarker> {
    self.markers
        .iter()
        .take_while(|marker| marker.start <= step)
        .last()
}
}

take_while stops at the first marker after step, and last gives the one before it, or None if the step comes before every marker. A Project builds its map with key_map(), and key_at(step) asks it.

Adding a marker keeps the list sorted with binary_search_by_key, which gives Ok(index) if a marker already starts on that step and Err(index), where it would go, if not:

#![allow(unused)]
fn main() {
let markers = &mut self.playlist.key_markers;
match markers.binary_search_by_key(&marker.start, |m| m.start) {
    Ok(index) => markers[index] = marker,
    Err(index) => markers.insert(index, marker),
}
}

A marker on step 0 is not added at all: it changes the key the song starts in.

The key of a pattern

A pattern is written once and played wherever its clips are, and a song can play it in different keys. The piano roll needs one key to light up its notes, so a pattern takes the key where its first clip starts:

#![allow(unused)]
fn main() {
pub fn pattern_key_at(&self, pattern: usize, step: usize) -> Scale {
    self.playlist
        .clips
        .iter()
        .filter(|clip| clip.pattern == pattern)
        .map(|clip| clip.start)
        .min()
        .map_or(self.scale, |start| self.key_at(start + step))
}
}

pattern_key(p) is this at step 0. The piano roll says This pattern: A major beside the Key menus, and the harmony keys say Now in A major when the playhead is in a key that is not the song’s own.

When the song moves, the markers move

When the Key menus move a song with key changes, every key change must move by the same interval, or the march of the Rondo would stay in A major while the rest went to C minor. moved_key moves a key the way the song’s key moved:

#![allow(unused)]
fn main() {
pub fn moved_key(key: Scale, from: Scale, to: Scale) -> Scale {
    let from = crate::tools::from_key(from, to);
    if !from.kind.notes_follow(to.kind) {
        return key;
    }
    let up = from.root.semitones_up_to(to.root);
    let root = key.root.transpose(up as i32);
    let kind = if key.kind == from.kind && from.kind.is_tonal() && to.kind.is_tonal() {
        to.kind
    } else {
        key.kind
    };
    if root == key.root && kind == key.kind {
        return key;
    }
    Scale::new(root, kind)
}
}

The root moves by the same semitones. A key in the song’s old mode takes the new mode: a song in C major with a passage in G major, moved to C Dorian, has that passage in G Dorian. A key in another mode keeps its own: in a song in A minor moved up a minor third to C minor, the A major march becomes C major and the F♯ minor episode A minor, as a transposed score would read. Then change_key moves each pattern from the key it was played in to the key it is played in now, which is what before and after held above.

MIDI files store key changes too, as key signature events; see MIDI: messages, devices, recording and files. A marker can also be written by the app itself, from the keys it hears in the notes (Hearing harmony: chords and keys from notes).

Snap to scale

The piano roll’s Snap to scale button makes a note you draw or drag land on the nearest note of the key, and its right-click menu adds Snap notes you play, for the typing keyboard and a MIDI keyboard. Both come down to tools::nearest_in_key(note, scale), in Editing tools as plain functions; the key it snaps to is the key at that point of the song, after key markers. How a MIDI keyboard’s keys are snapped on the MIDI thread is in MIDI: messages, devices, recording and files.

Testing it

The tests in scale.rs state the rules for every key at once, not just for the keys someone thought of. Every seven-note scale, on every root, written either way, uses seven different letters, and each name still names the right key of the piano:

#![allow(unused)]
fn main() {
let mut letters: Vec<_> = scale
    .pitch_classes()
    .map(|p| scale.spell(p).letter)
    .collect();
letters.sort();
letters.dedup();
assert_eq!(letters.len(), 7, "{}", scale.name());
}

The signs a key’s notes are written with add up to its key signature, for every mode on every root, so a staff never needs an accidental for a note of the key:

#![allow(unused)]
fn main() {
let (signature, _) = scale.key_signature().unwrap();
let written: i8 = scale
    .pitch_classes()
    .map(|note| scale.spell(note).accidental)
    .sum();
assert_eq!(written, signature, "{}", scale.name());
}

And the signatures of all twelve major keys, from C upwards, are the circle of fifths:

#![allow(unused)]
fn main() {
// Sharps count up, flats count down, round the circle of fifths:
// C, Db, D, Eb, E, F, F#, G, Ab, A, Bb, B.
let major = [0, -5, 2, -3, 4, -1, 6, 1, -4, 3, -2, 5];
}

tools.rs checks the moves of a key change: C to G goes down a fourth, C major to C minor lowers E, A and B, a note outside the scale only takes the new root, and a D7 taken to the chromatic scale and back is still a D7. key_map.rs checks that the Rondo’s march and episode land in C major and A minor when the song goes up a minor third.

Exercises on keys are in Exercises: music theory in code.