Book Image

Instant Audio Processing with Web Audio

By : Chris Khoo
Book Image

Instant Audio Processing with Web Audio

By: Chris Khoo

Overview of this book

Web Audio is an upcoming industry standard for web audio processing. Using the API, developers today can develop web games and applications with real-time audio effects to rival their desktop counterparts. Instant Audio Processing with Web Audio is your hands-on guide to the Web Audio API. Using clear, step-by-step exercises, this book explores the API and how to apply it to produce real-time audio effects such as audio stitching, audio ducking, and audio equalization. This book is an in-depth study of the Web Audio API. Through a series of practical, step-by-step exercises, this book will guide you through the basics of playing audio all the way to the task of building a 5-band audio equalizer. Along the way, we'll learn how to utilize Web Audio's scripting functionality to build real-time audio effects such as audio stitching and ducking. Then, we'll use this knowledge to build a basic audio layer step-by-step which can be used in our web applications/games. With its in-depth coverage of the Web Audio API and its practical advice on various audio implementation scenarios, Instant Audio Processing with Web Audio How-to is your ultimate guide to Web Audio.
Table of Contents (7 chapters)

Playing audio in a loop (Simple)


Now that we can play sounds using an audio buffer, let's take a look at playing them in a loop. In this recipe, we'll create an application with a toggle button which toggles the playback of a choir loop.

Getting ready

The complete source code for this recipe is available in the code bundle at recipes/Recipe3_1.

How to do it...

  1. Start with a clean copy of the base framework template. The template bundle is located at tools/RecipeFramework in the code bundle.

  2. Open index.html with a text editor.

  3. In the HTML section, declare the HTML toggle control:

    <div id="appwindow">
        <h2>Playing Audio In A Loop</h2>
        <form>
            <input type="checkbox" id="choir" />
            <label for="choir">Choir Loop</label>
        </form>
    </div>
  4. We'll need the Web Audio initialization and the audio file loading routines covered in previous recipes—we'll add these to the JavaScript section:

    function loadAudioFromUrl( url, loadedCallbackFn, 
                               callbackContext ) {
        var request = new XMLHttpRequest();
        request.open("GET", url, true);
        request.responseType = "arraybuffer";
    
        request.onload = function() {
            consoleout( "Loaded audio '" + url + "'" );
            later( 0, loadedCallbackFn, callbackContext, 
                   request.response );
        };
    
        request.onerror = function() {
            consoleout( "ERROR: Failed to load audio from " 
                        + url );
        };
    
        request.send();
    }
    
    WebAudioApp.prototype.initWebAudio = function() {
        var audioContextClass = window.webkitAudioContext 
                                || window.AudioContext;
    
        if( audioContextClass == null )
            return false;
    
        this.audioContext = new audioContextClass();
        return true;
    };
  5. We'll implement the HTML toggle control initialization and loop playback logic in WebAudioApp.initBufferedAudioLoopToggle():

    WebAudioApp.prototype.initBufferedAudioLoopToggle = 
    function( elemId, audioSrc ) {
        // Initialize the button and disable it by default
        var jqButton = $( elemId ).button({ disabled: true });
    
        // Load the audio
        var audioBuffer;
        loadAudioFromUrl( audioSrc, function(audioData){
            // Decode the audio data into an audio buffer
            this.audioContext.decodeAudioData(
                    audioData,
                    function( audioBufferIn ) {
                        consoleout( "Decoded audio for '" 
                                    + audioSrc + "'" );
    
                        // Cache the audio buffer
                        audioBuffer = audioBufferIn;
    
                        // Audio ready? Enable the button
                        jqButton.button( 
                                   "option", 
                                   "disabled", 
                                   false );
                    }
            );
        }, this );
    
        // Register a click event listener to trigger playback
        var me = this;
        var activeNode;
        jqButton.click(function( event ) {
    
            // Stop the active source node...
            if( activeNode != null ) {
                if( activeNode.stop instanceof Function )
                    activeNode.stop( 0 );
                if( activeNode.noteOff instanceof Function )
                    activeNode.noteOff( 0 );
    
                activeNode = null;
    
                consoleout( "Stopped audio loop '" 
                            + audioSrc + "'" );
            }
    
            // Start new playback if the button is checked
            if($(this).is(':checked')) {
                var sourceNode = me.audioContext
                                   .createBufferSource();
                sourceNode.buffer = audioBuffer;
    
                // Connect it to the speakers
                sourceNode.connect( me.audioContext.destination );
    
                // Start the audio playback
                if( sourceNode.start instanceof Function )
                    sourceNode.start( 0 );
                if( sourceNode.noteOn instanceof Function )
                    sourceNode.noteOn( 0 );
    
                // Turn on looping
                sourceNode.loop = true;
    
                // Keep track of the active sound loop                
                activeNode = sourceNode;
    
                consoleout( "Played audio loop '" 
                            + audioSrc + "'" );
            }
        });
    };
  6. Finally, we'll initialize Web Audio to set up the loop playback logic in WebAudioApp.start():

        WebAudioApp.prototype.start = function() {
            if( !this.initWebAudio() ) {
                consoleout( "Browser does not support WebAudio" );
                return;
            }
    
            this. initBufferedAudioLoopToggle( "#choir", "assets/looperman-morpheusd-amazing-pt2-choir-120-bpm.wav" );
        };

Launch the application test URL in a web browser (http://localhost/myaudiomixer) and click on the Choir Loop button to toggle the loop playback. The following is a screenshot of what we should see in the web browser:

How it works...

Let's take a closer look at the toggle control's click event handler in WebAudioApp.initBufferedAudioLoopToggle():

  1. When the user deactivates playback, we use the method stop() or noteOff() of the AudioBufferSourceNode instance to explicitly stop the active AudioNode class:

    jqButton.click(function( event ) {
    
        // Stop the active source node...
        if( activeNode != null ) {
            if( activeNode.stop instanceof Function )
                activeNode.stop( 0 );
            if( activeNode.noteOff instanceof Function )
                activeNode.noteOff( 0 );
    
            activeNode = null;
    
            ...
        }
  2. When a user activates playback, we trigger sound playback as per the previous recipe:

        if($(this).is(':checked')) {
            // Decode the audio data into an audio buffer
            var sourceNode = me.audioContext.createBufferSource();
            sourceNode.buffer = audioBuffer;
    
            // Connect it to the AudioContext destination node
            sourceNode.connect( me.audioContext.destination );
    
            // Start the audio playback
            if( sourceNode.start instanceof Function )
                sourceNode.start( 0 );
            if( sourceNode.noteOn instanceof Function )
                sourceNode.noteOn( 0 );
  3. Then, we turn on the looping behavior by setting the AudioBufferSourceNode instance's loop attribute to true:

            // Turn on looping
            sourceNode.loop = true;

    Tip

    Applications may change the loop attribute of AudioBufferSourceNode to alter its playback behavior at runtime.

The following are the new Web Audio API members covered in this recipe:

  1. Set the loop attribute of AudioBufferSourceNode to true to enable looping; by default, the playback will loop from start to end:

    interface AudioBufferSourceNode : AudioSourceNode {
        var loop:Boolean;
  2. The loopStart and loopEnd attributes of AudioBufferSourceNode allow the application to customize the audio section to play in a loop. When specified, playback will loop infinitely once inside the loop time frame:

        var loopStart:Number;
        var loopEnd:Number;
    };

    Tip

    loopEnd must always be at a later time than loopStart. Otherwise, the custom loop time frame is ignored.

There's more...

The ability to limit looping to a subsection within a larger playing segment is a powerful mojo—it lets developers implement some pretty complicated playback behavior with just a few lines of code. Let's use this functionality to improve our choir loop quality.

The choir loop audio sample is actually composed of two audio segments of equal lengths:

  • A lead-in segment with a soft start

  • A looping segment

As a result, the choir loop is disjointed whenever the sound loops from start to end. Using the loopStart and loopEnd attributes of AudioBufferSourceNode, we can modify the looping behavior so that looping only occurs in the looping segment. The following diagram describes what we're trying to accomplish:

Let's modify the toggle's click event handler to fix the loop; add the following highlighted code snippet to the handler:

jqButton.click(function( event ) {

    ...

    // Turn on looping
    sourceNode.loop = true;

    // Specify a custom loop segment
    sourceNode.loopStart = audioBuffer.duration * 0.5;
    sourceNode.loopEnd = audioBuffer.duration;

    // Keep track of the active sound loop
    activeNode = sourceNode;

    ...

});

We retrieve the sample's duration from the duration attribute of AudioBuffer and use it to calculate the start and end points for the loop segments. Then, we fill the loopStart and loopEnd attributes of AudioBufferSourceNode with the loop start and end times, respectively.

Tip

The loopEnd attribute must be set if the loopStart attribute is set. Otherwise, the loop segment settings will have no effect.

Now, the choir loop playback starts with a gentle lead-in before entering a seamless infinite loop.

The AudioBuffer class

The AudioBuffer class is much more than a container for audio data. Applications can gather metrics about the audio sample from an AudioBuffer instance. Applications can even retrieve and modify the waveform data!

The following is the AudioBuffer class definition:

interface AudioBuffer {

    var duration:Number;          // readonly
    var numberOfChannels:Number;  // readonly

The duration attribute contains the audio sample's duration measured in seconds and the numberOfChannels attributes contain the number of audio channels in the sample:

    var sampleRate:Number;        // readonly
    var length:Number;            // readonly

    function getChannelData(channel:Number):Float32Array;
};

The sampleRate attribute contains the sample rate of the audio data measured in Hz.

The length attribute contains the length of the audio data measured in sample frames.

The function getChannelData() retrieves the channel waveform for the target channel. It returns the waveform as an array of the size length containing all the frame samples. Each sample is the waveform's PCM float value normalized to the [-1, +1] range.