смещение буфера getFloatTimeDomainData веб-анализатора относительно буферов в другое время и буфер «полного файла» - PullRequest
0 голосов
/ 02 апреля 2020

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

Я использую analyser=audioContext.createAnalyser() для обработки аудиоданных, и я пытаюсь лучше понять детали.

Я выбираю fftSize, скажем, 2048, затем создаю массив buffer из 2048 чисел с плавающей точкой с помощью Float32Array, а затем в цикле анимации (на большинстве машин вызывается 60 раз в секунду через window.requestAnimationFrame). ), Я делаю

analyser.getFloatTimeDomainData(buffer);

, который заполнит мой буфер 2048 точками выборочных данных с плавающей запятой.

Когда в следующий раз вызывается обработчик, прошло 1/60 секунды. Чтобы вычислить, сколько это в единицах выборок, мы должны разделить его на продолжительность 1 выборки и получить (1/60) / (1/44100) = 735. Таким образом, происходит следующий вызов обработчика (в среднем) Спустя 735 сэмплов.

Таким образом, между последующими буферами есть перекрытие, например:

buffer overlap

Мы знаем из спец. c (поиск по «кванту визуализации»), что все происходит в «размерах чанка», кратных 128. Таким образом (с точки зрения обработки звука) можно ожидать, что следующий вызов обработчика обычно будет либо 5 * 128 = 640 отсчетов позже, или же 6 * 128 = 768 отсчетов позже - те, которые кратны 128, ближайшим к 735 отсчетам = (1/60) секунды.

Называя это количество "Δ-отсчетами", Как мне узнать, что это (во время каждого вызова обработчика), 640 или 768 или что-то еще?

Надежно, как это:

Рассмотрим «старый буфер» (из предыдущего вызова обработчика) ). Если вы удалили «Δ-samples» много выборок в начале, скопируйте остаток, а затем добавьте «Δ-samples» много новых выборок, которые должны быть текущим буфером. И действительно, я попробовал это, и это так. Оказывается, «Δ-выборки» часто составляют 384, 512, 896. Это тривиально, но требует времени, чтобы определить «Δ-выборки» в al oop.

Я хотел бы вычислить «Δ-выборки» «без выполнения этого l oop.

Можно подумать, что сработает следующее:

(audioContext.currentTime() - (результат audioContext.currentTime() во время последнего запуска обработчика)) / (продолжительность из 1 образца)

Я попробовал это (см. код ниже, где я также «склеиваю» различные буферы, пытаясь восстановить исходный буфер), и - удивительно - это работает примерно в 99,9% времени в Chrome, и примерно в 95% случаев в Firefox.

Я также пытался audioContent.getOutputTimestamp().contextTime, который не работает в Chrome и работает на 9?% В Firefox.

Есть ли способ найти «Δ-samples» (не глядя на буферы), который работает надежно?

Второй вопрос, «восстановленный» буфер (все буферы из обратных вызовов, сшитые вместе), и оригинальный звуковой буфер не точно такой же, есть некоторые (маленький, но заметный, больше, чем обычно ошибка "), и это больше в Firefox.

Откуда это? - Вы знаете, как я понимаю spe c, они должны быть одинаковыми.

var soundFile = 'https://mathheadinclouds.github.io/audio/sounds/la.mp3';
var audioContext = null;
var isPlaying = false;
var sourceNode = null;
var analyser = null;
var theBuffer = null;
var reconstructedBuffer = null;
var soundRequest = null;
var loopCounter = -1;
var FFT_SIZE = 2048;
var rafID = null;
var buffers = [];
var timesSamples = [];
var timeSampleDiffs = [];
var leadingWaste = 0;

window.addEventListener('load', function() {
  soundRequest = new XMLHttpRequest();
  soundRequest.open("GET", soundFile, true);
  soundRequest.responseType = "arraybuffer";
  //soundRequest.onload = function(evt) {}
  soundRequest.send();
  var btn = document.createElement('button');
  btn.textContent = 'go';
  btn.addEventListener('click', function(evt) {
    goButtonClick(this, evt)
  });
  document.body.appendChild(btn);
});

function goButtonClick(elt, evt) {
  initAudioContext(togglePlayback);
  elt.parentElement.removeChild(elt);
}

function initAudioContext(callback) {
  audioContext = new AudioContext();
  audioContext.decodeAudioData(soundRequest.response, function(buffer) {
    theBuffer = buffer;
    callback();
  });
}

function createAnalyser() {
  analyser = audioContext.createAnalyser();
  analyser.fftSize = FFT_SIZE;
}

function startWithSourceNode() {
  sourceNode.connect(analyser);
  analyser.connect(audioContext.destination);
  sourceNode.start(0);
  isPlaying = true;
  sourceNode.addEventListener('ended', function(evt) {
    sourceNode = null;
    analyser = null;
    isPlaying = false;
    loopCounter = -1;
    window.cancelAnimationFrame(rafID);
    console.log('buffer length', theBuffer.length);
    console.log('reconstructedBuffer length', reconstructedBuffer.length);
    console.log('audio callback called counter', buffers.length);
    console.log('root mean square error', Math.sqrt(checkResult() / theBuffer.length));
    console.log('lengths of time between requestAnimationFrame callbacks, measured in audio samples:');
    console.log(timeSampleDiffs);
    console.log(
      timeSampleDiffs.filter(function(val) {
        return val === 384
      }).length,
      timeSampleDiffs.filter(function(val) {
        return val === 512
      }).length,
      timeSampleDiffs.filter(function(val) {
        return val === 640
      }).length,
      timeSampleDiffs.filter(function(val) {
        return val === 768
      }).length,
      timeSampleDiffs.filter(function(val) {
        return val === 896
      }).length,
      '*',
      timeSampleDiffs.filter(function(val) {
        return val > 896
      }).length,
      timeSampleDiffs.filter(function(val) {
        return val < 384
      }).length
    );
    console.log(
      timeSampleDiffs.filter(function(val) {
        return val === 384
      }).length +
      timeSampleDiffs.filter(function(val) {
        return val === 512
      }).length +
      timeSampleDiffs.filter(function(val) {
        return val === 640
      }).length +
      timeSampleDiffs.filter(function(val) {
        return val === 768
      }).length +
      timeSampleDiffs.filter(function(val) {
        return val === 896
      }).length
    )
  });
  myAudioCallback();
}

function togglePlayback() {
  sourceNode = audioContext.createBufferSource();
  sourceNode.buffer = theBuffer;
  createAnalyser();
  startWithSourceNode();
}

function myAudioCallback(time) {
  ++loopCounter;
  if (!buffers[loopCounter]) {
    buffers[loopCounter] = new Float32Array(FFT_SIZE);
  }
  var buf = buffers[loopCounter];
  analyser.getFloatTimeDomainData(buf);
  var now = audioContext.currentTime;
  var nowSamp = Math.round(audioContext.sampleRate * now);
  timesSamples[loopCounter] = nowSamp;
  var j, sampDiff;
  if (loopCounter === 0) {
    console.log('start sample: ', nowSamp);
    reconstructedBuffer = new Float32Array(theBuffer.length + FFT_SIZE + nowSamp);
    leadingWaste = nowSamp;
    for (j = 0; j < FFT_SIZE; j++) {
      reconstructedBuffer[nowSamp + j] = buf[j];
    }
  } else {
    sampDiff = nowSamp - timesSamples[loopCounter - 1];
    timeSampleDiffs.push(sampDiff);
    var expectedEqual = FFT_SIZE - sampDiff;
    for (j = 0; j < expectedEqual; j++) {
      if (reconstructedBuffer[nowSamp + j] !== buf[j]) {
        console.error('unexpected error', loopCounter, j);
        // debugger;
      }
    }
    for (j = expectedEqual; j < FFT_SIZE; j++) {
      reconstructedBuffer[nowSamp + j] = buf[j];
    }
    //console.log(loopCounter, nowSamp, sampDiff);
  }
  rafID = window.requestAnimationFrame(myAudioCallback);
}

function checkResult() {
  var ch0 = theBuffer.getChannelData(0);
  var ch1 = theBuffer.getChannelData(1);
  var sum = 0;
  var idxDelta = leadingWaste + FFT_SIZE;
  for (var i = 0; i < theBuffer.length; i++) {
    var samp0 = ch0[i];
    var samp1 = ch1[i];
    var samp = (samp0 + samp1) / 2;
    var check = reconstructedBuffer[i + idxDelta];
    var diff = samp - check;
    var sqDiff = diff * diff;
    sum += sqDiff;
  }
  return sum;
}

В приведенном выше фрагменте я делаю следующее. Я загружаю XMLHttpRequest 1-секундный аудиофайл mp3 со своей страницы github.io (я пою 'la' в течение 1 секунды). После загрузки отображается кнопка с надписью «go», и после нажатия на нее звук воспроизводится путем помещения его в узел bufferSource и последующего выполнения команды .start. bufferSource является источником для нашего анализатора, и так далее

связанный вопрос

У меня также есть код сниппета на моей странице github.io - облегчает чтение консоли.

Ответы [ 3 ]

1 голос
/ 02 апреля 2020

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

fftSize на самом деле не имеет значения здесь вы хотите рассчитать, сколько отсчетов было пройдено с момента последнего кадра.

Чтобы рассчитать это, вам просто нужно

  • Измерить время, прошедшее с последнего кадра.
  • Разделите это время на время одного кадра.

Время одного кадра просто 1 / context.sampleRate.
Так что на самом деле все, что вам нужно, это currentTime - previousTime * ( 1 / sampleRate) и вы найдете индекс в последнем кадре, где данные начинают повторяться в новом кадре.

И только тогда, если вам нужен индекс в новом кадре, вы вычли бы этот индекс из fftSize.

Теперь, почему у вас иногда есть пробелы, это потому, что AudioContext.prototype.currentTime возвращает временную метку начала блока next для передачи на график.
Нам нужен здесь AudioContext.prototype.getOuputTimestamp().contextTime, который представляет метку времени сейчас , на той же основе, что и currentTime (т.е. создание контекста).

(function loop(){requestAnimationFrame(loop);})();
(async()=>{
  const ctx = new AudioContext();
  
  const buf = await fetch("https://upload.wikimedia.org/wikipedia/en/d/d3/Beach_Boys_-_Good_Vibrations.ogg").then(r=>r.arrayBuffer());
  const aud_buf = await ctx.decodeAudioData(buf);
  const source = ctx.createBufferSource();
  source.buffer = aud_buf;
  source.loop = true;
  
  const analyser = ctx.createAnalyser();
  const fftSize = analyser.fftSize = 2048;
  source.loop = true;
  source.connect( analyser );
  source.start(0);
  
  // for debugging we use two different buffers
  const arr1 = new Float32Array( fftSize );
  const arr2 = new Float32Array( fftSize );

  const single_sample_dur = (1 / ctx.sampleRate);
  console.log( 'single sample duration (ms)', single_sample_dur * 1000);

  onclick = e => {
    if( ctx.state === "suspended" ) {
      ctx.resume();
      return console.log( 'starting context, please try again' );
    }
    
    console.log( '-------------' );
    
    requestAnimationFrame( () => {
      // first frame
      const time1 = ctx.getOutputTimestamp().contextTime;
      analyser.getFloatTimeDomainData( arr1 );
      
      requestAnimationFrame( () => {
        // second frame
        const time2 = ctx.getOutputTimestamp().contextTime;
        analyser.getFloatTimeDomainData( arr2 );
                
        const elapsed_time = time2 - time1;
        console.log( 'elapsed time between two frame (ms)', elapsed_time * 1000 );
        
        const calculated_index = fftSize - Math.round( elapsed_time / single_sample_dur );
        console.log( 'calculated index of new data', calculated_index );

        // for debugging we can just search for the first index where the data repeats
        const real_time = fftSize - arr1.indexOf( arr2[ 0 ] );
        console.log( 'real index', real_time > fftSize ? 0 : real_time );
        
        if( calculated_index !== real_time > fftSize ? 0 : real_time ) {
          console.error( 'different' );
        }
       
      });
    });
  };
  document.body.classList.add('ready');

})().catch( console.error );
body:not(.ready) pre { display: none; }
click to record two new frames
1 голос
/ 02 апреля 2020

Я думаю, что AnalyserNode - это не то, что вам нужно в этой ситуации. Вы хотите получить данные и синхронизировать их с raf. Используйте ScriptProcessorNode или AudioWorkletNode, чтобы получить данные. Тогда вы получите все данные по мере поступления. Никаких проблем с перекрытием, отсутствием данных или чем-либо еще.

Обратите также внимание, что часы для raf и аудио могут отличаться и, следовательно, со временем все может дрейфовать. Вам придется компенсировать это самостоятельно, если вам нужно.

1 голос
/ 02 апреля 2020

К сожалению, нет способа узнать точный момент времени, когда были получены данные, возвращенные AnalyserNode. Но вы можете быть на правильном пути с вашим текущим подходом.

Все значения, возвращаемые AnalyserNode, основаны на "current-time-domain-data" . Это в основном внутренний буфер AnalyserNode в определенный момент времени. Поскольку API Web Audio имеет фиксированный квант рендеринга из 128 сэмплов, я ожидаю, что этот буфер будет развиваться с шагом 128 сэмплов. Но currentTime обычно развивается с шагом 128 сэмплов.

Кроме того, AnalyserNode обладает свойством smoothingTimeConstant. Он отвечает за «размывание» возвращаемых значений. Значение по умолчанию составляет 0,8. Для вашего случая использования вы, вероятно, захотите установить его на 0.

EDIT : Как отметил Рэймонд Той в комментариях, smoothingtimeconstant влияет только на данные частоты. Поскольку вопрос касается getFloatTimeDomainData (), это не повлияет на возвращаемые значения.

Надеюсь, это поможет, но я думаю, что будет проще получить все сэмплы вашего аудиосигнала, используя AudioWorklet , Это определенно будет более надежным.

...