<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P057-scales-and-chords/ | updated: 2026-10-10 -->

# P057 Scales and chords

Major and minor scales and nine kinds of chords from any root, spelled on the right letters (F major has Bb, not A#), intervals named from their letters and semitones with their simple frequency ratio and where the two notes' overtones meet, and chord progressions moved to another key.

EML-P project `projects/scales-and-chords` in the EML language repo: 3 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/scales-and-chords` runs it and `eml project verify projects/scales-and-chords` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: the-two-notes-shared-every-third-overtone (https://efficientnewlanguage.org/cases/954-the-two-notes-shared-every-third-overtone/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
0
x
1
H
C#b

1
B#
1
E##
2
Hm
Cm9

2
Fbdim7
3
C

3
C
C
3
C
B#
4

4
C X
4
C, G
12
x
-5
6
```

Screen:

```text
== Scales and chords ==
Notes are spelled: C# and Db sound the same but are written on different letters.

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 0
Pick a number from 1 to 6.

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> x
Pick a number from 1 to 6.

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 1
root> H
Type a note: a letter A to G, then # or b, up to two, like F#, Bb or C.
root> C#b
Type a note: a letter A to G, then # or b, up to two, like F#, Bb or C.
root> 
Cancelled.

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 1
root> B#
B# major            B# C## D## E# F## G## A##
B# natural minor    B# C## D# E# F## G# A#
B# harmonic minor   B# C## D# E# F## G# A##
B# melodic minor    B# C## D# E# F## G## A##

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 1
root> E##
E## major           needs triple sharps or flats - try the same pitch spelled another way.
E## natural minor   needs triple sharps or flats - try the same pitch spelled another way.
E## harmonic minor  needs triple sharps or flats - try the same pitch spelled another way.
E## melodic minor   needs triple sharps or flats - try the same pitch spelled another way.

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 2
chord (like C, Am, F#m7, Bbmaj7, Cdim7)> Hm
Type a root note and one of: (nothing) m dim aug 7 maj7 m7 m7b5 dim7.
chord (like C, Am, F#m7, Bbmaj7, Cdim7)> Cm9
Type a root note and one of: (nothing) m dim aug 7 maj7 m7 m7b5 dim7.
chord (like C, Am, F#m7, Bbmaj7, Cdim7)> 
Cancelled.

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 2
chord (like C, Am, F#m7, Bbmaj7, Cdim7)> Fbdim7
Fbdim7 needs triple sharps or flats.

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 3
lower note> C
upper note> 
Cancelled.

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 3
lower note> C
upper note> C
From C up to C: the same note, or an octave.

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 3
lower note> C
upper note> B#
From C up to B#: augmented seventh, 12 semitones.
On a piano, 12 semitones stand for the simple ratio 2 : 1, so every 2nd overtone of the lower note meets every 1st of the upper one - first at 2 x 220 = 440 Hz when the lower note is 220 Hz.

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 4
chords, like C Am F G> 
Cancelled.

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 4
chords, like C Am F G> C X
Not a chord: X.

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 4
chords, like C Am F G> C, G
semitones up (-11 to 11)> 12
Type a whole number from -11 to 11.
semitones up (-11 to 11)> x
Type a whole number from -11 to 11.
semitones up (-11 to 11)> -5
Moved -5 semitones: G D

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 6
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
C
1
F#
2
Bbmaj7
2
F#m7b5
3
C
G
3
C
E
3
C
Fb
3
B
F
4
C Am F G7
2
5
6
```

Screen:

```text
== Scales and chords ==
Notes are spelled: C# and Db sound the same but are written on different letters.

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 1
root> C
C major             C D E F G A B
C natural minor     C D Eb F G Ab Bb
C harmonic minor    C D Eb F G Ab B
C melodic minor     C D Eb F G A B

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 1
root> F#
F# major            F# G# A# B C# D# E#
F# natural minor    F# G# A B C# D E
F# harmonic minor   F# G# A B C# D E#
F# melodic minor    F# G# A B C# D# E#

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 2
chord (like C, Am, F#m7, Bbmaj7, Cdim7)> Bbmaj7
Bbmaj7 is Bb major seventh: Bb (root), D (third), F (fifth), A (seventh).

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 2
chord (like C, Am, F#m7, Bbmaj7, Cdim7)> F#m7b5
F#m7b5 is F# half-diminished seventh: F# (root), A (third), C (fifth), E (seventh).

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 3
lower note> C
upper note> G
From C up to G: perfect fifth, 7 semitones.
On a piano, 7 semitones stand for the simple ratio 3 : 2, so every 3rd overtone of the lower note meets every 2nd of the upper one - first at 3 x 220 = 660 Hz when the lower note is 220 Hz.

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 3
lower note> C
upper note> E
From C up to E: major third, 4 semitones.
On a piano, 4 semitones stand for the simple ratio 5 : 4, so every 5th overtone of the lower note meets every 4th of the upper one - first at 5 x 220 = 1100 Hz when the lower note is 220 Hz.

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 3
lower note> C
upper note> Fb
From C up to Fb: diminished fourth, 4 semitones.
On a piano, 4 semitones stand for the simple ratio 5 : 4, so every 5th overtone of the lower note meets every 4th of the upper one - first at 5 x 220 = 1100 Hz when the lower note is 220 Hz.

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 3
lower note> B
upper note> F
From B up to F: diminished fifth, 6 semitones.
On a piano, 6 semitones stand for the simple ratio 45 : 32, so every 45th overtone of the lower note meets every 32nd of the upper one - first at 45 x 220 = 9900 Hz when the lower note is 220 Hz.
Overtones that far up are faint, so these two notes blend less than the simpler ratios do.

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 4
chords, like C Am F G> C Am F G7
semitones up (-11 to 11)> 2
Moved 2 semitones: D Bm G A7

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 5
Pitch classes, C = 0, each with its spellings:
 0: C = Dbb = B#
 1: C# = Db = B##
 2: C## = D = Ebb
 3: D# = Eb = Fbb
 4: D## = E = Fb
 5: E# = F = Gbb
 6: E## = F# = Gb
 7: F## = G = Abb
 8: G# = Ab
 9: G## = A = Bbb
10: Cbb = A# = Bb
11: Cb = A## = B

1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit
choice> 6
Bye.
```

## Modules

### main.eml

```eml
# P057 scales and chords: scales and chords spelled with the right letter
# names, the interval between two notes - its name, its ratio and where the
# two notes' overtones meet - and chord progressions moved to another key.
import notes
import theory

def trim(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def words(s):
    [] => out
    "" => word
    for c in s + " ":
        if c == " " or c == ",":
            if word != "":
                out + [word] => out
            "" => word
        else:
            word + c => word
    return out

def ask_note(prompt):
    while True:
        trim(input(prompt + "> ")) => answer
        if answer == "":
            return []
        notes.parsed(answer) => n
        if len(n) > 0:
            return n
        "Type a note: a letter A to G, then # or b, up to two, like F#, Bb or C." ^0

def show_scale():
    ask_note("root") => root
    if len(root) == 0:
        "Cancelled." ^0
        return
    for kind in theory.scale_kinds():
        theory.scale(root, kind[1]) => sc
        notes.name(root) + " " + kind[0] => label
        while len(label) < 20:
            label + " " => label
        if len(sc) == 0:
            (label + "needs triple sharps or flats - try the same pitch spelled another way.") ^0
        else:
            (label + notes.names(sc)) ^0

def show_chord():
    while True:
        trim(input("chord (like C, Am, F#m7, Bbmaj7, Cdim7)> ")) => answer
        if answer == "":
            "Cancelled." ^0
            return
        theory.chord_parsed(answer) => c
        if len(c) > 0:
            break
        "Type a root note and one of: (nothing) m dim aug 7 maj7 m7 m7b5 dim7." ^0
    theory.chord(c[0], c[1][2]) => tones
    if len(tones) == 0:
        (answer + " needs triple sharps or flats.") ^0
        return
    ["root", "third", "fifth", "seventh"] => roles
    "" => s
    for k in [0:len(tones) - 1]:
        if s != "":
            s + ", " => s
        s + notes.name(tones[k]) + " (" + roles[k] + ")" => s
    (answer + " is " + notes.name(c[0]) + " " + c[1][1] + ": " + s + ".") ^0

def show_interval():
    ask_note("lower note") => a
    if len(a) == 0:
        "Cancelled." ^0
        return
    ask_note("upper note") => b
    if len(b) == 0:
        "Cancelled." ^0
        return
    theory.interval(a, b) => iv
    iv[0] => nm
    iv[1] => semis
    if a[0] == b[0] and a[1] == b[1]:
        ("From " + notes.name(a) + " up to " + notes.name(b) + ": the same note, or an octave.") ^0
        return
    ("From " + notes.name(a) + " up to " + notes.name(b) + ": " + nm + ", " + semitones(semis) + ".") ^0
    theory.just_ratio(semis) => r
    # With frequencies upper : lower = p : q, overtone q * k of the upper note
    # and overtone p * k of the lower note are the same frequency, so every
    # p-th overtone of the lower note is shared (the corpus case: 220 and
    # 330 Hz, 3 : 2, share every third overtone of the lower tone).
    ("On a piano, " + semitones(semis) + " stand for the simple ratio " + str(r[0]) + " : " + str(r[1]) + ", so every " + ordinal(r[0]) + " overtone of the lower note meets every " + ordinal(r[1]) + " of the upper one - first at " + str(r[0]) + " x 220 = " + str(r[0] * 220) + " Hz when the lower note is 220 Hz.") ^0
    if r[0] >= 9:
        "Overtones that far up are faint, so these two notes blend less than the simpler ratios do." ^0

def semitones(n):
    if n == 1:
        return "1 semitone"
    return str(n) + " semitones"

def ordinal(n):
    n % 10 => last
    n % 100 => last2
    if last == 1 and last2 != 11:
        return str(n) + "st"
    if last == 2 and last2 != 12:
        return str(n) + "nd"
    if last == 3 and last2 != 13:
        return str(n) + "rd"
    return str(n) + "th"

def transpose():
    words(trim(input("chords, like C Am F G> "))) => ws
    if len(ws) == 0:
        "Cancelled." ^0
        return
    [] => parsed
    for w in ws:
        theory.chord_parsed(w) => c
        if len(c) == 0:
            ("Not a chord: " + w + ".") ^0
            return
        parsed + [c] => parsed
    while True:
        trim(input("semitones up (-11 to 11)> ")) => answer
        if answer == "":
            "Cancelled." ^0
            return
        answer => digits
        1 => sign
        if len(answer) > 1 and answer[0] == "-":
            answer[1:len(answer)] => digits
            -1 => sign
        if len(digits) >= 1 and len(digits) <= 2 and digits[0] >= "0" and digits[0] <= "9" and digits[len(digits) - 1] >= "0" and digits[len(digits) - 1] <= "9":
            sign * int(digits) => n
            if n >= -11 and n <= 11:
                break
        "Type a whole number from -11 to 11." ^0
    "" => out
    for c in parsed:
        theory.preferred((notes.pitch(c[0]) + n + 12) % 12) => root
        if out != "":
            out + " " => out
        out + notes.name(root) + c[1][0] => out
    ("Moved " + str(n) + " semitones: " + out) ^0

def table():
    "Pitch classes, C = 0, each with its spellings:" ^0
    for pc in [0:11]:
        "" => s
        for letter in [0:6]:
            notes.spelled(letter, pc) => n
            if len(n) > 0:
                if s != "":
                    s + " = " => s
                s + notes.name(n) => s
        str(pc) => label
        if len(label) == 1:
            " " + label => label
        (label + ": " + s) ^0

"== Scales and chords ==" ^0
"Notes are spelled: C# and Db sound the same but are written on different letters." ^0
True => running
while running:
    "" ^0
    "1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit" ^0
    trim(input("choice> ")) => choice
    if choice == "1":
        show_scale()
    elif choice == "2":
        show_chord()
    elif choice == "3":
        show_interval()
    elif choice == "4":
        transpose()
    elif choice == "5":
        table()
    elif choice == "6":
        False => running
    else:
        "Pick a number from 1 to 6." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import notes
import theory

def trim(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def words(s):
    out = []
    word = ""
    for c in s + " ":
        if c == " " or c == ",":
            if word != "":
                out = out + [word]
            word = ""
        else:
            word = word + c
    return out

def ask_note(prompt):
    while True:
        answer = trim(input(prompt + "> "))
        if answer == "":
            return []
        n = notes.parsed(answer)
        if len(n) > 0:
            return n
        print("Type a note: a letter A to G, then # or b, up to two, like F#, Bb or C.")

def show_scale():
    root = ask_note("root")
    if len(root) == 0:
        print("Cancelled.")
        return
    for kind in theory.scale_kinds():
        sc = theory.scale(root, kind[1])
        label = notes.name(root) + " " + kind[0]
        while len(label) < 20:
            label = label + " "
        if len(sc) == 0:
            print(label + "needs triple sharps or flats - try the same pitch spelled another way.")
        else:
            print(label + notes.names(sc))

def show_chord():
    while True:
        answer = trim(input("chord (like C, Am, F#m7, Bbmaj7, Cdim7)> "))
        if answer == "":
            print("Cancelled.")
            return
        c = theory.chord_parsed(answer)
        if len(c) > 0:
            break
        print("Type a root note and one of: (nothing) m dim aug 7 maj7 m7 m7b5 dim7.")
    tones = theory.chord(c[0], c[1][2])
    if len(tones) == 0:
        print(answer + " needs triple sharps or flats.")
        return
    roles = ["root", "third", "fifth", "seventh"]
    s = ""
    for k in range(0, len(tones)):
        if s != "":
            s = s + ", "
        s = s + notes.name(tones[k]) + " (" + roles[k] + ")"
    print(answer + " is " + notes.name(c[0]) + " " + c[1][1] + ": " + s + ".")

def show_interval():
    a = ask_note("lower note")
    if len(a) == 0:
        print("Cancelled.")
        return
    b = ask_note("upper note")
    if len(b) == 0:
        print("Cancelled.")
        return
    iv = theory.interval(a, b)
    nm = iv[0]
    semis = iv[1]
    if a[0] == b[0] and a[1] == b[1]:
        print("From " + notes.name(a) + " up to " + notes.name(b) + ": the same note, or an octave.")
        return
    print("From " + notes.name(a) + " up to " + notes.name(b) + ": " + nm + ", " + semitones(semis) + ".")
    r = theory.just_ratio(semis)
    print("On a piano, " + semitones(semis) + " stand for the simple ratio " + str(r[0]) + " : " + str(r[1]) + ", so every " + ordinal(r[0]) + " overtone of the lower note meets every " + ordinal(r[1]) + " of the upper one - first at " + str(r[0]) + " x 220 = " + str(r[0] * 220) + " Hz when the lower note is 220 Hz.")
    if r[0] >= 9:
        print("Overtones that far up are faint, so these two notes blend less than the simpler ratios do.")

def semitones(n):
    if n == 1:
        return "1 semitone"
    return str(n) + " semitones"

def ordinal(n):
    last = n % 10
    last2 = n % 100
    if last == 1 and last2 != 11:
        return str(n) + "st"
    if last == 2 and last2 != 12:
        return str(n) + "nd"
    if last == 3 and last2 != 13:
        return str(n) + "rd"
    return str(n) + "th"

def transpose():
    ws = words(trim(input("chords, like C Am F G> ")))
    if len(ws) == 0:
        print("Cancelled.")
        return
    parsed = []
    for w in ws:
        c = theory.chord_parsed(w)
        if len(c) == 0:
            print("Not a chord: " + w + ".")
            return
        parsed = parsed + [c]
    while True:
        answer = trim(input("semitones up (-11 to 11)> "))
        if answer == "":
            print("Cancelled.")
            return
        digits = answer
        sign = 1
        if len(answer) > 1 and answer[0] == "-":
            digits = answer[1:len(answer)]
            sign = -1
        if len(digits) >= 1 and len(digits) <= 2 and digits[0] >= "0" and digits[0] <= "9" and digits[len(digits) - 1] >= "0" and digits[len(digits) - 1] <= "9":
            n = sign * int(digits)
            if n >= -11 and n <= 11:
                break
        print("Type a whole number from -11 to 11.")
    out = ""
    for c in parsed:
        root = theory.preferred((notes.pitch(c[0]) + n + 12) % 12)
        if out != "":
            out = out + " "
        out = out + notes.name(root) + c[1][0]
    print("Moved " + str(n) + " semitones: " + out)

def table():
    print("Pitch classes, C = 0, each with its spellings:")
    for pc in range(0, 12):
        s = ""
        for letter in range(0, 7):
            n = notes.spelled(letter, pc)
            if len(n) > 0:
                if s != "":
                    s = s + " = "
                s = s + notes.name(n)
        label = str(pc)
        if len(label) == 1:
            label = " " + label
        print(label + ": " + s)

print("== Scales and chords ==")
print("Notes are spelled: C# and Db sound the same but are written on different letters.")
running = True
while running:
    print("")
    print("1) scales  2) chord  3) interval  4) transpose  5) note table  6) quit")
    choice = trim(input("choice> "))
    if choice == "1":
        show_scale()
    elif choice == "2":
        show_chord()
    elif choice == "3":
        show_interval()
    elif choice == "4":
        transpose()
    elif choice == "5":
        table()
    elif choice == "6":
        running = False
    else:
        print("Pick a number from 1 to 6.")
print("Bye.")
```

### notes.eml

```eml
# P057 scales and chords - spelled notes. A note is [letter, accidental]:
# letter 0 to 6 for C D E F G A B, accidental -2 to 2 for double flat to
# double sharp. Its pitch class (0 to 11, C = 0) is where it sounds; two
# spellings of one pitch - C# and Db - are different notes on paper.

def naturals():
    return [0, 2, 4, 5, 7, 9, 11]

def pitch(note):
    return (naturals()[note[0]] + note[1] + 12) % 12

def name(note):
    "CDEFGAB"[note[0]] => s
    if note[1] > 0:
        s + "#" * note[1] => s
    elif note[1] < 0:
        s + "b" * (0 - note[1]) => s
    return s

def parsed(s):
    # "C", "f#", "Bb", "Ebb", "G##" as a note, or [] when it is not one.
    if s == "" or len(s) > 3:
        return []
    -1 => letter
    for k in [0:6]:
        if s[0] == "CDEFGAB"[k] or s[0] == "cdefgab"[k]:
            k => letter
    if letter == -1:
        return []
    0 => acc
    for ch in s[1:len(s)]:
        if ch == "#" and acc >= 0:
            acc + 1 => acc
        elif ch == "b" and acc <= 0:
            acc - 1 => acc
        else:
            return []
    return [letter, acc]

def spelled(letter, target):
    # The note on this letter that sounds at pitch class target, or [] when
    # it would need more than two sharps or flats.
    (target - naturals()[letter] + 12) % 12 => d
    if d > 6:
        d - 12 => d
    if d < -2 or d > 2:
        return []
    return [letter, d]

def built(root, steps, letter_steps):
    # Notes from root, each the given semitones above the last and the given
    # number of letters further on - so a seven-note scale uses every letter
    # once and a chord stacks letters a third apart. Returns [] when some
    # note cannot be written with at most two sharps or flats.
    [root] => out
    root[0] => letter
    pitch(root) => p
    for k in [0:len(steps) - 1]:
        (letter + letter_steps[k]) % 7 => letter
        (p + steps[k]) % 12 => p
        spelled(letter, p) => n
        if len(n) == 0:
            return []
        out + [n] => out
    return out

def names(notes):
    "" => s
    for n in notes:
        if s != "":
            s + " " => s
        s + name(n) => s
    return s
```

Python projection of notes.eml:

```python
def naturals():
    return [0, 2, 4, 5, 7, 9, 11]

def pitch(note):
    return (naturals()[note[0]] + note[1] + 12) % 12

def name(note):
    s = "CDEFGAB"[note[0]]
    if note[1] > 0:
        s = s + "#" * note[1]
    elif note[1] < 0:
        s = s + "b" * (0 - note[1])
    return s

def parsed(s):
    if s == "" or len(s) > 3:
        return []
    letter = -1
    for k in range(0, 7):
        if s[0] == "CDEFGAB"[k] or s[0] == "cdefgab"[k]:
            letter = k
    if letter == -1:
        return []
    acc = 0
    for ch in s[1:len(s)]:
        if ch == "#" and acc >= 0:
            acc = acc + 1
        elif ch == "b" and acc <= 0:
            acc = acc - 1
        else:
            return []
    return [letter, acc]

def spelled(letter, target):
    d = (target - naturals()[letter] + 12) % 12
    if d > 6:
        d = d - 12
    if d < -2 or d > 2:
        return []
    return [letter, d]

def built(root, steps, letter_steps):
    out = [root]
    letter = root[0]
    p = pitch(root)
    for k in range(0, len(steps)):
        letter = (letter + letter_steps[k]) % 7
        p = (p + steps[k]) % 12
        n = spelled(letter, p)
        if len(n) == 0:
            return []
        out = out + [n]
    return out

def names(notes):
    s = ""
    for n in notes:
        if s != "":
            s = s + " "
        s = s + name(n)
    return s
```

### theory.eml

```eml
# P057 scales and chords - scales, chords and intervals, spelled by letter.
import notes

def scale_kinds():
    # [name, semitone steps between the seven notes]; every step moves one
    # letter on.
    return [["major", [2, 2, 1, 2, 2, 2]],
            ["natural minor", [2, 1, 2, 2, 1, 2]],
            ["harmonic minor", [2, 1, 2, 2, 1, 3]],
            ["melodic minor", [2, 1, 2, 2, 2, 2]]]

def chord_kinds():
    # [suffix, name, semitones above the root]; each note is a third - two
    # letters - above the one before.
    return [["", "major", [0, 4, 7]],
            ["m", "minor", [0, 3, 7]],
            ["dim", "diminished", [0, 3, 6]],
            ["aug", "augmented", [0, 4, 8]],
            ["7", "dominant seventh", [0, 4, 7, 10]],
            ["maj7", "major seventh", [0, 4, 7, 11]],
            ["m7", "minor seventh", [0, 3, 7, 10]],
            ["m7b5", "half-diminished seventh", [0, 3, 6, 10]],
            ["dim7", "diminished seventh", [0, 3, 6, 9]]]

def scale(root, steps):
    return notes.built(root, steps, [1, 1, 1, 1, 1, 1])

def chord(root, tones):
    [] => steps
    [] => letters
    for k in [1:len(tones) - 1]:
        steps + [tones[k] - tones[k - 1]] => steps
        letters + [2] => letters
    return notes.built(root, steps, letters)

def chord_parsed(s):
    # A chord symbol - a note, then a suffix - as [root, kind], or [].
    # The longest root that reads as a note wins, so "Bbm" is Bb minor.
    for size in [3, 2, 1]:
        if len(s) >= size:
            notes.parsed(s[0:size]) => root
            if len(root) > 0:
                s[size:len(s)] => suffix
                for kind in chord_kinds():
                    if kind[0] == suffix:
                        return [root, kind]
    return []

def interval(a, b):
    # The interval from note a up to note b, as [name, semitones spanned]:
    # named from the letters it spans (third, fifth ...) and how far the
    # semitones are from that size's usual count. C to E is a major third
    # and C to Fb a diminished fourth, though both span 4 semitones; C up to
    # B# is an augmented seventh, 12 semitones, and C up to Cb a diminished
    # octave, 11.
    (b[0] - a[0] + 7) % 7 + 1 => number
    (notes.pitch(b) - notes.pitch(a) + 12) % 12 => semis
    [0, 2, 4, 5, 7, 9, 11][number - 1] => reference
    semis - reference => diff
    if diff > 6:
        diff - 12 => diff
    if diff < -6:
        diff + 12 => diff
    reference + diff => span
    ["unison", "second", "third", "fourth", "fifth", "sixth", "seventh"][number - 1] => kind
    if number == 1 and diff < 0:
        "octave" => kind
        12 + diff => span
    if number == 1 or number == 4 or number == 5:
        if diff == 0:
            return ["perfect " + kind, span]
        if diff == 1:
            return ["augmented " + kind, span]
        if diff == -1:
            return ["diminished " + kind, span]
    else:
        if diff == 0:
            return ["major " + kind, span]
        if diff == -1:
            return ["minor " + kind, span]
        if diff == 1:
            return ["augmented " + kind, span]
        if diff == -2:
            return ["diminished " + kind, span]
    return [str(semis) + " semitones, with no plain name", semis]

def just_ratio(semis):
    # The small whole-number frequency ratio each span of 0 to 12 semitones
    # stands for in equal temperament, [upper, lower].
    return [[1, 1], [16, 15], [9, 8], [6, 5], [5, 4], [4, 3], [45, 32],
            [3, 2], [8, 5], [5, 3], [9, 5], [15, 8], [2, 1]][semis]

def preferred(pc):
    # The usual spelling of a pitch class on its own, for transposing chord
    # roots: sharps for F# only, flats elsewhere.
    return [[0, 0], [1, -1], [1, 0], [2, -1], [2, 0], [3, 0], [3, 1],
            [4, 0], [5, -1], [5, 0], [6, -1], [6, 0]][pc]
```

Python projection of theory.eml:

```python
import notes

def scale_kinds():
    return [["major", [2, 2, 1, 2, 2, 2]], ["natural minor", [2, 1, 2, 2, 1, 2]], ["harmonic minor", [2, 1, 2, 2, 1, 3]], ["melodic minor", [2, 1, 2, 2, 2, 2]]]

def chord_kinds():
    return [["", "major", [0, 4, 7]], ["m", "minor", [0, 3, 7]], ["dim", "diminished", [0, 3, 6]], ["aug", "augmented", [0, 4, 8]], ["7", "dominant seventh", [0, 4, 7, 10]], ["maj7", "major seventh", [0, 4, 7, 11]], ["m7", "minor seventh", [0, 3, 7, 10]], ["m7b5", "half-diminished seventh", [0, 3, 6, 10]], ["dim7", "diminished seventh", [0, 3, 6, 9]]]

def scale(root, steps):
    return notes.built(root, steps, [1, 1, 1, 1, 1, 1])

def chord(root, tones):
    steps = []
    letters = []
    for k in range(1, len(tones)):
        steps = steps + [tones[k] - tones[k - 1]]
        letters = letters + [2]
    return notes.built(root, steps, letters)

def chord_parsed(s):
    for size in [3, 2, 1]:
        if len(s) >= size:
            root = notes.parsed(s[0:size])
            if len(root) > 0:
                suffix = s[size:len(s)]
                for kind in chord_kinds():
                    if kind[0] == suffix:
                        return [root, kind]
    return []

def interval(a, b):
    number = (b[0] - a[0] + 7) % 7 + 1
    semis = (notes.pitch(b) - notes.pitch(a) + 12) % 12
    reference = [0, 2, 4, 5, 7, 9, 11][number - 1]
    diff = semis - reference
    if diff > 6:
        diff = diff - 12
    if diff < -6:
        diff = diff + 12
    span = reference + diff
    kind = ["unison", "second", "third", "fourth", "fifth", "sixth", "seventh"][number - 1]
    if number == 1 and diff < 0:
        kind = "octave"
        span = 12 + diff
    if number == 1 or number == 4 or number == 5:
        if diff == 0:
            return ["perfect " + kind, span]
        if diff == 1:
            return ["augmented " + kind, span]
        if diff == -1:
            return ["diminished " + kind, span]
    else:
        if diff == 0:
            return ["major " + kind, span]
        if diff == -1:
            return ["minor " + kind, span]
        if diff == 1:
            return ["augmented " + kind, span]
        if diff == -2:
            return ["diminished " + kind, span]
    return [str(semis) + " semitones, with no plain name", semis]

def just_ratio(semis):
    return [[1, 1], [16, 15], [9, 8], [6, 5], [5, 4], [4, 3], [45, 32], [3, 2], [8, 5], [5, 3], [9, 5], [15, 8], [2, 1]][semis]

def preferred(pc):
    return [[0, 0], [1, -1], [1, 0], [2, -1], [2, 0], [3, 0], [3, 1], [4, 0], [5, -1], [5, 0], [6, -1], [6, 0]][pc]
```

## README

# P057 - Scales and chords

Scales and chords from any root, spelled on the right letters - F# major
has E#, not F; Bb major seventh has A, not Bbb. The interval between two
notes, named from its letters and semitones, with the simple frequency
ratio its semitones stand for and where the two notes' overtones meet.
Chord progressions moved to another key, and a table of how each of the
twelve pitches can be spelled.

- `main.eml` - the menu, reading notes and chord symbols, and the messages
- `notes.eml` - spelled notes: letters and sharps or flats, their pitch,
  and building a run of notes letter by letter
- `theory.eml` - the scale and chord kinds, chord symbols, interval names
  and ratios, and the usual spelling of a transposed root

How each part works:

- A note is a letter and up to two sharps or flats, so C# and Db are two
  notes that sound the same. A scale moves one letter and a fixed number of
  semitones at each step; a chord moves two letters, a third, at each step.
  The letter fixes the name, and the semitones fix the sharps or flats. When
  a note would need three, the scale or chord is reported instead of being
  misspelled.
- An interval's number comes from the letters it spans, and its quality -
  perfect, major, minor, augmented, diminished - from how far its semitones
  are from that number's usual size. So C up to E is a major third and C
  up to Fb a diminished fourth, both 4 semitones. C up to B# is an
  augmented seventh of 12 semitones, and C up to Cb a diminished octave.
- Each span of 0 to 12 semitones stands for a simple ratio of frequencies:
  3 : 2 for 7, 5 : 4 for 4, and so on. With upper : lower = p : q, every
  p-th overtone of the lower note has the same frequency as every q-th
  overtone of the upper one. The corpus case
  `the-two-notes-shared-every-third-overtone` works this out for 220 and
  330 Hz, a fifth: every third overtone of the lower tone is shared, the
  first at 660 Hz. Big numbers in a ratio mean the shared overtones are
  high and faint, so the two notes blend less.
- Transposing moves each chord's root by semitones and spells it the
  usual way - flats, except F# - keeping the chord's kind.

What is checked: menu choices 1 to 6; a note as a letter A to G (either
case) with up to two # or up to two b; a chord as a note and one of
nothing, m, dim, aug, 7, maj7, m7, m7b5 and dim7; chords for transposing,
separated by spaces or commas; a shift from -11 to 11 semitones. An empty
answer cancels.

Sessions: `sessions/basic.in` gives:
- the four scales on C and on F#;
- the chords Bbmaj7 and F#m7b5;
- intervals: C to G (3 : 2, overtones meeting at 660 Hz), C to E (5 : 4,
  at 1100 Hz), C to Fb (a diminished fourth, also 4 semitones), and B to F
  (a diminished fifth, 45 : 32, which blends less than the simpler ratios);
- C Am F G7 moved up 2 semitones, to D Bm G A7;
- the table of spellings.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- roots H and C#b, and an empty root;
- B#, whose scales need double sharps, and E##, whose scales would need
  triple sharps;
- the chords Hm and Cm9, an empty chord, and Fbdim7, which would need a
  triple flat;
- an interval cancelled at the upper note, C to C, and C to B#;
- transposing: nothing; "C X"; "C, G" with shifts 12 and x, then -5, to
  G D.

Built on the verified corpus case `the-two-notes-shared-every-third-overtone`
(where the overtones of two tones coincide, and where they narrowly miss).
