Source: quiet.js

/* Copyright 2016, Brian Armstrong
 * quiet.js includes compiled portions from other sources
 *  - liquid DSP, Copyright (c) 2007-2016 Joseph Gaeddert
 *  - libjansson, Copyright (c) 2009-2016 Petri Lehtinen
 *  - emscripten, Copyright (c) 2010-2016 Emscripten authors
 */

/** @namespace */
var Quiet = (function() {
    // sampleBufferSize is the number of audio samples we'll write per onaudioprocess call
    // must be a power of two. we choose the absolute largest permissible value
    // we implicitly assume that the browser will play back a written buffer without any gaps
    var sampleBufferSize = 16384;

    // initialization flags
    var emscriptenInitialized = false;
    var profilesFetched = false;

    // profiles is the string content of quiet-profiles.json
    var profiles;

    // our local instance of window.AudioContext
    var audioCtx;

    // consumer callbacks. these fire once quiet is ready to create transmitter/receiver
    var readyCallbacks = [];
    var readyErrbacks = [];
    var failReason = "";

    // these are used for receiver only
    var gUM;
    var audioInput;
    var audioInputFailedReason = "";
    var audioInputReadyCallbacks = [];
    var audioInputFailedCallbacks = [];
    var frameBufferSize = Math.pow(2, 14);

    // anti-gc
    var receivers = [];

    // isReady tells us if we can start creating transmitters and receivers
    // we need the emscripten portion to be running and we need our
    // async fetch of the profiles to be completed
    function isReady() {
        return emscriptenInitialized && profilesFetched;
    };

    function isFailed() {
        return failReason !== "";
    };

    // start gets our AudioContext and notifies consumers that quiet can be used
    function start() {
        audioCtx = new (window.AudioContext || window.webkitAudioContext)();
        console.log(audioCtx.sampleRate);
        var len = readyCallbacks.length;
        for (var i = 0; i < len; i++) {
            readyCallbacks[i]();
        }
    };

    function fail(reason) {
        failReason = reason;
        var len = readyErrbacks.length;
        for (var i = 0; i < len; i++) {
            readyErrbacks[i](reason);
        }
    };

    function checkInitState() {
        if (isReady()) {
            start();
        }
    };

    function onProfilesFetch(p) {
        profiles = p;
        profilesFetched = true;
        checkInitState();
    };

    // this is intended to be called only by emscripten
    function onEmscriptenInitialized() {
        emscriptenInitialized = true;
        checkInitState();
    };

    /**
     * Set the path prefix of quiet-profiles.json and do an async fetch of that path.
     * This file is used to configure transmitter and receiver parameters.
     * <br><br>
     * This function must be called before creating a transmitter or receiver.
     * @function setProfilesPrefix
     * @memberof Quiet
     * @param {string} prefix - The path prefix where Quiet will fetch quiet-profiles.json
     * @example
     * setProfilesPrefix("/js/");  // fetches /js/quiet-profiles.json
     */
    function setProfilesPrefix(prefix) {
        if (profilesFetched) {
            return;
        }
        if (!prefix.endsWith("/")) {
            prefix += "/";
        }
        var profilesPath = prefix + "quiet-profiles.json";

        var fetch = new Promise(function(resolve, reject) {
            var xhr = new XMLHttpRequest();
            xhr.overrideMimeType("application/json");
            xhr.open("GET", profilesPath, true);
            xhr.onload = function() {
                if (this.status >= 200 && this.status < 300) {
                    resolve(this.responseText);
                } else {
                    reject(this.statusText);
                }
            };
            xhr.onerror = function() {
                reject(this.statusText);
            };
            xhr.send();
        });

        fetch.then(function(body) {
            onProfilesFetch(body);
        }, function(err) {
            fail("fetch of quiet-profiles.json failed: " + err);
        });
    };

    /**
     * Set the path prefix of quiet-emscripten.js.mem.
     * This file is used to initialize the memory state of emscripten.
     * <br><br>
     * This function must be called before quiet-emscripten.js has started loading.
     * If it is not called first, then emscripten will default to a prefix of "".
     * @function setMemoryInitializerPrefix
     * @memberof Quiet
     * @param {string} prefix - The path prefix where emscripten will fetch quiet-emscripten.js.mem
     * @example
     * setMemoryInitializerPrefix("/");  // fetches /quiet-emscripten.js.mem
     */
    function setMemoryInitializerPrefix(prefix) {
        Module.memoryInitializerPrefixURL = prefix;
    }

    /**
     * Set the path prefix of libfec.js.
     * Although not strictly required, it is highly recommended to include this library.
     * <br><br>
     * This function, if used, must be called before quiet-emscripten.js has started loading.
     * If it is not called first, then emscripten will not load libfec.js.
     * @function setLibfecPrefix
     * @memberof Quiet
     * @param {string} prefix - The path prefix where emscripten will fetch libfec.js
     * @example
     * setLibfecPrefix("/");  // fetches /libfec.js
     */
    function setLibfecPrefix(prefix) {
        Module.dynamicLibraries = Module.dynamicLibraries || [];
        Module.dynamicLibraries.push(prefix + "libfec.js");
    }

    /**
     * Callback to notify user that quiet.js failed to initialize
     *
     * @callback onError
     * @memberof Quiet
     * @param {string} reason - error message related to failure
     */

    /**
     * Add a callback to be called when Quiet is ready for use, e.g. when transmitters and receivers can be created.
     * @function addReadyCallback
     * @memberof Quiet
     * @param {function} c - The user function which will be called
     * @param {onError} [onError] - User errback function
     * @example
     * addReadyCallback(function() { console.log("ready!"); });
     */
    function addReadyCallback(c, errback) {
        if (isReady()) {
            c();
            return;
        }
        readyCallbacks.push(c);
        if (errback !== undefined) {
            if (isFailed()) {
                errback(failReason);
                return;
            }
            readyErrbacks.push(errback);
        }
    }

    /**
     * Callback used by transmit to notify user that transmission has finished
     * @callback onTransmitFinish
     * @memberof Quiet
     */

    /**
     * Callback for user to provide data to a Quiet transmitter
     * <br><br>
     * This callback may be used multiple times, but the user must wait for the finished callback between subsequent calls.
     * @callback transmit
     * @memberof Quiet
     * @param {ArrayBuffer} payload - bytes which will be encoded and sent to speaker
     * @param {onTransmitFinish} [done] - callback to notify user that transmission has completed
     * @example
     * transmit(Quiet.str2ab("Hello, World!"), function() { console.log("transmission complete"); });
     */

    /**
     * Create a new transmitter configured by the given profile name.
     * @function transmitter
     * @memberof Quiet
     * @param {string} profile - name of profile to use, must be a key in quiet-profiles.json
     * @returns {transmit} transmit - transmit callback which user calls to start transmission
     * @example
     * var transmit = transmitter("robust");
     * transmit(Quiet.str2ab("Hello, World!"), function() { console.log("transmission complete"); });
     */
    function transmitter(profile) {
        // get an encoder_options object for our quiet-profiles.json and profile key
        var c_profiles = Module.intArrayFromString(profiles);
        var c_profile = Module.intArrayFromString(profile);
        var opt = Module.ccall('quiet_encoder_profile_str', 'pointer', ['array', 'array'], [c_profiles, c_profile]);

        // libquiet internally works at 44.1kHz but the local sound card may be a different rate. we inform quiet about that here
        var encoder = Module.ccall('quiet_encoder_create', 'pointer', ['pointer', 'number'], [opt, audioCtx.sampleRate]);

        // some profiles have an option called close_frame which prevents data frames from overlapping multiple
        //     sample buffers. this is very convenient if our system is not fast enough to feed the sound card
        //     without any gaps between subsequent buffers due to e.g. gc pause. inform quiet about our
        //     sample buffer size here so that it can reduce the frame length if this profile has close_frame enabled.
        var frame_len = Module.ccall('quiet_encoder_clamp_frame_len', 'number', ['pointer', 'number'], [encoder, sampleBufferSize]);
        var samples = Module.ccall('malloc', 'pointer', ['number'], [4 * sampleBufferSize]);

        // return user transmit function
        return function(buf, done) {
            var payload = new Uint8Array(buf);
            var payloadOffset = 0;

            // fill as much of quiet's transmit queue as possible
            var writebuf = function() {
                if (payloadOffset == payload.length) {
                    return;
                }
                for (var i = payloadOffset; i < payload.length; ) {
                    var frame = payload.subarray(payloadOffset, payloadOffset + frame_len);
                    var written = Module.ccall('quiet_encoder_send', 'number', ['pointer', 'array', 'number'], [encoder, frame, frame.length]);
                    if (written === -1) {
                        break;
                    }
                    payloadOffset += frame.length;
                    i += frame.length;
                }
            };

            writebuf();

            // yes, this is pointer arithmetic, in javascript :)
            var sample_view = Module.HEAPF32.subarray((samples/4), (samples/4) + sampleBufferSize);

            var script_processor = (audioCtx.createScriptProcessor || audioCtx.createJavaScriptNode);
            var transmitter = script_processor.call(audioCtx, sampleBufferSize, 1, 2);

            var finished = false;
            transmitter.onaudioprocess = function(e) {
                var output_l = e.outputBuffer.getChannelData(0);

                if (finished) {
                    for (var i = 0; i < sampleBufferSize; i++) {
                        output_l[i] = 0;
                    }
                    return;
                }

                var written = Module.ccall('quiet_encoder_emit', 'number', ['pointer', 'pointer', 'number'], [encoder, samples, sampleBufferSize]);
                output_l.set(sample_view);

                // libquiet notifies us that the payload is finished by returning written < number of samples we asked for
                if (written < sampleBufferSize) {
                    // be extra cautious and 0-fill what's left
                    //   (we want the end of transmission to be silence, not potentially loud noise)
                    for (var i = written; i < sampleBufferSize; i++) {
                        output_l[i] = 0;
                    }
                    // user callback
                    if (done !== undefined) {
                            done();
                    }
                    finished = true;
                    window.setTimeout(function() { transmitter.disconnect(); }, 1500);
                }
                window.setTimeout(writebuf, 0);
            };

            // put an input node on the graph. some browsers require this to run our script processor
            // this oscillator will not actually be used in any way
            var dummy_osc = audioCtx.createOscillator();
            dummy_osc.type = 'square';
            dummy_osc.frequency.value = 420;
            dummy_osc.connect(transmitter);

            transmitter.connect(audioCtx.destination);
        };
    };

    // receiver functions

    function audioInputReady() {
        var len = audioInputReadyCallbacks.length;
        for (var i = 0; i < len; i++) {
            audioInputReadyCallbacks[i]();
        }
    };

    function audioInputFailed(reason) {
        audioInputFailedReason = reason;
        var len = audioInputFailedCallbacks.length;
        for (var i = 0; i < len; i++) {
            audioInputFailedCallbacks[i](audioInputFailedReason);
        }
    };

    function addAudioInputReadyCallback(c, errback) {
        if (errback !== undefined) {
            if (audioInputFailedReason !== "") {
                errback(audioInputFailedReason);
                return
            }
            audioInputFailedCallbacks.push(errback);
        }
        if (audioInput instanceof MediaStreamAudioSourceNode) {
            c();
            return
        }
        audioInputReadyCallbacks.push(c);
    }

    function gUMConstraints() {
        if (navigator.webkitGetUserMedia !== undefined) {
            return {
                audio: {
                    optional: [
                      {googAutoGainControl: false},
                      {googAutoGainControl2: false},
                      {echoCancellation: false},
                      {googEchoCancellation: false},
                      {googEchoCancellation2: false},
                      {googDAEchoCancellation: false},
                      {googNoiseSuppression: false},
                      {googNoiseSuppression2: false},
                      {googHighpassFilter: false},
                      {googTypingNoiseDetection: false},
                      {googAudioMirroring: false}
                    ]
                }
            };
        }
        if (navigator.mozGetUserMedia !== undefined) {
            return {
                audio: {
                    echoCancellation: false,
                    mozAutoGainControl: false,
                    mozNoiseSuppression: false
                }
            };

        }
        return {
            audio: {
                echoCancellation: false
            }
        };
    };


    function createAudioInput() {
        audioInput = 0; // prevent others from trying to create
        gUM.call(navigator, gUMConstraints(),
            function(e) {
                audioInput = audioCtx.createMediaStreamSource(e);

                // stash a very permanent reference so this isn't collected
                window.quiet_receiver_anti_gc = audioInput;

                audioInputReady();
            }, function(reason) {
                audioInputFailed(reason.name);
        });
    };

    /**
    * Callback used by receiver to notify user that a frame was received but
    * failed checksum. Frames that fail checksum are not sent to onReceive.
    *
    * @callback onReceiveFail
    * @memberof Quiet
    * @param {number} total - total number of frames failed across lifetime of receiver
    */

    /**
     * Callback used by receiver to notify user of errors in creating receiver.
     * This is a callback because frequently this will result when the user denies
     * permission to use the mic, which happens long after the call to create
     * the receiver.
     *
     * @callback onReceiverCreateFail
     * @memberof Quiet
     * @param {string} reason - error message related to create fail
    */

    /**
     * Callback used by receiver to notify user of data received via microphone/line-in.
     *
     * @callback onReceive
     * @memberof Quiet
     * @param {ArrayBuffer} payload - chunk of data received
    */

    /**
     * Create a new receiver with the profile specified by profile (should match profile of transmitter).
     * @function receiver
     * @memberof Quiet
     * @param {string} profile - name of profile to use, must be a key in quiet-profiles.json
     * @param {onReceive} onReceive - callback which receiver will call to send user received data
     * @param {onReceiverCreateFail} [onCreateFail] - callback to notify user that receiver could not be created
     * @param {onReceiveFail} [onReceiveFail] - callback to notify user that receiver received corrupted data
     * @example
     * receiver("robust", function(payload) { console.log("received chunk of data: " + Quiet.ab2str(payload)); });
     */
    function receiver(profile, onReceive, onCreateFail, onReceiveFail) {
        var c_profiles = Module.intArrayFromString(profiles);
        var c_profile = Module.intArrayFromString(profile);
        var opt = Module.ccall('quiet_decoder_profile_str', 'pointer', ['array', 'array'], [c_profiles, c_profile]);

        // quiet creates audioCtx when it starts but it does not create an audio input
        // getting microphone access requires a permission dialog so only ask for it if we need it
        if (gUM === undefined) {
            gUM = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia);
        }

        if (gUM === undefined) {
            // we couldn't find a suitable getUserMedia, so fail fast
            if (onCreateFail !== undefined) {
                onCreateFail("getUserMedia undefined (mic not supported by browser)");
            }
            return;
        }

        if (audioInput === undefined) {
            createAudioInput()
        }

        // TODO investigate if this still needs to be placed on window.
        // seems this was done to keep it from being collected
        var scriptProcessor = audioCtx.createScriptProcessor(16384, 2, 1);
        receivers.push(scriptProcessor);

        // inform quiet about our local sound card's sample rate so that it can resample to its internal sample rate
        var decoder = Module.ccall('quiet_decoder_create', 'pointer', ['pointer', 'number'], [opt, audioCtx.sampleRate]);

        var samples = Module.ccall('malloc', 'pointer', ['number'], [4 * sampleBufferSize]);

        var frame = Module.ccall('malloc', 'pointer', ['number'], [frameBufferSize]);

        var readbuf = function() {
            while (true) {
                var read = Module.ccall('quiet_decoder_recv', 'number', ['pointer', 'pointer', 'number'], [decoder, frame, frameBufferSize]);
                if (read === -1) {
                    break;
                }
                // convert from emscripten bytes to js string. more pointer arithmetic.
                var frameArray = Module.HEAP8.slice(frame, frame + read);
                onReceive(frameArray);
            }
        };

        var lastChecksumFailCount = 0;
        var consume = function() {
            Module.ccall('quiet_decoder_consume', 'number', ['pointer', 'pointer', 'number'], [decoder, samples, sampleBufferSize]);

            window.setTimeout(readbuf, 0);

            var currentChecksumFailCount = Module.ccall('quiet_decoder_checksum_fails', 'number', ['pointer'], [decoder]);
            if ((onReceiveFail !== undefined) && (currentChecksumFailCount > lastChecksumFailCount)) {
                window.setTimeout(function() { onReceiveFail(currentChecksumFailCount); }, 0);
            }
            lastChecksumFailCount = currentChecksumFailCount;
        }


        scriptProcessor.onaudioprocess = function(e) {
            var input = e.inputBuffer.getChannelData(0);
            var sample_view = Module.HEAPF32.subarray(samples/4, samples/4 + sampleBufferSize);
            sample_view.set(input);

            window.setTimeout(consume, 0);
        }

        // if this is the first receiver object created, wait for our input node to be created
        addAudioInputReadyCallback(function() {
            audioInput.connect(scriptProcessor);
        }, onCreateFail);

        // more unused nodes in the graph that some browsers insist on having
        var fakeGain = audioCtx.createGain();
        fakeGain.value = 0;
        scriptProcessor.connect(fakeGain);
        fakeGain.connect(audioCtx.destination);
    };

    /**
     * Convert a string to array buffer in UTF8
     * @function str2ab
     * @memberof Quiet
     * @param {string} s - string to be converted
     * @returns {ArrayBuffer} buf - converted arraybuffer
     */
    function str2ab(s) {
        var s_utf8 = unescape(encodeURIComponent(s));
        var buf = new ArrayBuffer(s_utf8.length);
        var bufView = new Uint8Array(buf);
        for (var i = 0; i < s_utf8.length; i++) {
            bufView[i] = s_utf8.charCodeAt(i);
        }
        return buf;
    };

    /**
     * Convert an array buffer in UTF8 to string
     * @function ab2str
     * @memberof Quiet
     * @param {ArrayBuffer} ab - array buffer to be converted
     * @returns {string} s - converted string
     */
    function ab2str(ab) {
        return decodeURIComponent(escape(String.fromCharCode.apply(null, new Uint8Array(ab))));
    };

    /**
     * Merge 2 ArrayBuffers
     * This is a convenience function to assist user receiver functions that
     * want to aggregate multiple payloads.
     * @function mergeab
     * @memberof Quiet
     * @param {ArrayBuffer} ab1 - beginning ArrayBuffer
     * @param {ArrayBuffer} ab2 - ending ArrayBuffer
     * @returns {ArrayBuffer} buf - ab1 merged with ab2
     */
    function mergeab(ab1, ab2) {
        var tmp = new Uint8Array(ab1.byteLength + ab2.byteLength);
        tmp.set(new Uint8Array(ab1), 0);
        tmp.set(new Uint8Array(ab2), ab1.byteLength);
        return tmp.buffer;
    };

    return {
        emscriptenInitialized: onEmscriptenInitialized,
        setProfilesPrefix: setProfilesPrefix,
        setMemoryInitializerPrefix: setMemoryInitializerPrefix,
        setLibfecPrefix: setLibfecPrefix,
        addReadyCallback: addReadyCallback,
        transmitter: transmitter,
        receiver: receiver,
        str2ab: str2ab,
        ab2str: ab2str,
        mergeab: mergeab
    };
})();

// extend emscripten Module
var Module = {
    onRuntimeInitialized: Quiet.emscriptenInitialized,
    memoryInitializerPrefixURL: ""
};