Search code examples
javascriptaudioweb-audio-apibeat-detection

How can I use JS WebAudioAPI for beat detection?


I'm interested in using the JavaScript WebAudioAPI to detect song beats, and then render them in a canvas.

I can handle the canvas part, but I'm not a big audio guy and really don't understand how to make a beat detector in JavaScript.

I've tried following this article but cannot, for the life of me, connect the dots between each function to make a functional program.

I know I should show you some code but honestly I don't have any, all my attempts have failed miserably and the relevant code it's in the previously mentioned article.

Anyways I'd really appreciate some guidance, or even better a demo of how to actually detect song beats with the WebAudioAPI.

Thanks!


Solution

  • The main thing to understand about the referenced article by Joe Sullivan is that even though it gives a lot of source code, it's far from final and complete code. To reach a working solution you will still need both some coding and debugging skills.

    This answer draws most of its code from the referenced article, original licensing applies where appropriate.

    Below is a naïve sample implementation for using the functions described by the above article, you still need to figure out correct thresholds for a functional solution.


    The code consists of preparation code written for the answer:

    and then, as described in the article:

    • filtering the audio, in this example with a low-pass filter
    • calculating peaks using a threshold
    • grouping interval counts and then tempo counts

    For the threshold I used an arbitrary value of .98 of the range between maximum and minimum values; when grouping I added some additional checks and arbitrary rounding to avoid possible infinite loops and make it an easy-to-debug sample.

    Note that commenting is scarce to keep the sample implementation brief because:

    • the logic behind processing is explained in the referenced article
    • the syntax can be referenced in the API docs of the related methods

    audio_file.onchange = function() {
      var file = this.files[0];
      var reader = new FileReader();
      var context = new(window.AudioContext || window.webkitAudioContext)();
      reader.onload = function() {
        context.decodeAudioData(reader.result, function(buffer) {
          prepare(buffer);
        });
      };
      reader.readAsArrayBuffer(file);
    };
    
    function prepare(buffer) {
      var offlineContext = new OfflineAudioContext(1, buffer.length, buffer.sampleRate);
      var source = offlineContext.createBufferSource();
      source.buffer = buffer;
      var filter = offlineContext.createBiquadFilter();
      filter.type = "lowpass";
      source.connect(filter);
      filter.connect(offlineContext.destination);
      source.start(0);
      offlineContext.startRendering();
      offlineContext.oncomplete = function(e) {
        process(e);
      };
    }
    
    function process(e) {
      var filteredBuffer = e.renderedBuffer;
      //If you want to analyze both channels, use the other channel later
      var data = filteredBuffer.getChannelData(0);
      var max = arrayMax(data);
      var min = arrayMin(data);
      var threshold = min + (max - min) * 0.98;
      var peaks = getPeaksAtThreshold(data, threshold);
      var intervalCounts = countIntervalsBetweenNearbyPeaks(peaks);
      var tempoCounts = groupNeighborsByTempo(intervalCounts);
      tempoCounts.sort(function(a, b) {
        return b.count - a.count;
      });
      if (tempoCounts.length) {
        output.innerHTML = tempoCounts[0].tempo;
      }
    }
    
    // http://tech.beatport.com/2014/web-audio/beat-detection-using-web-audio/
    function getPeaksAtThreshold(data, threshold) {
      var peaksArray = [];
      var length = data.length;
      for (var i = 0; i < length;) {
        if (data[i] > threshold) {
          peaksArray.push(i);
          // Skip forward ~ 1/4s to get past this peak.
          i += 10000;
        }
        i++;
      }
      return peaksArray;
    }
    
    function countIntervalsBetweenNearbyPeaks(peaks) {
      var intervalCounts = [];
      peaks.forEach(function(peak, index) {
        for (var i = 0; i < 10; i++) {
          var interval = peaks[index + i] - peak;
          var foundInterval = intervalCounts.some(function(intervalCount) {
            if (intervalCount.interval === interval) return intervalCount.count++;
          });
          //Additional checks to avoid infinite loops in later processing
          if (!isNaN(interval) && interval !== 0 && !foundInterval) {
            intervalCounts.push({
              interval: interval,
              count: 1
            });
          }
        }
      });
      return intervalCounts;
    }
    
    function groupNeighborsByTempo(intervalCounts) {
      var tempoCounts = [];
      intervalCounts.forEach(function(intervalCount) {
        //Convert an interval to tempo
        var theoreticalTempo = 60 / (intervalCount.interval / 44100);
        theoreticalTempo = Math.round(theoreticalTempo);
        if (theoreticalTempo === 0) {
          return;
        }
        // Adjust the tempo to fit within the 90-180 BPM range
        while (theoreticalTempo < 90) theoreticalTempo *= 2;
        while (theoreticalTempo > 180) theoreticalTempo /= 2;
    
        var foundTempo = tempoCounts.some(function(tempoCount) {
          if (tempoCount.tempo === theoreticalTempo) return tempoCount.count += intervalCount.count;
        });
        if (!foundTempo) {
          tempoCounts.push({
            tempo: theoreticalTempo,
            count: intervalCount.count
          });
        }
      });
      return tempoCounts;
    }
    
    // http://stackoverflow.com/questions/1669190/javascript-min-max-array-values
    function arrayMin(arr) {
      var len = arr.length,
        min = Infinity;
      while (len--) {
        if (arr[len] < min) {
          min = arr[len];
        }
      }
      return min;
    }
    
    function arrayMax(arr) {
      var len = arr.length,
        max = -Infinity;
      while (len--) {
        if (arr[len] > max) {
          max = arr[len];
        }
      }
      return max;
    }
    <input id="audio_file" type="file" accept="audio/*"></input>
    <audio id="audio_player"></audio>
    <p>
      Most likely tempo: <span id="output"></span>
    </p>