JavaScript
Article

Create a Music Jam Station with Vanilla JavaScript

By Myles English

This article was peer reviewed by Chris Perry, Michaela Lehr and Matt Burnett. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

The HTML5 audio player has given rise to some new and exciting possibilities, especially when it comes to music related web applications. I hope to introduce you to some of these possibilities by walking you through how I created this jam station. This project originally began as an experiment, but over time evolved into an open ended practice and teaching tool for guitar players.

A solid understanding of Javascript fundamentals is necessary for following this tutorial.

In the spirit of keeping things concise, we’ll be building a slightly simpler version. Here’s exactly what we will be making today.

See the Pen Jam Station by SitePoint (@SitePoint) on CodePen.

If you aren’t particularly interested in recreating this tutorial exactly, that’s all right. Much of what I cover in this tutorial is applicable to different types of musical applications.

A basic grasp on music theory will help you in following this tutorial. An understanding of time signatures, measures and beats are a must.

What You Will Need

The audio track you choose will need to be recorded to a click track or metronome. In other words, the length of each beat will need to be uniform across the entire track.

There are a few things you will need to know about your audio track in order to find the current beat and measure.

  • Beats Per Minute (BPM or Tempo)
  • Time Signature
  • Time the Track Starts
  • How many measures are used to count in (not always applicable)

You can use the audio track I’ve used in the CodePen demo (right click to save) if you’d like.

Here’s the relevant data for that track.

  • BPM = 117
  • Time Signature = 4/4
  • Start time of the first beat = 0.2s
  • Measures used for count in = 1

If you are using your own track and you are unsure of some of these specifics, a free audio editor like Audacity should help. It won’t be able to tell you everything, but you will be able to find the exact start time of the first beat. If you are recording your own track it’s best to make note of this information from the start.

The Markup and CSS

Before getting into the fun stuff, here’s HTML and CSS we’ll be using.

 <div class="wrapper">
   <div id="chord-diagram"></div>
   <audio id="jam-track"  src="https://myguitarpal.com/wp-content/uploads/2014/09/12-Bar-Blues-in-A-Version-1.mp3" controls></audio>
  <br>

  <label>Beat: </label>
  <div class="data" id="beat"></div>

  <label>Measure: </label>
  <div class="data" id="measure"></div>

  <label>Section: </label>
  <div class="data" id="section"></div>

  <label>Chord: </label>
  <div class="data" id="chord"></div>

  <div id="chord-progression"></div>
</div>
.wrapper {
  max-width: 400px;
}

#chord-progression {
  margin-top: 20px;
  padding-top: 20px;
  border-top: 1px solid lightgray;
}

.section {
  margin-bottom: 20px;
  display: none;
}

.measure {
  width: 25%;
  display: inline-block;
}

.m-line {
  float: right;
  width: 10px;
}

audio {
  width: 100%;
}

.data {
  display: inline;
  margin-right: 10px;
}

label {
  font-weight: bold;
}

Setting up the Variables

First we’ll take all of the track data we discussed and set up some variables.

// beats per minute
var BPM = 117;

// beats per second
var BPS = 60 / BPM;

// measures used for count in
var measuresCount = 1;

// time the track starts
var offsetSeconds = 0.2;

// time signature
var timeSigTop = 4;
var timeSigBottom = 4;

The Chord Progression

I’ve kept the chord progression data model pretty simple for the purposes of this tutorial and used a multidimensional array.

The top level array is the chord progression sections. These could be verses, choruses, bridges, etc.

The nested arrays contain the chords for each measure in their respective sections.

var sectionOne = ['A7', 'A7', 'A7', 'A7', 'D7', 'D7', 'A7', 'A7', 'E7', 'D7', 'A7', 'E7'];
var sectionTwo = ['A7', 'A7', 'A7', 'A7', 'D7', 'D7', 'A7', 'A7', 'E7', 'D7', 'A7', 'A7', 'A7', 'A7', 'A7', 'A7', 'A7', 'A7'];

var chordProgression = [
  sectionOne,
  sectionOne,
  sectionOne,
  sectionTwo
];

Music often repeats certain sections like the chorus, so in order to keep things DRY, store each section like shown above in a variable, then include the sections in the chordProgression array.

Running the Jam Station

This jam station uses the timeupdate event which is similar to using a game loop. Each time the timeupdate event fires (several times per second while the track is playing) we’re going to run a function and update some data such as the current beat, measure and chord. The only time the data will not update is when the track is paused.

When the timeupdate event fires we’ll run the jamStation function. The function will be called several times per second while the audio is playing.

var audio = document.getElementById('jam-track');

audio.ontimeupdate = function() {
  jamStation();
};

The Fun Part

This function should only deal with data, not presentation. We’ll use another function later (renderJamStation) to deal with presentation.

To find the current beat we will use the expression (audio.currentTime - offsetSeconds + BPS) / BPS, and we’ll store this value in the variable beat.

We can then find the current measure with the expression (beat - 1) / timeSigTop, which we’ll store in the variable measure.

function jamStation() {
  var beat = (audio.currentTime - offsetSeconds + BPS) / BPS; 
  var measure = (beat - 1) / timeSigTop;
}

The beat and measure variables should now be rounded down. This will give us easier numbers to work with when we are doing comparisons. I wouldn’t suggest doing this however if you were to do a more complex app where you needed to use fractions of a beat.

We’ll store these whole numbers in cleanBeat and cleanMeasure.

If you want to display the current beat in a measure you can use the following expression: ((cleanBeat - 1) % timeSigTop) + 1. Let’s say we’re on beat 13. The expressions values would then be: ((13 - 1) % 4) + 1. We add 1 because there is no such thing as beat 0.

Now instead of having the beat increment infinitely it will only increment as high as the number in the top of the time signature. measureBeat will contain the value of the beat within the measure. So, cleanMeasure will count 1,2,3,4,5,6,7,8, etc. whereas measureBeat will count 1,2,3,4,1,2,3,4 etc.

function jamStation() {
  var beat = (audio.currentTime - offsetSeconds + BPS) / BPS; 
  var measure = (beat - 1) / timeSigTop;

  // round down for beat and measure
  var cleanBeat = Math.floor(beat);
  cleanMeasure = Math.floor(measure);

  // find the current beat within the measure
  measureBeat = ((cleanBeat - 1) % timeSigTop) + 1;
}

Don’t put var in front of cleanMeasure as we’ve already declared this variable outside of the function (see the CodePen) as we want to access this value from outside of jamStation().

At this point we actually have the most important data we’ll need. The tricky part though is going to be dealing with the chord progression.

The next part of the jamStation function is for determining two things: the currentSection and the currentChord.

First we need to use an if…else statement to tests if we are past the first measure. If we are, we will find the value of the current section and current chord. If we aren’t, currentSection and currentChord will be set to null.

To find the current section and current chord we loop through the sections, then run a nested loop that loops through the measures in each section.

The variable count is set and incremented by 1 every time we loop through a measure of each section. Now, if cleanMeasure equals count, we know we’ve found the section and measure we’re on. Because we’ve found the correct section and measure the audio track is currently at, we need store those values and we need to break both loops so the correct data doesn’t get overwritten the next time the loop runs.

Don’t use var in front of currentSection or currentChord, as again, these variables have been declared outside of jamStation() so we can share them between functions.

function jamStation() {
  var beat = (audio.currentTime - offsetSeconds + BPS) / BPS; 
  var measure = (beat - 1) / timeSigTop;

  // round down for beat and measure
  var cleanBeat = Math.floor(beat);
  cleanMeasure = Math.floor(measure);

  // find the current beat within the measure
  measureBeat = ((cleanBeat - 1) % timeSigTop) + 1;

  if (cleanMeasure > 0) {
    // find the currentSection and currentChord
    var count = 0;
    var br = false;
      for (var s = 0; s < chordProgression.length; s++) {
        for (var m = 0; m < chordProgression[s].length; m++) {
          count++;
          if (cleanMeasure == count) {
            currentSection = s + 1;
            currentChord = chordProgression[s][m];
            br = true;
            break;
          }
        }
        if (br === true) {
          break;
        }
      }
  } else {
    currentSection = null;
    currentChord = null;
  }

  // display the jam station and its data
  renderJamStation();
}

Now we have all of the data we need and it’s accessible globally.

At the end of the jamStation function you can see that renderJamStation() is run. This function will be used for presentation purposes only and we will cover it in a moment.

Rendering the Chord Progression

We need to display the chord progression. Let’s wrap it in a function called renderChordProgression to keep things tidy. This function will only be run once as the chord progression data is never updated.

First we loop through the sections in the chord progression. We create a div with the class of “section” and id of “section-[number]” each time the loop loops. Each section has a unique id so we can display that specific section when it is being played.

// take the chordProgression array and render the HTML
function renderChordProgression() {
  var progression = document.getElementById('chord-progression');

  // make the sections
  for (var s = 0; s < chordProgression.length; s++) {
    progression.innerHTML += '<div class="section" id="section-' + (s + 1) + '"></div>';
  }
}

Next we loop through the sections again so we can then loop through all the measures in a nested loop. Each measure will then be included in it’s respective section.

You probably noticed the count variable was set and incremented. This is done in order to increment through every measure in every section.

The complete function looks like this:

// take the chordProgression array and render the HTML
function renderChordProgression(){
  var progression = document.getElementById('chord-progression');

  // make the sections
  for (var s = 0; s < chordProgression.length; s++){
    progression.innerHTML += '<div class="section" id="section-' + (s + 1) + '"></div>';
  }

  var count = 0;

  for (var s = 0; s < chordProgression.length; s++) {
    for (var m = 0; m < chordProgression[s].length; m++) {
      count++;
      var section = document.getElementById('section-' + (s + 1));
      section.innerHTML += '<div class="measure" id="measure-' + count + '">' 
                        + chordProgression[s][m] 
                        + '<div class="m-line">|</div></div>';
    }
  }
}

You will probably want to style the chord progression. You can reference the CSS in the CodePen at the top of this post.

Presentation

The function renderJamStation is called from inside the function jamStation.

function renderJamStation() {

  // show the beat within the measure, not overall
  document.getElementById('beat').innerHTML = measureBeat;

  // show the current measure, but only if the jam track is past the count in measures
  var measureElem = document.getElementById('measure');

  // only show the current measure if it's > 0
  if (cleanMeasure > 0) {
    measureElem.innerHTML = cleanMeasure;
  } else {
    measureElem.innerHTML = '';
  }

  // show the section number
  document.getElementById('section').innerHTML = currentSection;
  // show the current chord name
  document.getElementById('chord').innerHTML = currentChord;

  // hide all sections before displaying only the section we want to see
  var allSections = document.getElementsByClassName('section');

  for (var i = 0; i < allSections.length; i++) {
    allSections[i].style.display = 'none';
  }

  // show the currently playing section
  if (currentSection != null) {
    document.getElementById('section-' + currentSection).style.display = 'block';
  } else {
    allSections[0].style.display = 'block';
  }

  // style the current chord in the chord progression
  if (cleanMeasure > 0) {
    // style all measures black
    var measures = document.getElementsByClassName('measure');
    for (var i = 0; i < measures.length; i++) {
       measures[i].style.color = 'black';
    }
    // style current measure red
    document.getElementById('measure-' + cleanMeasure).style.color = 'red';
  }
}

I won’t cover every part of this function as most of it should be self-explanatory if you’re familiar with JavaScript. For the most part, it takes some data such as the current chord and current measure and displays it.

The main bit of this function you should look closely at is how it displays the right section.

// hide all sections before displaying only the section we want to see
var allSections = document.getElementsByClassName('section');

for (var i = 0; i < allSections.length; i++) {
  allSections[i].style.display = 'none';
}

// show the currently playing section
if (currentSection != null) {
  document.getElementById('section-' + currentSection).style.display = 'block';
} else {
  allSections[0].style.display = 'block';
}

You can see every section is first hidden. If this isn’t done, each section will appear when it’s time is ready, but the sections which aren’t playing will still be visible.

To display the current section being played we select it by its ID and set its display property back to block.

If currentSection is null, we show the first section. If we don’t do this, the first section will not be visible before playing the track and arriving at a point in time where the section should be visible.

Rendering the Initial Display

Finally, we run two functions. renderChordProgression() needs to be run once in order to render the chord progression and jamStation() should be run once as well. Yes, the function jamStation() will be run whenever timeupdate fires, but it should also be run once on its own, otherwise the chord progression will not render at first.

renderChordProgression();
jamStation();

Further Thoughts and Ideas

If you are interested in displaying chord diagrams for an instrument, you have a lot of the data you need, most importantly the chord name and type.

Let’s say you wanted to display chord diagram images when the chord progression was on the appropriate chord. We already have the current chord stored in the variable currentChord.

You could create an array of chord objects.

var chords = [
  {
    name: 'A',
    type: '7',
    src: '/images/a7.jpg',
  },
  {
    name: 'D',
    type: '7',
    src: '/images/d7.jpg',
  },
  {
    name: 'E',
    type: '7',
    src: '/images/e7.jpg',
  }
];

Then, run this bit of logic inside the jamStation function which will display the right chord at the right time.

for (var i = 0; i < chords.length; i++) {
  if (chords[i].name + chords[i].type == currentChord) {
    document.getElementById('chord-diagram').innerHTML = '<img src="' + chords[i].src + '"/>';
  }
}

Conclusion

Maybe you don’t want to build exactly what I’ve covered today and that’s OK. I can think of so many ways you could apply what I’ve covered in this article to all different types of projects. Here are a few ideas:

  • A new take on galleries or sliders. Transition images on certain beats or measures.
  • Animate images to a beat. Tapping feet? Poses of someone dancing?
  • Audio visualizations.
  • Annotations or diagrams used for teaching purposes which are synced to a piece of music.
  • There are all sorts of things you could do with this amazing, open source JavaScript music notation API

Thanks for reading and I hope you learned a thing or two. If this tutorial gave you an idea for something cool to build, let me know in the comments!

  • wjamyers

    // beats per second
    var BPS = 60 / BPM; << shouldn't this be BPM / 60? Am I missing something?

    • Myles English

      Good point. The variable name is a bit misleading. It contains not the number of beats ber second, but the length in seconds of a single beat. So if the tempo is 120, the length in seconds of a single beat would be 0.5 seconds.

  • http://skytechgeek.com Gagan Chhatwal

    Here is the collection of some cool JavaScript Music Libs which you can easily implement to create stations like this. ..http://www.bestdevlist.com/javascript-audio-libraries/

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in JavaScript, once a week, for free.