Reputation: 887
My problem:
I'm trying to merge multiple blob audio
files to a single blob and download it on the page.
What I tried:
I tried to concatenate the Audio blobs in the following ways:
Method - 1:
const url = window.URL.createObjectURL(new Blob(fullBlobArray), {
type: 'audio/*'
});
const a = document.createElement("a");
document.body.appendChild(a);
a.style = "display: none";
a.href = url;
a.download = "testing.wav";
a.click();
URL.revokeObjectURL(url);
a.remove();
Method - 2 (Using - ConcatenateBlobs.js plugin - ConcatenateJS)
ConcatenateBlobs(fullBlobArray, 'audio/wav', function (fullBlob) {
const url = window.URL.createObjectURL(fullBlob);
const a = document.createElement("a");
document.body.appendChild(a);
a.style = "display: none";
a.href = url;
a.download = "testing.wav";
a.click();
URL.revokeObjectURL(url);
a.remove();
//Close the window if it downloaded.
window.close();
Output is explained below:
If you have the following audio blobs:
[audio1, audio2, audio3]
Then, after downloading from the above code, only the Audio
from the first file (i.e. audio1
) is getting played. But the file size of the full blob is the total size of audio1 + audio2 + audio3
I couldn't figure out where I went wrong. Kindly help me in this to get rid of this problem.
Upvotes: 3
Views: 4896
Reputation: 330
Have the same problem, thank @Vikash to bring it here. I'm using ConcatenateBlobs.js
to concat wav blobs and it seems only working on Chrome. Your solution is great but the source is a bit long, so I tried to fix ConcatenateBlobs.js
base on the fact that file length in the header need to be fixed. Luckily, it works:
function ConcatenateBlobs(blobs, type, callback) {
var buffers = [];
var index = 0;
function readAsArrayBuffer() {
if (!blobs[index]) {
return concatenateBuffers();
}
var reader = new FileReader();
reader.onload = function(event) {
buffers.push(event.target.result);
index++;
readAsArrayBuffer();
};
reader.readAsArrayBuffer(blobs[index]);
}
readAsArrayBuffer();
function audioLengthTo32Bit(n) {
n = Math.floor(n);
var b1 = n & 255;
var b2 = (n >> 8) & 255;
var b3 = (n >> 16) & 255;
var b4 = (n >> 24) & 255;
return [b1, b2, b3, b4];
}
function concatenateBuffers() {
var byteLength = 0;
buffers.forEach(function(buffer) {
byteLength += buffer.byteLength;
});
var tmp = new Uint8Array(byteLength);
var lastOffset = 0;
var newData;
buffers.forEach(function(buffer) {
if (type=='audio/wav' && lastOffset > 0) newData = new Uint8Array(buffer, 44);
else newData = new Uint8Array(buffer);
tmp.set(newData, lastOffset);
lastOffset += newData.length;
});
if (type=='audio/wav') {
tmp.set(audioLengthTo32Bit(lastOffset - 8), 4);
tmp.set(audioLengthTo32Bit(lastOffset - 44), 40); // update audio length in the header
}
var blob = new Blob([tmp.buffer], {
type: type
});
callback(blob);
}
}
Upvotes: 3
Reputation: 887
Finally, found a solution!!! Thanks for this StackOverflow article. Highly appreciated for the efforts for this article.
Thanks for the commenting out (@Bergi, @Zac, @Peter Krebs in the comments) that we need to Format the blob according to WAV Format
For merging multiple WAV files into a single file and below is the code:
wav_merger.js
var _index;
function readFileAsync(blob) {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.addEventListener("loadend", function () {
resolve(reader.result);
});
reader.onerror = reject;
reader.readAsArrayBuffer(blob);
})
}
function getBufferFromBlobs(blobArray) {
return new Promise((resolve, reject) => {
var _arrBytes = [];
var _promises = [];
if (blobArray.length > 0) {
$.each(blobArray, function (index, blob) {
_index = index;
var dfd = $.Deferred();
readFileAsync(blob).then(function (byteArray) {
_arrBytes.push(byteArray);
dfd.resolve();
});
_promises.push(dfd);
});
$.when.apply($, _promises).done(function () {
var _blob = combineWavsBuffers(_arrBytes);
resolve(_blob);
});
}
});
}
function loadWav(blobArray) {
return getBufferFromBlobs(blobArray);
debugger;
// .then(function (bufferArray) {
// return combineWavsBuffers(bufferArray); //Combine original wav buffer and play
//});
}
function combineWavsBuffers(bufferArray) {
if (bufferArray.length > 0) {
var _bufferLengths = bufferArray.map(buffer => buffer.byteLength);
// Getting sum of numbers
var _totalBufferLength = _bufferLengths.reduce(function (a, b) {
return a + b;
}, 0);
var tmp = new Uint8Array(_totalBufferLength);
//Get buffer1 audio data to create the new combined wav
var audioData = getAudioData.WavHeader.readHeader(new DataView(bufferArray[0]));
var _bufferLength = 0;
$.each(bufferArray, function (index, buffer) {
//Combine array bytes of original wavs buffers.
tmp.set(new Uint8Array(buffer), _bufferLength);
_bufferLength+= buffer.byteLength;
});
//Send combined buffer and send audio data to create the audio data of combined
var arrBytesFinal = getWavBytes(tmp, {
isFloat: false, // floating point or 16-bit integer
numChannels: audioData.channels,
sampleRate: audioData.sampleRate,
});
//Create a Blob as Base64 Raw data with audio/wav type
return new Blob([arrBytesFinal], { type: 'audio/wav; codecs=MS_PCM' });
}
return null;
}
//Combine two audio .wav buffers.
function combineWavsBuffers1(buffer1, buffer2) {
//Combine array bytes of original wavs buffers
var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
tmp.set(new Uint8Array(buffer1), 0);
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
//Get buffer1 audio data to create the new combined wav
var audioData = getAudioData.WavHeader.readHeader(new DataView(buffer1));
console.log('Audio Data: ', audioData);
//Send combined buffer and send audio data to create the audio data of combined
var arrBytesFinal = getWavBytes(tmp, {
isFloat: false, // floating point or 16-bit integer
numChannels: audioData.channels,
sampleRate: audioData.sampleRate,
});
//Create a Blob as Base64 Raw data with audio/wav type
return new Blob([arrBytesFinal], { type: 'audio/wav; codecs=MS_PCM' });
}
//Other functions //////////////////////////////////////////////////////////////
// Returns Uint8Array of WAV bytes
function getWavBytes(buffer, options) {
const type = options.isFloat ? Float32Array : Uint16Array
const numFrames = buffer.byteLength / type.BYTES_PER_ELEMENT
const headerBytes = getWavHeader(Object.assign({}, options, { numFrames }))
const wavBytes = new Uint8Array(headerBytes.length + buffer.byteLength);
// prepend header, then add pcmBytes
wavBytes.set(headerBytes, 0)
wavBytes.set(new Uint8Array(buffer), headerBytes.length)
return wavBytes
}
// adapted from https://gist.github.com/also/900023
// returns Uint8Array of WAV header bytes
function getWavHeader(options) {
const numFrames = options.numFrames
const numChannels = options.numChannels || 2
const sampleRate = options.sampleRate || 44100
const bytesPerSample = options.isFloat ? 4 : 2
const format = options.isFloat ? 3 : 1
const blockAlign = numChannels * bytesPerSample
const byteRate = sampleRate * blockAlign
const dataSize = numFrames * blockAlign
const buffer = new ArrayBuffer(44)
const dv = new DataView(buffer)
let p = 0
function writeString(s) {
for (let i = 0; i < s.length; i++) {
dv.setUint8(p + i, s.charCodeAt(i))
}
p += s.length
}
function writeUint32(d) {
dv.setUint32(p, d, true)
p += 4
}
function writeUint16(d) {
dv.setUint16(p, d, true)
p += 2
}
writeString('RIFF') // ChunkID
writeUint32(dataSize + 36) // ChunkSize
writeString('WAVE') // Format
writeString('fmt ') // Subchunk1ID
writeUint32(16) // Subchunk1Size
writeUint16(format) // AudioFormat
writeUint16(numChannels) // NumChannels
writeUint32(sampleRate) // SampleRate
writeUint32(byteRate) // ByteRate
writeUint16(blockAlign) // BlockAlign
writeUint16(bytesPerSample * 8) // BitsPerSample
writeString('data') // Subchunk2ID
writeUint32(dataSize) // Subchunk2Size
return new Uint8Array(buffer)
}
function getAudioData() {
function WavHeader() {
this.dataOffset = 0;
this.dataLen = 0;
this.channels = 0;
this.sampleRate = 0;
}
function fourccToInt(fourcc) {
return fourcc.charCodeAt(0) << 24 | fourcc.charCodeAt(1) << 16 | fourcc.charCodeAt(2) << 8 | fourcc.charCodeAt(3);
}
WavHeader.RIFF = fourccToInt("RIFF");
WavHeader.WAVE = fourccToInt("WAVE");
WavHeader.fmt_ = fourccToInt("fmt ");
WavHeader.data = fourccToInt("data");
WavHeader.readHeader = function (dataView) {
var w = new WavHeader();
var header = dataView.getUint32(0, false);
if (WavHeader.RIFF != header) {
return;
}
var fileLen = dataView.getUint32(4, true);
if (WavHeader.WAVE != dataView.getUint32(8, false)) {
return;
}
if (WavHeader.fmt_ != dataView.getUint32(12, false)) {
return;
}
var fmtLen = dataView.getUint32(16, true);
var pos = 16 + 4;
switch (fmtLen) {
case 16:
case 18:
w.channels = dataView.getUint16(pos + 2, true);
w.sampleRate = dataView.getUint32(pos + 4, true);
break;
default:
throw 'extended fmt chunk not implemented';
}
pos += fmtLen;
var data = WavHeader.data;
var len = 0;
while (data != header) {
header = dataView.getUint32(pos, false);
len = dataView.getUint32(pos + 4, true);
if (data == header) {
break;
}
pos += (len + 8);
}
w.dataLen = len;
w.dataOffset = pos + 8;
return w;
};
getAudioData.WavHeader = WavHeader;
}
getAudioData();
custom_script.js
getBufferFromBlobs(fullBlobArray).then(function (singleBlob) {
const url = window.URL.createObjectURL(singleBlob);
const a = document.createElement("a");
document.body.appendChild(a);
a.style = "display: none";
a.href = url;
a.download = "testing.wav";
a.click();
URL.revokeObjectURL(url);
a.remove();
});
Upvotes: 5