rowid,title,contents,year,author,author_slug,published,url,topic
209,Feeding the Audio Graph,"In 2004, I was given an iPod.
I count this as one of the most intuitive pieces of technology I’ve ever owned. It wasn’t because of the the snazzy (colour!) menus or circular touchpad. I loved how smoothly it fitted into my life. I could plug in my headphones and listen to music while I was walking around town. Then when I got home, I could plug it into an amplifier and carry on listening there.
There was no faff. It didn’t matter if I could find my favourite mix tape, or if my WiFi was flakey - it was all just there.
Nowadays, when I’m trying to pair my phone with some Bluetooth speakers, or can’t find my USB-to-headphone jack, or even access any music because I don’t have cellular reception; I really miss this simplicity.
The Web Audio API
I think the Web Audio API feels kind of like my iPod did.
It’s different from most browser APIs - rather than throwing around data, or updating DOM elements - you plug together a graph of audio nodes, which the browser uses to generate, process, and play sounds.
The thing I like about it is that you can totally plug it into whatever you want, and it’ll mostly just work.
So, let’s get started. First of all we want an audio source.
(Song - Night Owl by Broke For Free)
This totally works. However, it’s not using the Web Audio API, so we can’t access or modify the sound it makes.
To hook this up to our audio graph, we can use an AudioSourceNode. This captures the sound from the element, and lets us connect to other nodes in a graph.
const audioCtx = new AudioContext()
const audio = document.querySelector('audio')
const input = audioCtx.createAudioSourceNode(audio)
input.connect(audioCtx.destination)
Great. We’ve made something that looks and sounds exactly the same as it did before. Go us.
Gain
Let’s plug in a GainNode - this allows you to alter the amplitude (volume) of an an audio stream.
We can hook this node up to an element by setting the gain property of the node. (The syntax for this is kind of weird because it’s an AudioParam which has options to set values at precise intervals).
const node = audioCtx.createGain()
const input = document.querySelector('input')
input.oninput = () => node.gain.value = parseFloat(input.value)
input.connect(node)
node.connect(audioCtx.destination)
You can now see a range input, which can be dragged to update the state of our graph. This input could be any kind of element, so now you’ll be free to build the volume control of your dreams.
There’s a number of nodes that let you modify/filter an audio stream in more interesting ways. Head over to the MDN Web Audio page for a list of them.
Analysers
Something else we can add to our graph is an AnalyserNode. This doesn’t modify the audio at all, but allows us to inspect the sounds that are flowing through it. We can put this into our graph between our AudioSourceNode and the GainNode.
const analyser = audioCtx.createAnalyser()
input.connect(analyser)
analyser.connect(gain)
gain.connect(audioCtx.destination)
And now we have an analyser. We can access it from elsewhere to drive any kind of visuals. For instance, if we wanted to draw lines on a canvas we could totally do that:
const waveform = new Uint8Array(analyser.fftSize)
const frequencies = new Uint8Array(analyser.frequencyBinCount)
const ctx = canvas.getContext('2d')
const loop = () => {
requestAnimationFrame(loop)
analyser.getByteTimeDomainData(waveform)
analyser.getByteFrequencyData(frequencies)
ctx.beginPath()
waveform.forEach((f, i) => ctx.lineTo(i, f))
ctx.lineTo(0,255)
frequencies.forEach((f, i) => ctx.lineTo(i, 255-f))
ctx.stroke()
}
loop()
You can see that we have two arrays of data available (I added colours for clarity):
The waveform - the raw samples of the audio being played.
The frequencies - a fourier transform of the audio passing through the node.
What’s cool about this is that you’re not tied to any specific functionality of the Web Audio API. If it’s possible for you to update something with an array of numbers, then you can just apply it to the output of the analyser node.
For instance, if we wanted to, we could definitely animate a list of emoji in time with our music.
spans.forEach(
(s, i) => s.style.transform = `scale(${1 + (frequencies[i]/100)})`
)
🔈🎤🎤🎤🎺🎷📯🎶🔊🎸🎺🎤🎸🎼🎷🎺🎻🎸🎻🎺🎸🎶🥁🎶🎵🎵🎷📯🎸🎹🎤🎷🎻🎷🔈🔊📯🎼🎤🎵🎼📯🥁🎻🎻🎤🔉🎵🎹🎸🎷🔉🔈🔉🎷🎶🔈🎸🎸🎻🎤🥁🎼📯🎸🎸🎼🎸🥁🎼🎶🎶🥁🎤🔊🎷🔊🔈🎺🔊🎻🎵🎻🎸🎵🎺🎤🎷🎸🎶🎼📯🔈🎺🎤🎵🎸🎸🔊🎶🎤🥁🎵🎹🎸🔈🎻🔉🥁🔉🎺🔊🎹🥁🎷📯🎷🎷🎤🎸🔉🎹🎷🎸🎺🎼🎤🎼🎶🎷🎤🎷📯📯🎻🎤🎷📯🎹🔈🎵🎹🎼🔊🔉🔉🔈🎶🎸🥁🎺🔈🎷🎵🔉🥁🎷🎹🎷🔊🎤🎤🔊🎤🎤🎹🎸🎹🔉🎷
Generating Audio
So far, we’ve been using the