diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index c4562f17f..76513d86a 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -143,8 +143,8 @@ class VoiceBroadcast extends VolumeInterface { * }) * .catch(console.error); */ - playStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { - const options = { seek, volume, passes, stream }; + playStream(stream, options = {}) { + this.setVolume(options.volume || 1); return this._playTranscodable(stream, options); } @@ -164,19 +164,17 @@ class VoiceBroadcast extends VolumeInterface { * }) * .catch(console.error); */ - playFile(file, { seek = 0, volume = 1, passes = 1 } = {}) { - const options = { seek, volume, passes }; + playFile(file, options = {}) { + this.setVolume(options.volume || 1); return this._playTranscodable(`file:${file}`, options); } _playTranscodable(media, options) { - OpusEncoders.guaranteeOpusEngine(); - this.killCurrentTranscoder(); const transcoder = this.prism.transcode({ type: 'ffmpeg', media, - ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek)]), + ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek || 0)]), }); /** * Emitted whenever an error occurs. @@ -206,31 +204,28 @@ class VoiceBroadcast extends VolumeInterface { } /** - * Plays a stream of 16-bit signed stereo PCM at 48KHz. + * Plays a stream of 16-bit signed stereo PCM. * @param {ReadableStream} stream The audio stream to play * @param {StreamOptions} [options] Options for playing the stream * @returns {VoiceBroadcast} */ - playConvertedStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { - OpusEncoders.guaranteeOpusEngine(); - + playConvertedStream(stream, options = {}) { this.killCurrentTranscoder(); - const options = { seek, volume, passes, stream }; - this.currentTranscoder = { options }; + this.setVolume(options.volume || 1); + this.currentTranscoder = { options: { stream } }; stream.once('readable', () => this._startPlaying()); return this; } /** - * Plays an Opus encoded stream at 48KHz. + * Plays an Opus encoded stream. * Note that inline volume is not compatible with this method. * @param {ReadableStream} stream The Opus audio stream to play * @param {StreamOptions} [options] Options for playing the stream * @returns {StreamDispatcher} */ - playOpusStream(stream, { seek = 0, passes = 1 } = {}) { - const options = { seek, passes, stream }; - this.currentTranscoder = { options, opus: true }; + playOpusStream(stream) { + this.currentTranscoder = { options: { stream }, opus: true }; stream.once('readable', () => this._startPlaying()); return this; } @@ -241,8 +236,9 @@ class VoiceBroadcast extends VolumeInterface { * @param {StreamOptions} [options] Options for playing the stream * @returns {VoiceBroadcast} */ - playArbitraryInput(input, { seek = 0, volume = 1, passes = 1 } = {}) { - const options = { seek, volume, passes, input }; + playArbitraryInput(input, options = {}) { + this.setVolume(options.volume || 1); + options.input = input; return this._playTranscodable(input, options); } diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index f7539021c..9163db449 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -431,6 +431,8 @@ class VoiceConnection extends EventEmitter { * @property {number} [seek=0] The time to seek to * @property {number} [volume=1] The volume to play at * @property {number} [passes=1] How many times to send the voice packet to reduce packet loss + * @property {number|string} [bitrate=48000] The bitrate (quality) of the audio. + * If set to 'auto', the voice channel's bitrate will be used */ /** @@ -481,7 +483,7 @@ class VoiceConnection extends EventEmitter { } /** - * Plays a stream of 16-bit signed stereo PCM at 48KHz. + * Plays a stream of 16-bit signed stereo PCM. * @param {ReadableStream} stream The audio stream to play * @param {StreamOptions} [options] Options for playing the stream * @returns {StreamDispatcher} @@ -491,7 +493,7 @@ class VoiceConnection extends EventEmitter { } /** - * Plays an Opus encoded stream at 48KHz. + * Plays an Opus encoded stream. * Note that inline volume is not compatible with this method. * @param {ReadableStream} stream The Opus audio stream to play * @param {StreamOptions} [options] Options for playing the stream @@ -504,6 +506,7 @@ class VoiceConnection extends EventEmitter { /** * Plays a voice broadcast. * @param {VoiceBroadcast} broadcast The broadcast to play + * @param {StreamOptions} [options] Options for playing the stream * @returns {StreamDispatcher} * @example * // Play a broadcast @@ -512,8 +515,8 @@ class VoiceConnection extends EventEmitter { * .playFile('./test.mp3'); * const dispatcher = voiceConnection.playBroadcast(broadcast); */ - playBroadcast(broadcast) { - return this.player.playBroadcast(broadcast); + playBroadcast(broadcast, options) { + return this.player.playBroadcast(broadcast, options); } /** diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index 68c83d380..24df9d069 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -119,6 +119,16 @@ class StreamDispatcher extends VolumeInterface { this.emit('speaking', value); } + + /** + * Set the bitrate of the current Opus encoder. + * @param {number} bitrate New bitrate, in kbps. + * If set to 'auto', the voice channel's bitrate will be used + */ + setBitrate(bitrate) { + this.player.setBitrate(bitrate); + } + sendBuffer(buffer, sequence, timestamp, opusPacket) { opusPacket = opusPacket || this.player.opusEncoder.encode(buffer); const packet = this.createPacket(sequence, timestamp, opusPacket); diff --git a/src/client/voice/opus/BaseOpusEngine.js b/src/client/voice/opus/BaseOpusEngine.js index b63b695f6..40c6204fd 100644 --- a/src/client/voice/opus/BaseOpusEngine.js +++ b/src/client/voice/opus/BaseOpusEngine.js @@ -4,21 +4,38 @@ */ class BaseOpus { /** - * @param {Object} [options] The options to apply to the Opus engine - * @param {boolean} [options.fec] Whether to enable forward error correction (defaults to false) - * @param {number} [options.plp] The expected packet loss percentage (0-1 inclusive, defaults to 0) + * @param {Object} [options] The options to apply to the Opus engine. + * @param {number} [options.bitrate=48] The desired bitrate (kbps). + * @param {boolean} [options.fec=false] Whether to enable forward error correction. + * @param {number} [options.plp=0] The expected packet loss percentage. */ - constructor(options = {}) { + constructor({ bitrate = 48, fec = false, plp = 0 } = {}) { this.ctl = { + BITRATE: 4002, FEC: 4012, PLP: 4014, }; - this.options = options; + this.samplingRate = 48000; + this.channels = 2; + + /** + * The desired bitrate (kbps) + * @type {number} + */ + this.bitrate = bitrate; + + /** + * Miscellaneous Opus options + * @type {Object} + */ + this.options = { fec, plp }; } init() { try { + this.setBitrate(this.bitrate); + // Set FEC (forward error correction) if (this.options.fec) this.setFEC(this.options.fec); diff --git a/src/client/voice/opus/NodeOpusEngine.js b/src/client/voice/opus/NodeOpusEngine.js index 72f2a818b..02e880637 100644 --- a/src/client/voice/opus/NodeOpusEngine.js +++ b/src/client/voice/opus/NodeOpusEngine.js @@ -10,10 +10,14 @@ class NodeOpusEngine extends OpusEngine { } catch (err) { throw err; } - this.encoder = new opus.OpusEncoder(48000, 2); + this.encoder = new opus.OpusEncoder(this.samplingRate, this.channels); super.init(); } + setBitrate(bitrate) { + this.encoder.applyEncoderCTL(this.ctl.BITRATE, Math.min(128, Math.max(16, bitrate)) * 1000); + } + setFEC(enabled) { this.encoder.applyEncoderCTL(this.ctl.FEC, enabled ? 1 : 0); } diff --git a/src/client/voice/opus/OpusEngineList.js b/src/client/voice/opus/OpusEngineList.js index 3b69cdaeb..e6ede9a5a 100644 --- a/src/client/voice/opus/OpusEngineList.js +++ b/src/client/voice/opus/OpusEngineList.js @@ -3,8 +3,6 @@ const list = [ require('./OpusScriptEngine'), ]; -let opusEngineFound; - function fetch(Encoder, engineOptions) { try { return new Encoder(engineOptions); @@ -25,10 +23,9 @@ exports.fetch = engineOptions => { const fetched = fetch(encoder, engineOptions); if (fetched) return fetched; } - return null; -}; exports.guaranteeOpusEngine = () => { if (typeof opusEngineFound === 'undefined') opusEngineFound = Boolean(exports.fetch()); if (!opusEngineFound) throw new Error('Couldn\'t find an Opus engine.'); + throw new Error('OPUS_ENGINE_MISSING'); }; diff --git a/src/client/voice/opus/OpusScriptEngine.js b/src/client/voice/opus/OpusScriptEngine.js index 81ca6206b..a5e046d40 100644 --- a/src/client/voice/opus/OpusScriptEngine.js +++ b/src/client/voice/opus/OpusScriptEngine.js @@ -10,10 +10,14 @@ class OpusScriptEngine extends OpusEngine { } catch (err) { throw err; } - this.encoder = new OpusScript(48000, 2); + this.encoder = new OpusScript(this.samplingRate, this.channels); super.init(); } + setBitrate(bitrate) { + this.encoder.encoderCTL(this.ctl.BITRATE, Math.min(128, Math.max(16, bitrate)) * 1000); + } + setFEC(enabled) { this.encoder.encoderCTL(this.ctl.FEC, enabled ? 1 : 0); } diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index afbc9a671..2a264f9e6 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -30,11 +30,6 @@ class AudioPlayer extends EventEmitter { * @type {Prism} */ this.prism = new Prism(); - /** - * The opus encoder that the player uses - * @type {NodeOpusEngine|OpusScriptEngine} - */ - this.opusEncoder = OpusEncoders.fetch(); this.streams = new Collection(); this.currentStream = {}; this.streamingData = { @@ -67,6 +62,7 @@ class AudioPlayer extends EventEmitter { destroy() { if (this.opusEncoder) this.opusEncoder.destroy(); + this.opusEncoder = null; } destroyCurrentStream() { @@ -83,13 +79,25 @@ class AudioPlayer extends EventEmitter { this.currentStream = {}; } - playUnknownStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { - OpusEncoders.guaranteeOpusEngine(); - const options = { seek, volume, passes }; + /** + * Set the bitrate of the current Opus encoder. + * @param {number} value New bitrate, in kbps. + * If set to 'auto', the voice channel's bitrate will be used + */ + setBitrate(value) { + if (!value) return; + if (!this.opusEncoder) return; + const bitrate = value === 'auto' ? this.voiceConnection.channel.bitrate : value; + this.opusEncoder.setBitrate(bitrate); + } + + playUnknownStream(stream, options = {}) { + this.destroy(); + this.opusEncoder = OpusEncoders.fetch(options); const transcoder = this.prism.transcode({ type: 'ffmpeg', media: stream, - ffmpegArguments: ffmpegArguments.concat(['-ss', String(seek)]), + ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek || 0)]), }); this.destroyCurrentStream(); this.currentStream = { @@ -105,9 +113,10 @@ class AudioPlayer extends EventEmitter { return this.playPCMStream(transcoder.output, options, true); } - playPCMStream(stream, { seek = 0, volume = 1, passes = 1 } = {}, fromUnknown = false) { - OpusEncoders.guaranteeOpusEngine(); - const options = { seek, volume, passes }; + playPCMStream(stream, options = {}, fromUnknown = false) { + this.destroy(); + this.opusEncoder = OpusEncoders.fetch(options); + this.setBitrate(options.bitrate); const dispatcher = this.createDispatcher(stream, options); if (fromUnknown) { this.currentStream.dispatcher = dispatcher; @@ -122,8 +131,8 @@ class AudioPlayer extends EventEmitter { return dispatcher; } - playOpusStream(stream, { seek = 0, passes = 1 } = {}) { - const options = { seek, passes, opus: true }; + playOpusStream(stream, options = {}) { + options.opus = true; this.destroyCurrentStream(); const dispatcher = this.createDispatcher(stream, options); this.currentStream = { @@ -134,8 +143,7 @@ class AudioPlayer extends EventEmitter { return dispatcher; } - playBroadcast(broadcast, { volume = 1, passes = 1 } = {}) { - const options = { volume, passes }; + playBroadcast(broadcast, options) { this.destroyCurrentStream(); const dispatcher = this.createDispatcher(broadcast, options); this.currentStream = { @@ -148,7 +156,9 @@ class AudioPlayer extends EventEmitter { return dispatcher; } - createDispatcher(stream, options) { + createDispatcher(stream, { seek = 0, volume = 1, passes = 1 } = {}) { + const options = { seek, volume, passes }; + const dispatcher = new StreamDispatcher(this, stream, options); dispatcher.on('end', () => this.destroyCurrentStream()); dispatcher.on('error', () => this.destroyCurrentStream()); diff --git a/src/structures/VoiceChannel.js b/src/structures/VoiceChannel.js index 617b4eee0..af69a1e00 100644 --- a/src/structures/VoiceChannel.js +++ b/src/structures/VoiceChannel.js @@ -25,7 +25,7 @@ class VoiceChannel extends GuildChannel { * The bitrate of this voice channel * @type {number} */ - this.bitrate = data.bitrate; + this.bitrate = data.bitrate * 0.001; /** * The maximum amount of users allowed in this channel - 0 means unlimited. @@ -76,16 +76,17 @@ class VoiceChannel extends GuildChannel { } /** - * Sets the bitrate of the channel. + * Sets the bitrate of the channel (in kbps). * @param {number} bitrate The new bitrate * @returns {Promise} * @example * // Set the bitrate of a voice channel - * voiceChannel.setBitrate(48000) - * .then(vc => console.log(`Set bitrate to ${vc.bitrate} for ${vc.name}`)) + * voiceChannel.setBitrate(48) + * .then(vc => console.log(`Set bitrate to ${vc.bitrate}kbps for ${vc.name}`)) * .catch(console.error); */ setBitrate(bitrate) { + bitrate *= 1000; return this.edit({ bitrate }); }