Skip to main content

AJAX loaded sounds which play via the new Web Audio API.
Far more efficient than the HTML5 audio element.

- (update: )

Efficient audio loading and playback

I was experiencing difficulties synching sounds to the CSS animation used on the Disqus comment button especially as it's intention was to distract the user while Disqus loaded a huge quantity of files in the background. Different browsers behaved differently when using <audio> elements. Sometimes the sound fired other times it skipped completely, not good.

So Web Audio API to the rescue? Well not quite, but it's far better than the original <audio> solution. Unfortunately loading files in the background still interrupts the playback method employed delaying both visual and audio. Not the APIs fault, I repeat play calls during loading. Perhaps looping a sound would prevent that but I preferred the staccato playback.

That said it is still a better solution so I use this method throughout this site. I'm still looking for a better methodology, one without using a full library.

This is a modification of Matt Harrison's excellent Perfect web audio, with the deprecated instructions replaced. For a full description please read his original article.

JavaScript

Determine support and fix cross-browser implementations.

Language JavaScript
try {
  window.AudioContext = window.AudioContext || window.webkitAudioContext;
  window.audioContext = new window.AudioContext();
} catch (e) {
  console.log("No Web Audio API support");
}

WebAudioAPISoundManager Constructor

Language JavaScript
var WebAudioAPISoundManager = function (context) {
  this.context = context;
  this.bufferList = {};
  this.playingSounds = {};
};

WebAudioAPISoundManager Prototype

Language JavaScript
WebAudioAPISoundManager.prototype = {

  addSound: function (url) {

    // Load buffer asynchronously
    var request = new XMLHttpRequest();
    request.open("GET", url, true);
    request.responseType = "arraybuffer";

    var self = this;

    request.onload = function () {

      // Asynchronously decode the
      // audio file data in request.response
      self.context.decodeAudioData(
        request.response,

        function (buffer) {
          if (!buffer) {
            console.log('error decoding file data: ' + url);
            return;
          }
          self.bufferList[url] = buffer;
        });
    };

    request.onerror = function () {
      console.log('BufferLoader: XHR error');
    };

    request.send();
  },

  stopSoundWithUrl: function(url) {
    if (this.playingSounds.hasOwnProperty(url)) {
      for (var i in this.playingSounds[url]) {
        if (this.playingSounds[url].hasOwnProperty(i)) {
          this.playingSounds[url][i].stop();
        }
      }
    }
  }

};

WebAudioAPISound Constructor

Language JavaScript
var WebAudioAPISound = function (url) {
  this.url = url + '.mp3';
  window.webAudioAPISoundManager = window.webAudioAPISoundManager || new WebAudioAPISoundManager(window.audioContext);
  this.manager = window.webAudioAPISoundManager;
  this.manager.addSound(this.url);
};

WebAudioAPISound Prototype

Language JavaScript
WebAudioAPISound.prototype = {

  play: function (options) {
    var buffer = this.manager.bufferList[this.url];

    this.settings = {
      loop: false,
      volume: 0.5
    };

    for (var i in options) {
      if (options.hasOwnProperty(i)) {
        this.settings[i] = options[i];
      }
    }

    //Only play if it's loaded yet
    if (typeof buffer !== "undefined") {
      var source = this.makeSource(buffer);
      source.loop = this.settings.loop;
      source.start(0);

      if (!this.manager.playingSounds.hasOwnProperty(this.url)) {
        this.manager.playingSounds[this.url] = [];
      }
      this.manager.playingSounds[this.url].push(source);
    }
  },

  stop: function () {
    this.manager.stopSoundWithUrl(this.url);
  },

  makeSource: function (buffer) {
    var source = this.manager.context.createBufferSource();
    var gainNode = this.manager.context.createGain();
    gainNode.gain.value = this.settings.volume;
    source.buffer = buffer;
    source.connect(gainNode);
    gainNode.connect(this.manager.context.destination);
    return source;
  }
};

To use

Note no .mp3 extension on file name.

Language JavaScript
var typingSound = new WebAudioAPISound("/audio/typing");
var pagefeedSound = new WebAudioAPISound("/audio/pagefeed");

// pagefeedSound.play({loop : false, volume : 0.8});
// typingSound.play({volume : 0.2});

Google closure compiled

682 bytes gzipped (1.67KB uncompressed)

Language JavaScript
try{window.AudioContext=window.AudioContext||window.webkitAudioContext,window.audioContext=new window.AudioContext}catch(a){console.log("No Web Audio API support")}var WebAudioAPISoundManager=function(a){this.context=a;this.bufferList={};this.playingSounds={}};WebAudioAPISoundManager.prototype={addSound:function(a){var b=new XMLHttpRequest;b.open("GET",a,!0);b.responseType="arraybuffer";var c=this;b.onload=function(){c.context.decodeAudioData(b.response,function(b){b?c.bufferList[a]=b:alert("error decoding file data: "+a)})};b.onerror=function(){console.log("BufferLoader: XHR error")};b.send()},stopSoundWithUrl:function(a){if(this.playingSounds.hasOwnProperty(a))for(var b in this.playingSounds[a])this.playingSounds[a].hasOwnProperty(b)&&this.playingSounds[a][b].stop()}};var WebAudioAPISound=function(a){this.url=a+".mp3";window.webAudioAPISoundManager=window.webAudioAPISoundManager||new WebAudioAPISoundManager(window.audioContext);this.manager=window.webAudioAPISoundManager;this.manager.addSound(this.url)};WebAudioAPISound.prototype={play:function(a){var b=this.manager.bufferList[this.url];this.settings={loop:!1,volume:.5};for(var c in a)a.hasOwnProperty(c)&&(this.settings[c]=a[c]);"undefined"!==typeof b&&(a=this.makeSource(b),a.loop=this.settings.loop,a.start(0),this.manager.playingSounds.hasOwnProperty(this.url)||(this.manager.playingSounds[this.url]=[]),this.manager.playingSounds[this.url].push(a))},stop:function(){this.manager.stopSoundWithUrl(this.url)},makeSource:function(a){var b=this.manager.context.createBufferSource(),c=this.manager.context.createGain();c.gain.value=this.settings.volume;b.buffer=a;b.connect(c);c.connect(this.manager.context.destination);return b}};

Socialise: