Finding Howler


This week, we got another awesome song from Michael Shawn Carbaugh II! (At the moment, I'm going to make you wait to hear it though.) After receiving the song, I realized that we have some serious issues with audio looping in Maven's engine. There are basically three methods to looping audio that I have pondered.

Audio Loop Using Single Audio Element

Currently, the engine uses HTML5's audio element with the loop attribute.




The great thing about this method is that it is built into HTML5 and is very easy to implement. For example, the HTML for the example above is simply this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<button id="playAudioSingle">Play</button>
<button id="pauseAudioSingle">Pause</button>
<br/>
<audio id="audioSingle" src="IntoDarknessHalfLoop.mp3" loop controls></audio>
<script>
	document.getElementById("playAudioSingle").onclick = function() {
		document.getElementById("audioSingle").play();
	};
	document.getElementById("pauseAudioSingle").onclick = function() {
		document.getElementById("audioSingle").pause();
	};
</script>

However, as you may have noticed, the playback of the loop is imperfect, and the extent of this imperfection varies depending on the browser.

Audio Loop Using Dual Audio Elements

A second method is to have two separate audio tracks loop back and forth between each other, as shown below.



The code for this is substantially more complex than the first. I have included a slightly truncated version of the JavaScript below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
var filename = "IntoDarknessHalfLoop.mp3";
var calibration = 0;

var doubleAudio = [];
var doubleAudioPlaying = 0;
function doubleAudioNotPlaying() {
	return (doubleAudioPlaying + 1)%2;
}

//Create audio elements
doubleAudio[0] = new Audio(filename);
doubleAudio[1] = new Audio(filename);

var timeoutSet = false;

//When audio is played, mark timeout as not yet set and correct audio playing index
doubleAudio[0].onplay = function() {
	doubleAudioPlaying = 0;
	timeoutSet = false;
};
doubleAudio[1].onplay = function() {
	doubleAudioPlaying = 1;
	timeoutSet = false;
};

function timeUpdate() {
	//If the timeout for the next audio is already set, don't set it again
	if(timeoutSet) return;

	var playing = doubleAudio[doubleAudioPlaying];
	var notPlaying = doubleAudio[doubleAudioNotPlaying()];
	notPlaying.pause();
	notPlaying.currentTime = 0;

	var dt = (playing.duration - playing.currentTime)*1000;
	if(dt < 500) {
		timeoutSet = true;
		setTimeout(function() {
			notPlaying.play();
			doubleAudioPlaying = doubleAudioNotPlaying();
		}, dt - calibration)
	}
}

doubleAudio[0].ontimeupdate = timeUpdate;
doubleAudio[1].ontimeupdate = timeUpdate;

The advantage of this method is that it can be calibrated to decrease the amount of delay between loops. Click the calibration button below and retry the two-track method of looping above after the calibration has finished.


This calibration method could be done silently and theoretically solve the problem, but you may have encountered some other problems with this method, such as volumes of tracks changing unpredictably or the calibration not quite getting things correctly. The calibration method is represented by the code below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
var runTimes = 4;//prompt("Number of times to run");
var timesRun = 0;

var cAudio = [];

cAudio[0] = new Audio("1000ms.mp3");
cAudio[1] = new Audio("1000ms.mp3");
var cTimeoutSet = false;
var cAudioPlaying = 0;
function cAudioNotPlaying() {
	return (cAudioPlaying + 1)%2;
}

function cTimeUpdate() {
	if(cTimeoutSet) return;

	var playing = cAudio[cAudioPlaying];
	var notPlaying = cAudio[cAudioNotPlaying()];

	notPlaying.pause();
	notPlaying.currentTime = 0;
	console.log(playing.currentTime + "/" + playing.duration);
	var dt = (playing.duration - playing.currentTime)*1000;
	if(dt < 500) {
		cTimeoutSet = true;
		timesRun++;
		setTimeout(function() {
			if(timesRun >= runTimes)  {
				giveResults();
				playing.pause();
				return;
			}
			cTimeoutSet = false;
			notPlaying.play();
			cAudioPlaying = cAudioNotPlaying();
		}, dt)
	}
}

cAudio[0].ontimeupdate = cTimeUpdate;
cAudio[1].ontimeupdate = cTimeUpdate;

function giveResults() {
	var bDate = new Date();
	var runtime = bDate.getTime() - aDate.getTime();
	var delay = ((runtime - (runTimes*1000))/runTimes);
	calibration = delay;
}

var aDate = new Date();
cAudio[0].play();

Audio Loop Using Howler.js

The difficulties of the previous solution left me wanting for more, until I discovered Web Audio API. Most modern browsers have Web Audio API, which basically just provides better audio functionality in browser. Since I am concerned about compatibility with browsers outside of this scope, Web Audio API was not a good solution by itself, but then I discovered Howler.js. Howler "defaults to Web Audio API and falls back to HTML5 Audio." Boom. That's what I'm looking for right there.

WARNING: This example may crash in Microsoft's new browser Edge.

The code for Howler.js is simple too, and it has a substantial API with extra audio features.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<button id="playHowl">Play</button>
<button id="pauseHowl">Pause</button>
<script>
	var sound = new Howl({
	  src: ['IntoDarknessHalfLoop.mp3'],
	  loop: true
	});

	document.getElementById("playHowl").onclick = function() {
		sound.play();
	};
	document.getElementById("pauseHowl").onclick = function() {
		sound.pause();
	};
</script>

My initial meddling with this was drawn from this article. Although it is a little outdated, it is also helpful.