Nevin's BLOG

article list

How to build a precise/reliable metronome with Javascript and React

Generic placeholder image
NevinPosted on Friday, November 22, 2019 · reading time : 30 min

Let’s talk about how to handle precise time management in Javascript inside a React app to create an accurate metronome for your training sessions.

Repository

Demo

A few month ago I decided to create a metronome App with React, because I wanted to get more familiar with React interactions and because I was not very happy with the free metronome apps I could find in the market.

The first thing to figure out was the timing process to achieve a precise beat cycle. The first idea that came to my head was to use setTimeout() and setInterval(), the native javascript functions that allow us to execute actions given a particular timing. But I already worked a lot with those functions and I knew that they are not reliable because the timing can be altered by device/browser performance. For tasks that doesn’t require precise timing, those functions are great, but if you need to build a very accurate beat cycle you will have to find something else


I recommend to clone and open the sample app I created while reading this article so you will be able to see live what’s going on in the code :

Repository

Git checkout simplified

Checkout to the “simplified” branch to see everything we need for this article.

1, Web Workers đŸ‘·

To solve the timing problem with javascript, we will use the combination of Web Worker and the Javascript Audio API.

A Web Worker represents a background task that can be easily created and can send messages back to its creator. Let’s look at the Web Worker we are going to use to have an idea of what it can do :

export default () => {
    var timerID = null;
    var interval = 0;
    this.addEventListener('message', e => { 
        if (e.data === "start") {
            console.log()
            timerID = setInterval(function () {
                postMessage("click");
            }, interval)
        } else if (e.data.interval) {
            interval = e.data.interval;
        } else if (e.data === "stop") {
            clearInterval(timerID);
            postMessage("stop");
            timerID = null;
        }
    });
}

As you can see, this worker is listening to message and it can send one back to the main thread. This is how those two entities can communicate. The code you see here will be run in a new thread; this means its job won’t be affected by the main thread performance. The goal of this worker is to continuously send a “click” message to the main thread at a given interval. We will come back to this later and I will explain how this Worker help us to achieve a perfect timing process.

One more thing, React will require a bit of code to execute our Web Worker, we need to create a WebWorker.js file with the following content :

  export default class WebWorker {
    constructor(worker) {
        const code = worker.toString();
        const blob = new Blob(['('+code+')()']);
        return new Worker(URL.createObjectURL(blob));
    }
}

This will wrap any Worker we create in an URL, so they can be part of our webpack bundle. To create a new Web Worker in our React component, we can now use :

import WebWorker from '../WebWorker';
new WebWorker(metronomeWorker);

.

2, The Javascript Web Audio API đŸŽ”

“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.”

The AudioContext from the javascript Web Audio API give us access to the audio hardware clock which is very precise. The currentTime property is a floated-number with 15 decimals that contains the number of seconds passed since the Audio Context started.

Also it will allow us to create an oscillator to play a sound at a precise given time.

To use it in our code, we can simply declare it like this in componentDidMount cycle function of Metronome.js React component :

componentDidMount = () => {
const AudioContext = window.AudioContext || window.webkitAudioContext;
    this.audioContext = new AudioContext();
    const source = this.audioContext.createBufferSource();
    source.buffer = this.audioContext.createBuffer(1, 1, 22050);
    source.start(0);
}

Our Audio Context is ready ! 🚀

In order to play a sound at a given time we can use the following function :

Let osc = this.audioContext.createOscillator();
osc.connect(this.audioContext.destination);
osc.start(when);

.

3, Combining Web Workers and AudioContext âČ

Let’s dive into the timing process by initializing some variables in the controller and our Web Worker in the React component:

  constructor(props) {
    super(props);
    this.state = {
      running: false, // is the metronome running ?
      bpm: 90, // Battement per minute value for the metronome
      currentMesure: 0, // how many mesure did we played ?
      measurePerCycle: 4 // How many mesure in a cycle (used for visual representation)
    };
    this.audioContext = null; 
    this.nextClickTime = 0.0;
    this.scheduleAheadTime = 0.1; // How far do we want to look ahead in our scheduler
    this.metronomeWorker = new WebWorker(metronomeWorker); // initialising the MetronomeWorker with Webworker
    this.osc = null;
  }
componentDidMount = () => {
this.metronomeWorker.onmessage = (e) => {
      if (e.data === "click") {
        this.scheduler();
      }
    };
    // Sets the interval on how frequently we want to call the scheduler
    this.metronomeWorker.postMessage({ "interval": 25 });
}
Then by pressing the start button, we fire our timing process :
  startStop = () => {
    // getting the value of running in this context
    const running = !this.state.running;

    // //we set the state for ui purpose
    this.setState({
      running: running
    });

    if (running) {
      // We set the first click at the current audiocontext time, and start the ServiceWorker job
      this.nextClickTime = this.audioContext.currentTime + 0.1;
      this.metronomeWorker.postMessage("start");
    } else {
      this.metronomeWorker.postMessage("stop");
    }
  }

The nextClickTime variable is initialised to the current hardware clock time from Audio Context with a short delay in order to make the first click play smoothly.

Our background process will trigger the scheduler() function every 25ms to check if there is a click to schedule in the future. The setInterval() function we use in our Web Worker may not be fully on time as I said before, but here we are not directly playing click, we are checking if there is a click to play “soon” . 25ms is really enough even if a lot of click needs to be scheduled; in a scenario where you have a fast tempo like 240 bpm, the scheduler will be triggered every 25ms and a click needs to be played every 250ms (240 bpm = 4bps, 1000ms/4 = 250ms).

Let’s have a look at how the scheduler() is scheduling the next click by looking ahead of time :

    // this function will look ahead of time to schedule a click for perfect timing and will update the time for the click 
  scheduler = () => {
// scheduleAheadTime = 0.1;
    while (this.nextClickTime < this.audioContext.currentTime + this.scheduleAheadTime) {
      this.scheduleClick();
      this.nextClick();
    }
  }

This means every 25ms, we are comparing 2 values :

1 : the current Audio Context time plus the time to look ahead (here it’s 100ms).

2 : the next click time based on the tempo bpm we set.

We need to find a convenient time to look ahead to avoid delays and to correct potential overlaps: 100ms correspond to 4 scheduler cycles (4 * 25ms) so it’s convenient for what we want to achieve with our metronome.

If you want a highest precision on your timing you can still lower the scheduler() intervals and increase the look ahead time, but you will need more performance for the execution . When the scheduler in its intervals meets the condition of nextClickTime < audioContext.currentTime + scheduleAheadTime it will trigger the playClick() function and schedule a click at the correct nextClickTime as below :

scheduleClick = () => {
    this.osc = this.audioContext.createOscillator();
    this.osc.connect(this.audioContext.destination);

    this.osc.frequency.value = 1200;

    //using the oscillator to play the click at the exact time
    this.osc.start(this.nextClickTime);
    this.osc.stop(this.nextClickTime + 0.05);
  }

We first connect our oscillator from Audio Context, then when assign a frequency __( the frequency of the sound that will be played). Then we __play the click from the oscillator at the given nextClickTime. Using the reliable Audio API hardware clock, we are sure that our click is played at the right time no matters the value of BPM.

Finally, the last function of the scheduler triggers to define the next click time :

nextClick = () => {
    //Calculating the next click
    this.nextClickTime += (60.0 / this.state.bpm);
    this.setState({ measureCount: this.state.measureCount + 1 });
  }

Starting from the current nextClickTime, we add the time calculated from our BPM variable.

After that the cycle will indefinitely continue to update time values from the hardware clock keeping timing precision reliable throughout the process.

That’s it for the timing mechanics ! ⏳

You can find an example of this work wrapped in a React app on the “simplified” branch of the following repository :

Repository

git checkout simplified

Also, you can find a more advanced metronome app on this same repository on the master branch :

git checkout master

This one has more capabilities and more settings, like BPM increase over time, measure/beat management.

A demo is available here :

demo

What about React ? đŸ’»

You may ask me:

Why do we need a React App to do that ? This process could totally work in a simple raw javascript.

That’s true, but here I wanted to use React for two reasons.

The first is that it’s way easier to interact with components in React, and I got many of them in my final implementation. To create my visual content, forms, I think React helps me a lot to do it fast and well.

The second reason, the most important one, is that I wanted to make my app available for smartphones, and allow users to install it directly on their mobile phone exactly like if it was a native app. To achieve that we need to transform our app into a Progressive Web App. This allows us to access a lot of cool features for mobile like caching, mobile installation, custom splashscreens and icons. It turns out that it’s very easy to enable this on a React app, but I will keep that for another article about PWA...

Conclusion

If you want to create music web app that requires a precise timing engine, you should avoid using setTimout() and setInterval() on their own and start combining them with Web Workers and the rich Web Audio API. It’s the best way to achieve reliable timing process and it’s not that hard to implement, you only need javascript. By checking my final version of the app, you can get ideas on what kind of features you can create for further reading.

Last but not least, huge thanks to Chris Wilson for his article about Javascript scheduling that helped me understand how to achieve reliable timing in Javascript. I based my article on his work and tried to simplified the technical part and added a implementation for React.

Thanks for reading !!! đŸ»


  • metronome
  • react
  • javascript
  • WebAudioApi
  • pwa

WRITTEN BY