1

I have the recorder.js which will record the audio and takes buffer inputs but I want to downsample the audio buffers but I am lot confused where to call it though I have written it. Please check my function and if possible please suggest where to call it.

import InlineWorker from 'inline-worker';

export class Recorder {
    config = {
        bufferLen: 4096,
        numChannels: 2,
        mimeType: 'audio/mp3'
    };

    recording = false;

    callbacks = {
        getBuffer: [],
        exportWAV: []
    };

    constructor(source, cfg) {
        Object.assign(this.config, cfg);
        this.context = source.context;
        this.node = (this.context.createScriptProcessor ||
        this.context.createJavaScriptNode).call(this.context,
            this.config.bufferLen, this.config.numChannels, this.config.numChannels);

        this.node.onaudioprocess = (e) => {
            if (!this.recording) return;

            var buffer = [];
            for (var channel = 0; channel < this.config.numChannels; channel++) {
                buffer.push(e.inputBuffer.getChannelData(channel));
            }
            this.worker.postMessage({
                command: 'record',
                buffer: buffer
            });
        };

        source.connect(this.node);
        this.node.connect(this.context.destination);    //this should not be necessary

        let self = {};
        this.worker = new InlineWorker(function () {
            let recLength = 0,
                recBuffers = [],
                sampleRate,
                numChannels;

            this.onmessage = function (e) {
                switch (e.data.command) {
                    case 'init':
                        init(e.data.config);
                        break;
                    case 'record':
                        record(e.data.buffer);
                        break;
                    case 'exportWAV':
                        exportWAV(e.data.type);
                        break;
                    case 'getBuffer':
                        getBuffer();
                        break;
                    case 'clear':
                        clear();
                        break;
                }
            };

            function init(config) {
                sampleRate = config.sampleRate;
                numChannels = config.numChannels;
                initBuffers();
            }

            function record(inputBuffer) {
                for (var channel = 0; channel < numChannels; channel++) {
                    recBuffers[channel].push(inputBuffer[channel]);
                }
                recLength += inputBuffer[0].length;
            }

             function exportWAV(type) {
            let buffers = [];
            for (let channel = 0; channel < numChannels; channel++) {
                buffers.push(mergeBuffers(recBuffers[channel], recLength));
            }
            let interleaved;
            if (numChannels === 2) {
                interleaved = interleave(downsampleBuffer(buffers[0]), downsampleBuffer(buffers[1]));
            } else {
                interleaved = buffers[0];
            }

            let dataview = encodeWAV(interleaved);
            let audioBlob = new Blob([dataview], {type: type});

            this.postMessage({command: 'exportWAV', data: audioBlob});
        }


    function downsampleBuffer(buffer) {
        if (16000 === sampleRate) {
          return buffer;
        }
    var sampleRateRatio = sampleRate / 16000;
    var newLength = Math.round(buffer.length / sampleRateRatio);
    var result = new Float32Array(newLength);
    var offsetResult = 0;
    var offsetBuffer = 0;
    while (offsetResult < result.length) {
      var nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
      var accum = 0,
        count = 0;
      for (var i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
        accum += buffer[i];
        count++;
      }
      result[offsetResult] = accum / count;
      offsetResult++;
      offsetBuffer = nextOffsetBuffer;
    }
    return result;
  }

            function getBuffer() {
                let buffers = [];
                for (let channel = 0; channel < numChannels; channel++) {
                    buffers.push(mergeBuffers(recBuffers[channel], recLength));
                }
                this.postMessage({command: 'getBuffer', data: buffers});
            }

            function clear() {
                recLength = 0;
                recBuffers = [];
                initBuffers();
            }

            function initBuffers() {
                for (let channel = 0; channel < numChannels; channel++) {
                    recBuffers[channel] = [];
                }
            }

            function mergeBuffers(recBuffers, recLength) {
                let result = new Float32Array(recLength);
                let offset = 0;
                for (let i = 0; i < recBuffers.length; i++) {
                    result.set(recBuffers[i], offset);
                    offset += recBuffers[i].length;
                }
                return result;
            }

            function interleave(inputL, inputR) {
                let length = inputL.length + inputR.length;
                let result = new Float32Array(length);

                let index = 0,
                    inputIndex = 0;

                while (index < length) {
                    result[index++] = inputL[inputIndex];
                    result[index++] = inputR[inputIndex];
                    inputIndex++;
                }
                return result;
            }

            function floatTo16BitPCM(output, offset, input) {
                for (let i = 0; i < input.length; i++, offset += 2) {
                    let s = Math.max(-1, Math.min(1, input[i]));
                    output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
                }
            }

            function writeString(view, offset, string) {
                for (let i = 0; i < string.length; i++) {
                    view.setUint8(offset + i, string.charCodeAt(i));
                }
            }

            function encodeWAV(samples) {
                let buffer = new ArrayBuffer(44 + samples.length * 2);
                let view = new DataView(buffer);

                /* RIFF identifier */
                writeString(view, 0, 'RIFF');
                /* RIFF chunk length */
                view.setUint32(4, 36 + samples.length * 2, true);
                /* RIFF type */
                writeString(view, 8, 'WAVE');
                /* format chunk identifier */
                writeString(view, 12, 'fmt ');
                /* format chunk length */
                view.setUint32(16, 16, true);
                /* sample format (raw) */
                view.setUint16(20, 1, true);
                /* channel count */
                view.setUint16(22, numChannels, true);
                /* sample rate */
                view.setUint32(24, sampleRate, true);
                /* byte rate (sample rate * block align) */
                view.setUint32(28, sampleRate * 4, true);
                /* block align (channel count * bytes per sample) */
                view.setUint16(32, numChannels * 2, true);
                /* bits per sample */
                view.setUint16(34, 16, true);
                /* data chunk identifier */
                writeString(view, 36, 'data');
                /* data chunk length */
                view.setUint32(40, samples.length * 2, true);

                floatTo16BitPCM(view, 44, samples);

                return view;
            }
        }, self);

        this.worker.postMessage({
            command: 'init',
            config: {
                sampleRate: this.context.sampleRate,
                numChannels: this.config.numChannels
            }
        });

        this.worker.onmessage = (e) => {
            let cb = this.callbacks[e.data.command].pop();
            if (typeof cb == 'function') {
                cb(e.data.data);
            }
        };
    }


    record() {
        this.recording = true;
    }

    stop() {
        this.recording = false;
    }

    clear() {
        this.worker.postMessage({command: 'clear'});
    }

    getBuffer(cb) {
        cb = cb || this.config.callback;
        if (!cb) throw new Error('Callback not set');

        this.callbacks.getBuffer.push(cb);

        this.worker.postMessage({command: 'getBuffer'});
    }

    exportWAV(cb, mimeType) {
        mimeType = mimeType || this.config.mimeType;
        cb = cb || this.config.callback;
        if (!cb) throw new Error('Callback not set');

        this.callbacks.exportWAV.push(cb);

        this.worker.postMessage({
            command: 'exportWAV',
            type: mimeType
        });
    }

    static
    forceDownload(blob, filename) {
        let url = (window.URL || window.webkitURL).createObjectURL(blob);
        let link = window.document.createElement('a');
        link.href = url;
        link.download = filename || 'output.wav';
        let click = document.createEvent("Event");
        click.initEvent("click", true, true);
        link.dispatchEvent(click);
    }
}

export default Recorder;

It's a code which I took from github repository but the sample rate is 48000

File after downsampling left and right buffers is uploaded here

Calling from my component class

 recorder && recorder.exportWAV(function(blob) {

            var formData=new FormData();

            formData.append("event_name",fileName);
            formData.append("file",new File([blob], fileName, {type: 'audio/mpeg;', lastModified: Date.now()}));
            formData.append("fileExtension", "mp3");
             fetch('http://localhost:6020/uploadFile', {
            method: "POST", // *GET, POST, PUT, DELETE, etc.
            cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached

            redirect: "follow", // manual, *follow, error
            referrer: "no-referrer", // no-referrer, *client
            body: formData, // body data type must match "Content-Type" header
            })
            .then(response => response.json())
            .then(function(data){ console.log( JSON.stringify( data ) ) });

          },"audio/mp3");
Kramer
  • 389
  • 8
  • 34
  • *please suggest where to call it.* - it's unclear what you're asking about. You can call it where you need it. FWIW, you need to use lowpass filter to avoid aliasing during resampling. – Estus Flask Dec 17 '18 at 07:11
  • Create a [MCVE]. – ceving Dec 17 '18 at 07:56
  • @estus the point I want to mention is that what do I pass in it since I have two channel and how do I merge it into a single buffer then pass it to downsampler? – Kramer Dec 17 '18 at 10:22
  • The question isn't very constructive because you just used somebody else's code, it's big enough to review. If you're after third-party code, there may be libs or examples that serve your purpose better (I cannot recommend one). I suppose you should downsample left and right channel separately, unless downsampleBuffer was written to support multiple channels - it seems that it wasn't and transforms one channel (buffer) at once, as its name says. – Estus Flask Dec 17 '18 at 10:34
  • @estus I tried to do as you mentioned and also uploaded the file you can check it's very fast I don't know the reason even though I am downsampling it....check method exportWav(type) – Kramer Dec 17 '18 at 10:50
  • The file is 48khz. It's unknown what happens with audio data after exportWAV . But you need to set 16khz for audio file. – Estus Flask Dec 17 '18 at 10:55
  • @estus but How do I do it can you please look at the code I am trying this for a long time – Kramer Dec 17 '18 at 10:56
  • the thing is I need to downsample the sampleRate to 16000 because if I don't the audio is 48khz but audio is okay no problem as soon as I use downsampler then I received the audio I attached it's very fast and If I force and use 16000 instead of `this.context.sampleRate` in a code then the audio is damn slow like a slo-mo – Kramer Dec 17 '18 at 11:01
  • Again, it isn't shown in the code what you do with data after it's exported. exportWAV just produces a blob. Blobs don't have sample rate. As the comment above mentioned, the question lacks https://stackoverflow.com/help/mcve – Estus Flask Dec 17 '18 at 11:02
  • @estus ok I added that part of the code also please check exactly what is missing or doing wrong last check pls from blob I am creating a file – Kramer Dec 17 '18 at 11:07
  • I see. The problem is that `sampleRate` in encodeWAV is not correct. Another problem is `type: 'audio/mpeg;'` .It's not mpeg. It's wav. – Estus Flask Dec 17 '18 at 11:24
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/185355/discussion-between-kramer-and-estus). – Kramer Dec 17 '18 at 11:28
  • @estus can you please reply to my question or come on discussion chat? – Kramer Dec 17 '18 at 11:29
  • @estus `config = { bufferLen: 4096, numChannels: 2, mimeType: 'audio/mp3' };` here I mentioned mp3 I have just used function which is WAV named – Kramer Dec 17 '18 at 11:30
  • I mean `new File([blob], fileName, {type: 'audio/mpeg;'`. It shouldn't be mpeg. Did fixing sampleRate in encodeWAV work for you? I'm unable to dig through your code and provide a specific answer how exactly it should be edited. The code is too complex. I don't use chats on SO because they don't work well with offline. – Estus Flask Dec 17 '18 at 11:35
  • @estus https://github.com/awslabs/aws-lex-browser-audio-capture I used this code but if you code and download the zip you will get example and lib folder I tried to used it but it turns out that recorder.js requires **webworkify** for loading `worker.js` module therefore when I installed it....it gave error inside this module – Kramer Dec 17 '18 at 12:13
  • @estus it worked if I change the sampleRate in encodedWAV since If I console log there it was still going 48000 therefore I hardcoded it but my file is PCM signed and I want mp3 signed how to do that any idea? – Kramer Dec 17 '18 at 12:55
  • You need to convert wav to mp3. That's a different task rather than original question. Google for 'js mp3 client side'. – Estus Flask Dec 17 '18 at 12:59
  • @estus Actually if you see I have two method as exportWAV one with cb,mimeType another only one parameter I gave mimetype to call another exportWAV but still it's showing PCM signed – Kramer Dec 17 '18 at 13:11
  • And what's the problem with this? It's PCM indeed. You meed to encode audio signal with MP3 encoder in order for it to become real MP3 and not just WAV with .mp3 extension. – Estus Flask Dec 17 '18 at 14:01
  • @estus please tell me what do I do in encodeWAV method above where I am passing "WAVE" i should directly pass "MP3"?? – Kramer Dec 17 '18 at 14:36
  • 1
    You possibly do. Or you can encode it to WAV and then to MP3. It's up to you and possible solutions you can find for MP3 encoding. Again, this is out of the scope of original question (downsampling). – Estus Flask Dec 17 '18 at 14:46
  • @estus I am not getting equivalent code for reactjs to implement If I require libmp3lame.js which is old js and it doesn't export class how will I use it reactjs – Kramer Dec 18 '18 at 07:40

1 Answers1

1

I downsampled my audio clips (wav files) successfully using recorder.js library referring the solution given by ilikerei in this thread.

Add this method to recorder.js to resample the buffer to change sample rate, before calling encodeWav() method.

function downsampleBuffer(buffer, rate) {
            if (rate == sampleRate) {
                return buffer;
            }
            if (rate > sampleRate) {
                throw "downsampling rate show be smaller than original sample rate";
            }
            var sampleRateRatio = sampleRate / rate;
            var newLength = Math.round(buffer.length / sampleRateRatio);
            var result = new Float32Array(newLength);
            var offsetResult = 0;
            var offsetBuffer = 0;
            while (offsetResult < result.length) {
                var nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
                 // Use average value of skipped samples
                var accum = 0, count = 0;
                for (var i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
                    accum += buffer[i];
                    count++;
                }
                result[offsetResult] = accum / count;
                // Or you can simply get rid of the skipped samples:
                // result[offsetResult] = buffer[nextOffsetBuffer];
                offsetResult++;
                offsetBuffer = nextOffsetBuffer;
            }
            return result;
        }

Then call encodeWav with the new sample buffer.

var downsampledBuffer = downsampleBuffer(interleaved, targetRate);
var dataview = encodeWAV(downsampledBuffer);

Now use the new sample rate for encoding.

/* sample rate */
view.setUint32(24, newRate, true);
/* byte rate (sample rate * block align) */
view.setUint32(28, newRate * 4, true);
Ishara Amarasekera
  • 1,387
  • 1
  • 20
  • 34