DEV Community

Je We
Je We

Posted on

Building a Simple Piano with Tone.js and NexusUI (Part 2)

Alt Text
Hello! I'm back this week to finish up what I started. I've got a visually working piano that I can control with my keyboard. Now it's time to hook up those key toggles to some sound toggles.

I'm going to use Tone.js for my audio. Tone.js is an easy-to-use web audio library. Let's start with a few basics. The first step to playing a sound is to create a new Tone Synth:

const synth = new Tone.Synth().toMaster();

This will create an instance of the Tone Synth, which can be called upon to play notes in the future. toMaster() tells that synth to route its output to the user's default speakers. Playing a note with the synth is very simple:

synth.triggerAttack('C4');

This will tell the synth to start playing the pitch denoted by C4. This article might be helpful for understanding what 'C4' means if you're not music-savvy. triggerAttack can also take arguments for a delay time to wait before triggering the note and a volume, but I won't need either because I want to play the note immediately and I want it at full ear-splitting volume. Now, triggerAttack is only half the story. Once I start the note, it will keep playing until I slowly lose my grip on reality. This where triggerRelease comes in:

synth.triggerRelease()

Once again, a delay argument is possible, but not needed in this case. As you may have noticed, we can actually use one synth to play and release any number of notes (provided they're one at a time). However, I want to be able to play any number of notes at any time, and hold them down for arbitrary lengths of time, so I'm going to create a dedicated synth for each piano key.

Let's go back to the NexusUI Piano for a second. I have my keystrokes toggling each of the piano's keys visually. Each of these visual changes is accompanied by a 'change' event which can be listened for on the Piano instance, like so:

piano.on('change', (k) => {
  // do something
});

The makeup of each change event is this:

// toggled on
{
  note: 72,
  state: true
}

// toggled off
{
  note: 72,
  state: true
}

An important thing to notice here is that the note properties are in MIDI, but Tone.js expects string representations of notes (like 'C4'). In MIDI, 72 is the equivalent to C4. Luckily, there's a nice little package called midi-note which can do this conversion for us. I'm importing midi-note as 'note', and I can use it like this:

note(72) // returns 'C4'

So I know that for each piano change event I want to either start or stop a note. I can tell this based on the event's state. If it's true I want to start the note, and if not I want to stop the already-playing note. So, I can set up my initial structure for the event listener like so:

piano.on('change', (k) => {
  if (k.state) {
   // play a note
  } else {
    // stop a note
  }
});

The next thing to consider is that once a synth for a particular note is started, I don't necessarily know when it's going to stop, but I do want to be able to locate it when it does, so that I can trigger the release. For this reason I'm going to create an object to store my synths:

const activeSynths = {};

When it comes time to trigger a particular note, I'll check to see if I've already created a synth for that note. If I haven't, I'll create one and add it to activeSynths:

piano.on('change', (k) => {
  if (k.state) {
    if (!activeSynth[k.note]) {
      activeSynths[k.note] = new Tone.Synth().toMaster();
    }
  } else {
    // stop a note
  }
});

Now, to play that note, I simply trigger a note (converted from MIDI to scientific pitch notation) on the synth stored at that note address:

piano.on('change', (k) => {
  if (k.state) {
    if (!activeSynth[k.note]) {
      activeSynths[k.note] = new Tone.Synth().toMaster();
    }
    activeSynths[k.note].triggerAttack(note(k.note));
  } else {
    // stop a note
  }
});

Finally, to stop a note, I just locate the same synth and release it:

piano.on('change', (k) => {
  if (k.state) {
    if (!activeSynth[k.note]) {
      activeSynths[k.note] = new Tone.Synth().toMaster();
    }
    activeSynths[k.note].triggerAttack(note(k.note));
  } else {
    activeSynths[k.note].triggerRelease();
  }

And there you have it. A working piano for all your next masterpiece.

Top comments (0)