Audio bitrate support (#1439)

* Audio bitrate support

Note: not implemented for VoiceBroadcasts

* Fix default args, auto bitrate

* Late night typos are the best

* Changes bitrate to kbps for VoiceChannel stuff

* Add methods to manipulate bitrate while encoding
This commit is contained in:
aemino
2017-07-26 01:06:40 -07:00
committed by Crawl
parent 7eb9e65c41
commit 4342ed29a8
9 changed files with 97 additions and 58 deletions

View File

@@ -143,8 +143,8 @@ class VoiceBroadcast extends VolumeInterface {
* }) * })
* .catch(console.error); * .catch(console.error);
*/ */
playStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { playStream(stream, options = {}) {
const options = { seek, volume, passes, stream }; this.setVolume(options.volume || 1);
return this._playTranscodable(stream, options); return this._playTranscodable(stream, options);
} }
@@ -164,19 +164,17 @@ class VoiceBroadcast extends VolumeInterface {
* }) * })
* .catch(console.error); * .catch(console.error);
*/ */
playFile(file, { seek = 0, volume = 1, passes = 1 } = {}) { playFile(file, options = {}) {
const options = { seek, volume, passes }; this.setVolume(options.volume || 1);
return this._playTranscodable(`file:${file}`, options); return this._playTranscodable(`file:${file}`, options);
} }
_playTranscodable(media, options) { _playTranscodable(media, options) {
OpusEncoders.guaranteeOpusEngine();
this.killCurrentTranscoder(); this.killCurrentTranscoder();
const transcoder = this.prism.transcode({ const transcoder = this.prism.transcode({
type: 'ffmpeg', type: 'ffmpeg',
media, media,
ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek)]), ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek || 0)]),
}); });
/** /**
* Emitted whenever an error occurs. * 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 {ReadableStream} stream The audio stream to play
* @param {StreamOptions} [options] Options for playing the stream * @param {StreamOptions} [options] Options for playing the stream
* @returns {VoiceBroadcast} * @returns {VoiceBroadcast}
*/ */
playConvertedStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { playConvertedStream(stream, options = {}) {
OpusEncoders.guaranteeOpusEngine();
this.killCurrentTranscoder(); this.killCurrentTranscoder();
const options = { seek, volume, passes, stream }; this.setVolume(options.volume || 1);
this.currentTranscoder = { options }; this.currentTranscoder = { options: { stream } };
stream.once('readable', () => this._startPlaying()); stream.once('readable', () => this._startPlaying());
return this; return this;
} }
/** /**
* Plays an Opus encoded stream at 48KHz. * Plays an Opus encoded stream.
* <warn>Note that inline volume is not compatible with this method.</warn> * <warn>Note that inline volume is not compatible with this method.</warn>
* @param {ReadableStream} stream The Opus audio stream to play * @param {ReadableStream} stream The Opus audio stream to play
* @param {StreamOptions} [options] Options for playing the stream * @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher} * @returns {StreamDispatcher}
*/ */
playOpusStream(stream, { seek = 0, passes = 1 } = {}) { playOpusStream(stream) {
const options = { seek, passes, stream }; this.currentTranscoder = { options: { stream }, opus: true };
this.currentTranscoder = { options, opus: true };
stream.once('readable', () => this._startPlaying()); stream.once('readable', () => this._startPlaying());
return this; return this;
} }
@@ -241,8 +236,9 @@ class VoiceBroadcast extends VolumeInterface {
* @param {StreamOptions} [options] Options for playing the stream * @param {StreamOptions} [options] Options for playing the stream
* @returns {VoiceBroadcast} * @returns {VoiceBroadcast}
*/ */
playArbitraryInput(input, { seek = 0, volume = 1, passes = 1 } = {}) { playArbitraryInput(input, options = {}) {
const options = { seek, volume, passes, input }; this.setVolume(options.volume || 1);
options.input = input;
return this._playTranscodable(input, options); return this._playTranscodable(input, options);
} }

View File

@@ -432,6 +432,8 @@ class VoiceConnection extends EventEmitter {
* @property {number} [seek=0] The time to seek to * @property {number} [seek=0] The time to seek to
* @property {number} [volume=1] The volume to play at * @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} [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
*/ */
/** /**
@@ -482,7 +484,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 {ReadableStream} stream The audio stream to play
* @param {StreamOptions} [options] Options for playing the stream * @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher} * @returns {StreamDispatcher}
@@ -492,7 +494,7 @@ class VoiceConnection extends EventEmitter {
} }
/** /**
* Plays an Opus encoded stream at 48KHz. * Plays an Opus encoded stream.
* <warn>Note that inline volume is not compatible with this method.</warn> * <warn>Note that inline volume is not compatible with this method.</warn>
* @param {ReadableStream} stream The Opus audio stream to play * @param {ReadableStream} stream The Opus audio stream to play
* @param {StreamOptions} [options] Options for playing the stream * @param {StreamOptions} [options] Options for playing the stream
@@ -505,6 +507,7 @@ class VoiceConnection extends EventEmitter {
/** /**
* Plays a voice broadcast. * Plays a voice broadcast.
* @param {VoiceBroadcast} broadcast The broadcast to play * @param {VoiceBroadcast} broadcast The broadcast to play
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher} * @returns {StreamDispatcher}
* @example * @example
* // Play a broadcast * // Play a broadcast
@@ -513,8 +516,8 @@ class VoiceConnection extends EventEmitter {
* .playFile('./test.mp3'); * .playFile('./test.mp3');
* const dispatcher = voiceConnection.playBroadcast(broadcast); * const dispatcher = voiceConnection.playBroadcast(broadcast);
*/ */
playBroadcast(broadcast) { playBroadcast(broadcast, options) {
return this.player.playBroadcast(broadcast); return this.player.playBroadcast(broadcast, options);
} }
/** /**

View File

@@ -119,6 +119,16 @@ class StreamDispatcher extends VolumeInterface {
this.emit('speaking', value); 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) { sendBuffer(buffer, sequence, timestamp, opusPacket) {
opusPacket = opusPacket || this.player.opusEncoder.encode(buffer); opusPacket = opusPacket || this.player.opusEncoder.encode(buffer);
const packet = this.createPacket(sequence, timestamp, opusPacket); const packet = this.createPacket(sequence, timestamp, opusPacket);

View File

@@ -4,21 +4,38 @@
*/ */
class BaseOpus { class BaseOpus {
/** /**
* @param {Object} [options] The options to apply to the Opus engine * @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.bitrate=48] The desired bitrate (kbps).
* @param {number} [options.plp] The expected packet loss percentage (0-1 inclusive, defaults to 0) * @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 = { this.ctl = {
BITRATE: 4002,
FEC: 4012, FEC: 4012,
PLP: 4014, 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() { init() {
try { try {
this.setBitrate(this.bitrate);
// Set FEC (forward error correction) // Set FEC (forward error correction)
if (this.options.fec) this.setFEC(this.options.fec); if (this.options.fec) this.setFEC(this.options.fec);

View File

@@ -10,10 +10,14 @@ class NodeOpusEngine extends OpusEngine {
} catch (err) { } catch (err) {
throw err; throw err;
} }
this.encoder = new opus.OpusEncoder(48000, 2); this.encoder = new opus.OpusEncoder(this.samplingRate, this.channels);
super.init(); super.init();
} }
setBitrate(bitrate) {
this.encoder.applyEncoderCTL(this.ctl.BITRATE, Math.min(128, Math.max(16, bitrate)) * 1000);
}
setFEC(enabled) { setFEC(enabled) {
this.encoder.applyEncoderCTL(this.ctl.FEC, enabled ? 1 : 0); this.encoder.applyEncoderCTL(this.ctl.FEC, enabled ? 1 : 0);
} }

View File

@@ -5,8 +5,6 @@ const list = [
require('./OpusScriptEngine'), require('./OpusScriptEngine'),
]; ];
let opusEngineFound;
function fetch(Encoder, engineOptions) { function fetch(Encoder, engineOptions) {
try { try {
return new Encoder(engineOptions); return new Encoder(engineOptions);
@@ -27,10 +25,6 @@ exports.fetch = engineOptions => {
const fetched = fetch(encoder, engineOptions); const fetched = fetch(encoder, engineOptions);
if (fetched) return fetched; if (fetched) return fetched;
} }
return null;
};
exports.guaranteeOpusEngine = () => { throw new Error('OPUS_ENGINE_MISSING');
if (typeof opusEngineFound === 'undefined') opusEngineFound = Boolean(exports.fetch());
if (!opusEngineFound) throw new Error('OPUS_ENGINE_MISSING');
}; };

View File

@@ -10,10 +10,14 @@ class OpusScriptEngine extends OpusEngine {
} catch (err) { } catch (err) {
throw err; throw err;
} }
this.encoder = new OpusScript(48000, 2); this.encoder = new OpusScript(this.samplingRate, this.channels);
super.init(); super.init();
} }
setBitrate(bitrate) {
this.encoder.encoderCTL(this.ctl.BITRATE, Math.min(128, Math.max(16, bitrate)) * 1000);
}
setFEC(enabled) { setFEC(enabled) {
this.encoder.encoderCTL(this.ctl.FEC, enabled ? 1 : 0); this.encoder.encoderCTL(this.ctl.FEC, enabled ? 1 : 0);
} }

View File

@@ -30,11 +30,6 @@ class AudioPlayer extends EventEmitter {
* @type {Prism} * @type {Prism}
*/ */
this.prism = new Prism(); this.prism = new Prism();
/**
* The opus encoder that the player uses
* @type {NodeOpusEngine|OpusScriptEngine}
*/
this.opusEncoder = OpusEncoders.fetch();
this.streams = new Collection(); this.streams = new Collection();
this.currentStream = {}; this.currentStream = {};
this.streamingData = { this.streamingData = {
@@ -67,6 +62,7 @@ class AudioPlayer extends EventEmitter {
destroy() { destroy() {
if (this.opusEncoder) this.opusEncoder.destroy(); if (this.opusEncoder) this.opusEncoder.destroy();
this.opusEncoder = null;
} }
destroyCurrentStream() { destroyCurrentStream() {
@@ -83,13 +79,25 @@ class AudioPlayer extends EventEmitter {
this.currentStream = {}; this.currentStream = {};
} }
playUnknownStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { /**
OpusEncoders.guaranteeOpusEngine(); * Set the bitrate of the current Opus encoder.
const options = { seek, volume, passes }; * @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({ const transcoder = this.prism.transcode({
type: 'ffmpeg', type: 'ffmpeg',
media: stream, media: stream,
ffmpegArguments: ffmpegArguments.concat(['-ss', String(seek)]), ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek || 0)]),
}); });
this.destroyCurrentStream(); this.destroyCurrentStream();
this.currentStream = { this.currentStream = {
@@ -105,9 +113,10 @@ class AudioPlayer extends EventEmitter {
return this.playPCMStream(transcoder.output, options, true); return this.playPCMStream(transcoder.output, options, true);
} }
playPCMStream(stream, { seek = 0, volume = 1, passes = 1 } = {}, fromUnknown = false) { playPCMStream(stream, options = {}, fromUnknown = false) {
OpusEncoders.guaranteeOpusEngine(); this.destroy();
const options = { seek, volume, passes }; this.opusEncoder = OpusEncoders.fetch(options);
this.setBitrate(options.bitrate);
const dispatcher = this.createDispatcher(stream, options); const dispatcher = this.createDispatcher(stream, options);
if (fromUnknown) { if (fromUnknown) {
this.currentStream.dispatcher = dispatcher; this.currentStream.dispatcher = dispatcher;
@@ -122,8 +131,8 @@ class AudioPlayer extends EventEmitter {
return dispatcher; return dispatcher;
} }
playOpusStream(stream, { seek = 0, passes = 1 } = {}) { playOpusStream(stream, options = {}) {
const options = { seek, passes, opus: true }; options.opus = true;
this.destroyCurrentStream(); this.destroyCurrentStream();
const dispatcher = this.createDispatcher(stream, options); const dispatcher = this.createDispatcher(stream, options);
this.currentStream = { this.currentStream = {
@@ -134,8 +143,7 @@ class AudioPlayer extends EventEmitter {
return dispatcher; return dispatcher;
} }
playBroadcast(broadcast, { volume = 1, passes = 1 } = {}) { playBroadcast(broadcast, options) {
const options = { volume, passes };
this.destroyCurrentStream(); this.destroyCurrentStream();
const dispatcher = this.createDispatcher(broadcast, options); const dispatcher = this.createDispatcher(broadcast, options);
this.currentStream = { this.currentStream = {
@@ -148,7 +156,9 @@ class AudioPlayer extends EventEmitter {
return dispatcher; 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); const dispatcher = new StreamDispatcher(this, stream, options);
dispatcher.on('end', () => this.destroyCurrentStream()); dispatcher.on('end', () => this.destroyCurrentStream());
dispatcher.on('error', () => this.destroyCurrentStream()); dispatcher.on('error', () => this.destroyCurrentStream());

View File

@@ -26,7 +26,7 @@ class VoiceChannel extends GuildChannel {
* The bitrate of this voice channel * The bitrate of this voice channel
* @type {number} * @type {number}
*/ */
this.bitrate = data.bitrate; this.bitrate = data.bitrate * 0.001;
/** /**
* The maximum amount of users allowed in this channel - 0 means unlimited. * The maximum amount of users allowed in this channel - 0 means unlimited.
@@ -77,16 +77,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 * @param {number} bitrate The new bitrate
* @returns {Promise<VoiceChannel>} * @returns {Promise<VoiceChannel>}
* @example * @example
* // Set the bitrate of a voice channel * // Set the bitrate of a voice channel
* voiceChannel.setBitrate(48000) * voiceChannel.setBitrate(48)
* .then(vc => console.log(`Set bitrate to ${vc.bitrate} for ${vc.name}`)) * .then(vc => console.log(`Set bitrate to ${vc.bitrate}kbps for ${vc.name}`))
* .catch(console.error); * .catch(console.error);
*/ */
setBitrate(bitrate) { setBitrate(bitrate) {
bitrate *= 1000;
return this.edit({ bitrate }); return this.edit({ bitrate });
} }