188 lines
5.2 KiB
JavaScript
188 lines
5.2 KiB
JavaScript
|
|
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;
|