View Sidebar

A Million Little Pieces Of My Mind

Step 5: Playlist

By: Paul S. Cilwa Viewed: 4/25/2024
Posted: 11/17/2017
Page Views: 757
Topics: #Computers #Cross-fadingMusicPlayer #JavaScript #MusicPlayer #OrganicaAudio #Programming #Projects #WebAudioAPI
Implementing the Playlist array and populator.

We now have a proof-of-concept for the OrganicaAudioTrack class, in that we can use it to play an MP3 (or any other supported music file format, for that matter). But our goal is the ability to play a playlist of tracks, a whole collection of them. And since a playlist is a collection of tracks, it won't be OrganicaAudioTrack's job to manage that list. We need to return to the top-level object, OrganicaAudio.

You may recall that OrganicaAudio's constructor created a property for the object called Playlist, that was initialized to an empty array. So now, let's add a method to the class called AddSource.

OrganicaAudio.prototype.AddTrack = function(aSource, anID)
	{
	switch(aSource)
		{
		case 'audio':
			if (typeof(anID) === undefined)
				aSource = $("audio > source").first().attr("src")
			else
				aSource = $(anID + '> source').first().attr("src");
			break;
		}
	
	this.PlaylistIndex = this.Playlist.push(new OrganicaAudioTrack(aSource)) - 1;
	
	if (this.PlaylistIndex > 0)
		this.Playlist[this.PlaylistIndex-1].ToPlayNext = this.Playlist[this.PlaylistIndex]
	}

The switch statement at the top of this function allows the caller to specify an <audio> tag. (It's a switch in case I want to expand it to other types of tags later.)

The array magic occurs next, when the push() method adds a newly-minted OrganicaAudioTrack object to the array, returning the zero-based index of the new item. We save that in this.PlaylistIndex so we can add a ToPlayNext, pointing to a subsequent track. We'll need this to seamlessly play track after track, as music players are supposed to do.

Now, once the Playlist has been populated, at some point the end user will want to play the tracks in it, one after the other. In response to (perhaps) a button click, the OrganicaAudio object's Play method can be invoked. (And, again, this works because the function will not be called asynchronously or as a callback.)

OrganicaAudio.prototype.Play = function()
	{
	MyOrganicaAudio.PlaylistIndex = 0;
	console.log(this.Playlist[0]);
	this.Playlist[0].Play();
	}

Now, in a more top-down programming language, the Playlist would be in charge of playing those tracks consecutively. But since JavaScript is an asynchronous environment, we have to pass that job on to the OrganicaAudioTrack object. Remember how we just added a ToPlayNext property? When the track completes, the next track will be started. That's why, in the above method, Play() merely starts the first track playing. But that call also schedules the second track to play when the first has completed. We need only add one line to the Play() method to let that happen.

OrganicaAudioTrack.prototype.Play = function(StartTime)
	{
	var Me = this;
	
	if (isNaN(StartTime))
		StartTime = 0;
		
	if (! Me.Loaded)
		{
		Me.Load().then(function(Me)
			{
			Me.Play(StartTime);
			});
		return;
		}
	else
		{
		//Actually play the damned thing...
		Me.SoundSource.onended = function()
			{
			console.log("Ended!");
			Me.Playing = false;
			};
		Me.SoundSource.connect(Me.Context.destination);
		Me.Playing = true;
		Me.SoundSource.start(StartTime + Me.Context.currentTime);
		Me.PlayNext(Me);
		}
	}

That one line invokes a new method to be added to the class: PlayNext(). We have to pass it Me (the object's this) because PlayNext() may well be called asynchronously).

OrganicaAudioTrack.prototype.PlayNext = function(Me)
	{
	console.log("Play next...");
	console.log(Me);
	
	if (Me.ToPlayNext === undefined)
		{
		console.log("Playlist empty.");
		return;
		}

	console.log("Something to play...");
	Me.ToPlayNext.Play(Me.Duration - Me.StartCrossFade);
	}

If there is nothing left to play—we just played the last track in the list, or there was only one to begin with—the method returns without doing anything more than a little logging. But if there is, we call its Play() method.

Previously, when we called Play(), we omitted the argument, StartTime, which will cause the track to begin playing immediately. In this code, we do supply a start time: The duration of the previous, currently playing, track, modified by a property called StartCrossFade. Currently, that value is zero so the next track will begin when the current track completes. But later, we'll be adding a cross-fade feature and this code structure is the only way I could figure to do it efficiently.

To test our new code, open the AudioTest.html file and change the test script to the following:

<script>
$(document).ready(function()
	{
	MyOrganicaAudio.AddTrack("audio","#Song1");
	MyOrganicaAudio.AddTrack("Danny_Boy.mp3");
	
	$("#Button1").click(function()
		{
		console.log('click');
		MyOrganicaAudio.Play();
		});
	});
</script>

Save your changes and load the file into Firefox. If there are no typos, just click the Play button and listen to the two tracks play, one after the other. (Or you can use the embedded page frame below.)