var util = require( 'util' ); var Transform = require( 'stream' ).Transform; var ogg_packet = require( 'ogg-packet' ); var OpusEncoder = require( './OpusEncoder' ); // These are the valid rates for libopus according to // https://www.opus-codec.org/docs/opus_api-1.1.2/group__opus__encoder.html#gaa89264fd93c9da70362a0c9b96b9ca88 var VALID_RATES = [ 8000, 12000, 16000, 24000, 48000 ]; var Encoder = function( rate, channels, frameSize ) { Transform.call( this, { readableObjectMode: true } ); this.rate = rate || 48000; // Ensure the range is valid. if( VALID_RATES.indexOf( this.rate ) === -1 ) { throw new RangeError( 'Encoder rate (' + this.rate + ') is not valid. ' + 'Valid rates are: ' + VALID_RATES.join( ', ' ) ); } this.channels = channels || 1; this.frameSize = frameSize || this.rate * 0.04; this.encoder = new OpusEncoder( this.rate, this.channels ); this.frameOverflow = Buffer.alloc(0); this.headerWritten = false; this.pos = 0; this.granulepos = 0; this.samplesWritten = 0; }; util.inherits( Encoder, Transform ); /** * Transform stream callback */ Encoder.prototype._transform = function( buf, encoding, done ) { // Transform the buffer this._processOutput( buf ); done(); }; Encoder.prototype._writeHeader = function() { // OpusHead packet var magicSignature = Buffer.from( 'OpusHead', 'ascii' ); var data = Buffer.from([ 0x01, // version this.channels, 0x00, 0x0f, // Preskip (default and recommended 3840) ( ( this.rate & 0x000000ff ) >> 0 ), ( ( this.rate & 0x0000ff00 ) >> 8 ), ( ( this.rate & 0x00ff0000 ) >> 16 ), ( ( this.rate & 0xff000000 ) >> 24 ), 0x00, 0x00, // gain 0x00, // Channel mappign (RTP, mono/stereo) ]); var header = Buffer.concat([ magicSignature, data ]); var packet = new ogg_packet(); packet.packet = header; packet.bytes = header.length; packet.b_o_s = 1; packet.e_o_s = 0; packet.granulepos = -1; packet.packetno = this.pos++; this.push( packet ); // OpusTags packet magicSignature = Buffer.from( 'OpusTags', 'ascii' ); var vendor = Buffer.from( 'node-opus', 'ascii' ); var vendorLength = Buffer.alloc( 4 ); vendorLength.writeUInt32LE( vendor.length, 0 ); var commentLength = Buffer.alloc( 4 ); commentLength.writeUInt32LE( 0, 0 ); header = Buffer.concat([ magicSignature, vendorLength, vendor, commentLength, Buffer.from([ 0xff ]) ]); packet = new ogg_packet(); packet.packet = header; packet.bytes = header.length; packet.b_o_s = 0; packet.e_o_s = 0; packet.granulepos = -1; packet.packetno = this.pos++; packet.flush = true; this.push( packet ); this.headerWritten = true; }; Encoder.prototype._processOutput = function( buf ) { // Calculate the total data available and data required for each frame. var totalData = buf.length + this.frameOverflow.length; var requiredData = this.frameSize * 2 * this.channels; // Process output while we got enough for a frame. while( totalData >= requiredData ) { // If we got overflow, use it up first. var buffer; if( this.frameOverflow ) { buffer = Buffer.concat([ this.frameOverflow, buf.slice( 0, requiredData - this.frameOverflow.length ) ]); // Cut the already used part off the buf. buf = buf.slice( requiredData - this.frameOverflow.length ); // Remove overflow. We'll set it later so it'll never be null // outside of this function. this.frameOverflow = null; } else { // We got no overflow. // Just cut the required bits from the buffer buffer = buf.slice( 0, requiredData ); buf = buf.slice( requiredData ); } // Flush frame and remove bits from the total data counter before // repeating loop. this._flushFrame( buffer ); totalData -= requiredData; } // Store the remainign buffer in the overflow. this.frameOverflow = buf; }; Encoder.prototype._flushFrame = function( frame, end ) { var encoded = this.encoder.encode( frame ); this._pushEncodedBuffer(encoded, end); }; Encoder.prototype._pushEncodedBuffer = function( encoded, end ) { // Write the header if it hasn't been written yet if( !this.headerWritten ) { this._writeHeader(); } if( this.lastPacket ) { this.push( this.lastPacket ); } // Scale the frame size into 48 kHz bitrate, which is used for the // granule positioning. We'll still update the samplesWritten just to // ensure backwards compatibility. this.granulepos += this.frameSize / this.rate * 48000; this.samplesWritten += this.frameSize; var packet = new ogg_packet(); packet.packet = encoded; packet.bytes = encoded.length, packet.b_o_s = 0; packet.e_o_s = 0; packet.granulepos = this.granulepos; packet.packetno = this.pos++; packet.flush = true; this.lastPacket = packet; }; Encoder.prototype._flush = function( done ) { if( this.lastPacket ) { this.lastPacket.e_o_s = 1; this.push( this.lastPacket ); } done(); }; module.exports = Encoder;