opus-encdec
Version:
A JavaScript library for encoding and decoding OPUS audio
375 lines (320 loc) • 14.8 kB
JavaScript
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;
}