【问题标题】:web audio analyser's getFloatTimeDomainData buffer offset wrt buffers at other times and wrt buffer of 'complete file'网络音频分析器的 getFloatTimeDomainData 缓冲区偏移量 wrt 缓冲区在其他时间和“完整文件”的 wrt 缓冲区
【发布时间】:2020-07-13 22:07:40
【问题描述】:

(问题重写整合了答案中的一些信息,并使其更简洁。)

我使用analyser=audioContext.createAnalyser() 来处理音频数据,并试图更好地了解细节。

我选择一个fftSize,比如 2048,然后使用 Float32Array 创建一个包含 2048 个浮点数的数组 buffer,然后在动画循环中 (在大多数机器上每秒调用 60 次,通过 window.requestAnimationFrame),我愿意

analyser.getFloatTimeDomainData(buffer);

这将用 2048 个浮点样本数据点填充我的缓冲区。

当下次调用处理程序时,已经过了 1/60 秒。要计算以样本为单位的数量, 我们必须将它除以 1 个样本的持续时间,得到 (1/60)/(1/44100) = 735。 因此,下一次处理程序调用(平均)在 735 个样本之后发生。

所以后面的缓冲区有重叠,像这样:

我们从the spec(搜索“渲染量子”)得知一切都发生在“块大小”中,即 128 的倍数。 所以(就音频处理而言),人们会期望下一个处理程序调用通常是 5*128 = 640 个样本之后, 否则 6*128 = 768 个样本之后 - 128 的倍数最接近 735 个样本 = (1/60) 秒。

将此数量称为“Δ-samples”,我如何找出它是什么(在每个处理程序调用期间),640 或 768 或其他什么?

可靠,像这样:

考虑“旧缓冲区”(来自先前的处理程序调用)。如果您在开头删除“Δ-samples”许多样本,复制剩余部分,然后附加“Δ-samples”许多新样本,那应该是当前缓冲区。确实,我试过了, 就是这样。事实证明,“Δ-samples”通常是 384、512、896。确定它很简单但很耗时 循环中的“Δ-samples”。

我想在不执行该循环的情况下计算“Δ-samples”。

人们会认为以下方法会起作用:

(audioContext.currentTime() - (audioContext.currentTime() 在上次处理程序运行期间的结果))/(1 个样本的持续时间)

我试过了(参见下面的代码,我还将各种缓冲区“缝合在一起”,试图重建原始缓冲区), 而且 - 令人惊讶的是 - 它在 Chrome 中大约 99.9% 的时间都可以正常工作,在 Firefox 中大约有 95% 的时间。

我也尝试了audioContent.getOutputTimestamp().contextTime,它在 Chrome 中不起作用,在 Firefox 中起作用 9?%。

有什么方法可以找到可靠的“Δ-samples”(不查看缓冲区)?

第二个问题,“重构”缓冲区(所有来自回调的缓冲区拼接在一起),以及原始声音缓冲区 不完全相同,有一些(小,但值得注意,比通常的“舍入误差”更大)差异,并且在 Firefox 中更大。

这是从哪里来的? - 你知道,据我了解,那些应该是一样的。

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;
}

在上面的 sn-p 中,我执行以下操作。我从我的 github.io 页面加载 XMLHttpRequest 一个 1 秒的 mp3 音频文件(我唱 'la' 1 秒)。加载后,会显示一个按钮,说“开始”,按下该按钮后,通过将音频放入 bufferSource 节点然后对其执行.start 来播放音频。 bufferSource 是我们的分析器等的馈送

related question

我还有 sn-p 代码 on my github.io page - 让阅读控制台更容易。

【问题讨论】:

  • 我所做的实验表明,如果“Δ-samples”(按照问题的详细说明计算)是关闭的,它总是太低,永远不会太高,并且它太低的数量总是128 的倍数。

标签: javascript html5-audio audio-streaming audio-recording web-audio-api


【解决方案1】:

不幸的是,无法找出捕获AnalyserNode 返回的数据的确切时间点。但您目前的方法可能走在正确的轨道上。

AnalyserNode 返回的所有值均基于"current-time-domain-data"。这基本上是AnalyserNode 在某个时间点的内部缓冲区。由于 Web 音频 API 具有 128 个样本的固定渲染量,我希望这个缓冲区也以 128 个样本的步长发展。但是currentTime 通常已经以 128 个样本为步长。

此外,AnalyserNode 有一个 smoothingTimeConstant 属性。它负责“模糊”返回值。默认值为 0.8。对于您的用例,您可能希望将其设置为 0。

编辑:正如 Raymond Toy 在 cmets 中指出的那样,smoothingtimeconstant 仅对频率数据有影响。由于问题是关于 getFloatTimeDomainData() 它不会影响返回值。

我希望这会有所帮助,但我认为使用 AudioWorklet 获取音频信号的所有样本会更容易。肯定会更靠谱。

【讨论】:

  • 啊,规范提到了布莱克曼窗口。这解释了很多——比如模糊,至少是潜在的。谢谢!我看了一个smoothingTimeConstant,然后摆弄了一下。它没有任何效果。另外,我马上推测 Firefox 可能有不同的平滑时间常数,这可以解释 FF 中更高的 rms 错误。但并非如此 - 它在 FF 中也是 0.8,就像在 Chrome 中一样。奇怪的。规范将 128 称为“渲染量子”,这很好。您有 AudioWorklet 的示例代码吗?
  • Blackman 窗口和 SmoothingTimeConstant 仅在您需要频率数据时适用。时域数据不会以任何方式修改。
  • 谢谢雷蒙德。我编辑了答案,提到smoothingtimeconstant 将无效。
  • 对不起 mathheadinclouds,smoothingtimeconstant 上的误导性信息。 Chrome 团队创建了一些有用的演示,展示了如何使用 AudioWorklet。 googlechromelabs.github.io/web-audio-samples/audio-worklet
  • Firefox 支持即将推出。它已经在 Nightly 中启用。你也可以使用像 standardized-audio-contextGoogleChromeLabs/audioworklet-polyfilljariseon/audioworklet-polyfill 这样的 polyfill。他们都使用AudioWorklet(如果可用),否则回退到ScriptProcessorNode
【解决方案2】:

我并没有真正关注你的数学,所以我不能确切地说出你做错了什么,但你似乎以一种过于复杂的方式看待这个问题。

fftSize 在这里并不重要,您要计算的是自上一帧以来通过了多少样本。

要计算这个,你只需要

  • 测量从上一帧经过的时间。
  • 将此时间除以一帧的时间。

单帧的时间,就是1 / context.sampleRate.
所以实际上你只需要currentTime - previousTime * ( 1 / sampleRate),你会在最后一帧中找到索引,数据开始在新帧中重复。

只有这样,如果你想要新框架中的索引,你会从 fftSize 中减去这个索引。

现在为什么有时会有间隙,这是因为 AudioContext.prototype.currentTime 返回要传递给图表的 下一个 块开始的时间戳。
这里我们想要的是AudioContext.prototype.getOuputTimestamp().contextTime,它代表now的时间戳,与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; }
&lt;pre&gt;click to record two new frames&lt;/pre&gt;

【讨论】:

  • @mathheadinclouds:然后请考虑删除您的“声音太大”的 cmets 并删除反对票。
  • @HovercraftFullOfEels 我同意 cmets 应该消失,投反对票,他们确实按照自己的意愿处理。没有人应该告诉他们如何处理它。
  • @mathheadinclouds cmets 在这里是短暂的。他们来这里是为了告诉作者他们所写内容的一些问题。你这样做了,这是对评论的合理使用。你以一种非常激进的方式这样做,这不好,但我没有冒犯。现在请记住下次。有时我会强迫自己睡个好觉,然后再在这里回复。但是,已经听到了这些 cmets 的信息,我编辑了我的问题,我认为您需要什么,或者至少以比以前更好的方式。您的 cmets 不再适用,您可以删除它们。
  • @mathheadinclouds 如果您现在对编辑还有其他顾虑,请随时编写新的 cmets,但我只会在 12 小时内处理它们。
  • 确实,我也试过了,而且 audioContent.getOutputTimestamp().contextTime 在 Firefox 上工作——大部分时间。奇怪的是,无论你做什么,它只在大多数时候有效。也许明年会奏效。
【解决方案3】:

我认为 AnalyserNode 在这种情况下不是您想要的。您想要获取数据并使其与 raf 保持同步。使用 ScriptProcessorNode 或 AudioWorkletNode 来获取数据。然后,您将获得所有数据。重叠、丢失数据或其他任何问题都没有问题。

另请注意,raf 和音频的时钟可能不同,因此事情可能会随着时间的推移而漂移。如果需要,您必须自己补偿。

【讨论】:

  • 我很困惑。 ScriptProcessorNode 具有 .onaudioprocess ,您可以向其提供回调函数,该回调函数已被定期调用。所以我的第一个猜测是你在那个回调函数中做了所有的事情。那么 raf (requestAnimationFrame) 是从哪里来的呢?我需要那个做什么?您所说的可能完全有道理,但是没有示例代码,我不太理解您的意思。至于不同的时钟,是的,确实如此。我正在尝试仅使用“音频时间”。你的建议是相同的还是不同的?
  • 我有接受你回答的冲动——因为我认为你是对的。我真的应该使用 ScriptProcessorNode 或 AudioWorkletNode。至于ScriptProcessorNode,我检查并确认:没有重叠。再说一次,我想知道是否有可能在不查看缓冲区的情况下可靠地找到 AnalyserNode 的缓冲区重叠量。仍然希望我错了,这是可能的。
  • 对于第一个问题,你大概可以缓冲从 ScriptProcessorNode 接收到的数据,当调用 raf 时,从缓冲中获取合适的数据集。或者,只要有新缓冲区,您就可以更新图表。我不做图形/raf,所以我在这里不是很了解。
  • 对于第二个问题,我认为您几乎必须检查来自 AnalyserNode 的数据。来自 raf 的时间并不完美,从 AnalyserNode 获取数据的时间也不完美,因为音频线程独立于主线程运行,它可以在意外时间更新数据。 (但是,当然,不是在您读取数据的时候!)
猜你喜欢
  • 2017-10-07
  • 1970-01-01
  • 2021-09-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-11-14
  • 1970-01-01
相关资源
最近更新 更多