@soundcut/decode-audio-data-fast
Version:
Decode audio data asynchronously, in the browser using BaseAudioContext.decodeAudioData(), as fast as possible.
158 lines (132 loc) • 4.46 kB
JavaScript
import parser from 'mp3-parser';
const CHUNK_MAX_SIZE = 1000 * 1000;
const DEFAULT_CONCURRENCY = 4;
const CONCURRENCY =
((typeof navigator !== 'undefined' && navigator.hardwareConcurrency) || 1) > 2
? navigator.hardwareConcurrency
: DEFAULT_CONCURRENCY;
/**
* Creates a new ArrayBuffer out of two Uint8Arrays
*
* @private
* @param {Uint8Array} baseUint8Array first Uint8Array.
* @param {Uint8Array} buffer second Uint8Array.
* @return {ArrayBuffer} The new ArrayBuffer
*/
function makeChunk(array1, array2) {
const tmp = new Uint8Array(array1.byteLength + array2.byteLength);
tmp.set(array1, 0);
tmp.set(array2, array1.byteLength);
return tmp.buffer;
}
function makeSaveChunk(chunkArrayBuffers, tagsUInt8Array, sourceUInt8Array) {
return function saveChunk(chunk) {
chunkArrayBuffers.push(
makeChunk(
tagsUInt8Array,
sourceUInt8Array.subarray(
chunk.frames[0]._section.offset,
chunk.frames[chunk.frames.length - 1]._section.offset +
chunk.frames[chunk.frames.length - 1]._section.byteLength
)
)
);
};
}
function emptyChunk(chunk) {
chunk.byteLength = 0;
chunk.frames.length = 0;
}
function addChunkFrame(chunk, frame) {
chunk.byteLength = chunk.byteLength + frame._section.byteLength;
chunk.frames.push(frame);
}
const asyncWorker = (source, items, fn, output) => async () => {
let next;
while ((next = items.pop())) {
output[source.get(next)] = await fn(next);
}
};
function getArrayBuffer(file) {
return new Promise((resolve) => {
let fileReader = new FileReader();
fileReader.onloadend = () => {
resolve(fileReader.result);
};
fileReader.readAsArrayBuffer(file);
});
}
// Use a promise wrapper on top of event based syntax
// for browsers (Safari) which do not support promise-based syntax.
function decodeArrayBuffer(audioCtx, arrayBuffer) {
return new Promise(audioCtx.decodeAudioData.bind(audioCtx, arrayBuffer));
}
async function getFileAudioBuffer(file, audioCtx, options = {}) {
/* Copyright (c) 2019, Timothée 'Tim' Pillard, @ziir @tpillard - ISC */
const { native = false, concurrency = CONCURRENCY } = options;
const arrayBuffer = await getArrayBuffer(file);
if (native) {
return decodeArrayBuffer(audioCtx, arrayBuffer);
}
const safari = !!window.webkitAudioContext;
if (safari) {
return getFileAudioBuffer(file, audioCtx, { native: true });
}
const view = new DataView(arrayBuffer);
const tags = parser.readTags(view);
const firstFrame = tags.pop();
const uInt8Array = new Uint8Array(arrayBuffer);
const tagsUInt8Array = uInt8Array.subarray(0, firstFrame._section.offset);
const chunkArrayBuffers = [];
const saveChunk = makeSaveChunk(
chunkArrayBuffers,
tagsUInt8Array,
uInt8Array
);
let chunk = { byteLength: 0, frames: [] };
let next = firstFrame._section.offset + firstFrame._section.byteLength;
while (next) {
const frame = parser.readFrame(view, next);
next = frame && frame._section.nextFrameIndex;
if (frame) {
const chunkEnd =
chunk && chunk.byteLength + frame._section.byteLength >= CHUNK_MAX_SIZE;
if (chunkEnd) {
saveChunk(chunk);
emptyChunk(chunk);
}
addChunkFrame(chunk, frame);
}
if (chunk && (!frame || !next)) {
saveChunk(chunk);
}
}
const workers = [];
const source = new Map(chunkArrayBuffers.map((chunk, idx) => [chunk, idx]));
const audioBuffers = new Array(chunkArrayBuffers.length);
const decode = decodeArrayBuffer.bind(null, audioCtx);
for (let i = 0; i < Math.min(concurrency, source.size); i++) {
workers.push(
asyncWorker(source, chunkArrayBuffers, decode, audioBuffers)()
);
}
await Promise.all(workers);
const { numberOfChannels, sampleRate } = audioBuffers[0];
let length = audioBuffers.reduce((acc, current) => acc + current.length, 0);
const audioBuffer = audioCtx.createBuffer(
numberOfChannels,
length,
sampleRate
);
for (let j = 0; j < numberOfChannels; j++) {
const channelData = audioBuffer.getChannelData(j);
let offset = 0;
for (let i = 0; i < audioBuffers.length; i++) {
channelData.set(audioBuffers[i].getChannelData(j), offset);
offset += audioBuffers[i].length;
}
}
return audioBuffer;
}
export { getFileAudioBuffer };
//# sourceMappingURL=decode-audio-data-fast.esm.js.map