Audio | Part 4 — Beat Timing Graphical Display with Tone.js

In this blog post, we will harness the power of Tone.js to create a web-based drum machine with an integrated beat timing display. In subsequent blog posts, we'll build upon the beat timing concepts introduced here to explore more advanced music production programming techniques.

UPDATE [July 30, 2023]: UI Synch Bug Fix

Please refer to the post titled Adding Track Lock and Disable Features to the Music Production App where we utilize the Tone.Draw method in order to schedule a draw callback within the Transport callback. This modification fixes the misalignment between visual and audio elements introduced in the current blog post.

Initializing Sounds

We first will initialize three different Player objects using Tone.js, each representing a specific drum sound. Each Player object is created with an audio file that is played when instructed. The files for this code are in the /assets/samples/drums/ directory and represent the kick, snare, and hi-hat sounds of a drum set.

const kick = new Tone.Player("/assets/samples/drums/kick.mp3").toDestination();
const snare = new Tone.Player("/assets/samples/drums/snare.mp3").toDestination();
const hihat = new Tone.Player("/assets/samples/drums/hi-hat.mp3").toDestination();

We're using the toDestination() method, which routes the output of these players to the default output, typically your device's speakers.

Setting up the Transport

Next, we create an alias T for Tone.Transport using a destructuring assignment. Tone.Transport is a central part of Tone.js, serving as the conductor for managing timing and synchronizing various musical events. Our alias T makes our code easier to read and write.

const { Transport: T } = Tone;

Tone.Transport handles timing and sequencing - it's essentially our clock. According to the Tone.js wiki: "Tone.Transport is the master timekeeper, allowing for application-wide synchronization of sources, signals and events along a shared timeline."

Creating the Rhythm

All the magic happens in our playBeat function, which is where we will schedule our samples to play in a drum beat pattern.

const playBeat = () => {
  T.scheduleRepeat((time) => {
    hihat.start(time);

    const [bars, beats, sixteenths] = T.position.split(':');
    console.log('count: ', count, 
      '\nbars: ', bars, 
      '\nbeats: ', beats, 
      '\nsixteenths: ', sixteenths
    );

    count = (count + 1) % 16;

  }, "8n");
  
  T.start();
};

This function is scheduling a callback function that will be repeatedly called to play the hi-hat sound at a regular interval, set by "8n" (eighth note).

Inside the scheduled callback function, we're logging count and the current bars, beats, and sixteenths of the Transport time, split by ':'. This gives us an insight into the state of our Transport as the hi-hat sound plays.

The count variable is incremented by 1 for each call and wrapped around mod 16 using the modulus operator %.

Starting and Stopping the Beat

We have two additional functions: startBeat and stopBeat. The startBeat function initiates the Transport to start ticking, and the stopBeat function halts it.

const startBeat = () => Tone.start().then(() => {
  playBeat();
});

const stopBeat = () => {
  T.stop();
  T.cancel();
};

User Interface

Finally, our script interacts with the user interface. It gets the start and stop button elements from the HTML document and adds 'click' event listeners to them.

const qs = x => document.querySelector(x);

const start_btn = qs('#start');
const stop_btn = qs('#stop');

start_btn.addEventListener('click', () => startBeat());
stop_btn.addEventListener('click', () => stopBeat());

Now, clicking the start button will start the beat and clicking the stop button will stop the beat, giving us a rudimentary but functional web-based drum machine!

Playing a drum beat pattern

Let's now play a complete drum pattern that includes a hi-hat, kick, and snare. We'll achieve this by modifying our playBeat function to take into account count and decide whether to play the hi-hat, kick, or the snare sound, creating a richer and more realistic drum beat.

const playBeat = () => {
  T.scheduleRepeat((time) => {
    
    hihat.start(time);

    if (count % 4 === 0) {
      kick.start(time);
    }
    if ((count + 2) % 4 === 0) {
      snare.start(time);
    }

    const [bars, beats, sixteenths] = T.position.split(':');
    console.log(
      'count: ', count, 
      '\ntime: ', round(time, 2), 
      '\nbars: ', bars, 
      '\nbeats: ', beats, 
      '\nsixteenths: ', round(sixteenths, 2)
    );

    count = (count + 1) % 16;
  }, "8n");
  
  T.start();
};

In this updated playBeat function, the hi-hat sound still plays on every eighth note, but we've added conditions for the kick and snare sounds. The kick drum plays whenever count is a multiple of 4 (count % 4 === 0), which corresponds to the first and third beats of a standard 4/4 drum pattern. Meanwhile, the snare drum plays whenever count is two greater than a multiple of 4 ((count + 2) % 4 === 0), which corresponds to the second and fourth beats.

This set of conditions creates a common rock or pop drum pattern: hi-hat plays on every eighth note, kick drum on the first and third beats, and snare drum on the second and fourth beats. You'll see that the beat now has a fuller, more rhythmic feel.

We've also updated the console log to include the current time value (rounded to two decimal places for readability) and the number of sixteenths (also rounded). These additions give us even more insight into the internal workings of our drum machine.

By enhancing our drum machine this way, we've significantly boosted its musical capabilities, and we're now creating a genuinely rhythmic drum beat!

Displaying the timing information

Now that we've set the rhythm in motion, it's time to make it visually tangible. We'll accomplish this by updating our drum machine to display real-time timing information on the screen.

Here's the updated code for our drum machine:

let count = 0;
const updateCount = () => count = (count + 1) % 16;

const playBeat = () => {

  T.scheduleRepeat((time) => {   
    hihat.start(time);
    if (count % 4 === 0) kick.start(time);
    if ((count + 2) % 4 === 0) snare.start(time);

    updateDisplay(time);
    updateCount();
  }, "8n");
  
  T.start();
};

// ==============================================

const qs = x => document.querySelector(x);

const start_btn = qs('#start');
const stop_btn = qs('#stop');

start_btn.addEventListener('click', () => startBeat());
stop_btn.addEventListener('click', () => stopBeat());

const time_display = qs('#time > span');
const count_display = qs('#count > span');
const bars_display = qs('#bars > span');
const beats_display = qs('#beats > span');

function updateDisplay(time) {
  const [bars, beats, sixteenths] = T.position.split(':');
  time_display.textContent = round(time, 2);
  count_display.textContent = count;
  bars_display.textContent = bars;
  beats_display.textContent = beats;
}

In this new version, we've added an updateDisplay function which updates the contents of the <span> elements in our timing display, showing the current time, count, bars, and beats. The function is called within our scheduleRepeat block, which ensures that the display is updated every eighth note - in sync with our beat.

<body>
  <button id="start">Start Beat</button>
  <button id="stop">Stop Beat</button>

  <div id="display">
    <div id="time">Time: <span></span></div>
    <div id="count">Count: <span></span></div>
    <div id="bars">Bars: <span></span></div>
    <div id="beats">Beats: <span></span></div>
  </div>
</body>

Demo

Try out the demo below. Also, download the fully working code in its native HTML & JS form from below to run the demo locally.

Time
0
Count
0
Bar
0
Beat
0

Wrapping Up

What we've learned here lays a solid foundation for diving deeper into the sea of possibilities that web-based music production offers us. Concretely, the principles of beat timing we've discussed in this blog post will serve as a launch pad for creating more complex Web Audio API based applications (e.g. multi track interactive audio sequencing DAW's, etc.) in our upcoming discussions.