JS Beat Detector with Web Audio Input from microphone source
New here? Learn about Bountify and follow @bountify to get notified of new bounties! x

Hi! My goal is to have a web-based beat detector that works for live music input (for singing along and strumming the guitar). I've used an Apple app for this and the BPM (Beats Per Minute) is quite accurate.
I am only a beginner javascript coder so I appreciate your help and also your patience.

I've been learning jQueryMobile for simpler tasks and have found the following beat detectors and Q&As. But, I'm not experienced enough a JS programmer to apply this.

Beat Detectors:
http://joesul.li/van/beat-detection-using-web-audio/ (I found this to be the most referenced)
https://www.npmjs.com/package/music-tempo
https://github.com/dlepaux/realtime-bpm-analyzer
https://stackoverflow.com/questions/30110701/how-can-i-use-js-webaudioapi-for-beat-detection
Using microphone:
https://stackoverflow.com/questions/51879587/using-web-audio-api-for-analyzing-input-from-microphone-convert-mediastreamsour

Deliverables:
I am looking for a working demo. As a user, I am able to play my guitar into the microphone and then have BPM (Beats per minute) returned. The BPM should display as I am playing and not only after pressing a button. I am expecting HTML, JS code. I am not seeking any native mobile apps. The result should work on a website.

Alternatively, if this is very challenging technically, then please outline other strategies

I have localhost server on my Mac so am able to test out code.

Thanks and Happy New Year!

Hey are you okay with a blackbox setup as in you just run a make file and its sets up a server and hosts the application...i mean to use a framework called node.
ST2-EV 16 days ago
Hi ST2-EV. Thanks for your question. Sorry, I don't understand all the technicalities of what you are suggesting. I assume this is still workable on most browsers? And can you tell me the benefit of doing it the way you suggested? I'm asking as I wouldn't want to waste your time if it doesn't match what I am seeking. Thank you!
00J 16 days ago
it will run on all modern browsers, I prefer this coz it's easier for me. it's just another library somewhat similar to jquery
ST2-EV 16 days ago
is it just for you or do you intend on hosting it ?
ST2-EV 16 days ago
Thanks ST2-EV for your response. I would like to have it hosted and share with friends. I'd prefer to stick with one framework. Though this beat detector is the heart of all of this, so if it works well - I would definitely consider it. Hope that answers your question.
00J 16 days ago
Hi drakmail, thanks for your question. I have replied to your solution below. Thanks.
00J 15 days ago
Still looking for a solution?
B44ken 9 days ago
@B44ken, if you have a solution in, I'd like to hear it. Note there are 6 hours of the Bounty deadline to offer it. Thanks!
00J 9 days ago
awarded to Bharath Nair

Crowdsource coding tasks.

2 Solutions

Winning solution

Js Beat Detector

Updated solution:

Live demo

Download the source code from here and follow the video in the given drive folder. Please try to install brew beforehand or this script has an untested part which automatically tries to install brew. As dependencies are being installed please enter your computer's admin password when prompted in the terminal and press enter.

The commands shown in the video are:

1) cd space and drag the folder into the terminal as shown in the video and press enter.

2) make install

After the above command installing all the dependencies and starting the server, Your browser should open up with the bpm meter :)

Download the source code from here and open index.html preferably using chrome.

Code for index.html

<!DOCTYPE html>
<html>
  <head>
    <title>MediaRecorder API - Sample</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="keywords" content="WebRTC getUserMedia MediaRecorder API" />
    <link
      type="text/css"
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
    />
    <style>
      button {
        margin: 10px 5px;
      }
      li {
        margin: 10px;
      }
      body {
        width: 90%;
        max-width: 960px;
        margin: 0px auto;
        background-image: url("https://www.androidguys.com/wp-content/uploads/2016/04/8bit_wallpaper7.jpg");
        background-repeat: no-repeat;
        background-size: 1920px 990px;
        background-color: #cccccc;
      }
      #btns {
        display: none;
      }
      h1 {
        margin: 100px;
      }
    </style>
  </head>
  <body>
    <nav class="navbar navbar-inverse">
      <a class="navbar-brand" href="#">JS Beat Detector</a>
    </nav>
    <div id="gUMArea">
      <div>
        Record:
        <!-- <input type="radio" name="media" value="video" id="mediaVideo" />Video -->
        <input type="radio" name="media" value="audio" checked />audio
      </div>
      <button class="btn btn-default" id="gUMbtn">Request Stream</button>
    </div>
    <div id="btns">
      <button class="btn btn-default" id="start">Start</button>
      <button class="btn btn-default" id="stop" disabled>Stop</button>
    </div>
    <div>
      <ul class="list-unstyled" id="ul"></ul>
    </div>
    <div>
      <h3 id="tempo"></h3>
    </div>
    <script src="https://code.jquery.com/jquery-2.2.0.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
    <script src="script.js"></script>
  </body>
</html>

Code for script.js

"use strict";
let bats;
let log = console.log.bind(console),
  id = val => document.getElementById(val),
  ul = id("ul"),
  gUMbtn = id("gUMbtn"),
  start = id("start"),
  stop = id("stop"),
  stream,
  recorder,
  counter = 1,
  chunks,
  media;

gUMbtn.onclick = e => {
  let mv = id("mediaVideo"),
    mediaOptions = {
      video: {
        tag: "video",
        type: "video/webm",
        ext: ".mp4",
        gUM: { video: true, audio: true }
      },
      audio: {
        tag: "audio",
        type: "audio/ogg",
        ext: ".ogg",
        gUM: { audio: true }
      }
    };
  media = mediaOptions.audio;
  navigator.mediaDevices
    .getUserMedia(media.gUM)
    .then(_stream => {
      stream = _stream;
      id("gUMArea").style.display = "none";
      id("btns").style.display = "inherit";
      start.removeAttribute("disabled");
      recorder = new MediaRecorder(stream);
      recorder.ondataavailable = e => {
        chunks.push(e.data);
        if (recorder.state == "inactive") makeLink();
      };
    })
    .catch(log);
};

start.onclick = e => {
  start.disabled = true;
  stop.removeAttribute("disabled");
  chunks = [];
  recorder.start();
};

stop.onclick = e => {
  stop.disabled = true;
  recorder.stop();
  start.removeAttribute("disabled");
};

async function makeLink() {
  let blob = new Blob(chunks, { type: media.type }),
    url = URL.createObjectURL(blob),
    li = document.createElement("li"),
    mt = document.createElement(media.tag),
    hf = document.createElement("a");
  mt.controls = true;
  mt.src = url;
  hf.href = url;
  hf.download = `${counter++}${media.ext}`;
  hf.innerHTML = `donwload ${hf.download}`;
  li.appendChild(mt);
  li.appendChild(hf);
  ul.appendChild(li);
  createBuffers(url);
}

function createBuffers(url) {
  // Fetch Audio Track via AJAX with URL
  var request = new XMLHttpRequest();

  request.open("GET", url, true);
  request.responseType = "arraybuffer";

  request.onload = function(ajaxResponseBuffer) {
    // Create and Save Original Buffer Audio Context in 'originalBuffer'
    var audioCtx = new AudioContext();
    var songLength = ajaxResponseBuffer.total;

    // Arguments: Channels, Length, Sample Rate
    var offlineCtx = new OfflineAudioContext(1, songLength, 44100);
    var source = offlineCtx.createBufferSource();
    var audioData = request.response;
    audioCtx.decodeAudioData(
      audioData,
      function(buffer) {
        window.originalBuffer = buffer.getChannelData(0);
        source = offlineCtx.createBufferSource();
        source.buffer = buffer;

        // Create a Low Pass Filter to Isolate Low End Beat
        var filter = offlineCtx.createBiquadFilter();
        filter.type = "lowpass";
        filter.frequency.value = 140;
        source.connect(filter);
        filter.connect(offlineCtx.destination);

        // Schedule start at time 0
        source.start(0);

        // Render this low pass filter data to new Audio Context and Save in 'lowPassBuffer'
        offlineCtx.startRendering().then(function(lowPassAudioBuffer) {
          var audioCtx = new (window.AudioContext ||
            window.webkitAudioContext)();
          var song = audioCtx.createBufferSource();
          song.buffer = lowPassAudioBuffer;
          song.connect(audioCtx.destination);

          // Save lowPassBuffer in Global Array
          bats = song.buffer.getChannelData(0);
          song.start();
          // play.onclick = function() {
          //   song.start();
          // };
          window.lowPassBuffer = getClip(10, 10, bats);
          window.lowPassBuffer = getSampleClip(window.lowPassBuffer, 300);
          window.lowPassBuffer = normalizeArray(window.lowPassBuffer);
          var final_tempo = countFlatLineGroupings(window.lowPassBuffer);

          final_tempo = final_tempo * 6;
          console.log("Tempo: " + final_tempo);
          document.getElementById("tempo").innerHTML = "BPM:" + final_tempo;
        });
      },
      function(e) {}
    );
  };
  request.send();
}

function getClip(length, startTime, data) {
  var clip_length = length * 44100;
  var section = startTime * 44100;
  var newArr = [];

  for (var i = 0; i < clip_length; i++) {
    newArr.push(data[startTime + i]);
  }

  return newArr;
}

function getSampleClip(data, samples) {
  var newArray = [];
  var modulus_coefficient = Math.round(data.length / samples);

  for (var i = 0; i < data.length; i++) {
    if (i % modulus_coefficient == 0) {
      newArray.push(data[i]);
    }
  }
  return newArray;
}

function normalizeArray(data) {
  var newArray = [];

  for (var i = 0; i < data.length; i++) {
    newArray.push(Math.abs(Math.round((data[i + 1] - data[i]) * 1000)));
  }

  return newArray;
}

function countFlatLineGroupings(data) {
  var groupings = 0;
  var newArray = normalizeArray(data);

  function getMax(a) {
    var m = -Infinity,
      i = 0,
      n = a.length;

    for (; i != n; ++i) {
      if (a[i] > m) {
        m = a[i];
      }
    }
    return m;
  }

  function getMin(a) {
    var m = Infinity,
      i = 0,
      n = a.length;

    for (; i != n; ++i) {
      if (a[i] < m) {
        m = a[i];
      }
    }
    return m;
  }

  var max = getMax(newArray);
  var min = getMin(newArray);
  var count = 0;
  var threshold = Math.round((max - min) * 0.2);

  for (var i = 0; i < newArray.length; i++) {
    if (
      newArray[i] > threshold &&
      newArray[i + 1] < threshold &&
      newArray[i + 2] < threshold &&
      newArray[i + 3] < threshold &&
      newArray[i + 6] < threshold
    ) {
      count++;
    }
  }

  return count;
}

Instructions:

  • Put both of these files in the same folder and open up the index.html in the browser.
  • Click request stream and allow the microphone to be used.
  • Hit start to record and stop to stop recording.
  • The BPM will be shown below.

Note:
The algorithm used was mainly perfected on high-quality audio files. Performance, therefore, depends on the quality of your microphone.

Hope this solves your problem!!!!

Hi Bharath, Thanks for your solution so far and for following my instructions for deliverables and supplying the instructions. I tried it out though found: The BPM is not correct. E.g. I played this 120bpm track here: https://www.youtube.com/watch?v=HTmKgbT3PFA and the result was 36bpm Also, I would need the BPM live as the music is being played. Hope you can refine your code. Thank you for your efforts so far!
00J 15 days ago
Hi, this algorithm is based on identifying bpm from high-quality audio. The issue you had is based on the quality drop from the microphone. I shall post an update with live BPM but that will not be based on Html and js but with a little more advanced library like node. As you are using a mac, I recommend you to have brew installed using your terminal. Then further I'll provide you with a one-click file which will install all dependencies and start the server. Brew can be found here - https://brew.sh/
Bharath Nair 14 days ago
I have updated the solution. Please take a look
Bharath Nair 14 days ago
Hi, thank you for your thoughtful instructions and screen recording. I ran a quick test and the BPM is promising I will do some more testing. I have some questions: 1) Is there a specific length of time required to 'listen' to calculate the BPM? Sometimes, it takes a short while to change - this is for my understanding 2) Sometimes when I refresh the page, it says "the site can't be reached". And sometimes it works again when I use the browser 'back' button 3) How would I go about installing this on a website? And what are the web hosting requirements? Thanks!
00J 14 days ago
The length depends on the audio file and the quality of the microphone. The BPM is updated the moment the program returns something. The page loading is something wrong with your setup. It works perfectly fine for me and will also do when hosting. For hosting a node app, my recommendation would be https://www.netlify.com/ They offer a 100 Gb bandwidth per month in the free package itself which would be more than sufficient for this website. You can simply drag and drop the folder into netlify after creating an account
Bharath Nair 14 days ago
This is a demo hosting I have done in netlify: https://bountify-demo-bpm-29d122.netlify.com/
Bharath Nair 14 days ago
Thanks, Bharath for your solution and for answering my questions with so much attention to detail!
00J 9 days ago

I'm checked several solutions, and found that https://github.com/dlepaux/realtime-bpm-analyzer working best for me (I'm checked results using several youtube videos with given BPM).

I'm builded and deployed example application to address https://bpm.maslov.dev/userMedia.html .

OLD answer:

Clone https://github.com/simianarmy/bpmcounter/

git clone https://github.com/simianarmy/bpmcounter/
cd bpmcounter

Fix js/main.js:

Replace line 6 (context = new webkitAudioContext(); with context = new (window.AudioContext || window.webkitAudioContext)();

Install dependencies and run the server:

npm install
node server.js

After that you can open http://localhost:8080, after that allow microphone usage and get BPM in browser

Hi drakmail, thank you for your response. I'm only a beginner JS coder, therefore it would be helpful if there is a demo of this so I can see it working. I clicked through the 2 links and I only found code or some other references. Can please supply working links? Thanks!
00J 15 days ago
Did you leave a solution previously? I don't see it anymore... I would need some instruction on how to install LoopBack, thanks.
00J 13 days ago
Updated answer to show first variant
drakmail 13 days ago
PS. You can click "View Timeline" button too see all revisions of answer: https://bountify.co/bounties/js-beat-detector-with-web-audio-input-from-microphone-source/timeline
drakmail 13 days ago
Hi Drakmail, thanks for your reply. I tried your example https://bpm.maslov.dev/userMedia.html and it doesn't work for me. I have pressed the Start button but the BPM remains 0. Microphone access is on.
00J 11 days ago
View Timeline