如何使用JS WebAudioAPI进行节拍检测?

时间:2015-05-07 20:06:14

标签: javascript audio web-audio beat-detection

我有兴趣使用JavaScript WebAudioAPI来检测歌曲节拍,然后在画布中渲染它们。

我可以处理画布部分,但我不是一个大音频人,真的不明白如何用JavaScript制作节拍探测器。

我已经尝试过关注this article,但在我的生活中,不能连接每个函数之间的点来制作功能性程序。

我知道我应该向你展示一些代码,但老实说,我没有任何代码,我的所有尝试都失败了,并且在前面提到的文章中都有相关的代码。

无论如何,我真的很感激一些指导,甚至更好地演示如何用WebAudioAPI实际检测歌曲节拍。

谢谢!

2 个答案:

答案 0 :(得分:14)

要理解the referenced article by Joe Sullivan的主要内容是即使它提供了大量的源代码,但它远不是最终的完整代码。要达到可行的解决方案,您仍然需要一些编码和调试技能。

此答案从引用的文章中抽取了大部分代码,原始许可适用于适当的情况。

以下是使用上述文章所述功能的简单示例实现。

代码包含为答案编写的准备代码:

然后,如文章所述:

  • 过滤音频,在此示例中使用low-pass filter
  • 使用阈值计算峰值
  • 分组间隔计数,然后是速度计数

对于阈值,我使用了最大值和最小值之间范围的任意值.98;在分组时,我添加了一些额外的检查和任意舍入,以避免可能的无限循环,并使其成为易于调试的样本。

请注意,评论很少,以保持示例实现简短,因为:

  • 处理背后的逻辑在参考文章中解释
  • 语法可以在相关方法的API文档中引用

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>

答案 1 :(得分:7)

我在这里写了一个教程,展示了如何使用javascript Web Audio API。

https://askmacgyver.com/blog/tutorial/how-to-implement-tempo-detection-in-your-application

步骤概要

  1. 将音频文件转换为数组缓冲区
  2. 通过低通滤波器运行阵列缓冲区
  3. 从阵列缓冲区中修剪10秒的剪辑
  4. 向下采样数据
  5. 规范化数据
  6. 数量分组
  7. 从分组计数中推断速度
  8. 下面的代码解决了这个问题。

    将音频文件加载到阵列缓冲区并运行低通滤波器

    function createBuffers(url) {
    
     // Fetch Audio Track via AJAX with URL
     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);
        source = offlineCtx.createBufferSource();
        var audioData = request.response;
        audioCtx.decodeAudioData(audioData, function(buffer) {
    
             window.originalBuffer = buffer.getChannelData(0);
             var 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);
    
                // 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
                 window.lowPassBuffer = song.buffer.getChannelData(0);
                 console.log("Low Pass Buffer Rendered!");
                });
    
            },
            function(e) {});
     }
     request.send();
    }
    
    
    createBuffers('https://askmacgyver.com/test/Maroon5-Moves-Like-Jagger-128bpm.mp3');
    

    你现在有一个低通过滤歌曲的数组缓冲区(原创)

    它由许多条目组成,sampleRate(44100乘以歌曲的秒数)。

    window.lowPassBuffer  // Low Pass Array Buffer
    window.originalBuffer // Original Non Filtered Array Buffer
    

    从歌曲中剪掉10秒剪辑

    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[section + i]);
      }
    
      return newArr;
    }
    
    // Overwrite our array buffer to a 10 second clip starting from 00:10s
    window.lowPassFilter = getClip(10, 10, lowPassFilter);
    

    羽绒样本

    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;
    }
    
    // Overwrite our array to down-sampled array.
    lowPassBuffer = getSampleClip(lowPassFilter, 300);
    

    规范化您的数据

    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;
    }
    
    // Overwrite our array to the normalized array
    lowPassBuffer = normalizeArray(lowPassBuffer);
    

    计算扁线分组

    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;
    }
    
    // Count the Groupings
    countFlatLineGroupings(lowPassBuffer);
    

    将10秒分组计数缩放至60秒以获得每分钟节拍数

    var final_tempo = countFlatLineGroupings(lowPassBuffer);
    
    // final_tempo will be 21
    final_tempo = final_tempo * 6;
    
    console.log("Tempo: " + final_tempo);
    // final_tempo will be 126