Before anything will work through Web Audio, a sound has to be triggered inside of an event handler triggered by a user, meaning you absolutely have to make a button.
Smus) which we’ll use to load sound files and in the assets/images folder is the image of our drums.
2.Markup for our drums
In our ‘public’ folder is a bare-bones HTML file. We’re going to create some divs to bind some event listeners to so that when we hit them, sounds play. Open it in your favourite editor and add the following between the <body></body> tags:
<div id="drumHolder">
<div class="drum" data-sound="china"></div> <div class="drum" data-sound="crash"></div> <div class="drum" data-sound="floor tom"></ div>
<div class="drum" data-sound="super kick"></ div>
<div class="drum" data-sound="super snare"></div>
</div>
<script src="scripts/libraries/bl.js"></ script>
<script src="scripts/core.js"></script>
3.Style the <div>
Run the Node app included in the project resources. If you head to http://localhost:8118/ you’ll see our HTML page. It’s nothing special at the moment, just a background image of our drums and a bunch of invisible <div> tags. We want to position those divs over each of our drums so that when we hit each drum, they each make a diff erent sound.
4.Move the parent element
We have a div #drumHolder. We’re going to position this over the middle of our page to position all of the divs
inside it relative to their parent (#drumHolder). This makes our life a little easier when it comes to doing things responsively. Just open up styles.css now and add the following: #drumHolder{ width: 700px; height: 290px; position: fixed; left: 50%; top: 50%; margin-left: -350px; margin-top: -145px; background-color: rgba(255,0,0,.2); }
5.Position each drum element
Now that we have our #drumHolder in the right place, we can now do the custom styling for each individual drum. Staying in styles.css now as well, add the following rules to your file:
#drumHolder .drum{ background-color: rgba(0,0,0,.5); position: absolute; } #drumHolder .drum:nth-child(1){ top: 14px; left: 235px; width: 87px; height: 69px; } #drumHolder .drum:nth-child(2){ left: 350px; top: 7px; width: 108px; height: 86px; } #drumHolder .drum:nth-child(3){ left: 274px;
top: 145px; width: 158px; height: 114px; } #drumHolder .drum:nth-child(4){ right: 62px; bottom: 84px; width: 144px; height: 104px; }
#drumHolder .drum:nth-child(5){ top: 103px; left: 74px; width: 144px; height: 104px; }
6.Core.js
Now that we have our elements in the right place, we can start writing some code to control them. When we added in our <div> we also added two <script> elements, one for our code, the other being our Buff erLoader method. Create a file called ‘core.js’ in our scripts folder and add the following to it:
var __web_audio_drums = (function(){ 'use strict';
var context = undefined, bufferLoader = undefined; function init(){ console.log("Initialised"); } return { init : init }; })(); (function(){ __web_audio_drums.init(); })();
7.The Web Audio context
Before we start doing anything with Web Audio, we need a context we can interact with. Think of it as an audio analogue to a <canvas> context, it’s simply an interface with which we deal in order to make our code do interesting things. Just after ‘use strict’; add:
Developer tutorials
Make drums with the Web Audio API
// Web Audio Polyfill
window.audioContext = (window.AudioContext || window.webkitAudioContext || window. mozAudioContext || window.oAudioContext);
…and inside our init function add:
context = new window.audioContext();
…Now we will also have an audio context that will work across all browsers.
8.Set our samples
In the /sounds folder of our project, there are a bunch of MP3 files. We’re going to add them to our code so we can load them in just a little bit. Add the following just after our Web Audio polyfill.
var samples = [{ name : "china", path : "/sounds/China.mp3" }, { name : "crash", path : "/sounds/Crash.mp3" }, {
name : "floor tom",
path : "/sounds/Floor_Tom.mp3" },
{
name : "super kick",
path : "/sounds/Super_Kick.mp3" },
{
name : "super snare",
path : "/sounds/Super_Snare.mp3" },
],
sounds = [];
…samples contain references that we’ll use to load our files and assign them to the right elements, sounds will contain the audio buff ers that will actually have the sound data in them, but we’ll get to that later.
9.Load our drum samples
Now that we know where we can find our drum samples it’s time to load them. Create a new function called ‘loadSamples()’ and add the following code to it:
function loadSamples(passedSamples){ var s = []; for(var x = 0; x < passedSamples.length; x += 1){ s.push(passedSamples[x].path); } }
This will simply create an array of file paths that we can work through to load our samples into our project
10.The BufferLoader
In order to load our samples into our project and make them useful, we need to create audio buff ers to store the sound data from our MP3 files. Think of an audio buff er as a bucket that you can put something into, but once you take it out again, you can’t get it back in. Normally, we would have to create all of the XHR and audio buff er code ourselves, but the Buff erLoader method takes care of that for us.
11.Load our samples
In loadSamples, add the following:
bufferLoader = new BufferLoader(context, s, function(list){
sounds = list; console.log(sounds); });
Top left
By preventing events, we can make sure that nothing untoward occurs during that crucial, epic drum solo
Top right
The properties of the returned AudioBuffer of our media files aren’t all that interesting, it’s the nifty methods that come with it that’s really interesting
Right
There’s no visual feedback in our little app. But, if you wanted to make it so our app responded visually to a tap as well as audibly, you can always use border-radius
getAttribute('data-sound')){ playSound(a); } } }, false); } console.log(sounds); ...
13.Detect events
On a lot of mobile devices (like iOS for example) ‘click’ events have a 300ms delay before they are dispatched to our event listeners. For sounds and media this is a disaster – timing is key! Fortunately, ‘touch’ events don’t have the same delay, so we’ll write a function that decides whether or not our device should use touch or click events; function touchOrClickEvent(){ if('ontouchend' in document){ return "touchend"; } else { return "click"; } }
14.Play our sounds
Now that we’ve got the appropriate listeners, we need a way to play our sound. Web Audio is not like <audio>, we can’t just call .play() on an audioBuffer to make it do its thing, we need to connect our audio buffer to our audio context in order to play it out through our speakers.
bufferLoader.load();
... and add the following to init(); loadSamples(samples);
BufferLoader takes an audio context, an array of paths to audio files and a callback as arguments. When we call bufferLoader.load(); our BufferLoader instance will work through the array we passed to it and load all of the sound files it can find. When all of the audio data has been loaded, the audio buffers that have been created are passed into the callback that we passed. Here we set the array of audio buffers as a global object, so we can access them elsewhere in the project.
12.Event listeners
Now that we have a bunch of audio buffers that contain our sound data, we’re going to need a way to trigger them. Inside of our bufferLoader callback, we’re going to create a for loop that gets all of the drum elements on our page, takes the data-sound attribute and looks for a sound with the same name. When our div is clicked or tapped, it will play that sound!
...
sounds = list;
var theDrums = document. getElementsByClassName('drum'); for(var y = 0; y < theDrums.length; y += 1) { theDrums[y].addEventListener(touchOrClickEve nt(), function(){ for(var a = 0; a < samples.length; a += 1){ if(samples[a].name === this.
Developer tutorials
Make drums with the Web Audio API
Think of it as a digital version of plugging your headphones into a computer, we need to connect to hear. Create a playSound() function and you then add the following…
function playSound(idx){
var src = context.createBufferSource(), newBuffer = cloneAudioBuffer(sounds[idx]); src.buffer = newBuffer;
var gainNode = context.createGain(); gainNode.gain.value = 1;
src.connect(gainNode);
gainNode.connect(context.destination); src.start(0);
}
15.Clone our buffers
Earlier, we noted that audio buffers can only be played once, which is kind of useless unless we want to be able to only hit each drum once. In order to play our drum sounds multiple times, we’re going to create copies of our audio buffers that we play and then forget about. So let’s create a new function with cloneAudioBuffer();.
function cloneAudioBuffer(audioBuffer){ var channels = [],
numChannels = audioBuffer.numberOfChannels; for (var i = 0; i < numChannels; i++){ channels[i] = new Float32Array(audioBuffer. getChannelData(i));
}
var newBuffer = context.createBuffer( audioBuffer.numberOfChannels,
87
<audio> vs. audio
So why do we need Web Audio if <audio> exists? Well, <audio> is great at playing sound… in a limited context, it’s not designed for audio manipulation – not in a low-level kind of way. The Web Audio API gives us direct access to the data of the audio content, and with this we can run a ton of complex operations to evaluate and manipulate that data in next to no time at all. Compared to a fast fourier transform, or applying a band filter, playing audio content through Web Audio is a comparative doddle – that’s why it can respond on a microsecond time scale whereas <audio> can take seconds to begin playback, depending on the availability of the content. Deciding when it is appropriate to use either interface is completely down to you!
88
Developer tutorials
Make drums with the Web Audio API
audioBuffer.length, audioBuffer.sampleRate );
for (var i = 0; i < numChannels; i++){ newBuffer.getChannelData(i). set(channels[i]); } return newBuffer; }
16.Clone walkthrough
Every sound file has a number of channels: if the sound is mono then there is one channel, and if the sound is stereo then there are two and so on. We want to clone all of the audio data from our samples, so we need to create a buffer for each channel and clone that data.
var channels = [],
numChannels = audioBuffer.numberOfChannels; for (var i = 0; i < numChannels; i++){ channels[i] = new Float32Array(audioBuffer. getChannelData(i));
}
Now that we have arrays with our data in it (a Float32Array is not like arrays as we usually know them, it’s more like a space in the computer’s memory) we can create a brand-new buffer with all of the properties we need to describe our sounds, which we do with:
var newBuffer = context.createBuffer( audioBuffer.numberOfChannels, audioBuffer.length,
audioBuffer.sampleRate );
Finally, we have a brand-new buffer, but our buffer is empty so we fix that by working through our newly created buffer’s channels and setting them with the channels we created just a moment ago. Then we return our cloned audio buffer:
for (var i = 0; i < numChannels; i++){ newBuffer.getChannelData(i).
set(channels[i]); }
return newBuffer;
17.Play the sound
Now that we’ve got our brand-new cloned audio context, it’s time to put it to work! In playSound, just before we cloned our audio buffer we created a buffer source in our context with context.createBufferSource(); This is our entry point to playing sounds through our speakers. To use our cloned audio buffer we set it as the source for our buffer source.
src.buffer = newBuffer;
18.Web Audio nodes
In Web Audio, nodes are like modules that you can use to change how a sound is played back. You can adjust the pitch, filter frequencies and much more. We’re going to use one of the simplest nodes to give our sound volume, a gain node. We create a gain node like so:
var gainNode = context.createGain(); gainNode.gain.value = 1;
src.connect(gainNode);
Then we connect our src (our buffer source) to that gain node. Now, any data that is in our buffer source will be
passed through and these will be affected by our gain node. If we wanted to double the volume of our sample, we could set gainNode.gain.value to 2, if we wanted to half it we could set the value to 0.5.
19.Test the sound
To play our sound, all that’s left to do is connect our gain node to our context’s destination (which could feasibly be anything, but in this case it’s our speakers) and call src.start(0). This will start our playback at 0 and the output will go through our gain node, to our context destination. If we can tell our buffer source at what point we want to play from, then can’t we just reset it to 0 and play again rather than all of this cloning business? Well, nope. An audio buffer becomes mostly useless after it’s been played, and besides even if we could do that, the effect would be undesirable – by resetting the sound mid-play, we’d get a horribly jarring stutter effect. It’s much better to let the sound continue on its own and create a new instance of it.
20.Use on iOS
If you want to use you iPad or iPhone to play sounds, you can, but if you get a little carried away and flick the screen instead of tapping it, it might start scrolling and refuse to do anything until it stops. In your init() function add the following code to stop this:
// Stop iOS bouncing scroll
window.addEventListener('touchstart', function(e){ e.preventDefault(); }, true);