How to create a Volume Meter (measure the sound level) in the Browser with JavaScript

How to create a Volume Meter (measure the sound level) in the Browser with JavaScript

As one of those weird projects that we like to share here in Our Code World, we bring you today one of those functions that you won't probably use in in your Laboral scene but in personal projects or just to expand your knowledge of Javascript. We are talking about an useful "Volume Meter" that comes in handy to display it graphically to warn the one that uses the system with the famous green and red bar, that he should speak quietly.

Let's get started !

1. Download Volume Meter

Volume Meter is not a plugin, but a collection of 2 useful functions that will help you to retrieve the input level of the microphone through the WebAudio API. If you're working in some kind of scientific project about how to retrieve the sound level of the microphone through the browser, this ain't probably what you're looking for. But if the goal is to get a general idea of a measurement of the sound level for your user, then it might be pretty useful.

Download the script of volume meter in the Github repository here. This script was written by Chris Wilson at Google, visit the official repository in Github for more information.

2. Using Volume Meter

As mentioned previously, the "plugin" are 2 functions, you can decide by your own if you want to declare them in the window or just add them as a new script in your document. The point is, that the 2 functions of the plugin are available : createAudioMeter and volumeAudioProcess.

With those functions, you can now retrieve the sound level of the microphone and you can do anything of the following tasks:

Important

It's worth to say that the scripts (getUserMedia) needs to be executed once the window is loaded, otherwise it will fail for obviously reasons.

Display level in Canvas

To show the known bar that goes green and red according to the input level of the microphone, you can use the following code. It will try to access the User Media API and through the stream received as first argument in the onMicrophoneGranted callback, the input level of the microphone can be retrieved thanks to the functions of the plugin createAudioMeter and volumeAudioProcess:

<!-- The canvas that will be used to render the input level -->
<canvas id="meter" width="500" height="50"></canvas>

<script>
/*
The MIT License (MIT)

Copyright (c) 2014 Chris Wilson

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
var audioContext = null;
var meter = null;
var canvasContext = null;
var WIDTH=500;
var HEIGHT=50;
var rafID = null;

window.onload = function() {

    // grab our canvas
	canvasContext = document.getElementById( "meter" ).getContext("2d");
	
    // monkeypatch Web Audio
    window.AudioContext = window.AudioContext || window.webkitAudioContext;
	
    // grab an audio context
    audioContext = new AudioContext();

    // Attempt to get audio input
    try {
        // monkeypatch getUserMedia
        navigator.getUserMedia = 
        	navigator.getUserMedia ||
        	navigator.webkitGetUserMedia ||
        	navigator.mozGetUserMedia;

        // ask for an audio input
        navigator.getUserMedia(
        {
            "audio": {
                "mandatory": {
                    "googEchoCancellation": "false",
                    "googAutoGainControl": "false",
                    "googNoiseSuppression": "false",
                    "googHighpassFilter": "false"
                },
                "optional": []
            },
        }, onMicrophoneGranted, onMicrophoneDenied);
    } catch (e) {
        alert('getUserMedia threw exception :' + e);
    }

}

function onMicrophoneDenied() {
    alert('Stream generation failed.');
}

var mediaStreamSource = null;

function onMicrophoneGranted(stream) {
    // Create an AudioNode from the stream.
    mediaStreamSource = audioContext.createMediaStreamSource(stream);

    // Create a new volume meter and connect it.
    meter = createAudioMeter(audioContext);
    mediaStreamSource.connect(meter);

    // kick off the visual updating
    onLevelChange();
}

function onLevelChange( time ) {
    // clear the background
    canvasContext.clearRect(0,0,WIDTH,HEIGHT);

    // check if we're currently clipping
    if (meter.checkClipping())
        canvasContext.fillStyle = "red";
    else
        canvasContext.fillStyle = "green";

    console.log(meter.volume);

    // draw a bar based on the current volume
    canvasContext.fillRect(0, 0, meter.volume * WIDTH * 1.4, HEIGHT);

    // set up the next visual callback
    rafID = window.requestAnimationFrame( onLevelChange );
}
</script>

The previous code should display a simple canvas in bar-style that changes when you use the microphone:

Microphone Input Level

You can check the working script that shows a basic implementation of the Volume Meter in your browser here (and displays it on a canvas).

Standalone

In case you want to display a custom volume meter with your own style, then you would only need the values of the volume meter instead of displaying them on a canvas. To retrieve only the values, you can simply remove all the code that is related to the canvas element from the previous script.

The following snippet will show, once started, the volume value of the meter object (from 0.00 to 1.00) in the console (respectively with console.log if the level is "normal" or with console.warn if the volume is "clipped") with the onLevelChange function:

/**
 * Create global accessible variables that will be modified later
 */
var audioContext = null;
var meter = null;
var rafID = null;
var mediaStreamSource = null;

// Retrieve AudioContext with all the prefixes of the browsers
window.AudioContext = window.AudioContext || window.webkitAudioContext;

// Get an audio context
audioContext = new AudioContext();

/**
 * Callback triggered if the microphone permission is denied
 */
function onMicrophoneDenied() {
    alert('Stream generation failed.');
}

/**
 * Callback triggered if the access to the microphone is granted
 */
function onMicrophoneGranted(stream) {
    // Create an AudioNode from the stream.
    mediaStreamSource = audioContext.createMediaStreamSource(stream);
    // Create a new volume meter and connect it.
    meter = createAudioMeter(audioContext);
    mediaStreamSource.connect(meter);

    // Trigger callback that shows the level of the "Volume Meter"
    onLevelChange();
}

/**
 * This function is executed repeatedly
 */
function onLevelChange(time) {
    // check if we're currently clipping

    if (meter.checkClipping()) {
        console.warn(meter.volume);
    } else {
        console.log(meter.volume);
    }

    // set up the next callback
    rafID = window.requestAnimationFrame(onLevelChange);
}


// Try to get access to the microphone
try {

    // Retrieve getUserMedia API with all the prefixes of the browsers
    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

    // Ask for an audio input
    navigator.getUserMedia(
        {
            "audio": {
                "mandatory": {
                    "googEchoCancellation": "false",
                    "googAutoGainControl": "false",
                    "googNoiseSuppression": "false",
                    "googHighpassFilter": "false"
                },
                "optional": []
            },
        },
        onMicrophoneGranted,
        onMicrophoneDenied
    );
} catch (e) {
    alert('getUserMedia threw exception :' + e);
}

The onLevelChange is the function that you need to pay attention to.

Create your custom wrapper

As the implementation of the code will be a little bit messy, you can create a custom script to make the usage pretty easy by just executing a single function. Check out the following example, it will create the microphoneLevel function in the window and it expects 2 callbacks (onResult and onError):

(function(){
    function wrapperAudioMeter(OPTIONS){
        var audioContext = null;
        var meter = null;
        var rafID = null;
        var mediaStreamSource = null;

        // Retrieve AudioContext with all the prefixes of the browsers
        window.AudioContext = window.AudioContext || window.webkitAudioContext;

        // Get an audio context
        audioContext = new AudioContext();

        function onMicrophoneDenied() {
            if(typeof(OPTIONS["onError"]) == "function"){
                OPTIONS.onError('Stream generation failed.');
            }
        }

        function onMicrophoneGranted(stream) {
            // Create an AudioNode from the stream.
            mediaStreamSource = audioContext.createMediaStreamSource(stream);
            // Create a new volume meter and connect it.
            meter = createAudioMeter(audioContext);
            mediaStreamSource.connect(meter);

            // Trigger callback that 
            onLevelChange();
        }

        function onLevelChange(time) {
            if(typeof(OPTIONS["onResult"]) == "function"){
                OPTIONS.onResult(meter, time);
            }

            // set up the next callback
            rafID = window.requestAnimationFrame(onLevelChange);
        }

        // Try to get access to the microphone
        try {

            // Retrieve getUserMedia API with all the prefixes of the browsers
            navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

            // Ask for an audio input
            navigator.getUserMedia(
                {
                    "audio": {
                        "mandatory": {
                            "googEchoCancellation": "false",
                            "googAutoGainControl": "false",
                            "googNoiseSuppression": "false",
                            "googHighpassFilter": "false"
                        },
                        "optional": []
                    },
                },
                onMicrophoneGranted,
                onMicrophoneDenied
            );
        } catch (e) {
            if(typeof(OPTIONS["onError"]) == "function"){
                OPTIONS.onError(e);
            }
        }
    }

    window["microphoneLevel"] = wrapperAudioMeter;
})();

This script can be used of the following way:

/**
 * Execute the script once the window loads 
 **/
window.onload = function(){

    // Request microphone to display level
    window.microphoneLevel({
        onResult: function(meter , time){
            if (meter.checkClipping()) {
                console.warn(meter.volume);
            } else {
                console.log(meter.volume);
            }
        },
        onError: function(err){
            console.error(err);
        }
    });
};

The onResult callback is triggered repeteadly an receives the meter object with the information and methods of the AudioContext. It's easy to read and to work with, isn't ?

Happy coding !

Become a more social person