Welcome to part 2 of the beat the sound module post. In the first post we took a look at creating the context, getting your sound, creating a buffer and play the sound. This part we will take a look at adding effects and panning. The result will be a sound module which you can use for many purposes.

The Audio API has a lot of ways to manipulate sounds. In this post we will take a look at 2 of them:

  • panning
  • the filter

If you understand the way this all comes together, most of the others are also pretty easy to implement.

Panning

Panning handles the distribution of sounds over 2 or more speakers. This will help you to create a better stereo sound. The Audio API has 2 different kind of panning nodes to use:

The StereoPannerNode has as only downside it’s incompatibility with Safari and Opera. But like for so many things: There’s a Polyfill for that. We need to check the availability of the Node. First we set myPanner to null (falsy). If we have a StereoPannerNode this will be overwritten (truthy).

var myPanner = null;
if (myContext.createStereoPanner !== undefined) {
    myPanner = myPanner.createStereoPanner();
    myPanner.pan.value = .7;
}

With the daisy-chaining as we did the last post we can add the StereoPannerNode to the chain.

Filter

The filter used within the Audio API is one of the BiQuad types and is called BiquadFilterNode. It’s a really flexible filter with a lot of settings and freedom. The sound of the filter itself works for a lot of use cases. We’re going to use this as a so called low-pass filter. This filter filters the high and midrange frequencies. This gives the sound a bit of the dance like sounds.

For the filter as we are going to use it, it has a few settings. We will only use the most common ones.

  • type: Will be set to ‘lowpass’
  • Q: The qualityfactor to set the bandwidth of the filter
  • frequency: The cutoff frequency for filtering

Adding the filter works just like we did with the panning:

var myFilter = self.context.createBiquadFilter();
myFilter.type = 'lowpass';
myFilter.Q.value = 18;
myFilter.frequency.value = 1000;

Optimising the module

Now we have all our parameters and settings we want to use in our sound module we can build it all together. This is a more flexible way then we did in the last post. We will start with the module, all it’s attributes. The file parameter is the link to the audio file we have to retrieve.

var Sound;
Sound = function (file) {
    var self = this;

    self.context = null;
    self.buffer = null;
    self.path = '';
    self.mute = false;
    self.gain = 0.7;
    self.pan = 0;
    self.filter = false;
    self.filterQ = 1;
    self.filterFreq = 8000;
}

All the parameters needs to be editable. This can be done by adding the next method to the module. This will make it possible to write new values to public attributes. The ones marked as private (starting with an _), will be skipped.

function set(key, value) {
    if (key[0] === '_') {
        return false;
    }
    self[key] = value;
}

We can now add a method to our module which will retrieve the audio file and puts it in the buffer like we did in the last post. In the result this one is called getFile(file).

The play method will be a lot more extended then we had it in the last post. This because we have some more Nodes to take care of.

  1. First we will define all create all our Nodes.
  2. We fill the bufferSource with the buffer content
  3. We check if we do have the StereoPanner. If not, we skip this one
  4. If mute has been selected, set the gain to 0, otherwise we set the value of self.gain
  5. Now we’re going to check what parts we want to/can use and chain them all together
  6. And finally play the sound.
function play() {
    /**
     * Create the buffersource and fill it with our buffer
     */
    var acSound = self.context.createBufferSource(),
        acGain = self.context.createGain(),
        acPanner = false,
        acFilter = self.context.createBiquadFilter();

    acSound.buffer = self.buffer;

    if (self.context.createStereoPanner !== undefined) {
        acPanner = self.context.createStereoPanner();
        acPanner.pan.value = self.pan;
    }
    /**
     * Set the Gain, Panner, and Filter
     */
    if (self.mute) {
        acGain.gain.value = 0;
    } else {
        acGain.gain.value = self.gain;
    }
    acFilter.Q.value = self.filterQ;
    acFilter.frequency.value = self.filterFreq;

    /**
     * Connect everything together
     */
    if (self.filter) {
        if (acPanner) {
            acSound.connect(acPanner);
            acPanner.connect(acFilter);                
        } else {
            acSound.connect(acFilter);
        }
        acFilter.connect(acGain);
    } else {
        if (acPanner) {
            acSound.connect(acPanner);
            acPanner.connect(acGain);                
        } else {
            acSound.connect(acGain);
        }
    }
    acGain.connect(self.context.destination);

    /**
     * Play the sound
     */
    acSound.start(0);
}

Add a init function and return all the public methods and we’re done. As you can see we also have set the getFile method to public. This can come in very handy if want to switch between different kind of sounds. This will be used later on in this serie.

function _init(file) {
    self.path = file;
    getFile();
}

_init(file);

return {
    play: play,
    setSound: getFile,
    set: set,
    get: get
};

Now we have a fully functional independently usable sound module. It can be used as follows:

var mySound = new Sound(filename);
mySound.set('gain', 1);
mySound.play();

I hope you enjoyed the last post on the sound module. The next post will be about the channel. This is the part of the BeatMachine which controls the separate sounds. Please leave comment if you like this post or not and see ya next time.

The complete working example with parameters of the sound module can be found on examples.navelpluisje.nl