Project 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.

3 modules · 2 recorded sessionstext-menu UI in the terminalupdated 2026-10-10

Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.

About

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).

Recorded sessions

What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.

bad-input

interpreter: byte-equal
== 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.
What was typed (35 lines)
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

basic

interpreter: byte-equal
== 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.
What was typed (25 lines)
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

Modules

The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.

main.eml(entry)

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 (main.py)
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 (notes.py)
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 (theory.py)
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]

Built on these corpus cases