mitchell vitez blog music art media dark mode

Perfect Pitch Training

My piano lessons have had me doing a bunch of ear training to learn absolute pitch. I wanted a way to practice with audio only, so I could continue to do other things while the training program played in the background.

We’ll assume the existence of a function that can take a frequency in Hertz and play the corresponding tone for a second or two. (It’s relatively easy to output pure sine waves, but you could also do something fancy like connect this to a VST plugin to output notes on whatever instrument you want).

play_frequency(hz=440.0)

An octave spans between \(x\) and \(2x\) Hz. Because there are 12 notes per octave, the ratio between successive notes is \(\sqrt[12]{2}\), or about \(1.0595\).

ratio = 2 ** (1 / 12)

The “reference pitch” I had been learning was bass C. Because I knew that 440 Hz was an A, and could tell which octave was playing by ear, I found that the closest A was at 110 Hz (divide by two a couple times). Then, we can step up from A to A♯ to B to C by successive multiplications of our ratio.

bassC = 110.0 * ratio * ratio * ratio * ratio

I also wanted the program to be able to say note names out loud. The say command makes this relatively easy, although sometimes it pronounces things in strange ways. This is easy enough to correct for after some trial and error (using the spelling ayy instead of a which say pronounced more like uh).

names = [
  'c',
  'c sharp d flat',
  'd',
  'd sharp e flat',
  'e',
  'f',
  'f sharp g flat',
  'g',
  'g sharp, ayy flat',
  'ayy',
  'ayy sharp b flat',
  'b'
]

We can then quite easily get a list of all frequencies with their corresponding names

freq = bassC
notes = []
for name in names:
    notes.append((name, freq))
    freq *= ratio

The last step was to set up some timings and useful features (such as playing the reference note once every five notes) and play random notes to test our knowledge. There we have it, an endless ear training program in ~20 lines of code.

i = 0
while True:
    if i % 5 == 0:
        os.system(f'say "reference bass c"')
        play_frequency(bassC)
        time.sleep(1)
    name, freq = random.choice(notes)
    play_frequency(freq)
    time.sleep(0.5)
    os.system(f'say "that was {name}"')
    time.sleep(2)
    i += 1