tutorial // Aug 27, 2021

How to Build a Soundboard with JavaScript

How to build a soundboard in JavaScript by creating a SoundPlayer class that dynamically injects players and makes it easy to map their playback to a DOM event.

How to Build a Soundboard with JavaScript

Getting Started

For this tutorial, we're going to use the CheatCode Next.js Boilerplate as a starting point for our work. To start, let's clone a copy:

Terminal

git clone https://github.com/cheatcode/nextjs-boilerplate

Next, cd into the project and install its dependencies:

Terminal

cd nextjs-boilerplate && npm install

Finally, start up the development server:

Terminal

npm run dev

With all of that, we're ready to get started.

Building a sound player

In order to actually play the sounds in our soundboard, we'll want an easy way to create audio players on-the-fly. To do that, we're going to begin by wiring up a JavaScript class that will handle the creation of the <audio></audio> elements that will play our sounds and automate the injection of those elements into the DOM.

/lib/soundPlayer.js

class SoundPlayer {
  constructor() {
    this.sounds = [];
  }

  // We'll implement the API for our class here...
}

export default SoundPlayer;

To start, here, we're creating a skeleton for our SoundPlayer class which will help us to load sounds into the DOM as well as play those sounds. Here, we set up a basic JavaScript class and export it as the default from /lib/soundPlayer.js.

Inside the class, we add the constructor function (this is what gets called right as our class is loaded into memory by JavaScript) and initialize the sounds property on the class, setting it to an empty [] array. Here, this is referring to the current class instance of SoundPlayer. We're creating an array here as we'll want a way to keep track of all the sounds we've loaded into the DOM.

Terminal

class SoundPlayer {
  constructor() {
    this.sounds = [];
  }

  load(name, path) {
    this.sounds = [...this.sounds, { name, path }];
    this.injectPlayerIntoPage(name, path);
  }

  injectPlayerIntoPage(name, path) {
    const player = document.createElement("audio");
    player.id = name;
    player.src = path;
    player.volume = 0.5;
    player.type = "audio/mpeg";
    document.body.appendChild(player);
  }
}

export default SoundPlayer;

Next, we need a simple API (application programming interface, here used colloquially to mean "the implementation of the player") for loading sounds into the DOM. To do it, above, we're adding two methods to our class: load() and injectPlayerIntoPage(). The first will be a publically-exposed function that we'll call from our UI to say "load this sound into the DOM."

Inside of that function, we can see two things happening. First, like we hinted at above, we want to keep track of the sounds we're loading in. Taking in a name argument (an easy-to-remember name to "label" our sound by) and a path (the literal path to the sound file in our app), we overwrite the this.sounds property on our class to be equal to the current value of this.sounds, concatenated with a new object containing the name and path passed into load().

Here, ...this.sounds is "unpacking" the entirety of the existing this.sounds array (whether or not it contains anything). The ... part is known as the spread operator in JavaScript (it "spreads out" the contents of the value immediatelly following the ...).

Next, with our this.sounds array updated, we need to dynamically create the <audio></audio> element we talked about above. To do it, we're adding a separate method injectPlayerIntoPage() which takes in the same two arguments from load(), name and path.

Inside of that function, the first thing we need to do is create the <audio></audio> element in memory. To do it, we run document.createElement('audio') to instruct JavaScript to create an in-memory (meaning not added to the screen/DOM yet) copy of our <audio></audio> element. We store the result of that (the in-memory DOM node for our <audio></audio> element) in the variable const player.

We do this to more easily modify the attributes of the player and then append it to the DOM. Specifically, we set four properties to our player before we append it to the DOM:

  1. id which is set to the name we passed in for our sound.
  2. src which is set to the path to the file on the computer for the sound.
  3. volume which is set to 0.5 or 50% to ensure we don't shatter our user's ear drums.
  4. type which is set to the file type we expect for our files (for our example, we're using .mp3 files so we used the audio/mpeg MIME-type-find others here).

Once we've set all of these properties, finally, we use appendChild on document.body to append our audio player to the DOM (the physical location of this in the DOM is irrelevant as we'll learn next).

/lib/soundPlayer.js

class SoundPlayer {
  constructor() {
    this.sounds = [];
  }

  load(name, path) {
    this.sounds = [...this.sounds, { name, path }];
    this.injectPlayerIntoPage(name, path);
  }

  injectPlayerIntoPage(name, path) {
    const player = document.createElement("audio");
    player.id = name;
    player.src = path;
    player.volume = 0.5;
    player.type = "audio/mpeg";
    document.body.appendChild(player);
  }

  play(name) {
    const player = document.getElementById(name);
    if (player) {
      player.pause();
      player.currentTime = 0;
      player.play();
    }
  }
}

export default SoundPlayer;

To wrap up our SoundPlayer class, we need to add one more method: play(). Like the name suggests, this will play a sound for us. To do it, first, we take in a name argument (one that we would have passed into load() earlier) and try to find an element on the page with an id attribute matching that name.

Recall that above we set the .id on our <audio></audio> tag to the name we passed in. This should find a match in the DOM. If it does, we first .pause() the player (in case we're mid-playback already), force the .currentTime attribute on the player to 0 (i.e., the start of our sound), and then .play() it.

That does it for our SoundPlayer class. Next, let's wire it up and start to play some sounds!

Adding a React page component to test our player

Because our boilerplate is based on Next.js, now, we're going to create a new page in our app using a React.js component where we can test out our SoundPlayer.

/pages/soundboard/index.js

import React from "react";
import SoundPlayer from "../../lib/soundPlayer";

class Soundboard extends React.Component {
  state = {
    sounds: [
      { name: "Kick", file: "/sounds/kick.mp3" },
      { name: "Snare", file: "/sounds/snare.mp3" },
      { name: "Hi-Hat", file: "/sounds/hihat.mp3" },
      { name: "Tom", file: "/sounds/tom.mp3" },
      { name: "Crash", file: "/sounds/crash.mp3" },
    ],
  };

  componentDidMount() {
    const { sounds } = this.state;
    this.player = new SoundPlayer();

    sounds.forEach(({ name, file }) => {
      this.player.load(name, file);
    });
  }

  render() {
    const { sounds } = this.state;

    return (
      <div>
        {sounds.map(({ name, file }) => {
          return (
            <button
              className="btn btn-primary btn-lg"
              style={{ marginRight: "15px" }}
              onClick={() => this.player.play(name)}
            >
              {name}
            </button>
          );
        })}
      </div>
    );
  }
}

Soundboard.propTypes = {};

export default Soundboard;

In Next.js, routes or URLs in our app are automatically created by the the framework based on the contents of the /pages folder at the root of our app. Here, to create the route /soundboard (this will ultimately be accessible via http://localhost:5000/soundboard in the browser), we create the folder /pages/soundboard and put an index.js file in that folder where the React component representing our page will live.

Because our test component is so simple, above, we've output the entire contents. Let's step through it to understand how all of this is fitting together.

First, up top we import our SoundPlayer class from our /lib/soundPlayer.js file.

Next, we define a React component using the class-based method (this makes it easier to work with our player and avoid performance issues). The first part we want to call attention to is the state property we're adding to the class and the sounds property we've set to an array of objects there.

This should be starting to make some sense. Here, we're creating all of the sounds that we want to load into the DOM using the load() method we wrote earlier on our SoundPlayer class. Remember, that function takes a name and a file argument which we're defining here.

We do this as an array of objects to make it easier to loop over and load all of our sounds at once, which we do in the componentDidMount() function on our React component. In there, we use JavaScript object destructuring to "pluck off" the sounds property we just defined on state (accessible in our component's methods as this.state) and then create an instance of our SoundPlayer class with new SoundPlayer() and then assign that instance back to this.player on our Soundboard component class (this will come in handy soon).

Next, using that sounds array we defined on state, we loop over it with a .forEach(), again using JavaScript destructuring to "pluck off" the name and file properties of each object in the array as we loop over them. With these values, we call to this.player.load(), passing them into the function. Like we learned earlier, we expect this to add each of the sounds in our array to the this.sounds array on our SoundPlayer class' instance and then append a DOM element for that sound's <audio></audio> player.

Where this all comes together is down in the render() method on our component class. Here, we again "pluck off" the sounds array from this.state, this time using a JavaScript .map() to loop over the array, allowing us to return some markup that we want React to render for each iteration (each sound) of our array.

Because we're building a soundboard, we add a <button></button> for each sound with an onClick attribute set to a function which calls this.player.play() passing in the name attribute from the sound's object in the this.state.sounds array. With this, we have a soundboard!

Now when we click on a button, we should hear the associated sound in the file play back.

Download a .zip containing all of the sounds referenced in the code above and unzip its contents into your app's /public/sounds folder from CheatCode's Amazon S3 Bucket.

That's it! If you'd like to add your own custom sounds, just make sure to add them to the /public/sounds folder in your app and then update the sounds array on state.

Wrapping up

In this tutorial, we learned how to create a soundboard using JavaScript. To do it, we began by creating a JavaScript class that helped us to dynamically create audio players that we could reference by a unique name. On that class, we also added a .play() method to streamline the playback of our sounds.

To build the UI for our soundboard, we defined a React component that created an instance of our soundboard class, loaded in our preferred list of sounds, and then rendered a list of buttons, each with a call to the .play() method for the sound represented by that button.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode