Vikash
Vikash

Reputation: 887

How can I merge multiple WAV audio blobs into one?

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

Answers (2)

Vu Viet Hung
Vu Viet Hung

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

Vikash
Vikash

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

Related Questions