Recently while working on Waveform Playlist, which can schedule multiple audio tracks playing with the web audio API, I ran into the problem of being able to stop and start my web audio playout in quick succession in javascript. This was needed when a user seeked to a different part of the playlist while it was currently playing. In brief, the code I had in Waveform Playlist to accomplish the above had looked something like this:

this.stop(); this.start(newStartTime);

The stop method was responsible for stopping each track in the playlist and clearing the old node graph, while the start method setup a new web audio node graph to continue playout from the new chosen starting time.

The result I got was

playlist stopped playlist started … then playlist immediately stopped again?

I was confused for a moment, but then realized I had just refactored the project to use the newer onended callback provided by AudioBufferSourceNode to better manage cleanup when the audio had stopped/ended. Originally when the project had started in early 2013, this callback wasn’t available. This incorrect behaviour was the workings of the javascript event loop. For those needing a refresher on how the event loop schedules tasks to execute, this great article from last week can catch you up.

It turns out now, my stop function was called, then my start function, but then all the onended callbacks would execute right afterwards. To get around this problem and make sure I knew the moment when every track in the playlist had stopped, I wrapped the web audio nodes setup into a Promise. Again if you’re new to promises, here’s a great article to get started with.

Below is the function used to setup the web audio node graph.

setUpSource: function() { var sourcePromise; var that = this; this.source = this.ac.createBufferSource(); this.source.buffer = this.buffer; sourcePromise = new Promise(function(resolve, reject) { //keep track of the buffer state. that.source.onended = function(e) { that.source.disconnect(); that.fadeGain.disconnect(); that.outputGain.disconnect(); that.masterGain.disconnect(); that.source = undefined; that.fadeGain = undefined; that.outputGain = undefined; that.masterGain = undefined; resolve(); } }); this.fadeGain = this.ac.createGain(); //used for track volume slider this.outputGain = this.ac.createGain(); //used for solo/mute this.masterGain = this.ac.createGain(); this.source.connect(this.fadeGain); this.fadeGain.connect(this.outputGain); this.outputGain.connect(this.masterGain); this.masterGain.connect(this.destination); return sourcePromise; }

The onended callback is fired both when the track plays out naturally, as well as when it’s manually stopped. Every time I scheduled a track to play, I would keep a reference to the Promise returned from the setup, something like this.

var playoutPromise = track.play(startTime, endTime)

Within the track’s play method setUpSource was called, which created the playoutPromise . Keeping a reference to a Promise for every track in the playlist, my newly updated code from before to seek to a different spot in the audio could now look something like

this.stop(); Promise.all(playoutPromises).then(this.start.bind(this, newStartTime));

This ensures that the start method will only run after all the onended callbacks for each track has finished executing.

Those are the basics to restarting multiple sounds using the web audio API! Now go back to making music!