(вопрос переписан с интеграцией битов информации из ответов, а также для того, чтобы сделать ее более краткой.)
Я использую analyser=audioContext.createAnalyser()
для обработки аудиоданных, и я пытаюсь лучше понять детали.
Я выбираю fftSize
, скажем, 2048, затем создаю массив buffer
из 2048 чисел с плавающей точкой с помощью Float32Array, а затем в цикле анимации (на большинстве машин вызывается 60 раз в секунду через window.requestAnimationFrame
). ), Я делаю
analyser.getFloatTimeDomainData(buffer);
, который заполнит мой буфер 2048 точками выборочных данных с плавающей запятой.
Когда в следующий раз вызывается обработчик, прошло 1/60 секунды. Чтобы вычислить, сколько это в единицах выборок, мы должны разделить его на продолжительность 1 выборки и получить (1/60) / (1/44100) = 735. Таким образом, происходит следующий вызов обработчика (в среднем) Спустя 735 сэмплов.
Таким образом, между последующими буферами есть перекрытие, например:
Мы знаем из спец. 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 - облегчает чтение консоли.