UNPKG

opus-encdec

Version:

A JavaScript library for encoding and decoding OPUS audio

375 lines (320 loc) 14.8 kB
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 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 { this.encodedData.push( new Uint8Array(this.encoderOutputBuffer.subarray(0, packetLength)) ); this.encodedDataLength += packetLength; } this.sampleBufferIndex = 0; } } return exportPages; }; 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 { 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 { this.encodedData.push( 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 { this.encodedData.push( 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; }