Playing Sound on the Web Using Tone.js and Alpine.js
Tags: javascript, webdev, music
Published: 2021-11-02 00:12:29

I've been having a ton of fun creating interactive musical tools and references over at [muted.io](https://muted.io/). Things like an [interactive circle of 5ths](https://muted.io/circle-of-fifths/), a [reference to all major and minor scales](https://muted.io/major-minor-scales/) and a tool to [play chords in keys](https://muted.io/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](https://developer.mozilla.org/en-US/docs/Web/API/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](https://alpinejs.dev/). 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](https://tonejs.github.io/) 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:

```html
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.32/Tone.min.js"></script>
```

And then similarly for [installing Alpine.js](https://alpinejs.dev/essentials/installation):

```html
<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](https://alpinejs.dev/plugins/intersect) 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 <strong>_ Tone.js v14.8.32 _ </strong>, 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](https://tonejs.github.io/docs/14.7.77/Sampler):

```js
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](https://archive.org/details/SalamanderGrandPianoV3) 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:

```js
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:

```js
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:

```js
<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:

```js
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 :

```html
<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](https://alpinejs.dev/directives/on#stop) on all events, and to prevent the default behavior I'm also using the [`prevent` modifier](https://alpinejs.dev/directives/on#prevent) 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:

```html
<div x-data="{ currentNote: 'A4' }">
  <button @click="play(currentNote);">Play note</button>
  ...
</div>
```