Using Web Audio API to decode and play an MP3 file (part 1)

Hi there! Thanks for coming!

This article is the first in a mini 3 part series where I will use the Web Audio API to decode and play an MP3 file. We’ll then build on this sample to introduce a Gain node to adjust the volume of your audio, and then finally a script processing node in order to get access to the raw audio samples to do a volume calculation.

By the end of this article I hope you’ll appreciate how powerful, yet simple the Web Audio API is for playback.

If you haven’t already, please consider reading the ‘Introduction to the Web Audio API‘ article that I wrote before proceeding.

What the sample does

Starting with the basics I’d like to show you how to play an AudioBuffer directly to your output device (which is more than likely your speakers).

First we’ll obtain the MP3 bytes from file system using the File API to read an MP3 file from the computer’s local file system.

In order to obtain the audio buffer object we will need to use the AudioContext.decodeAudioData() method that will use your browser (or system) audio decoder to transform the Mp3 data into a buffer of PCM samples.

After we’ve obtained this buffer we’ll then create an ‘AudioBufferSourceNode‘ object that will consume the audio samples and play them out at the correct speed to your audio output device.

The full code

Below you’ll find the completed sample source code, the comments should pretty much explain everything that you need to know, but I’ll also explain a few things below.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Decode an MP3 using Web Audio API</title>


<style>
            body {
                font-family: sans-serif;
                font-size: 9pt; 
            }
        </style>


    </head> 
    <body>
        


<form id="playerForm">


<h1>Decode an MP3 using Web Audio API</h1>




This example will show you how to decode an MP3 file using the Web Audio API.



<input type="file" id="mp3FileSelect" />



<input type="button" id="playButton" value="Play" />

        </form>


        
        <script>


            var audioContext = new (window.AudioContext || window.webKitAudioContext)(); // Our audio context
            var source = null; // This is the BufferSource containing the buffered audio
            
            
            // Used the File API in order to asynchronously obtain the bytes of the file that the user selected in the 
            // file input box. The bytes are returned using a callback method that passes the resulting ArrayBuffer. 
            function obtainMp3BytesInArrayBufferUsingFileAPI(selectedFile, callback) {

                var reader = new FileReader(); 
                reader.onload = function (ev) {
                    // The FileReader returns us the bytes from the computer's file system as an ArrayBuffer  
                    var mp3BytesAsArrayBuffer = reader.result; 
                    callback(mp3BytesAsArrayBuffer); 
                }
                reader.readAsArrayBuffer(selectedFile);
                
            }
              
                        
            function decodeMp3BytesFromArrayBufferAndPlay(mp3BytesAsArrayBuffer) {
                
                // The AudioContext will asynchronously decode the bytes in the ArrayBuffer for us and return us
                // the decoded samples in an AudioBuffer object.  
                audioContext.decodeAudioData(mp3BytesAsArrayBuffer, function (decodedSamplesAsAudioBuffer) {
                        
                    // Clear any existing audio source that we might be using
                    if (source != null) {
                        source.disconnect(audioContext.destination);
                        source = null; // Leave existing source to garbage collection
                    } 
                    
                    // In order to play the decoded samples contained in the audio buffer we need to wrap them in  
                    // an AudioBufferSourceNode object. This object will stream the audio samples to any other 
                    // AudioNode or AudioDestinationNode object. 
                    source = audioContext.createBufferSource();
                    source.buffer = decodedSamplesAsAudioBuffer; // set the buffer to play to our audio buffer
                    source.connect(audioContext.destination); // connect the source to the output destinarion 
                    source.start(0); // tell the audio buffer to play from the beginning
                }); 
                
            }
            
            
            // Assign event handler for when the 'Play' button is clicked
            playerForm.playButton.onclick = function (event) {
                
                event.stopPropagation();
                
                // I've added two basic validation checks here, but in a real world use case you'd probably be a little more stringient. 
                // Be aware that Firefox uses 'audio/mpeg' as the MP3 MIME type, Chrome uses 'audio/mp3'. 
                var fileInput = document.forms[0].mp3FileSelect; 
                if (fileInput.files.length > 0 && ["audio/mpeg", "audio/mp3"].includes(fileInput.files[0].type)) {
                    
                    // We're using the File API to obtain the MP3 bytes, here but they could also come from an XMLHttpRequest 
                    // object that has downloaded an MP3 file from the internet, or any other ArrayBuffer containing MP3 data. 
                    obtainMp3BytesInArrayBufferUsingFileAPI(fileInput.files[0], function (mp3BytesAsArrayBuffer) {
                       
                        // Pass the ArrayBuffer to the decode method
                        decodeMp3BytesFromArrayBufferAndPlay(mp3BytesAsArrayBuffer);  
                                          
                    });
                    
                } 
                else alert("Error! No attached file or attached file was of the wrong type!");
                                    
            }


        </script>
        
    </body>
</html>

Explaining a few things

Mime type differences between browsers

Unfortunately we don’t live in a perfect world, and these imperfections extend to how different browsers register MIME types for audio files. In the following line you notice that we test the selected file’s MIME type against “audio/mpeg” and “audio/mp3“.

["audio/mpeg", "audio/mp3"].includes(fileInput.files[0].type)

This is because Chrome regards an MP3 file as “audio/mp3” whereas Firefox and Edge regard an MP3 file as “audio/mpeg“.

Usage of the File API

Why I separated obtaining the MP3 bytes and decoding them into two methods

So in this example I am using the File API in order to obtain the MP3 file bytes.

You’ll notice that I split the code into two sections, one to obtain the MP3 bytes and the other is to decode those bytes using the AudioContext.

The reason for this is that the AudioContextdecodeAudioData()‘ method doesn’t care how you obtain the MP3 bytes, so it makes sense to separate these two functions to better illustrate that. In a real world app you might not separate these two methods.

Briefly on the File API

The File API is a very neat way to obtain the data from a file on the user’s file system. The File API is granted access to a particular file on the file system when the user selects it in a file input element.

Processing the file contents in JavaScript before it is sent to the server yields advantages in that you can work with a file before it is sent to a server. For example; assume you run an image gallery web site, instead of blocking a user when they select a larger than allowed image, you could automatically process it, by resizing it, and converting it to the correct format before sending it to the server.

Likewise here we’re able to play the audio before it is sent to the server. In practice you could so something cool like render the file’s waveform and use it as a progress bar as it uploads to a remote server.

Disconnecting existing audio nodes

if (source != null) {
    source.disconnect(audioContext.destination);
    source = null;
} 

Just to explain the above code snippet. If we don’t include this code then every time we click the play button whatever is already playing will continue to play and our new audio data will be mixed into it.

In order to stop the existing audio from playing we disconnect the existing audio source if it exists. By setting our reference to the existing audio source to null the garbage collector will automatically clean it up for us on it’s next run.

About the AudioBufferSourceNode object

source = audioContext.createBufferSource();
source.buffer = decodedSamplesAsAudioBuffer; 
source.connect(audioContext.destination); 
source.start(0); 

In this part you can see that we ask the AudioContext to create an AudioBufferSourceNode object. This object streams the contents of an AudioBuffer to a AudioDestinationNode.

The sequence of this code can be explained as follows:

  1. Ask our AudioContext to create an AudioBufferSourceNode object.
  2. Assign our AudioBuffer of decoded MP3 samples as the buffer to be played.
  3. Connect the output of our AudioBufferSourceNode object to the AudioDestinationNode of our AudioContext.
  4. Tell the AudioBufferSourceNode object to start streaming the buffer from the start.

Chrome and support for the “buffer” property of AudioBufferSourceNode

For some reason the MDN documentation states “[1] The buffer property was removed in Chrome 44.0.” for AudioBufferSourceNode (link), and on other pages it states that object has been deprecated (link).

In my experience with writing this article there was no problem with using this property in Chrome 47 and I’ve found no information with regards to the property being deprecated in the latest Web Audio API draft on the W3C documentation web site (see here).

However I did find these two links here and here that state that the ability to set the buffer property more than once should be deprecated. So in English, it appears that Chrome may have just removed the ability to set the “buffer” property more than once.

Therefore I’ve concluded that the otherwise excellent MDN documentation is wrong in this case and I’ve suggested the correction to them.

What’s next?

Hopefully this sample gives you an example on how to decode an MP3 file to a buffer and play that buffer using the Web Audio API. So far we’ve only just connected directly to the playback destination node. In the next example I’ll be showing you how to insert an AudioNode in the middle to transform your audio slightly.

Further reading and references

My stuff (opens in same tab)

External references (opens in different tab)

1 thought on “Using Web Audio API to decode and play an MP3 file (part 1)”

Leave a Reply

Your email address will not be published. Required fields are marked *