DIY Ear training with Python and Music21, part 1
For the past 3 years I've been studying classical (and now also jazz) guitar, music theory, and doing a little composing. I tend to dive into things, and in that time I would say that I've made it to intermediate level as a classical guitarist, I've learned a fair bit of music theory, and I've written a few pieces for guitar.
One piece has been missing, however. That is ear training. I hear and learn melodies and rhythms just fine, but I've been missing the ability to recognize intervals by ear. Is that a perfect 5th? Or is it a minor 3rd? Maybe an octave? My guesses were not nearly as reliable as I thought they should be.
The answer, of course, is ear training, basically practicing identifying intervals until one's recognition improves. There are, of course, tons of apps and web sites that will help you do that for free.
That would be the easy way, or at least the obvious way. But I've tried them (very briefly) before and they never engaged me. In fact, they just seemed frustrating. After decades of teaching, I know that frustrating = un-motivating = doomed to fail.
So my solution was to create my own ear training program. The process is inherently more interesting (at least for me). You have to think a bit about how to go about the training, you have to think about the structure of a program to do that, and you have to write the code. And you have to TEST the code, which in itself assures more time using it.
I should also note that so far, this entire project has been free from AI. Yes, I know I could get a chatbot to generate the code, the approach, the design, or even a whole ear training website. If I were mainly interested in using AI to generate code, then maybe that would make sense. But again, that would diminish, or even eliminate, my involvement and motivation in the whole process. In the end, it would be no better, if not worse, than just using one of the many pre-existing apps.
So here begins my ear training journey, coded the old fashioned way.
Basic app idea
The basic idea is just play intervals, try to guess them, and evaluate whether the guess is right or wrong. So that means that the pseudocode at a high level might be something like:
loop: Get a note name at random from list of notes Get an interval name at random from list of intervals Create first note from note name Create second note of interval by transposing the first note by the interval Play the notes Get the user's response Check response against actual interval name
The implementation of that basic idea was the next step.
Platform
I was pretty clear from the beginning that I would use a Jupyter notebook for this experiment, since I had no need for a standalone app, and the convenience of a notebook is hard to beat. For generating the sounds, Music21 seemed right for me, since it can generate midi sounds and can manipulate and transpose notes, making it easy to create pairs of sounds separated by arbitrary intervals.
I knew I needed to install music21 so once I had a notebook ready (I used pyenv to create a Python 3.13 environment for the project), I installed music21 from within my notebook:
%pip install music21
The code, first iteration
The first thing I needed to do was load dependencies:
from music21 import * # import everything from music21 import random from IPython.display import Audio # load the Jupyter audio widget from IPython.display import clear_output # used to clear the cell after an attempt
I needed music21 of course, the random library for selecitng notes and intervals, and from the IPython.display library I would need both the audio player and the clear_output
function to clear the output after each guess.
Since I'm primarily a guitarist, I wanted to use the range of sounds available on the guitar. I had already experimented with music21 a couple of years ago, so I had the code to define the notes of the fretboard ready to go:
chromatic_guitar = ["E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3", "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", "C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5", "C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6" ]
Transposing is pretty easy to do with music21, you can either use a number of steps or a string with an interval name (in either case, the default is to transpose up, and prepending a "-" will transpose down.) For example:
from music21.note import Note Note("E").transpose(1) # E to F Note("E").transpose(-1) # E to Eb Note("E").transpose("m3") # E to G
Using a list of interval names is more useful for the current purpose than using integers so I created a list of intervals, but commented out all but the perfect 5th and perfect octave to make it a bit easier on my untrained ears:
intervals = [ # "m2", #minor 2nd # "M2", #major 2nd # "m3", #minor 3rd # "M3", #major 3rd # "P4", #perfect 4th # "D5", #aug 4th/dim 5th "P5", #perfect 5th # "m6", #minor 6th # "M6", #major 6th # "m7", #minor 7th # "M7", #major 7th "P8", #perfect octave
After those three cells of preliminaries, the basic app looked like this:
Disclaimer: of course my actual first iteration was messier than this, I've cleaned up the false starts, errors, and extraneous bits.
# initialize choice = "" correct = [] incorrect = [] # loop while choice.lower() != "q": test_interval = random.choice(intervals) base_note_name = random.choice(chromatic_guitar[:-12]) base_note = note.Note(base_note_name) test_stream = stream.Stream([base_note, base_note.transpose(test_interval)]) test_stream.show("midi", autoplay=True) choice = input("interval: ") clear_output() if choice == test_interval: correct.append(choice) elif choice.lower() == "q": break else: incorrect.append( (test_interval, base_note, choice, base_note.transpose(test_interval)) ) print(f"{len(correct)}/{len(correct)+len(incorrect)}") print( f"{base_note.name}, {base_note.transpose(test_interval).name} --> {test_interval}, {choice}" )
Reflection
This was enough to get me started, and I soon discovered that I was every bit as bad at telling perfect fifths and octaves apart as I'd feared, and I struggled to call 2/3 of the pairs correctly.
That pushed me back into teacher mode, and I started thinking about how I could modify the code to help me learn. As I thought about it, there were several things that were not optimal.
First of all, the feed back on whether a guess was right or wrong, was only indicated by the change in the counter of correct over total attempts. More obvious and forceful feedback would be helpful,
Secondly, there was no way to learn from the mistakes, other than displaying the incorrect list. In fact, I'd decided to use lists for correct and incorrect, rather than just counters, because I was pretty sure I'd want to do something to help learn from the mistakes.
I'll talk about what I did to address these issues (and why) in the next post, coming soon.
If you want to leave a comment, or just prefer a more interactive experience, this post can also be found on Sustack at https://codacode.substack.com/p/diy-ear-training-with-python-and