mirror of
https://github.com/parabirb/freeremote-client.git
synced 2026-01-20 20:15:29 -05:00
378 lines
15 KiB
JavaScript
378 lines
15 KiB
JavaScript
// Code courtesy of Symbl, see https://github.com/symblai/opus-encdec for license
|
|
|
|
var OggOpusEncoder = function( config, Module ){
|
|
|
|
if ( !Module ) {
|
|
throw new Error('Module with exports required to initialize an encoder instance');
|
|
}
|
|
|
|
this.config = Object.assign({
|
|
encoderApplication: 2049, // 2048 = Voice (Lower fidelity)
|
|
// 2049 = Full Band Audio (Highest fidelity)
|
|
// 2051 = Restricted Low Delay (Lowest latency)
|
|
encoderFrameSize: 20, // Specified in ms.
|
|
encoderSampleRate: 48000, // Desired encoding sample rate. Audio will be resampled
|
|
maxFramesPerPage: 40, // Tradeoff latency with overhead
|
|
numberOfChannels: 1,
|
|
originalSampleRate: 44100,
|
|
resampleQuality: 3, // Value between 0 and 10 inclusive. 10 being highest quality.
|
|
serial: Math.floor(Math.random() * 4294967296)
|
|
}, config );
|
|
|
|
// encode "raw" opus stream?
|
|
// -> either config.rawOpus = true/false,
|
|
// or config.mimeType = 'audio/opus'
|
|
// (instead of 'audio/ogg; codecs=opus')
|
|
this.rawOpus = typeof this.config.rawOpus === 'boolean'?
|
|
this.config.rawOpus :
|
|
/^audio\/opus\b/i.test(this.config.mimeType);
|
|
var useOgg = !this.rawOpus;
|
|
|
|
this._opus_encoder_create = Module._opus_encoder_create;
|
|
this._opus_encoder_destroy = Module._opus_encoder_destroy;
|
|
this._opus_encoder_ctl = Module._opus_encoder_ctl;
|
|
this._speex_resampler_process_interleaved_float = Module._speex_resampler_process_interleaved_float;
|
|
this._speex_resampler_init = Module._speex_resampler_init;
|
|
this._speex_resampler_destroy = Module._speex_resampler_destroy;
|
|
this._opus_encode_float = Module._opus_encode_float;
|
|
this._free = Module._free;
|
|
this._malloc = Module._malloc;
|
|
this.HEAPU8 = Module.HEAPU8;
|
|
this.HEAP32 = Module.HEAP32;
|
|
this.HEAPF32 = Module.HEAPF32;
|
|
|
|
this.pageIndex = 0;
|
|
this.granulePosition = 0;
|
|
this.segmentData = useOgg? new Uint8Array( 65025 ) : new Uint8Array( 255 ); // Maximum length of oggOpus data
|
|
this.segmentDataIndex = 0;
|
|
this.segmentTable = useOgg? new Uint8Array( 255 ) : null; // Maximum data segments
|
|
this.segmentTableIndex = 0;
|
|
this.framesInPage = 0;
|
|
|
|
this.encodedData = !useOgg? [] : undefined;
|
|
this.encodedDataLength = 0;
|
|
this.isReady = Module.isReady;
|
|
if(!this.isReady){
|
|
Module.onready = function(){
|
|
this.isReady = true;
|
|
this.onready && this.onready();
|
|
}
|
|
}
|
|
|
|
if(useOgg){
|
|
this.initChecksumTable();
|
|
}
|
|
this.initCodec();
|
|
this.initResampler();
|
|
|
|
if ( this.config.numberOfChannels === 1 ) {
|
|
this.interleave = function( buffers ) { return buffers[0]; };
|
|
}
|
|
};
|
|
|
|
OggOpusEncoder.prototype.encode = function( buffers ) {
|
|
|
|
// Determine bufferLength dynamically
|
|
if ( !this.bufferLength ) {
|
|
this.bufferLength = buffers[0].length;
|
|
this.interleavedBuffers = new Float32Array( this.bufferLength * this.config.numberOfChannels );
|
|
}
|
|
|
|
var useOgg = !this.rawOpus;
|
|
var samples = this.interleave( buffers );
|
|
var sampleIndex = 0;
|
|
var exportPages = useOgg? [] : null;
|
|
var encodedData = useOgg? null : [];
|
|
var bufferLength = this.resampler? this.resampleBufferLength : this.encoderBufferLength;
|
|
var buffer = this.resampler? this.resampleBuffer : this.encoderBuffer;
|
|
|
|
while ( sampleIndex < samples.length ) {
|
|
|
|
var lengthToCopy = Math.min(bufferLength - this.sampleBufferIndex, samples.length - sampleIndex );
|
|
buffer.set( samples.subarray( sampleIndex, sampleIndex+lengthToCopy ), this.sampleBufferIndex );
|
|
sampleIndex += lengthToCopy;
|
|
this.sampleBufferIndex += lengthToCopy;
|
|
|
|
if ( this.sampleBufferIndex === bufferLength ) {
|
|
|
|
if (this.resampler) {
|
|
this._speex_resampler_process_interleaved_float( this.resampler, this.resampleBufferPointer, this.resampleSamplesPerChannelPointer, this.encoderBufferPointer, this.encoderSamplesPerChannelPointer );
|
|
}
|
|
var packetLength = this._opus_encode_float( this.encoder, this.encoderBufferPointer, this.encoderSamplesPerChannel, this.encoderOutputPointer, this.encoderOutputMaxLength );
|
|
|
|
if(useOgg){
|
|
exportPages.concat(this.segmentPacket( packetLength ));
|
|
|
|
this.framesInPage++;
|
|
if ( this.framesInPage >= this.config.maxFramesPerPage ) {
|
|
exportPages.push( this.generatePage() );
|
|
}
|
|
} else {
|
|
encodedData.push({page: new Uint8Array(this.encoderOutputBuffer.subarray(0, packetLength))});
|
|
this.encodedDataLength += packetLength;
|
|
}
|
|
this.sampleBufferIndex = 0;
|
|
}
|
|
}
|
|
|
|
return useOgg ? exportPages : encodedData;
|
|
};
|
|
|
|
OggOpusEncoder.prototype.destroy = function() {
|
|
if ( this.encoder ) {
|
|
this._free(this.encoderSamplesPerChannelPointer);
|
|
delete this.encoderSamplesPerChannelPointer;
|
|
this._free(this.encoderBufferPointer);
|
|
delete this.encoderBufferPointer;
|
|
this._free(this.encoderOutputPointer);
|
|
delete this.encoderOutputPointer;
|
|
this._opus_encoder_destroy(this.encoder);
|
|
delete this.encoder;
|
|
if(this.resampler){
|
|
this._free(this.resampleSamplesPerChannelPointer);
|
|
delete this.resampleSamplesPerChannelPointer;
|
|
this._free(this.resampleBufferPointer);
|
|
delete this.resampleBufferPointer;
|
|
this._speex_resampler_destroy(this.resampler);
|
|
delete this.resampler;
|
|
}
|
|
if(this.encodedData){
|
|
this.encodedData = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
OggOpusEncoder.prototype.flush = function() {
|
|
var exportPage;
|
|
if ( this.framesInPage ) {
|
|
exportPage = this.generatePage();
|
|
}
|
|
// discard any pending data in resample buffer (only a few ms worth)
|
|
this.sampleBufferIndex = 0;
|
|
return exportPage;
|
|
};
|
|
|
|
OggOpusEncoder.prototype.encodeFinalFrame = function() {
|
|
var useOgg = !this.rawOpus;
|
|
var exportPages = useOgg? [] : null;
|
|
|
|
// Encode the data remaining in the resample buffer.
|
|
if ( this.sampleBufferIndex > 0 ) {
|
|
var dataToFill = (this.resampleBufferLength - this.sampleBufferIndex) / this.config.numberOfChannels;
|
|
var numBuffers = Math.ceil(dataToFill / this.bufferLength);
|
|
|
|
for ( var i = 0; i < numBuffers; i++ ) {
|
|
var finalFrameBuffers = [];
|
|
for ( var j = 0; j < this.config.numberOfChannels; j++ ) {
|
|
finalFrameBuffers.push( new Float32Array( this.bufferLength ));
|
|
}
|
|
if(useOgg){
|
|
exportPages.concat(this.encode( finalFrameBuffers ));
|
|
} else {
|
|
return this.encode( finalFrameBuffers );
|
|
}
|
|
}
|
|
}
|
|
|
|
if(useOgg){
|
|
this.headerType += 4;
|
|
exportPages.push(this.generatePage());
|
|
return exportPages;
|
|
}
|
|
};
|
|
|
|
OggOpusEncoder.prototype.getChecksum = function( data ){
|
|
var checksum = 0;
|
|
for ( var i = 0; i < data.length; i++ ) {
|
|
checksum = (checksum << 8) ^ this.checksumTable[ ((checksum>>>24) & 0xff) ^ data[i] ];
|
|
}
|
|
return checksum >>> 0;
|
|
};
|
|
|
|
OggOpusEncoder.prototype.generateCommentPage = function(){
|
|
var segmentDataView = new DataView( this.segmentData.buffer );
|
|
segmentDataView.setUint32( 0, 1937076303, true ) // Magic Signature 'Opus'
|
|
segmentDataView.setUint32( 4, 1936154964, true ) // Magic Signature 'Tags'
|
|
segmentDataView.setUint32( 8, 10, true ); // Vendor Length
|
|
segmentDataView.setUint32( 12, 1868784978, true ); // Vendor name 'Reco'
|
|
segmentDataView.setUint32( 16, 1919247474, true ); // Vendor name 'rder'
|
|
segmentDataView.setUint16( 20, 21322, true ); // Vendor name 'JS'
|
|
segmentDataView.setUint32( 22, 0, true ); // User Comment List Length
|
|
if(!this.rawOpus){
|
|
this.segmentTableIndex = 1;
|
|
this.segmentDataIndex = this.segmentTable[0] = 26;
|
|
this.headerType = 0;
|
|
return this.generatePage();
|
|
} else {
|
|
const encodedData = new Uint8Array(this.segmentData.subarray(0, 26));
|
|
this.encodedDataLength += 26;
|
|
}
|
|
};
|
|
|
|
OggOpusEncoder.prototype.generateIdPage = function(){
|
|
var segmentDataView = new DataView( this.segmentData.buffer );
|
|
segmentDataView.setUint32( 0, 1937076303, true ) // Magic Signature 'Opus'
|
|
segmentDataView.setUint32( 4, 1684104520, true ) // Magic Signature 'Head'
|
|
segmentDataView.setUint8( 8, 1, true ); // Version
|
|
segmentDataView.setUint8( 9, this.config.numberOfChannels, true ); // Channel count
|
|
segmentDataView.setUint16( 10, 3840, true ); // pre-skip (80ms)
|
|
segmentDataView.setUint32( 12, this.config.originalSampleRateOverride || this.config.originalSampleRate, true ); // original sample rate
|
|
segmentDataView.setUint16( 16, 0, true ); // output gain
|
|
segmentDataView.setUint8( 18, 0, true ); // channel map 0 = mono or stereo
|
|
if(!this.rawOpus){
|
|
this.segmentTableIndex = 1;
|
|
this.segmentDataIndex = this.segmentTable[0] = 19;
|
|
this.headerType = 2;
|
|
return this.generatePage();
|
|
} else {
|
|
const encodedData = new Uint8Array(this.segmentData.subarray(0, 19));
|
|
this.encodedDataLength += 19;
|
|
}
|
|
};
|
|
|
|
OggOpusEncoder.prototype.generatePage = function(){
|
|
var granulePosition = ( this.lastPositiveGranulePosition === this.granulePosition) ? -1 : this.granulePosition;
|
|
var pageBuffer = new ArrayBuffer( 27 + this.segmentTableIndex + this.segmentDataIndex );
|
|
var pageBufferView = new DataView( pageBuffer );
|
|
var page = new Uint8Array( pageBuffer );
|
|
|
|
pageBufferView.setUint32( 0, 1399285583, true); // Capture Pattern starts all page headers 'OggS'
|
|
pageBufferView.setUint8( 4, 0, true ); // Version
|
|
pageBufferView.setUint8( 5, this.headerType, true ); // 1 = continuation, 2 = beginning of stream, 4 = end of stream
|
|
|
|
// Number of samples upto and including this page at 48000Hz, into signed 64 bit Little Endian integer
|
|
// Javascript Number maximum value is 53 bits or 2^53 - 1
|
|
pageBufferView.setUint32( 6, granulePosition, true );
|
|
if (granulePosition < 0) {
|
|
pageBufferView.setInt32( 10, Math.ceil(granulePosition/4294967297) - 1, true );
|
|
}
|
|
else {
|
|
pageBufferView.setInt32( 10, Math.floor(granulePosition/4294967296), true );
|
|
}
|
|
|
|
pageBufferView.setUint32( 14, this.config.serial, true ); // Bitstream serial number
|
|
pageBufferView.setUint32( 18, this.pageIndex++, true ); // Page sequence number
|
|
pageBufferView.setUint8( 26, this.segmentTableIndex, true ); // Number of segments in page.
|
|
page.set( this.segmentTable.subarray(0, this.segmentTableIndex), 27 ); // Segment Table
|
|
page.set( this.segmentData.subarray(0, this.segmentDataIndex), 27 + this.segmentTableIndex ); // Segment Data
|
|
pageBufferView.setUint32( 22, this.getChecksum( page ), true ); // Checksum
|
|
|
|
var exportPage = { message: 'page', page: page, samplePosition: this.granulePosition };
|
|
this.segmentTableIndex = 0;
|
|
this.segmentDataIndex = 0;
|
|
this.framesInPage = 0;
|
|
if ( granulePosition > 0 ) {
|
|
this.lastPositiveGranulePosition = granulePosition;
|
|
}
|
|
|
|
return exportPage;
|
|
};
|
|
|
|
OggOpusEncoder.prototype.initChecksumTable = function(){
|
|
this.checksumTable = [];
|
|
for ( var i = 0; i < 256; i++ ) {
|
|
var r = i << 24;
|
|
for ( var j = 0; j < 8; j++ ) {
|
|
r = ((r & 0x80000000) != 0) ? ((r << 1) ^ 0x04c11db7) : (r << 1);
|
|
}
|
|
this.checksumTable[i] = (r & 0xffffffff);
|
|
}
|
|
};
|
|
|
|
OggOpusEncoder.prototype.setOpusControl = function( control, value ){
|
|
var location = this._malloc( 4 );
|
|
this.HEAP32[ location >> 2 ] = value;
|
|
this._opus_encoder_ctl( this.encoder, control, location );
|
|
this._free( location );
|
|
};
|
|
|
|
OggOpusEncoder.prototype.initCodec = function() {
|
|
var errLocation = this._malloc( 4 );
|
|
this.encoder = this._opus_encoder_create( this.config.encoderSampleRate, this.config.numberOfChannels, this.config.encoderApplication, errLocation );
|
|
this._free( errLocation );
|
|
|
|
if ( this.config.encoderBitRate ) {
|
|
this.setOpusControl( 4002, this.config.encoderBitRate );
|
|
}
|
|
|
|
if ( this.config.encoderComplexity ) {
|
|
this.setOpusControl( 4010, this.config.encoderComplexity );
|
|
}
|
|
|
|
this.encoderSamplesPerChannel = this.config.encoderSampleRate * this.config.encoderFrameSize / 1000;
|
|
this.encoderSamplesPerChannelPointer = this._malloc( 4 );
|
|
this.HEAP32[ this.encoderSamplesPerChannelPointer >> 2 ] = this.encoderSamplesPerChannel;
|
|
|
|
this.sampleBufferIndex = 0;
|
|
this.encoderBufferLength = this.encoderSamplesPerChannel * this.config.numberOfChannels;
|
|
this.encoderBufferPointer = this._malloc( this.encoderBufferLength * 4 ); // 4 bytes per sample
|
|
this.encoderBuffer = this.HEAPF32.subarray( this.encoderBufferPointer >> 2, (this.encoderBufferPointer >> 2) + this.encoderBufferLength );
|
|
|
|
this.encoderOutputMaxLength = 4000;
|
|
this.encoderOutputPointer = this._malloc( this.encoderOutputMaxLength );
|
|
this.encoderOutputBuffer = this.HEAPU8.subarray( this.encoderOutputPointer, this.encoderOutputPointer + this.encoderOutputMaxLength );
|
|
};
|
|
|
|
OggOpusEncoder.prototype.initResampler = function() {
|
|
if ( this.config.originalSampleRate === this.config.encoderSampleRate ) {
|
|
this.resampler = null;
|
|
return;
|
|
}
|
|
|
|
var errLocation = this._malloc( 4 );
|
|
this.resampler = this._speex_resampler_init( this.config.numberOfChannels, this.config.originalSampleRate, this.config.encoderSampleRate, this.config.resampleQuality, errLocation );
|
|
this._free( errLocation );
|
|
|
|
this.resampleSamplesPerChannel = this.config.originalSampleRate * this.config.encoderFrameSize / 1000;
|
|
this.resampleSamplesPerChannelPointer = this._malloc( 4 );
|
|
this.HEAP32[ this.resampleSamplesPerChannelPointer >> 2 ] = this.resampleSamplesPerChannel;
|
|
|
|
this.resampleBufferLength = this.resampleSamplesPerChannel * this.config.numberOfChannels;
|
|
this.resampleBufferPointer = this._malloc( this.resampleBufferLength * 4 ); // 4 bytes per sample
|
|
this.resampleBuffer = this.HEAPF32.subarray( this.resampleBufferPointer >> 2, (this.resampleBufferPointer >> 2) + this.resampleBufferLength );
|
|
};
|
|
|
|
OggOpusEncoder.prototype.interleave = function( buffers ) {
|
|
for ( var i = 0; i < this.bufferLength; i++ ) {
|
|
for ( var channel = 0; channel < this.config.numberOfChannels; channel++ ) {
|
|
this.interleavedBuffers[ i * this.config.numberOfChannels + channel ] = buffers[ channel ][ i ];
|
|
}
|
|
}
|
|
|
|
return this.interleavedBuffers;
|
|
};
|
|
|
|
OggOpusEncoder.prototype.segmentPacket = function( packetLength ) {
|
|
var packetIndex = 0;
|
|
var exportPages = [];
|
|
|
|
while ( packetLength >= 0 ) {
|
|
|
|
if ( this.segmentTableIndex === 255 ) {
|
|
exportPages.push( this.generatePage() );
|
|
this.headerType = 1;
|
|
}
|
|
|
|
var segmentLength = Math.min( packetLength, 255 );
|
|
this.segmentTable[ this.segmentTableIndex++ ] = segmentLength;
|
|
this.segmentData.set( this.encoderOutputBuffer.subarray( packetIndex, packetIndex + segmentLength ), this.segmentDataIndex );
|
|
this.segmentDataIndex += segmentLength;
|
|
packetIndex += segmentLength;
|
|
packetLength -= 255;
|
|
}
|
|
|
|
this.granulePosition += ( 48 * this.config.encoderFrameSize );
|
|
if ( this.segmentTableIndex === 255 ) {
|
|
exportPages.push( this.generatePage() );
|
|
this.headerType = 0;
|
|
}
|
|
|
|
return exportPages;
|
|
};
|
|
|
|
if(typeof exports !== 'undefined'){
|
|
exports.OggOpusEncoder = OggOpusEncoder;
|
|
} else if(typeof module === 'object' && module && module.exports){
|
|
module.exports.OggOpusEncoder = OggOpusEncoder;
|
|
}
|
|
|