Dear Developers, today I'll guiding you to creating an MP3 Player with HTML programming step-by-step. Let's go!
What is HTML5 Audio Player? It is a subject of the HTML5 specification, incorporating audio input, playback, and synthesis, as well as speech to text, in the browser, given rise to some new and exciting possibilities, especially when
it comes to music related web applications... etc.
We 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.
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.
Things 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.
- BMP = 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.
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;
}
Set 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;
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();
};
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 now
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
Suggest for you: