I've been having a ton of fun creating interactive musical tools and references over at muted.io. Things like an interactive circle of 5ths, a reference to all major and minor scales and a tool to play chords in keys.
Under the hood, these tools are powered by the Tone.js library, which is a set of utilities build on top of the Web Audio API, which makes it easier to deal with audio in the browser from a musician's perspective. For the aformentioned tools, the user interactions are handled using Alpine.js. I've found that the combination of Tone.js + Alpine.js really works like a charm.
This short post gives you a little primer on how you'd go about setting things up to play audio files in the browser in such a fashion.
First things first, you'll want to have both Tone.js and Alpine.js loaded onto your page. If you have a look at the Tone.js documentation it'll tell you installation instruction via npm
, but personally I've been enjoying working with just a call to the minified script file itself. To do that via a CDN, you can add this in your page's head
section:
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.32/Tone.min.js"></script>
And then similarly for installing Alpine.js:
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
Note that on a site like muted.io I've decided to load Tone.js only when the user has scrolled passed the relevant portion of the page. I'm using Alpine's Intersect plugin to accomplish that. This is of course optional and I may talk about the details of that in a future post.
With the setup out of the way, you should now see a message in your browser console that says something like _ Tone.js v14.8.32 _ , meaning that Tone.js has been properly loaded and is ready to go.
Tone.js Sampler
A sampler is an instrument that makes it easy to playback different audio files. Tone.js offers its own sampler instrument:
const sampler = new Tone.Sampler({
urls: {
C3: 'C3.mp3',
'D#3': 'Ds3.mp3',
'F#3': 'Fs3.mp3',
A3: 'A3.mp3',
C4: 'C4.mp3',
'D#4': 'Ds4.mp3',
'F#4': 'Fs4.mp3',
A4: 'A4.mp3',
},
release: 0.5,
baseUrl: '/sounds/piano/',
}).toDestination();
In the above code block I'm instantiating a sampler and passing in a path to audio files for different musical notes on the piano. In this case I'm using piano samples from the Salamander Grand Piano V3 project, but you could use any of your own samples. In this case, the sounds are in my project's directory under /sounds/piano/
. You'll notice also that not all notes are included, that's because Tone.js is smart enough to repitch the samples and make up for any missing pitches in that way. This is really useful in saving on loading time for samples.
This setup works great in a musical contact for playing sounds that actually correspond to musical pitches, but you could of course use a sampler to trigger totally unrelated sounds. You could for example decide that C4
triggers the sound of a toucan while A4
is for an abrasive dog bark. π
Playing the Sounds
Now that we have our sampler instrument setup, we're ready to start listening to user interactions and trigger the sounds. Let's first define a simple function that triggers the passed-in note:
function play(note = "C4") {
sampler.triggerAttackRelease(note, "8n");
}
With this, calling play()
will trigger the audio file associated with the note provided (or default to C4
) in your sampler for a duration of an 8th note. The default BPM value in Tone.js is 120
, which will be what controls how long a 8th
note is. You can tweak the BPM value like this:
Tone.Transport.bpm.value = 96; // 96 BPM instead of 120
Now that we have our play
function in place, we can use Alpine to setup a listnener on something like a button:
<button @click="play('A3')">Play A3</button>
And done! You should now hear the sample that your sampler has for A3
. Note here that the button click is important because modern browsers require a user interaction like a button click to start playing sounds on a page.
Separating the attack from the release
Earlier we made use of the triggerAttackRelease
on our sampler, which takes care of triggering the sample and also of releasing that trigger after the duration provided (a 8th
note in our example). What if instead we wanted to play a sound for as long as the user is currently pushing a button? This is often useful for long samples that are to be played only while a note is activated (e.g.: a button is pressed). We can easily decouple the operation by using the triggerAttack
and triggerRelease
methods instead:
function startPlay(note) {
sampler.triggerAttack(note);
}
function stopPlay(note) {
sampler.triggerRelease(note);
}
Note that you could also pass in an array with multiple notes at once to any of those methods (triggerAttackRelease
, triggerAttack
, triggerRelease
), allowing you to trigger things like chords, if you're triggering sounds in a musical context.
And now, we can once again make use of Alpine's event handling capabilities to :
<button
@mousedown.stop="startPlay('A4');"
@mouseup.stop="stopPlay('A4');"
@touchstart.stop.prevent="startPlay('A4');"
@touchend.stop.prevent="stopPlay('A4');"
>
Play long sample
</button>
Here I'm using the mousedown
and mouseup
events to decouple the button press and button unpress. You'll also notice that I'm using touchstart
and touchend
, which fixes the issue that touch screen devices don't have a mousedown or mouseup event. To stop the event's propagation, I'm using the stop
modifier on all events, and to prevent the default behavior I'm also using the prevent
modifier on the touch events. This fixes an issue where the event would otherwise be triggered twice on devices with a mouse.
That's it! Hopefully this short introduction was enough to show you how easy it can be to trigger sounds in the browser and start having fun with that in your own projects! β¨ π
For the sake of brevity, I kept the part involving Alpine.js very short and sweet in this post. In a real-world scenario, you'll likely want to make use of x-data
to do things like keep track of the notes/sounds being played:
<div x-data="{ currentNote: 'A4' }">
<button @click="play(currentNote);">Play note</button>
...
</div>
Top comments (0)