DEV Community

loading...
Cover image for Building a piano with tone.js!

Building a piano with tone.js!

shimphillip profile image Phillip Shim ・4 min read

 

The piano

Here is the final product of what we will be building! You can click on it or type matching letters on your keyboard.

What is Tone.js?

Tone.js is a very popular Web Audio API library with over 8k stars on its GitHub repository. Before the advent of the Web Audio API, browsers had to make use of audio files to play sounds and music. It made the size of your apps bigger and modifying sounds meant bringing in different audio files every time. Well, what can the Web Audio API do for us?

The Web Audio API provides a powerful and versatile system for controlling audio on the Web, allowing developers to choose audio sources, add effects to audio, create audio visualizations, apply spatial effects (such as panning) and much more.

From: https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API

One very powerful feature it has is you can now generate digital sounds with just JavaScript without the use of audio files!. With a little bit of knowledge of musical concepts such as oscillators and frequency rates, we are able to create various audios suited for our needs. Using pure Web Audio API is a little bit daunting because of low-level concepts so we will be using Tone.js in our project which abstracts away underlying methods.

The HTML

<ul id="piano">
  <li data-note="C4" class="key">
    <div data-note="C#4" class="black-key">R</div>
    D
  </li>
  <li data-note="D4" class="key">
    <div data-note="D#4" class="black-key">T</div>
    F
  </li>
  <li data-note="E4" class="key">
    G
  </li>
  <li data-note="F4" class="key">
    <div data-note="F#4" class="black-key">U</div>
    H
  </li>
  <li data-note="G4" class="key">
    <div data-note="G#4" class="black-key">I</div>
    J
  </li>
  <li data-note="A4" class="key">
    <div data-note="A#4" class="black-key">O</div>
    K
  </li>
  <li data-note="B4" class="key">
    L
  </li>
</ul>

Our markup is fairly simple. The white keys are denoted with a class of 'key' and the black keys have a class of 'black-key'. Notice that black keys are wrapped inside the white keys in the HTML hierarchy to be positioned relatively. Each key also has data-note attributes with corresponding notes.

The CSS

@import url('https://fonts.googleapis.com/css?family=Roboto:400,700&display=swap');
 body {
     font-family: 'Roboto', sans-serif;
     display: flex;
     justify-content: center;
     align-items: center;
     min-height: 100vh;
}
 ul {
     list-style: none;
     display: flex;
}
 ul .key {
     position: relative;
     width: 60px;
     height: 180px;
     border: 1px solid black;
     border-right: none;
     background: #fffff0;
     border-radius: 5px;
     box-shadow: 0px 3px 5px #666;
     cursor: pointer;
     display: flex;
     justify-content: center;
     align-items: flex-end;
     padding-bottom: 10px;
     font-weight: bold;
}
 ul .key:last-child {
     border-right: 1px solid black;
}
 ul .black-key {
     position: absolute;
     top: -1px;
     left: 37.5px;
     width: 45px;
     height: 120px;
     background: black;
     border-radius: 5px;
     box-shadow: 0px 3px 5px #666;
     z-index: 999;
     display: flex;
     justify-content: center;
     align-items: flex-end;
     padding-bottom: 10px;
     color: white;
}

.key styles our white keys. It uses align-items: flex-end to place letters to the bottom of the keys.

The JavaScript

// Tone.Synth is a basic synthesizer with a single oscillator
const synth = new Tone.Synth();
// Set the tone to sine
synth.oscillator.type = "sine";
// connect it to the master output (your speakers)
synth.toMaster();

const piano = document.getElementById("piano");

piano.addEventListener("mousedown", e => {
  // fires off a note continously until trigger is released
  synth.triggerAttack(e.target.dataset.note);
});

piano.addEventListener("mouseup", e => {
  // stops the trigger
  synth.triggerRelease();
});

// handles keyboard events
document.addEventListener("keydown", e => {
  // e object has the key property to tell which key was pressed
  switch (e.key) {
    case "d":
      return synth.triggerAttack("C4");
    case "r":
      return synth.triggerAttack("C#4");
    case "f":
      return synth.triggerAttack("D4");
    case "t":
      return synth.triggerAttack("D#4");
    case "g":
      return synth.triggerAttack("E4");
    case "h":
      return synth.triggerAttack("F4");
    case "u":
      return synth.triggerAttack("F#4");
    case "j":
      return synth.triggerAttack("G4");
    case "i":
      return synth.triggerAttack("G#4");
    case "k":
      return synth.triggerAttack("A4");
    case "o":
      return synth.triggerAttack("A#4");
    case "l":
      return synth.triggerAttack("B4");
    default:
      return;
  }
});
// when the key is released, audio is released as well
document.addEventListener("keyup", e => {
  switch (e.key) {
    case "d":
    case "r":
    case "f":
    case "t":
    case "g":
    case "h":
    case "u":
    case "j":
    case "i":
    case "k":
    case "o":
    case "l":
       synth.triggerRelease(); 
  }
});

Let's break this up. The first three lines use methods provided by Tone.js and set up our sounds. Then we attach event listeners to the piano div, it uses event bubbling to identify which element was targeted after listening to a click event. We grab the element's data-note attribute to play its sound.

We can't do quite the same with keyboard events because the event object that is generated with keypress is different than the event object generated by mouse clicks. That's why we have to manually map keyboard letters to corresponding notes.

Finale

I hope this demo was easy to digest and conquered your fear of exploring Web Audio API! Feel free to leave me any questions or thoughts in the comments in you have any! Thank you for reading this blog!!! 😆😊😃

Discussion

pic
Editor guide