Building a JavaScript Yak Bak clone with Tone.js — Part 1
Tags: webdev, tonejs, webaudioapi, tutorial
Published: 2023-02-07 16:55:20

---
title: Building a JavaScript Yak Bak clone with Tone.js — Part 1
published: true
date: 2023-02-07 15:16:29 UTC
tags: webdevelopment,tonejs,webaudioapi,tutorial
canonical_url: https://medium.com/@cgustin/building-a-javascript-yak-bak-clone-with-tone-js-part-1-cb01b3b10529
---

### Building a JavaScript Yak Bak clone with Tone.js — Part 1

{% youtube PvP5gR5_Il4 %}

If you don’t remember Yak Bak’s, or you’re not familiar with them, they were these little handheld recording toys that came out in the mid-90’s. The original version was pretty simple: you hold down a button to record a sound, then push another button to play it back. Later versions included sound effects, and the Yak Bakward had a button that would play your sound back in reverse. As a kid who was obsessed with spy gadgets and James Bond movies, I loved them. And as a web dev obsessed adult, I thought a Yak Bak clone would be a fun weekend project, while getting to take a trip down memory lane.

Here’s the finished project:

<iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fcodepen.io%2Fcmgustin%2Fembed%2Fpreview%2FExpRLJj%3Fdefault-tabs%3Dcss%252Cresult%26height%3D600%26host%3Dhttps%253A%252F%252Fcodepen.io%26slug-hash%3DExpRLJj&amp;display_name=CodePen&amp;url=https%3A%2F%2Fcodepen.io%2Fcmgustin%2Fpen%2FExpRLJj&amp;image=https%3A%2F%2Fshots.codepen.io%2Fcmgustin%2Fpen%2FExpRLJj-512.jpg%3Fversion%3D1675206346&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=codepen" width="800" height="600" frameborder="0" scrolling="no"><a href="https://medium.com/media/76a7a9f72a864dda8a9e110b3c584daa/href">https://medium.com/media/76a7a9f72a864dda8a9e110b3c584daa/href</a></iframe>
### Step 1: Scope

Before you start writing any code, or even open your IDE, it’s important to understand exactly what you’re trying to build. This doesn’t have to take long, and it doesn’t have to be a super official document, we just need to take a few minutes to outline the main features, and maybe brainstorm a couple nice-to-haves. I’m partial to the [user story format](https://www.atlassian.com/agile/project-management/user-stories), so using a Yak Bak as an example, here’s what our project scope might look like:

- The user should be able to press a button to begin recording audio, and release the same button to stop recording audio
- The user should be able to press a button to play back the audio they recorded
- [Nice-to-have] The user should be able to press a button to play back their audio in reverse
- [Nice-to-have] The user should be able to change the color scheme of their Yak Bak

Pretty straightforward. We have two key features, and two features that aren’t necessary, but will add to the users overall experience. When writing a user story, it’s important to keep it simple and avoid making technical judgements or recommendations. We’re simply describing how the feature should work from the users point of view, and leaving the implementation details for a later phase. Doing this allows the scoping phase to move quickly, while avoiding getting bogged down with technical details.

Now that we’ve outlined our scope, it’s time to start writing some code. For this tutorial, I recommend following along in [CodePen](https://codepen.io/).

### Step 2: HTML

In most of my web dev projects that include HTML, CSS and JavaScript, I usually like to start with the HTML, then tackle the JavaScript, and finally the CSS. Typically, the HTML is the least technically intensive piece, which is a great way to build momentum. It also creates the interface elements which your JavaScript will hook into, and it can give you an idea of how much additional CSS will be needed to get the styles you’re after.

For our Yak Bak, the first thing we’ll need is a container to put our interface elements in. A `div` is perfect for wrapping or containing other elements, so let’s add an empty `div` with an id of `yakbak` to our HTML:

```html
<div id="yakbak"></div>
```

[**Note** : in a larger project I would avoid using ID’s to tag elements, however in a small project like this where I’m the only developer, there are only a few elements, and I have a lot of control over the id names, I find that it helps keep things simple.]

Inside the container, our interface will have three buttons: one to record (and stop recording when released), one to playback the recording, and one to playback the recording in reverse. On the Yak Bak, these buttons are labeled “Say,” “Play” and “Yalp.” Let’s add those to our HTML using `button` elements:

```html
<div id="yakbak">
  <button type="button">Say</button>
  <button type="button">Play</button>
  <button type="button">Yalp</button>
</div>
```

[**Note:** `type="button"` is the default type for button elements, so technically we could leave it off. However, explicitly declaring it ensures that the buttons behave exactly how we expect in all browsers.]

We also need a way to target these buttons with our JavaScript. There are several ways to do it, but `document.getElementById()` is easy and performant, so it’s perfect for a small project like this. Let’s add an id to each button:

```html
<div id="yakbak">
  <button type="button" id="sayButton">Say</button>
  <button type="button" id="playButton">Play</button>
  <button type="button" id="yalpButton">Yalp</button>
</div>
```

Including “button” in the ID name will make it clear what element is being targeted when writing the JavaScript and help keep our code readable.

Our interface still needs some decorative elements, but in terms of core functionality, we’re good to go. Let’s move on to the JavaScript!

### Step 3: JavaScript

We’ve added the buttons to our HTML, now we want something to happen when a user presses those buttons. The first thing we need to do is target our button elements in our JavaScript file. We can use `document.getElementById("some-element-id")` to target an element by its ID, and we can assign the result to a variable so we have a useful reference to that element that we can use throughout our code. Let’s target each button and save it to a variable:

```javascript
const sayButton = document.getElementById("sayButton");
const playButton = document.getElementById("playButton");
const yalpButton = document.getElementById("yalpButton");
```

**Note:** In JavaScript, variables can be declared with `var`, `let` or `const`. If you’re unfamiliar with the difference, [FreeCodeCamp has a great rundown](https://www.freecodecamp.org/news/var-let-and-const-whats-the-difference/). For our purposes, it’s important to know that `const`, short for constant, is used to define a variable that will not be reassigned. In this case, it’s perfect for defining these interface elements since they will not be reassigned in our code (and we will get an error if we try to reassign them).

We’ve identified each of the buttons and saved it to a variable, but they still don’t do anything when clicked. To change this, we can add an event listener to each element that will wait for a specific user event, then fire off a function when that event happens. We want the “Say” button to start recording when the user presses down, so we’ll need to target the `mousedown` event. We can do that like this:

```javascript
const sayButton = document.getElementById("sayButton");
const playButton = document.getElementById("playButton");
const yalpButton = document.getElementById("yalpButton");

sayButton.addEventListener("mousedown", function() {
  // Do stuff while pushed down
  console.log("Say button pushed down");
});
```

We’ve added a `console.log` inside the event listener so we can test our button and make sure our code is working as expected so far. When you click the button, you should see the text in the console (on CodePen, click the “Console” button in the bottom left corner to open the console).

We also need the “Say” button to stop recording when released, which we can listen for with the `mouseup` event. The “Play” and “Yalp” buttons will play back the audio when clicked, so we’ll listen for the `click` event on those. Here’s how our code will look:

```javascript
const sayButton = document.getElementById("sayButton");
const playButton = document.getElementById("playButton");
const yalpButton = document.getElementById("yalpButton");

sayButton.addEventListener("mousedown", function() {
  // Do stuff while pushed down
  console.log("Say button pushed down");
});

sayButton.addEventListener("mouseup", function() {
  // Do stuff when released
  console.log("Say button released");
});

playButton.addEventListener("click", function() {
  // Do stuff when clicked
  console.log("Play button clicked");
});

yalpButton.addEventListener("click", function() {
  // Do stuff when released
  console.log("Yalp button clicked");
});
```

We’ve built our HTML interface, and used JavaScript to target the interface elements and listen for specific actions, now we’re ready to add our recording and playback features.

### Step 4: Tone.js and wiring up the “Say” button

[Tone.js](https://tonejs.github.io/) is a JavaScript framework that adds a layer of abstraction to the [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) that’s included natively in the browser. While we could work directly with the Web Audio API to set up our recording and playback functionality, Tone.js makes achieving those things a little bit easier.

The first thing we need to do is include the Tone.js framework in our CodePen. In your CodePen, click the gear at the top right of the “JS” pane and under “Add External Scripts/Pens” search for “tone” and click on the package when it appears (the description should say “a Web Audio framework for making interactive music in the browser”).

Alternately, you can paste this URL **https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js** directly into one of the fields underneath the search bar.

Click “Save and Close.”

Now that we’ve added the package, we have access to the Tone.js framework within our code.

The first thing we want to do is record some audio from the user when they click the “Say” button. Looking through the [documentation](https://tonejs.github.io/docs/14.7.77/index.html), Tone.js comes with a `Tone.Recorder()` class that is able to record audio. However, it needs some sort of input. In our case, we want to record from the users microphone, which we can do with the `Tone.UserMedia()` class.

So we’re going to access the users mic, feed it into a recorder, and eventually play back the result. We want this to happen when the user presses the “Say” button, but we also need the `mic` and `recorder` variables to be available to the other event listener functions. Let’s add an empty variable for each, and then initialize each one within the `mousedown` event listener.

```javascript
const sayButton = document.getElementById("sayButton");
const playButton = document.getElementById("playButton");
const yalpButton = document.getElementById("yalpButton");

let mic, recorder;

sayButton.addEventListener("mousedown", function() {
  // Do stuff while pushed down
  mic = new Tone.UserMedia();
  recorder = new Tone.Recorder();
});

sayButton.addEventListener("mouseup", function() {
  // Do stuff when released
  console.log("Say button released");
});

playButton.addEventListener("click", function() {
  // Do stuff when clicked
  console.log("Play button clicked");
});

yalpButton.addEventListener("click", function() {
  // Do stuff when clicked
  console.log("Yalp button clicked");
});
```

We have our mic and our recorder, and they’ll be available to the other event listeners. Right now, `mic` and `recorder` will be re-initialized each time the user pushes the “Say” button, which is unnecessary. Let’s add a variable to check if they’ve been initialized or not, and then only initialize them if that variable if false.

```javascript
const sayButton = document.getElementById("sayButton");
const playButton = document.getElementById("playButton");
const yalpButton = document.getElementById("yalpButton");

let initialized = false;
let mic, recorder;

sayButton.addEventListener("mousedown", function() {
  // Do stuff while pushed down
  if (!initialized) {
    mic = new Tone.UserMedia();
    recorder = new Tone.Recorder();
    initialized = true;
  }
});

sayButton.addEventListener("mouseup", function() {
  // Do stuff when released
  console.log("Say button released");
});

playButton.addEventListener("click", function() {
  // Do stuff when clicked
  console.log("Play button clicked");
});

yalpButton.addEventListener("click", function() {
  // Do stuff when clicked
  console.log("Yalp button clicked");
});
```

We have our mic and recorder initialized on the first press, now we need to connect them together and start the recorder. Connecting the mic and recorder should only happen on the first initialization, but starting the recorder should happen every time the user presses the button. We use `mic.connect(recorder)` and `mic.open()` to prepare the mic for recording, and `recorder.start()` to start the recording. Here’s our code so far:

```javascript
const sayButton = document.getElementById("sayButton");
const playButton = document.getElementById("playButton");
const yalpButton = document.getElementById("yalpButton");

let initialized = false;
let mic, recorder;

sayButton.addEventListener("mousedown", function() {
  // Do stuff while pushed down
  if (!initialized) {
    mic = new Tone.UserMedia();
    recorder = new Tone.Recorder();
    mic.connect(recorder);
    mic.open();
    initialized = true;
  }
  
  recorder.start();
});

sayButton.addEventListener("mouseup", function() {
  // Do stuff when released
  console.log("Say button released");
});

playButton.addEventListener("click", function() {
  // Do stuff when clicked
  console.log("Play button clicked");
});

yalpButton.addEventListener("click", function() {
  // Do stuff when clicked
  console.log("Yalp button clicked");
});
```

Finally, we need to add `Tone.Context.Resume()` after the user pushes the button, but before we start initializing our variables to fix a bug that otherwise pops up in some cases (full details here if you’re curious [https://github.com/Tonejs/Tone.js/issues/341](https://github.com/Tonejs/Tone.js/issues/341)). Our code should now look like this, and when clicking the “Say” button, you should get a prompt to allow the browser to access the microphone (or see a “recording” indicator if you’ve already allowed access).

```javascript
const sayButton = document.getElementById("sayButton");
const playButton = document.getElementById("playButton");
const yalpButton = document.getElementById("yalpButton");

let initialized = false;
let mic, recorder;

sayButton.addEventListener("mousedown", function() {
  // Do stuff while pushed down
  Tone.context.resume();

  if (!initialized) {
    mic = new Tone.UserMedia();
    recorder = new Tone.Recorder();
    mic.connect(recorder);
    mic.open();
    initialized = true;
  }
  
  recorder.start();
});

sayButton.addEventListener("mouseup", function() {
  // Do stuff when released
  console.log("Say button released");
});

playButton.addEventListener("click", function() {
  // Do stuff when clicked
  console.log("Play button clicked");
});

yalpButton.addEventListener("click", function() {
  // Do stuff when clicked
  console.log("Yalp button clicked");
});
```

Our “Say” button is halfway there, now it’s time to stop recording and retrieve the audio when the user lets go.

### Step 5: Finishing the “Say” button

When the user lets go of the “Say” button, we need to stop recording, retrieve the audio clip, and load it into a player for playback. The player that the audio will get loaded into, like the mic and recorder variables, will need to be accessible in other event listeners, meaning we need to declare it outside of any other functions. Let’s do that now:

```javascript
const sayButton = document.getElementById("sayButton");
const playButton = document.getElementById("playButton");
const yalpButton = document.getElementById("yalpButton");

let initialized = false;
let mic, recorder, player; // Add the player variable

sayButton.addEventListener("mousedown", function() {
  // Do stuff while pushed down
  Tone.context.resume();

  if (!initialized) {
    mic = new Tone.UserMedia();
    recorder = new Tone.Recorder();
    mic.connect(recorder);
    mic.open();
    initialized = true;
  }
  
  recorder.start();
});

sayButton.addEventListener("mouseup", function() {
  // Do stuff when released
  console.log("Say button released");
});

playButton.addEventListener("click", function() {
  // Do stuff when clicked
  console.log("Play button clicked");
});

yalpButton.addEventListener("click", function() {
  // Do stuff when clicked
  console.log("Yalp button clicked");
});
```

Next, when the user releases the “Say” button, we want to stop the recording. This can be done by calling `recorder.stop()`. When we call this method, it’s going to return the recorded audio data, so we want to not only call the method, but assign the result to a variable so we can capture the recorded audio data it returns. Here’s how we can do that:

```javascript
const sayButton = document.getElementById("sayButton");
const playButton = document.getElementById("playButton");
const yalpButton = document.getElementById("yalpButton");

let initialized = false;
let mic, recorder, player;

sayButton.addEventListener("mousedown", function() {
  // Do stuff while pushed down
  Tone.context.resume();

  if (!initialized) {
    mic = new Tone.UserMedia();
    recorder = new Tone.Recorder();
    mic.connect(recorder);
    mic.open();
    initialized = true;
  }
  
  recorder.start();
});

sayButton.addEventListener("mouseup", function() {
  // Do stuff when released
  const data = recorder.stop();
});

playButton.addEventListener("click", function() {
  // Do stuff when clicked
  console.log("Play button clicked");
});

yalpButton.addEventListener("click", function() {
  // Do stuff when clicked
  console.log("Yalp button clicked");
});
```

We use `const` because the `data` variable is only assigned once and will not be reassigned. To load our raw audio data into our player, we need to first convert it to a URL which the player will be able to read from. We can use the built-in browser method `URL.createObjectURL(data)` to convert the raw data to a URL.

```javascript
const sayButton = document.getElementById("sayButton");
const playButton = document.getElementById("playButton");
const yalpButton = document.getElementById("yalpButton");

let initialized = false;
let mic, recorder, player;

sayButton.addEventListener("mousedown", function() {
  // Do stuff while pushed down
  Tone.context.resume();

  if (!initialized) {
    mic = new Tone.UserMedia();
    recorder = new Tone.Recorder();
    mic.connect(recorder);
    mic.open();
    initialized = true;
  }
  
  recorder.start();
});

sayButton.addEventListener("mouseup", function() {
  // Do stuff when released
  const data = recorder.stop();
  const audioUrl = URL.createObjectURL(data);
});

playButton.addEventListener("click", function() {
  // Do stuff when clicked
  console.log("Play button clicked");
});

yalpButton.addEventListener("click", function() {
  // Do stuff when clicked
  console.log("Yalp button clicked");
});
```

Finally, we initialize a Tone.js player and load the file URL into it so it’s ready for playback. We use `Tone.Player(url)` to initialize the player and pass in the audio, and we chain the method `toDestination()` to route the audio through the users default audio output.

```javascript
const sayButton = document.getElementById("sayButton");
const playButton = document.getElementById("playButton");
const yalpButton = document.getElementById("yalpButton");

let initialized = false;
let mic, recorder, player;

sayButton.addEventListener("mousedown", function() {
  // Do stuff while pushed down
  Tone.context.resume();

  if (!initialized) {
    mic = new Tone.UserMedia();
    recorder = new Tone.Recorder();
    mic.connect(recorder);
    mic.open();
    initialized = true;
  }
  
  recorder.start();
});

sayButton.addEventListener("mouseup", function() {
  // Do stuff when released
  const data = recorder.stop();
  const audioUrl = URL.createObjectURL(data);
  player = new Tone.Player(audioUrl).toDestination();
});

playButton.addEventListener("click", function() {
  // Do stuff when clicked
  console.log("Play button clicked");
});

yalpButton.addEventListener("click", function() {
  // Do stuff when clicked
  console.log("Yalp button clicked");
});
```

We need to do one last thing to get this to work. The `recorder.stop()` method is an asynchronous function, meaning it returns a Promise object and behaves a little different from a normal JavaScript function. If you’re not familiar with asynchronous JavaScript, I would recommend [digging in](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous) as it’s an important concept to grasp, but it is also a little advanced so if you’re just starting out, all you need to know is that we can add the `async` and `await` keywords to make everything behave more like the synchronous JavaScript we’re used to.

```javascript
const sayButton = document.getElementById("sayButton");
const playButton = document.getElementById("playButton");
const yalpButton = document.getElementById("yalpButton");

let initialized = false;
let mic, recorder, player;

sayButton.addEventListener("mousedown", function() {
  // Do stuff while pushed down
  Tone.context.resume();

  if (!initialized) {
    mic = new Tone.UserMedia();
    recorder = new Tone.Recorder();
    mic.connect(recorder);
    mic.open();
    initialized = true;
  }
  
  recorder.start();
});

sayButton.addEventListener("mouseup", async function() {
  // Do stuff when released
  const data = await recorder.stop();
  const audioUrl = URL.createObjectURL(data);
  player = new Tone.Player(audioUrl).toDestination();
});

playButton.addEventListener("click", function() {
  // Do stuff when clicked
  console.log("Play button clicked");
});

yalpButton.addEventListener("click", function() {
  // Do stuff when clicked
  console.log("Yalp button clicked");
});
```

We’ve made it through the meat of our Yak Bak functionality, now we just need to get our audio to play back.

### Step 6: The “Play” and “Yalp” buttons

We’re recording input, converting it to audio, and loading it into a player, but we still need the audio to play back when the user pushes the “Play” button. This part is as simple as calling `player.start()` from within the `playButton` event listener.

```javascript
const sayButton = document.getElementById("sayButton");
const playButton = document.getElementById("playButton");
const yalpButton = document.getElementById("yalpButton");

let initialized = false;
let mic, recorder, player;

sayButton.addEventListener("mousedown", function() {
  // Do stuff while pushed down
  Tone.context.resume();

  if (!initialized) {
    mic = new Tone.UserMedia();
    recorder = new Tone.Recorder();
    mic.connect(recorder);
    mic.open();
    initialized = true;
  }
  
  recorder.start();
});

sayButton.addEventListener("mouseup", async function() {
  // Do stuff when released
  const data = await recorder.stop();
  const audioUrl = URL.createObjectURL(data);
  player = new Tone.Player(audioUrl).toDestination();
});

playButton.addEventListener("click", function() {
  // Do stuff when clicked
  player.start();
});

yalpButton.addEventListener("click", function() {
  // Do stuff when clicked
  console.log("Yalp button clicked");
});
```

Just like that, you should now be able to hold down the “Say” button, record some audio, let go to end the recording, and hear it play back when you click “Play”. The last piece of functionality is playing the audio back in reverse when the user presses the “Yalp” button.

Luckily the Tone.js player has a `reverse` property, so reversing our audio is a simple as setting this property to `true`, then calling the `player.start()` method from within our `yalpButton` listener.

```javascript
const sayButton = document.getElementById("sayButton");
const playButton = document.getElementById("playButton");
const yalpButton = document.getElementById("yalpButton");

let initialized = false;
let mic, recorder, player;

sayButton.addEventListener("mousedown", function() {
  // Do stuff while pushed down
  Tone.context.resume();

  if (!initialized) {
    mic = new Tone.UserMedia();
    recorder = new Tone.Recorder();
    mic.connect(recorder);
    mic.open();
    initialized = true;
  }
  
  recorder.start();
});

sayButton.addEventListener("mouseup", async function() {
  // Do stuff when released
  const data = await recorder.stop();
  const audioUrl = URL.createObjectURL(data);
  player = new Tone.Player(audioUrl).toDestination();
});

playButton.addEventListener("click", function() {
  // Do stuff when clicked
  player.start();
});

yalpButton.addEventListener("click", function() {
  // Do stuff when clicked
  player.reverse = true;
  player.start();
});
```

We’re almost there, but we have a small bug. When the user clicks the “Yalp” button, it causes the “Play” button to playback in reverse too, since they both share the `player` variable. To fix this, we just need to set `player.reverse` to `false` when the user clicks the “Play” button. Here’s our final code:

```javascript
const sayButton = document.getElementById("sayButton");
const playButton = document.getElementById("playButton");
const yalpButton = document.getElementById("yalpButton");

let initialized = false;
let mic, recorder, player;

sayButton.addEventListener("mousedown", function() {
  // Do stuff while pushed down
  Tone.context.resume();

  if (!initialized) {
    mic = new Tone.UserMedia();
    recorder = new Tone.Recorder();
    mic.connect(recorder);
    mic.open();
    initialized = true;
  }
  
  recorder.start();
});

sayButton.addEventListener("mouseup", async function() {
  // Do stuff when released
  const data = await recorder.stop();
  const audioUrl = URL.createObjectURL(data);
  player = new Tone.Player(audioUrl).toDestination();
});

playButton.addEventListener("click", function() {
  // Do stuff when clicked
  player.reverse = false; // Make sure reverse playback is off
  player.start();
});

yalpButton.addEventListener("click", function() {
  // Do stuff when clicked
  player.reverse = true;
  player.start();
});
```

And just like that, the core functionality is done, plus one of the “nice-to-haves” on our list. In part 2, I’ll walk through the styling and some UI enhancements, and in part 3 I’ll go through how we can allow users to change the color scheme.

If you’ve made it this far, thanks for following along!