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 };