/* 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: ""
};