How to display sound

Since I’m having so much fun messing around with JavaScript, I thought let’s try something with sound. How about creating a spectrum analyzer with JavaScript? here’s a tutorial how to use the Web Audio API to draw the waveforms of the current audio being picked up by the microphone.

Steps

What’s going on in this example can be boiled down to three parts:

  1. Request permission to use the microphone
  2. Setup the sound libraries
  3. Draw the soundwave onto a canvas (continously).

Permission to use the microphone

Asking permission to use the microphone works like this (be aware of the object prepareAudio that we pass to .then())

navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: false} })
    .then(prepareAudio)
    .catch((err) => {
        console.log("some errrorer");
    })

Wiring the audio libraries

If we get permission for the microphone, we will get a stream object. This is an asynchronous operation. It might work, it might cause an error or the user just doesn’t react to the prompt and we never receive anything.

So, when permission is granted, our function perpareAudio (actually a stupid name) get’s called, with a stream object as parameter. Based on that, we create the necesseray objects from the web audio API.

It starts with the the AudioContext. It’s important to create it after permission to use the microphone is granted, i.e. the call to getUserMedia was successful and returned a stream:

function prepareAudio(stream) {

    // IMPORTANT: The audio context needs to be created AFTER the getUserMedia call was successful!!1
    // Otherwise there will be an error in the log saying "the AudioContext was not allowed to start."
    const AudioContext = window.AudioContext || window.webkitAudioContext;
    const audioCtx = new AudioContext(); 
    
    // Create the analyser
    analyser = audioCtx.createAnalyser();
    analyser.fftSize = FFT_SIZE;

    // we need to create a source using the stream
    const source = audioCtx.createMediaStreamSource(stream);
    
    // ... and plug the analyser to the source
    source.connect(analyser);

    draw();
}

We use the context to create an analyser, which will give us the necessary data to make some fancy displays. To wire up the analyzier to microphone, we first need to create a source object using the AudioContext. We then plug the analyzer to the source using source.connect(analyser). Now we’re ready to grab some data!

Drawing nice pictures

The last statement in the fragment above is the call to the draw() function. This is the function where the magic happens. It uses the analyser we plugged into the microphone’s audio to draw us some nice waveforms:

function draw() {

    var drawVisual = requestAnimationFrame(draw);
    analyser.getByteTimeDomainData(dataArray);

    clearCanvas(ctx);

    ctx.lineWidth = 2;
    ctx.strokeStyle = 'rgb(255, 0, 255)';
    ctx.beginPath();

    var sliceWidth = WIDTH * 1.0 / BUFFER_LENGTH;
    x = 0;

    for (var i = 0; i < BUFFER_LENGTH; i++) {

        var v = dataArray[i] / 128.0;
        var y = v * HEIGHT / 2;

        if (i === 0) {
            ctx.moveTo(x, y);
        } else {
            ctx.lineTo(x, y);
        }

        x += sliceWidth;
    }

    ctx.lineTo(WIDTH, HEIGHT / 2);
    ctx.closePath();
    ctx.stroke();
}

Full example

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext("2d");
const WIDTH = canvas.width;
const HEIGHT = canvas.height;
const FFT_SIZE = 2048;

const BUFFER_LENGTH = 1024;
let dataArray = new Uint8Array(BUFFER_LENGTH);
let analyser;

clearCanvas(ctx);

navigator.mediaDevices.getUserMedia({ audio: true })
    .then(prepareAudio)
    .catch((err) => {
        console.log("some errrorer");
    })

function prepareAudio(stream) {

    // IMPORTANT: The audio context needs to be created AFTER the getUserMedia call was successful!!1
    // Otherwise there will be an error in the log saying "the AudioContext was not allowed to start."
    const AudioContext = window.AudioContext || window.webkitAudioContext;
    const audioCtx = new AudioContext(); 
    

    // from the docs:
    // A MediaStreamAudioSourceNode has no inputs and exactly one output,
    // and is created using the AudioContext.createMediaStreamSource() method.

    // Create the analyser
    analyser = audioCtx.createAnalyser();
    analyser.fftSize = FFT_SIZE;

    // we need to create a source using the stream
    const source = audioCtx.createMediaStreamSource(stream);
    
    // ... and plug the analyser to the source
    source.connect(analyser);

    draw();
}

function draw() {

    var drawVisual = requestAnimationFrame(draw);
    analyser.getByteTimeDomainData(dataArray);

    clearCanvas(ctx);

    ctx.lineWidth = 2;
    ctx.strokeStyle = 'rgb(255, 0, 255)';
    ctx.beginPath();

    var sliceWidth = WIDTH * 1.0 / BUFFER_LENGTH;
    x = 0;

    for (var i = 0; i < BUFFER_LENGTH; i++) {

        var v = dataArray[i] / 128.0;
        var y = v * HEIGHT / 2;

        if (i === 0) {
            ctx.moveTo(x, y);
        } else {
            ctx.lineTo(x, y);
        }

        x += sliceWidth;
    }

    ctx.lineTo(WIDTH, HEIGHT / 2);
    ctx.closePath();
    ctx.stroke();
}

function clearCanvas(canvasContext) {

    canvasContext.fillStyle = 'rgb(0, 0, 0)';
    canvasContext.fillRect(0, 0, WIDTH, HEIGHT);
}