rkeys
Version:
A platform for creating tablet/HTML5 virtual-keyboard apps to send keystrokes to remote X11
281 lines (236 loc) • 7.77 kB
JavaScript
/**
* WaveEncoder
* Converts [0,1]-valued array --> base64-encoded data uri.
* The optimized base64 encoding is implemented using 16-bit words.
*
* Since this encoder does not rely on window.btoa for encoding,
* it can be used also in Web Worker threads.
*
* Copyright (c) 2012, Fritz Obermeyer
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://www.opensource.org/licenses/MIT
* http://www.opensource.org/licenses/GPL-2.0
*/
var WavEncoder = (function(){
/** @constructor */
WavEncoderException = function (message) {
this.message = message || '(unspecified)';
};
WavEncoderException.prototype.toString = function () {
return 'WavEncoder Error: ' + this.message;
};
var assert = function (condition, message) {
if (!condition) {
throw new WavEncoderException(message);
}
};
var assertEqual = function (actual, expected, message) {
if (!(actual instanceof String) || !(expected instanceof String)) {
actual = JSON.stringify(actual);
expected = JSON.stringify(expected);
}
assert(actual === expected,
(message || '') +
'\n actual = ' + actual +
'\n expected = ' + expected);
};
/** @constructor */
var WavEncoder = function (numSamples, options) {
this.numSamples = numSamples;
options = options || {};
var defaults = WavEncoder.defaults;
var bytesPerSample = options.bytesPerSample || defaults.bytesPerSample;
var numChannels = options.numChannels || defaults.numChannels;
var sampleRateHz = options.sampleRateHz || defaults.sampleRateHz;
this.clip = 'clip' in options ? !!options.clip : defaults.clip;
var PCM_FORMAT = 1;
var bitsPerSample = bytesPerSample * 8;
var byteRateHz = sampleRateHz * bytesPerSample * numChannels;
var byteAlignment = numChannels * bytesPerSample;
var formatBytes = 16;
var dataBytes = numSamples * bytesPerSample * numChannels;
var chunkBytes = 4 + (8 + formatBytes) + (8 + dataBytes);
var getString = this._getString;
var getUint16 = this._getUint16;
var getUint32 = this._getUint32;
switch (bytesPerSample) {
case 1:
this.encode = this._encode8;
break;
case 2:
this.encode = this._encode16;
break;
default: throw new WavEncoderException(
'unsupported bytesPerSample: ' + bytesPerSample);
}
// we encode using 16-bit words
var words = this.words = [].concat(
getString('RIFF'),
// only one chunk
getUint32(chunkBytes),
getString('WAVE'),
// format subchunk
getString('fmt '),
getUint32(formatBytes),
getUint16(PCM_FORMAT),
getUint16(numChannels),
getUint32(sampleRateHz),
getUint32(byteRateHz),
getUint16(byteAlignment),
getUint16(bitsPerSample),
// data subchunk
getString('data'),
getUint32(dataBytes),
[]);
var bytesPerWord = 2;
var dataWords = dataBytes / bytesPerWord;
words.length = this._headerWords + dataWords;
while (words.length % 3) words.push(0);
};
WavEncoder.prototype = {
_headerBytes: 44,
_headerWords: 22, // for 16-bit words
/**
* @param {string}
* @returns {number[]}
*/
_getString: function (s) {
assert(s.length % 2 === 0, 'expected a string length to be even');
var result = [];
for (var i = 0, I = s.length; i < I; i += 2) {
var c1 = s.charCodeAt(i + 0);
var c2 = s.charCodeAt(i + 1);
assert(c1 < 256, 'bad character: ' + c1);
assert(c2 < 256, 'bad character: ' + c2);
result.push((c1 << 8) | c2);
}
return result;
},
/**
* @param {number}
* @returns {number[]}
*/
_getUint16: function (i) {
var swapBytes = function (j) { return ((j >> 8) | (j << 8)) & 65535; };
return [i & 65535].map(swapBytes);
},
/**
* @param {number}
* @returns {number[]}
*/
_getUint32: function (i) {
var swapBytes = function (j) { return ((j >> 8) | (j << 8)) & 65535; };
return [i & 65535, (i >> 16) & 65535].map(swapBytes);
},
/**
* @param {number[]}
* @returns {string}
*/
_encode8: function (samples) {
// this is hard-coded for 8-bit mono
assertEqual(samples.length, this.numSamples, 'Wrong number of samples');
var floor = Math.floor;
var sqrt = Math.sqrt;
var clip = this.clip;
var words = this.words;
var h = this._headerWords;
for (var t = 0, T = this.numSamples - 1; t < T; t += 2) {
var x1 = samples[t + 0];
var x2 = samples[t + 1];
if (clip) {
x1 /= sqrt(1 + x1 * x1);
x2 /= sqrt(1 + x2 * x2);
}
// 8-bit samples are unsigned
var sample1 = floor(128 * (x1 + 1));
var sample2 = floor(128 * (x2 + 1));
words[h++] = (sample1 << 8) | sample2;
}
if (this.numSamples % 2) {
var x1 = samples[t + 0];
if (clip) x1 /= sqrt(1 + x1 * x1);
var sample1 = floor(128 * (x1 + 1));
words[h++] = sample1 << 8;
}
return this._encodeWords();
},
/**
* @param {number[]}
* @returns {string}
*/
_encode16: function (samples) {
// this is hard-coded for 16-bit mono
assertEqual(samples.length, this.numSamples, 'Wrong number of samples');
var floor = Math.floor;
var sqrt = Math.sqrt;
var clip = this.clip;
var words = this.words;
var h = this._headerWords;
for (var t = 0, T = this.numSamples; t < T; ++t) {
var x = samples[t];
if (clip) x /= sqrt(1 + x * x);
// 16-bit samples are signed
var sample = floor(32768 * x);
if (sample < 0) sample += 65536; // 2's compliment
words[h++] = ((sample >> 8) | (sample << 8)) & 65535;
}
return this._encodeWords();
},
/**
* @returns {string}
*/
_encodeWords: function () {
var words = this.words;
var pairTable = WavEncoder._pairTable;
var result = new Array(1 + words.length * 4/3);
var r = 0;
result[r++] = 'data:audio/wav;base64,';
for (var r = 1, t = 0, T = words.length; t < T;) {
var a16 = words[t++];
var b16 = words[t++];
var c16 = words[t++];
// with 4 bits per letter:
// A A A A B B B B C C C C
// A A A B B B C C C D D D
var a12 = (a16 >> 4) & 4095;
var b12 = ((a16 << 8) | (b16 >> 8)) & 4095;
var c12 = ((b16 << 4) | (c16 >> 12)) & 4095;
var d12 = c16 & 4095;
result[r++] = pairTable[a12];
result[r++] = pairTable[b12];
result[r++] = pairTable[c12];
result[r++] = pairTable[d12];
}
return result.join('');
}
};
WavEncoder.defaults = {
numChannels: 1, // mono
sampleRateHz: 22050, // 22050 Hz
bytesPerSample: 2, // 16 bit
clip: true
};
(function(){
var charTable =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
var pairTable = [];
for (var ij = 0, IJ = 64*64; ij < IJ; ++ij) {
pairTable[ij] = charTable[ij >> 6] + charTable[ij & 63];
}
WavEncoder._pairTable = pairTable;
})();
WavEncoder.Exception = WavEncoderException;
/**
* WavEncoder is optimized to encode many data sequences of the same length,
* but we provide this one-off function for convenience.
*
* @param {number[]}
* @param {object?}
* @returns {string}
*/
WavEncoder.encode = function (data, options) {
var encoder = new WavEncoder(data.length, options);
return encoder.encode(data);
};
return WavEncoder;
}());