My first soundwave
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:
- Request permission to use the microphone
- Setup the sound libraries
- 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);
}