Track24 Ghosts III
AlbumGhosts III
ArtistNine Inch Nails
Time00:00

Cet article est également disponible en français

There are two approaches to making almost anything. The first is to dive in head-first, building from the top down, squirrelling into problems as they are found and hoping you’ll get everything right. The second is to create a series of simple test cases: building upwards, proving each step before using the work to generate a final, complete piece.

There are advantages to both methods, but the second tends to yield better results. I’ll demonstrate that approach here, in building the initial prototype of the web audio player I introduced in part 1 of this series.

One of the advantages of hosting your own content is the ability to customize everything about it: rather than having to make do with the UI for audio files from SoundCloud, or built-in browser controls, we can make our own using customized HTML5 elements by knowing just a little JavaScript. The result won’t quite be the version shown above, which will be achieved in the articles ahead, but this code will get you significant part of the way there.

Initial Elements

First, let’s ensure we can get an audio file to play on a page. Here, I’m using Track 24 from NIN’s Ghosts III, licensed under Creative Commons:

<figure id="audioplayer">
	<audio controls src="24-ghosts-III.mp3" id="audiotrack"></audio>
</figure>

Since the latest versions of Firefox now supports .mp3, along with every other modern browser, I’m using just that codec. I’ve placed a <figure> element around the <audio> as we’ll be adding controls within it, together with captions.

Once you’re satisfied that the audio works, the next task is to create a custom UI for the player.

Working With Progressive Enhancement

If the user doesn’t have JavaScript running, or if the script we’re about to add fails to work, we still want them to have access to basic audio controls. Therefore, we remove the default UI only in the context of our script:

var audioPlayer = document.getElementById("audioplayer"),
audioTrack = document.getElementById("audiotrack"),
playButton = document.createElement("button");
playButton.type = "button";
audioPlayer.appendChild(playButton);
audioTrack.removeAttribute("controls");

Added to the bottom of our HTML page, this script effectively reduces our audio player to a simple <button> element. Right now, the play button doesn’t actually have any text in it. We’re going to be doing a lot of swapping text inside button elements, so it makes sense to separate that script into a function:

function setText(el,text) {
	el.innerHTML = text;
}

Now we can set the text of any HTML element easily:

setText(playButton,"Play");

Next, we have to make the button actually do something:

playButton.addEventListener("click", player);
function player() {
	if (audioTrack.paused) {
		setText(playButton,"Pause");
		audioTrack.play();
	} else {
		setText(playButton,"Play");
		audioTrack.pause();
	}
}

Note that we’re not trying to make separate functions for “play” and “stop”, but thinking in terms of the activity of the button.

Making A Mute Button

We also need to create controls for the volume of the audio. The simplest is a “Mute” button, which uses a very similar pattern to the Play/Pause button:

var muteButton = document.createElement("button");
setText(muteButton,"Mute");
muteButton.type = "button";
audioPlayer.appendChild(muteButton);
muteButton.addEventListener("click", muter);
function muter() {
	if (audioTrack.volume == 0) {
		setText(this,"Mute");
		audioTrack.volume = 1;
	} else {
		setText(this,"Unmute");
		audioTrack.volume = 0;
	}
}

This function is a little crude: when unmuted, it sets the volume level for the audio to its absolute maximum (within the limits of the system), but that’s acceptable for right now.

Making A Volume Control

Next, we need a more fine-grained volume slider. The obvious candidate for this UI control is a range element:

volumeSlider = document.createElement("input");
volumeSlider.type = "range";

The range element will need a lot of attributes set in JavaScript, which we could do in the same way I’ve just set the type for the new input… but just because we’re making a prototype, doesn’t mean that we should be doing things the long way. Ideally, at least some of our code will be ported into the final version of the media player, meaning we should be looking for efficient ways to write our code right now.

Unfortunately, there’s no plural setAttributes method we can use define the values of multiple attributes simultaneously. However, we can make a fairly simple function to do so:

function setAttributes(el, attrs) {
	for(var key in attrs){
		el.setAttribute(key, attrs[key]);
	}
}

Now we can set the attributes for the range slider in a single line of code:

setAttributes(volumeSlider, { "type": "range", "min": "0", "max": "1", "step": "any", "value": "1" });

Note that the minimum and maximum values of the slider correspond to those in the muter() function.

Next, we add a listener to the range input, setting the volume of the audio track to the value of the slider in an anonymous function:

volumeSlider.addEventListener("input", function(){ 
	audioTrack.volume = volumeSlider.value;
});

Here we encounter our first real problem: functionally, lowering the volume slider to 0 is the same as clicking on Mute, so that change of state should be shown in the button. How do we make that happen?

Syncing Controls

My solution is to divorce the question from the buttons, and look at any changes in the volume of the <audio> element itself:

audioTrack.addEventListener('volumechange', volumizer);
function volumizer() {
	if (audioTrack.volume == 0) { 
		setText(muteButton,"Unmute");
	} else {
		setText(muteButton,"Mute");
	}
}

There’s just one further fix to make. When the music track finishes, the music should return to the start, and the play button should display “Play”, rather than “Pause” or “Stop”:

audioTrack.addEventListener('ended', finish);
function finish() {
	audioTrack.currentTime = 0;
	setText(playButton,"Play");
}

That’s it! The entire code, with a slight improvement to the muter() function, is at the bottom of this page; I’ve also linked to the more polished version on Codepen shown the top of this article. I’ll be demonstrating how to achieve the enhancements shown in that version in the next two articles in .

function player() {
	if (audioTrack.paused) {
		setText(this, "Stop");
		audioTrack.play();
	} else {
		setText(this,"Play");
		audioTrack.pause();
	}
}
function setText(el,text) {
	el.innerHTML = text;
}
function finish() {
	audioTrack.currentTime = 0;
	setText(playButton,"Play");
}
function volumizer() {
	if (audioTrack.volume == 0) { 
		setText(muteButton,"Unmute"); 
	} else { 
		setText(muteButton,"Mute");
	}
}
function muter() {
	if (audioTrack.volume == 0) {
		audioTrack.volume = restoreValue;
		volumeSlider.value = restoreValue;
	} else {
		audioTrack.volume = 0;
		restoreValue = volumeSlider.value;
		volumeSlider.value = 0;
	}
}
function setAttributes(el, attrs) {
	for(var key in attrs){
		el.setAttribute(key, attrs[key]);
	}
}
var audioPlayer = document.getElementById("audioplayer"),
audioTrack = document.getElementById("audiotrack"),
playButton = document.createElement("button"),
muteButton = document.createElement("button"),
volumeSlider = document.createElement("input");
setText(playButton, "Play");
setText(muteButton, "Mute");
setAttributes(playButton, { "type": "button" });
setAttributes(muteButton, { "type": "button" });
setAttributes(volumeSlider, { "type": "range", "min": "0", "max": "1", "step": "any", "value": "1" });
audioPlayer.appendChild(volumeSlider);
audioPlayer.appendChild(muteButton);
audioPlayer.appendChild(playButton);
audioTrack.removeAttribute("controls");
playButton.addEventListener("click", player, false);
muteButton.addEventListener("click", muter, false);
volumeSlider.addEventListener("input", function(){ 
	audioTrack.volume = volumeSlider.value; 
}, false);
audioTrack.addEventListener('volumechange', volumizer, false);
audioTrack.addEventListener('ended', finish, false);

Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.
Check out the CodePen demo for this article at https://codepen.io/dudleystorey/pen/GEyzn