buddy

node MVC discord bot
Log | Files | Refs | README

Opus.js (5803B)


      1 // Partly based on https://github.com/Rantanen/node-opus/blob/master/lib/Encoder.js
      2 
      3 const { Transform } = require('stream');
      4 const loader = require('../util/loader');
      5 
      6 const CTL = {
      7   BITRATE: 4002,
      8   FEC: 4012,
      9   PLP: 4014,
     10 };
     11 
     12 let Opus = {};
     13 
     14 function loadOpus(refresh = false) {
     15   if (Opus.Encoder && !refresh) return Opus;
     16 
     17   Opus = loader.require([
     18     ['@discordjs/opus', opus => ({ Encoder: opus.OpusEncoder })],
     19     ['node-opus', opus => ({ Encoder: opus.OpusEncoder })],
     20     ['opusscript', opus => ({ Encoder: opus })],
     21   ]);
     22   return Opus;
     23 }
     24 
     25 const charCode = x => x.charCodeAt(0);
     26 const OPUS_HEAD = Buffer.from([...'OpusHead'].map(charCode));
     27 const OPUS_TAGS = Buffer.from([...'OpusTags'].map(charCode));
     28 
     29 // frame size = (channels * rate * frame_duration) / 1000
     30 
     31 /**
     32  * Takes a stream of Opus data and outputs a stream of PCM data, or the inverse.
     33  * **You shouldn't directly instantiate this class, see opus.Encoder and opus.Decoder instead!**
     34  * @memberof opus
     35  * @extends TransformStream
     36  * @protected
     37  */
     38 class OpusStream extends Transform {
     39   /**
     40    * Creates a new Opus transformer.
     41    * @private
     42    * @memberof opus
     43    * @param {Object} [options] options that you would pass to a regular Transform stream
     44    */
     45   constructor(options = {}) {
     46     if (!loadOpus().Encoder) {
     47       throw Error('Could not find an Opus module! Please install @discordjs/opus, node-opus, or opusscript.');
     48     }
     49     super(Object.assign({ readableObjectMode: true }, options));
     50     if (Opus.name === 'opusscript') {
     51       options.application = Opus.Encoder.Application[options.application];
     52     }
     53     this.encoder = new Opus.Encoder(options.rate, options.channels, options.application);
     54 
     55     this._options = options;
     56     this._required = this._options.frameSize * this._options.channels * 2;
     57   }
     58 
     59   _encode(buffer) {
     60     return this.encoder.encode(buffer, this._options.frameSize);
     61   }
     62 
     63   _decode(buffer) {
     64     return this.encoder.decode(buffer, Opus.name === 'opusscript' ? null : this._options.frameSize);
     65   }
     66 
     67   /**
     68    * Returns the Opus module being used - `opusscript`, `node-opus`, or `@discordjs/opus`.
     69    * @type {string}
     70    * @readonly
     71    * @example
     72    * console.log(`Using Opus module ${prism.opus.Encoder.type}`);
     73    */
     74   static get type() {
     75     return Opus.name;
     76   }
     77 
     78   /**
     79    * Sets the bitrate of the stream.
     80    * @param {number} bitrate the bitrate to use use, e.g. 48000
     81    * @public
     82    */
     83   setBitrate(bitrate) {
     84     (this.encoder.applyEncoderCTL || this.encoder.encoderCTL)
     85       .apply(this.encoder, [CTL.BITRATE, Math.min(128e3, Math.max(16e3, bitrate))]);
     86   }
     87 
     88   /**
     89    * Enables or disables forward error correction.
     90    * @param {boolean} enabled whether or not to enable FEC.
     91    * @public
     92    */
     93   setFEC(enabled) {
     94     (this.encoder.applyEncoderCTL || this.encoder.encoderCTL)
     95       .apply(this.encoder, [CTL.FEC, enabled ? 1 : 0]);
     96   }
     97 
     98   /**
     99    * Sets the expected packet loss over network transmission.
    100    * @param {number} [percentage] a percentage (represented between 0 and 1)
    101    */
    102   setPLP(percentage) {
    103     (this.encoder.applyEncoderCTL || this.encoder.encoderCTL)
    104       .apply(this.encoder, [CTL.PLP, Math.min(100, Math.max(0, percentage * 100))]);
    105   }
    106 
    107   _final(cb) {
    108     if (Opus.name === 'opusscript' && this.encoder) this.encoder.delete();
    109     cb();
    110   }
    111 }
    112 
    113 /**
    114  * An Opus encoder stream.
    115  *
    116  * Outputs opus packets in [object mode.](https://nodejs.org/api/stream.html#stream_object_mode)
    117  * @extends opus.OpusStream
    118  * @memberof opus
    119  * @example
    120  * const encoder = new prism.opus.Encoder({ frameSize: 960, channels: 2, rate: 48000 });
    121  * pcmAudio.pipe(encoder);
    122  * // encoder will now output Opus-encoded audio packets
    123  */
    124 class Encoder extends OpusStream {
    125   /**
    126    * Creates a new Opus encoder stream.
    127    * @memberof opus
    128    * @param {Object} options options that you would pass to a regular OpusStream, plus a few more:
    129    * @param {number} options.frameSize the frame size in bytes to use (e.g. 960 for stereo audio at 48KHz with a frame
    130    * duration of 20ms)
    131    * @param {number} options.channels the number of channels to use
    132    * @param {number} options.rate the sampling rate in Hz
    133    */
    134   constructor(options) {
    135     super(options);
    136     this._buffer = Buffer.alloc(0);
    137   }
    138 
    139   async _transform(chunk, encoding, done) {
    140     this._buffer = Buffer.concat([this._buffer, chunk]);
    141     let n = 0;
    142     while (this._buffer.length >= this._required * (n + 1)) {
    143       const buf = await this._encode(this._buffer.slice(n * this._required, (n + 1) * this._required));
    144       this.push(buf);
    145       n++;
    146     }
    147     if (n > 0) this._buffer = this._buffer.slice(n * this._required);
    148     return done();
    149   }
    150 
    151   _destroy(err, cb) {
    152     super._destroy(err, cb);
    153     this._buffer = null;
    154   }
    155 }
    156 
    157 /**
    158  * An Opus decoder stream.
    159  *
    160  * Note that any stream you pipe into this must be in
    161  * [object mode](https://nodejs.org/api/stream.html#stream_object_mode) and should output Opus packets.
    162  * @extends opus.OpusStream
    163  * @memberof opus
    164  * @example
    165  * const decoder = new prism.opus.Decoder({ frameSize: 960, channels: 2, rate: 48000 });
    166  * input.pipe(decoder);
    167  * // decoder will now output PCM audio
    168  */
    169 class Decoder extends OpusStream {
    170   _transform(chunk, encoding, done) {
    171     const signature = chunk.slice(0, 8);
    172     if (signature.equals(OPUS_HEAD)) {
    173       this.emit('format', {
    174         channels: this._options.channels,
    175         sampleRate: this._options.rate,
    176         bitDepth: 16,
    177         float: false,
    178         signed: true,
    179         version: chunk.readUInt8(8),
    180         preSkip: chunk.readUInt16LE(10),
    181         gain: chunk.readUInt16LE(16),
    182       });
    183       return done();
    184     }
    185     if (signature.equals(OPUS_TAGS)) {
    186       this.emit('tags', chunk);
    187       return done();
    188     }
    189     this.push(this._decode(chunk));
    190     return done();
    191   }
    192 }
    193 
    194 module.exports = { Decoder, Encoder };