Audio | Part 5 — Master Volume and BPM Controls with Tone.js

Today, we're going to level up our beat-playing application by introducing two essential features: a tempo control to alter the beats per minute (BPM), and a master volume control.

Quick Recap

Before we jump into the new features, let's quickly recap the core functionalities of our starting code:

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();

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

const { Transport: T } = Tone;
const round = (x, places) =>  Number.parseFloat(x).toFixed(places);

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

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 startBeat = () => Tone.start().then(() => playBeat());

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

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

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;
}

Here is a breakdown of what each part of the code does:

  • Drum sample loading: The first three lines create Tone.Player instances, each of which is set up to play a different drum sample (a kick, a snare, and a hi-hat). The files are located at specified paths and toDestination() routes the audio to the default output.

  • Transport and helper function: This code sets up a reference to Tone's Transport (aliased to T) which is used to schedule events in time. We also defines a helper function round() to round numbers to a given number of decimal places.

  • Beat count: A count variable is declared and a function updateCount() is defined that increments count every time it's called, wrapping back to zero when it reaches 16.

  • Beat playing function: playBeat() uses Tone's Transport to schedule a repeated function every eighth note ("8n"). This function plays the hi-hat on every beat, plays the kick drum every 4 beats, and plays the snare drum every 4 beats offset by 2. It then updates the visual display and increments the count.

  • Start and stop functions: startBeat() and stopBeat() functions are defined. The former waits for Tone.js to start, then calls playBeat(), while the latter stops and cancels the Transport.

  • DOM references and event listeners: This code gets references to the start and stop buttons and the display elements in the HTML, using the document.querySelector() function. It then sets up event listeners on the buttons to start and stop the beat when clicked.

  • Display update function: The updateDisplay() function takes the current time and updates the content of the display elements with the rounded time, the current count, and the bars and beats from the Transport's position.

So, in essence, the code is making a simple, looped beat machine with a visual display for basic timing information. When you press the start button, the drum machine begins playing a loop of kick, snare, and hi-hat sounds in a certain rhythm, and the display is updated in real-time. The stop button halts the loop and cancels the scheduled events.

Feature 1: Master Volume Control

Our first added feature will be to control the master volume. We'll utilize the HTML5 <input> element of type range to create a slider, and then we'll link this to the volume attribute in Tone.js.

Let's add the HTML code for the volume slider:

<div id="volume-control">
  <label for="volume">Master Volume: </label>
  <input type="range" id="volume" min="0" max="100" value="50" step="1">
</div>

Now, in our JavaScript, we'll grab this slider and add an input event listener that adjusts the volume based on the slider's position:

const volume_control = qs('#volume');

volume_control.addEventListener('input', ({ target: { value } }) => {
  Tone.Destination.volume.value = value - 50; // Tone.js uses a decibel scale for volume where 0 is maximum and -Infinity is minimum.
});

Note that Tone.js uses a decibel scale for volume where zero is maximum and -Infinity is minimum. Our code is set up such that the range slider for the volume is in the range of 0 to 100, where zero is muted and 100 is the maximum volume level.

Feature 2: BPM Control

Next, let's add a BPM control. We will again use an <input> of type range, and we'll link this to the bpm attribute in Tone.Transport.

Here's the HTML code for the BPM slider:

<div id="bpm-control">
  <label for="bpm">BPM: </label>
  <input type="range" id="bpm" min="40" max="220" value="120" step="1">
</div>

And here's the JavaScript to connect this slider to our drum loop's BPM:

const bpm_control = qs('#bpm');

bpm_control.addEventListener('input', ({ target: { value } }) => {
  T.bpm.value = value;
});

Wrapping Up and Demo

That's it! We now have a drum machine with two crucial controls: Master Volume and BPM. 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