Как я могу использовать JS WebAudioAPI для обнаружения ударов?

Я заинтересован в использовании JavaScript WebAudioAPI обнаруживать ритмы песен, а затем рендерить их на холсте.

Я могу справиться с частью холста, но я не большой аудио-парень и действительно не понимаю, как сделать детектор ударов в JavaScript.

Я пытался следовать этой статье, но не могу, на всю жизнь, соединить точки между каждой функцией, чтобы создать функциональную программу.

Я знаю, что должен показать вам некоторый код, но, честно говоря, у меня его нет, все мои попытки потерпели неудачу, и соответствующий код находится в ранее упомянутой статье.

В любом случае, я бы очень признателен за некоторые рекомендации или даже лучше за демонстрацию того, как на самом деле определять ритмы песен с помощью WebAudioAPI,

Спасибо!

2 ответа

Решение

Главное, что нужно понять в ссылочной статье Джо Салливана, это то, что, хотя она дает много исходного кода, она далека от окончательного и полного кода. Чтобы найти работающее решение, вам все равно понадобятся навыки кодирования и отладки.

Этот ответ берет большую часть своего кода из ссылочной статьи, где применимо оригинальное лицензирование.

Ниже приведен простой пример реализации функций, описанных в статье выше.


Код состоит из кода подготовки, написанного для ответа:

а затем, как описано в статье:

  • фильтрация звука, в этом примере с фильтром нижних частот
  • расчет пиков с использованием порога
  • отсчет интервала группировки и затем темпа

Для порога я использовал произвольное значение 0,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://stackru.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>

Я написал здесь учебное пособие, в котором показано, как это сделать с помощью 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. Infer Tempo от количества групп

Этот код ниже делает тяжелую работу.

Загрузите аудиофайл в буфер массива и пропустите фильтр низких частот

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
Другие вопросы по тегам