How to calculate BPM from screen clicks

Kamie Robinson
5 min readJun 27, 2017

A song’s BPM (beats per minute) isn’t something I intrinsically know when I hear it. Song BPM, is a great site for solving that mystery, but you have to search by artist or song title.

While I was wishing I could tap a beat then use it to search through a genre of songs for other songs with matching tempos, it occurred to me that I could build something like that.

This tutorial is the first step in that project. It walks through how to calculate BPM from screen clicks using React. Here is a link to the demo, http://interdigitize.com/beat-match/.

This is my initial concept diagram. I chose to stick to 4/4 time since it is most common, and to take two measures into account for a better sample size and more accurate calculation.

The first thing needed to track interval time is a counter acting like a clock. The counter should start counting and listening for clicks when “Set the Beat” button is clicked. After the 8th click, it should shut itself off. To build this functionality, I created three functions:

  1. increment() that adds 1 to the clock value.
  2. count() that calls increment every hundredth of a second.
  3. startListening() triggered when “Set the Beat” is clicked that sets the runClock var to true and calls the count function.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
};
this.startListening = this.startListening.bind(this)
this.count = this.count.bind(this);
this.increment = this.increment.bind(this);
this.timeInterval = [];
this.runClock = false;
this.clock = 0;
this.counter;
}
increment() {
if(this.runClock){
this.clock = this.clock + 1;
}
}
count() {
clearInterval(this.counter);
this.counter = setInterval(this.increment, 10);
}

startListening() {
if (this.runClock === false) {
this.runClock = true;
this.count();
}
}
}

That takes care of starting the count, but I also needed a way to stop the count after eight clicks. To do that, I added another function, noteClickTime(), with three conditions:

  1. If this.runClock is false, then don’t do anything. Meaning if the screen is clicked without “Set the Beat” being activated, nothing will happen.
  2. If the length of the timeInterval array is greater than six, add the seventh interval and stop the clock. This will happen on the eight click.
  3. In all other cases, add the interval to the timeInterval array.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
};
this.startListening = this.startListening.bind(this)
this.count = this.count.bind(this);
this.increment = this.increment.bind(this);
this.noteClickTime = this.noteClickTime.bind(this)
this.timeInterval = [];
this.runClock = false;
this.clock = 0;
this.counter;
}
increment() {
if(this.runClock){
this.clock = this.clock + 1;
}
}
count() {
clearInterval(this.counter);
this.counter = setInterval(this.increment, 10);
}

startListening() {
if (this.runClock === false) {
this.timeInterval = [];
this.runClock = true;
this.count();
}
}
markTheBeat() {
this.timeInterval.push(this.clock);
}
noteClickTime() {
if(this.runClock === false) {
} else if (this.timeInterval.length > 6) {
this.markTheBeat();
this.runClock = false;
this.clock = 0;
} else {
this.markTheBeat();
this.clock = 0;
}
}
}

Now that there is an array of time intervals, the next step is to turn it into beats per minute. Since currently I’m not interested in the time interval from clicking “Set the Beat” to the first screen click, I removed it. What I don’t have that I would like is the interval time between beat eight and the downbeat, but asking a user to click nine times is weird. To remedy that, I duplicated the last interval.

Next up is adding the intervals together to get the total time for eight beats. Then multiplying it by 10, to convert it into milliseconds. Then dividing it by eight to determine how many milliseconds one beat gets. Finally dividing 60000 by the one-beat value, resolves to the BPM value. Check it out in the calculateBPM() function below.

import React from 'react';
import ReactDOM from 'react-dom';
import $ from 'jquery';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
};
this.startListening = this.startListening.bind(this)
this.count = this.count.bind(this);
this.increment = this.increment.bind(this);
this.noteClickTime = this.noteClickTime.bind(this)
this.timeInterval = [];
this.runClock = false;
this.clock = 0;
this.bpm = null;
this.counter;
}
increment() {
if(this.runClock){
this.clock = this.clock + 1;
}
}
count() {
clearInterval(this.counter);
this.counter = setInterval(this.increment, 10);
}

startListening() {
if (this.runClock === false) {
this.timeInterval = [];
this.runClock = true;
this.count();
this.bpm = null;
}
}
markTheBeat() {
this.timeInterval.push(this.clock);
}
noteClickTime() {
if(this.runClock === false) {
} else if (this.timeInterval.length > 6) {
this.markTheBeat();
this.runClock = false;
this.clock = 0;
this.calculateBPM(this.timeInterval);
} else {
this.markTheBeat();
this.clock = 0;
}
}
calculateBPM(intervalArr) {
intervalTimeArr.shift();
intervalTimeArr.push(intervalArr[intervalArr.length - 1]);
var totalTime8beats = intervalArr.reduce((sum, interval) => {
return sum + interval;
});
var bpm = Math.floor(60000 / ((totalTime8beats * 10) / 8));
}
}

All time intervals between clicks should be close to the same length. To prevent sporadic clicking from resulting in a calculated BPM, I added a filter to check if all the intervals are within a range of the interval average.

...
calculateBPM(intervalArr) {
intervalTimeArr.shift();
intervalTimeArr.push(intervalArr[intervalArr.length - 1]);
var totalTime8beats = intervalArr.reduce((sum, interval) => sum
+ interval);
var bpm = Math.floor(60000 / ((totalTime8beats * 10) / 8));
var intervalAverage = totalTime8beats / 8;
var inAverageRange = intervalTimeArr.filter(interval => {
return interval < (intervalAverage + 10) &&
interval > (intervalAverage - 10);
});
}
...

The last thing to do in this tutorial is to render info to the screen so that the user knows how many clicks they have left, and what the result is.

class App extends React.Component {
constructor(props) {
super(props);
this.state = {
};
this.count = this.count.bind(this);
this.increment = this.increment.bind(this);
this.startListening = this.startListening.bind(this)
this.noteClickTime = this.noteClickTime.bind(this)
this.timeInterval = [];
this.runClock = false;
this.clock = 0;
this.clicksLeft = 9;
this.bpm = null;
this.counter;
}
increment() {
if(this.runClock){
this.clock = this.clock + 1;
}
}
count() {
clearInterval(this.counter);
this.counter = setInterval(this.increment, 10);
}

startListening() {
if (this.runClock === false) {
this.timeInterval = [];
this.runClock = true;
this.bpm = null;
this.count();
this.updateClickCount();
}
}
markTheBeat() {
this.timeInterval.push(this.clock);
}
noteClickTime() {
if(this.runClock === false) {
} else if (this.timeInterval.length > 6) {
this.markTheBeat();
this.updateClickCount();
this.runClock = false;
this.clock = 0;
this.calculateBPM(this.timeInterval);
} else {
this.markTheBeat();
this.updateClickCount();
this.clock = 0;
}
}
updateClickCount() {
this.clicksLeft--;
$('#clickCount').html('clicks left' + '\n' + this.clicksLeft);
}
calculateBPM(intervalArr) {
intervalTimeArr.shift();
intervalTimeArr.push(intervalArr[intervalArr.length - 1]);
var totalTime8beats = intervalArr.reduce((sum, interval) => sum
+ interval);
var bpm = Math.floor(60000 / ((totalTime8beats * 10) / 8));
var intervalAverage = totalTime8beats / 8;
var inAverageRange = intervalTimeArr.filter(interval => {
return interval < (intervalAverage + 10) &&
interval > (intervalAverage - 10);
});
if (inAverageRange.length === 8) {
$('#clickCount').text(bpm + ' bpm');
} else {
$('#clickCount').text('That was some nice clicking, but a
little too creative to get a pulse. Please try again.')
;
}
}
render () {
return (
<div >
<button onClick={this.startListening}>Set the Beat</button>
<div onClick={this.noteClickTime} id="clickCount"></div>
</div>
)
}
}

--

--

Kamie Robinson

Software Engineer rooted as a Creative. Writing how-tos and about 'ahas' and 'gotchas' of making.