How To Build An Ear Trainer with JavaScript

song cover music note

Hey all you cool cats and kittens! Since we are all stuck at home trying to avoid the apocalypse, I thought it would be a good time for a new programming tutorial.

Carole Baskin

I’ve heard some encouraging news that at least one teacher has been using these tutorials to keep their kids doing something constructive during the quarantines. So that has made me happy and increased my motivation to be productive.

Instead of building another game like minesweeper or hangman, this tutorial will focus on another one of my passions: music.

Click here to check out the ear trainer you’ll be building!

Click here to view the code!

I’m no professional by any stretch of the imagination, but I’ve always had love for music. Whether it was church hymns or classic rock, music was an important part of every car ride as kid, and I’m still trying to get better at singing and playing guitar (You can check out my progress if you’re interested).

Ear Training

One area I’ve been working on is my ability to recognize common music intervals, like major thirds, perfect fifths, and octaves. A common term for this is “ear training”. You can start doing this by associating the different intervals with the beginning of certain songs ( fourth = Amazing Grace, fifth = Star Wars Theme, octave = The Office Theme, etc). But my goal is for my ear to eventually recognize these intervals instantaneously.

I’ve downloaded various apps to help me do this, but it occurred to me that I could combine my passions and build one myself!

So that’s what we’ll be covering in this tutorial: how to build an ear trainer with JavaScript. For those less musically inclined, we’re also going to be using two programming techniques I haven’t covered in previous tutorials: playing audio and local client-side storage. Let’s get started! If you get lost, the full project is on github.

Making the Music

The first thing that I had to do was create the audio files. I used a program called Audacity to record myself playing various intervals on my guitar. I used a naming convention of {intervalName}{integer} in order to make the programming task easier.

You can record your own intervals and follow my naming convention, or if you’re in a hurry you can just download or point your code to the files I created. You can find them all here on my blog, or here on github.

Structure and Style

I’m not going to focus too much on describing how the page is structured and styled. I’ll leave it as an exercise for you to explore the HTML and CSS on your own. You’re welcome to ask questions in the comment section if you run into any issues with the files provided.

The main takeaways are that we have a start button, a replay interval button, a confirm answer button, a drop down list to pick our answer, and an area for displaying messages. Our JavaScript code will interact with each of these elements as the state of the test progresses.

Also, you’ll want to make sure you update any paths so that they are pointed to where your files are located. For example, if you add your own audio files, you’ll have to make sure your code is looking in the right directory for them.

Variables

Alright it is time to move on and finally start talking about the JavaScript. We’ll begin by discussing the variables that will be changed as the player progresses through the test:

let questions = [];
let answers = [];
let questionIndex = 0;
let correctAnswers = 0;
let questionCount = 20;
let beginTime = "";

As you can see, we’re initializing an two empty arrays. One array is for keeping track of the questions, and the other is for keeping track of the answers.

We’re also initializing three variables holding integers. The questionIndex variable keeps track of which question we are on, while the questionCount variable keeps track of the total number of questions. The correctAnswers variable will be updated every time an interval is guessed correctly.

Also, we have a beginTime variable which will be set when the quiz begins. This variable will help us determine if the player has beaten his/her previous high score.

Helper Functions

Next, let’s talk about some helper functions we are going to create to make our lives easier. We’ll reuse them throughout the code, so it’s best to familiarize yourself with them early on.

Play Interval

let playInterval = function( id )
{
document.getElementById(id).play();
}

This function takes the id of an html element as a parameter. It attempts to get the element by id and call play on it. This will work if the id belongs to an audio element, but you won’t have much luck if it doesn’t. The audio elements are present in the HTML document, and each one is associated with a unique audio file.

Hide

let hide = function( id )
{
  document.getElementById(id).style.display="none";
}

The hide function takes an id and sets its display property to none. We could write this every time we need it, but “hide” is much more terse and easier to read.

Show

let show = function( id, value )
{
let element = document.getElementById(id);
element.style.display = "";
if( value !== undefined )
{
element.innerHTML = value;
}
}

The show function is the opposite of the hide function in that it makes an element visible. It also has the optional ability of setting the inner HTML content of the element. If no content is provided it won’t set it to anything and the element’s content will remain unchanged.

Get Random Integer

let getRandomInteger = function( min, max )
{
   return Math.floor( 
          Math.random() * ( max - min ) ) + min;
}

This function takes a min and max value as parameters and returns a random integer lying somewhere in between them. It will include the min but not the max as possible outputs.

Get Random Index

let getRandomIndex = function()
{
return getRandomInteger( 0, 10 );
}

This function builds on the getRandomInteger function. It’s purpose is to return a random valid index. I’ve created ten audio files for each interval, with indexes ranging from 0 to 9. So we’ll pass in 0 as the min value and 10 as the max value to satisfy our requirements.

Get Random Interval

let getRandomInterval = function()
{
    let interval = getRandomInteger( 0, 5 );
    return getInterval( interval );
}

This function is similar to getRandomIndex. You’ll call this function when you want to retrieve a random interval. In the select drop down you’ll notice that we have mapped each interval type to an integer. We have five intervals: major thirds, minor thirds, perfect fourths, perfect fifths, and octaves. These correspond to the integers 0 through 4.

After getting the random integer we call a function called getInterval to get the interval object. Keep reading to find out what that function does.

Get Interval

let getInterval = function( id )
{
    id = id + "";
    let interval = { 
                     code: "third", 
                     name: "Major Third"
                   }
    switch( id )
    {
      case "0":
        interval = { 
                     code: "third", 
                     name: "Major Third" 
                   };
        break;
      case "1":
        interval = { 
                     code: "minorthird", 
                     name: "Minor Third"
                   };
        break;
      case "2":
        interval = {
                     code: "fourth", 
                     name: "Perfect Fourth" 
                   };
        break;
      case "3":
        interval = { 
                     code: "fifth", 
                     name: "Perfect Fifth"
                   };
        break;
      case "4":
        interval = { 
                     code: "octave", 
                     name: "Octave"
                   };
        break;
      default:
        // Return whatever was set as the default
    }

    return interval;
}

The purpose of this function is to retrieve the interval object for whatever interval id was passed in. An interval object consists of a code and a name. The code matches the name of the audio file without the attached index number. The name is for display purposes.

Generating the Test

Now that we have some helpful functions created, let’s start building the test itself. For starters, we are going to need some values in our questions and answers arrays. Let’s look at where this get populated, in a function called, generateTest.

How to build an ear trainer with javascript: Generate Test

This function is called when the player clicks on the start test button. The first thing that happens is we hide and show some elements on the screen. We show the question area, which includes our buttons for replaying sounds and choosing answers. We hide the start test button because at this point the test is already underway. Also, we show the message area with a non-breaking space. This is to keep the size of the container the same whether there is a message to display or not.

The next thing we do, after initializing a few local variables, is start adding questions and answers until we have reached the number set by the questionCount variable. We set each question to a random index appended to a random interval code. The random interval is then stored at the same index in the parallel answers array.

Finally, we set the beginTime of the test, and proceed to the next question by calling the appropriately named, nextQuestion function.

Proceeding to the Next Question

How to build an ear trainer with javascript: Next question function

The nextQuestion function is a pretty simple one. It does two things. First, it will show which question the player is on. Next it will play the audio file corresponding to the interval in question.

Replaying the Interval

What if the player needs to hear the interval again before answering? In that case they can trigger the replayInterval function by clicking on the replay interval button. Notice how boring my variable and function names are? That’s a good thing.

How to build an ear trainer with javascript: replay interval

This function is even simpler than nextQuestion. All we do is call play interval and pass in the current question that we are on.

Choosing an Answer

choosing a final answer for interval

The player selects his/her desired answer from the drop down list, and then clicks on the confirm answer button to guess what the interval is. When this happens, the confirmAnswer function is called.

Confirm answer function

The first thing that happens is the answer is evaluated to see if the player guess correctly. We’ll cover that function in a bit.

Next, the questionIndex is updated. If the questionIndex is less than the questionCount, we call the nextQuestion function. Otherwise, we call the finishGame function.

Evaluating the Answer

evaluating the answer for the interval

The purpose of the evaluateAnswer function is to determine if the player answered correctly and update the state of the game appropriately. Fortunately, the consequences for answering incorrectly will not be nearly as dire as in Indiana Jones.

How to build an ear trainer with javascript: evaluate answer

The first thing we do is retrieve the answer from the selected option in the drop down list. Then we get the interval object by calling getInterval.

Finally, we check if the chosen interval’s code matches the one we have stored in our answers array. If the answer is correct, we increment the correctAnswers count and don’t show a message. If the answer is wrong, we show a message to the player informing them of what the correct answer was.

Finishing the Game

How to build an ear trainer with javascript: Finish the game

After the last question has been answered we need to show the start button again and hide the question area.

Next, we need to determine the player’s score, which consists of the number of correct questions and the total time it took him/her to complete the test.

Before showing the score to the player we will determine if he/she beat his/her previous high score. This will help us determine which message to show on the screen.

We find this out by calling the saveHighScore function.

Saving a High Score

George from Seinfeld with frogger

We will be storing the high score client side in the player’s web browser. This means that the high score will persist even if the player closes their browser. This feature will allow players to compete against themselves and become faster and more accurate in their ability to guess intervals.

Save high score

We start out by assuming that the player did not beat his/her high score. After grabbing the score and time out of local storage, we check if they did beat it. We consider a score better if the player answered more questions correctly or answered the same number correctly in less time. If the player beat his/her high score, we override the values in local storage.

Conclusion

That’s it! You have successfully created your own ear trainer. You should be proud of yourself.

From here you can go in lots of different directions. I can think of so many features to add that are beyond the scope of this tutorial. You could add more intervals, descending and ascending intervals, or the ability to choose a subset of intervals you want to train on. The list goes on and on.

Have you implemented other cool feature ideas? Are you stuck on anything you saw here? I’d love to hear from you in the comments. Be sure to subscribe to stay up to date with the latest content.