【发布时间】: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 是我们的分析器等的馈送
我还有 sn-p 代码 on my github.io page - 让阅读控制台更容易。
【问题讨论】:
-
我所做的实验表明,如果“Δ-samples”(按照问题的详细说明计算)是关闭的,它总是太低,永远不会太高,并且它太低的数量总是128 的倍数。
标签: javascript html5-audio audio-streaming audio-recording web-audio-api