JavaScript 30 – Day 1: JavaScript Drum Kit

Welcome to my recap of Day 1 of the free JavaScript 30 online video course by Wes Bos. In case you really have not yet heard of the course, quickly head over to it and get started right away! 🙂

Please refer to the JavaScript 30 archive for all recap posts.

Objective

On Day 1, the task is to build a JavaScript drum kit. In essence, this should allow to be played via some predefined keys, which are also displayed on the screen.

The provided starter files include an HTML file that contains both the UI and the <audio> tags, a CSS file that takes care of all the styling, and all the sounds that will be used for the drum kit. This means the only missing part is the JavaScript code to actually make the drum kit play (and indicate the pressed key).

I stopped the video right after the objective has been made clear, and started implementing it on my own.

Conception

Thinking about how to bring this drum kit to life, I wrote down some comments to outline how the code could look like. I’m not sure if you can call this four-liner comment-driven development, though. 🙂

Anyway, here it is:

/**
 * Add event listener (keydown):
 * - find audio and div elements (with [data-key="KEYCODE"]);
 * - add the "playing" class to the div (and remove it after some time);
 * - play the sound.
 */

Implementation

First off, I never did anything with <audio> elements before. I did, however, have in mind their interface was pretty straightforward, so I basically expected a simple .play() method, or similar. To find out, I read my way through <audio> to HTMLAudioElement to HTMLMediaElement, and guess what: audio.play() it is. 😀

Having learned how to play some audio, I coded up the following:

const timers = {};

window.addEventListener( 'keydown', ( e ) => {
    const key = e.keyCode;
    const audio = document.querySelector( `audio[data-key="${key}"]` );

    if ( audio ) {
        const timerId = `timer-${key}`;

        if ( timers[ timerId ] && timers[ timerId ].clearTimeout ) {
            timers[ timerId ].clearTimeout();
        }

        audio.play();

        const div = document.querySelector( `div.key[data-key="${key}"]` );
        div.classList.add( 'playing' );

        timers[ timerId ] = setTimeout( () => div.classList.remove( 'playing' ), 200 );
    }
} );

The above code does pretty much exactly what the comment already included. I set up a single keydown event handler that grabs the according <audio> element and plays it, and adds the playing class to the <div> element so the styling kicks in. Removing the classes is handled by individual timers for each key.

One thing that I didn’t like very much was that I couldn’t play one and the same audio element (and thus file) again if it was still playing. So I searched for some .stop() or .rewind() method, but I couldn’t find any. 🙁

Refinement

Feeling somewhat OK with the above code, I continued the video to see how Wes realized the drum kit.

Improvement number one was fixing the thing that I didn’t like: quickly playing a sound multiple times. The solution is to set the (public) property currentTime of the <audio> element to 0, and thus rewind the sound. When I looked through the documentation, I honestly didn’t look at the properties at all. I guess this is thanks to me being a PHP developer with a passion for (writing and using) proper object-oriented code as well as immutable objects. 😉

Next, Wes talked about using setTimeout() to remove the playing class (and thus styling), which is exactly what I did. However, Wes didn’t seem to like it very much, because he wanted the <div> to have the class only as long as the CSS transition lasts. This can be done by using an individual transitionend event handler for each <div> element.

My final code looks like the following, which is almost my original code, with the addition of rewinding a sound before playing it (again):

const timers = {};

window.addEventListener( 'keydown', ( e ) => {
    const key = e.keyCode;
    const audio = document.querySelector( `audio[data-key="${key}"]` );

    if ( audio ) {
        const timerId = `timer-${key}`;

        if ( timers[ timerId ] && timers[ timerId ].clearTimeout ) {
            timers[ timerId ].clearTimeout();
        }

        audio.currentTime = 0;
        audio.play();

        const div = document.querySelector( `div.key[data-key="${key}"]` );
        div.classList.add( 'playing' );

        timers[ timerId ] = setTimeout( () => div.classList.remove( 'playing' ), 200 );
    }
} );

Live Demo

Want to see this in action? Well, here is a live demo. 🙂

Retrospection

Well, that was fun! 😀

I learned about interacting with <audio> elements, and I also learned about (but didn’t use 😉 ) the transitionend event.

And You?

JavaScript 30 is free! So, unless you already did this on your own, what’s your reason not to? 🙂

6 Comments

  1. Thorsten, thanks for sharing your take on the exercise. I’m curious – why didn’t you want to make use of the `transitionend` event?

    1. Hey Rowan,

      first of all, as you already know, I didn’t watch the whole video, but started right after the introduction. That’s why I didn’t know about Wes wanting the second transition to directly follow after the first one.

      For me, having the key box highlighted is all there is to it. And I didn’t think about, nor do I think it necessary having a single in—out transition sequence.

      I hope that makes sense.

      BTW, on another day, I of course made use of the transitionend event. And I would’ve done here as well, if the objective had clearly included to have a single smooth transition. 😉

      Cheers,
      Thorsten

  2. Amirul Abu

    Hi Thorsten, i would like to ask why did you use const instead of var. thank you in advance

    1. TL;DR: var is both outdated and not the best fit (here).

      Hi there,

      in the above code, I never initialize a variable and redefine (i.e., set) its value later on. With ES6, there are two new ways to define a variable, one of which is const. If this is news to you, you might want to have a look at this and that post.

      Cheers,
      Thorsten

  3. Jimmy

    Hi!
    Thx for your nice posts, really inspiring!
    I was experimenting a bit with adding a click event to the drums as well, that is when you click the boxes with the mouse it plays the sound. I didn’t get it right… at all. Do you have any suggestions or quick tips?

    1. Hey Jimmy,

      glad you like the posts.

      Regarding your question…
      Without seeing your code, I can’t say that much, of course.
      In general, it is almost always a good ide to separate event handlers from business logic functions.

      Here is one way to do it:

      const timers = {};
      
      function playAudio( key ) {
          const audio = document.querySelector( `audio[data-key="${key}"]` );
          if ( audio ) {
              const timerId = `timer-${key}`;
      
              if ( timers[ timerId ] && timers[ timerId ].clearTimeout ) {
                  timers[ timerId ].clearTimeout();
              }
      
              audio.currentTime = 0;
              audio.play();
      
              const div = document.querySelector( `div.key[data-key="${key}"]` );
              div.classList.add( 'playing' );
      
              timers[ timerId ] = setTimeout( () => div.classList.remove( 'playing' ), 200 );
          }
      }
      
      window.addEventListener( 'keydown', ( e ) => {
          playAudio( e.keyCode );
      } );
      
      document.querySelectorAll( 'div.key' ).forEach( ( div ) => {
          div.addEventListener( 'click', function () {
              const key = this.dataset.key;
              if ( key ) {
                  playAudio( key );
              }
          } );
      } );

      Cheers,
      Thorsten

Leave a Reply

Your email address will not be published. Required fields are marked *