Added Opus stream support, added volume interface (#1102)

* Added opus stream support, added volume interface

* Remove setImmediate

* Fix weird syntax error

* Most useless commit ever

You're welcome, @PgBiel

* Fix potential memory leak with OpusScript

Emscripten has the tendency to not free resources even when the Opus engine instance has been garbage collected. Thanks to @abalabahaha for pointing this out.

* Typo

* VoiceReceiver.destroy: destroy opus encoder
This commit is contained in:
Programmix
2017-01-29 11:07:33 -08:00
committed by Amish Shah
parent 6fae17912e
commit 7ed58f5f7f
9 changed files with 262 additions and 190 deletions

View File

@@ -1,4 +1,4 @@
const EventEmitter = require('events').EventEmitter; const VolumeInterface = require('./util/VolumeInterface');
const Prism = require('prism-media'); const Prism = require('prism-media');
const OpusEncoders = require('./opus/OpusEngineList'); const OpusEncoders = require('./opus/OpusEngineList');
const Collection = require('../../util/Collection'); const Collection = require('../../util/Collection');
@@ -15,7 +15,7 @@ const ffmpegArguments = [
* A voice broadcast can be played across multiple voice connections for improved shared-stream efficiency. * A voice broadcast can be played across multiple voice connections for improved shared-stream efficiency.
* @extends {EventEmitter} * @extends {EventEmitter}
*/ */
class VoiceBroadcast extends EventEmitter { class VoiceBroadcast extends VolumeInterface {
constructor(client) { constructor(client) {
super(); super();
/** /**
@@ -51,55 +51,12 @@ class VoiceBroadcast extends EventEmitter {
return d; return d;
} }
applyVolume(buffer, volume = this._volume) {
if (volume === 1) return buffer;
const out = Buffer.alloc(buffer.length);
for (let i = 0; i < buffer.length; i += 2) {
if (i >= buffer.length - 1) break;
const uint = Math.min(32767, Math.max(-32767, Math.floor(volume * buffer.readInt16LE(i))));
out.writeInt16LE(uint, i);
}
return out;
}
/**
* Sets the volume relative to the input stream - i.e. 1 is normal, 0.5 is half, 2 is double.
* @param {number} volume The volume that you want to set
*/
setVolume(volume) {
this._volume = volume;
}
/**
* Set the volume in decibels
* @param {number} db The decibels
*/
setVolumeDecibels(db) {
this.setVolume(Math.pow(10, db / 20));
}
/**
* Set the volume so that a perceived value of 0.5 is half the perceived volume etc.
* @param {number} value The value for the volume
*/
setVolumeLogarithmic(value) {
this.setVolume(Math.pow(value, 1.660964));
}
/**
* The current volume of the broadcast
* @readonly
* @type {number}
*/
get volume() {
return this._volume;
}
get _playableStream() { get _playableStream() {
if (!this.currentTranscoder) return null; const currentTranscoder = this.currentTranscoder;
return this.currentTranscoder.transcoder.output || this.currentTranscoder.options.stream; if (!currentTranscoder) return null;
const transcoder = currentTranscoder.transcoder;
const options = currentTranscoder.options;
return (transcoder && transcoder.output) || options.stream;
} }
unregisterDispatcher(dispatcher, old) { unregisterDispatcher(dispatcher, old) {
@@ -115,6 +72,7 @@ class VoiceBroadcast extends EventEmitter {
container.delete(dispatcher); container.delete(dispatcher);
if (!container.size) { if (!container.size) {
this._encoders.get(volume).destroy();
this._dispatchers.delete(volume); this._dispatchers.delete(volume);
this._encoders.delete(volume); this._encoders.delete(volume);
} }
@@ -175,8 +133,7 @@ class VoiceBroadcast extends EventEmitter {
* .catch(console.error); * .catch(console.error);
*/ */
playStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { playStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
const options = { seek, volume, passes }; const options = { seek, volume, passes, stream };
options.stream = stream;
return this._playTranscodable(stream, options); return this._playTranscodable(stream, options);
} }
@@ -202,6 +159,8 @@ class VoiceBroadcast extends EventEmitter {
} }
_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',
@@ -242,6 +201,8 @@ class VoiceBroadcast extends EventEmitter {
* @returns {VoiceBroadcast} * @returns {VoiceBroadcast}
*/ */
playConvertedStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { playConvertedStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
OpusEncoders.guaranteeOpusEngine();
this.killCurrentTranscoder(); this.killCurrentTranscoder();
const options = { seek, volume, passes, stream }; const options = { seek, volume, passes, stream };
this.currentTranscoder = { options }; this.currentTranscoder = { options };
@@ -249,6 +210,20 @@ class VoiceBroadcast extends EventEmitter {
return this; return this;
} }
/**
* Plays an Opus encoded stream at 48KHz.
* <warn>Note that inline volume is not compatible with this method.</warn>
* @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 };
stream.once('readable', () => this._startPlaying());
return this;
}
/** /**
* Play an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description) * Play an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description)
* @param {string} input the arbitrary input * @param {string} input the arbitrary input
@@ -256,8 +231,10 @@ class VoiceBroadcast extends EventEmitter {
* @returns {VoiceBroadcast} * @returns {VoiceBroadcast}
*/ */
playArbitraryInput(input, { seek = 0, volume = 1, passes = 1 } = {}) { playArbitraryInput(input, { seek = 0, volume = 1, passes = 1 } = {}) {
const options = { seek, volume, passes }; this.guaranteeOpusEngine();
return this.player.playUnknownStream(input, options);
const options = { seek, volume, passes, input };
return this._playTranscodable(input, options);
} }
/** /**
@@ -284,6 +261,10 @@ class VoiceBroadcast extends EventEmitter {
} }
} }
guaranteeOpusEngine() {
if (!this.opusEncoder) throw new Error('Couldn\'t find an Opus engine.');
}
_startPlaying() { _startPlaying() {
if (this.tickInterval) clearInterval(this.tickInterval); if (this.tickInterval) clearInterval(this.tickInterval);
// this.tickInterval = this.client.setInterval(this.tick.bind(this), 20); // this.tickInterval = this.client.setInterval(this.tick.bind(this), 20);
@@ -301,9 +282,9 @@ class VoiceBroadcast extends EventEmitter {
setTimeout(() => this.tick(), 20); setTimeout(() => this.tick(), 20);
return; return;
} }
const stream = this._playableStream;
const bufferLength = 1920 * 2; const opus = this.currentTranscoder.opus;
let buffer = stream.read(bufferLength); const buffer = this.readStreamBuffer();
if (!buffer) { if (!buffer) {
this._missed++; this._missed++;
@@ -318,12 +299,6 @@ class VoiceBroadcast extends EventEmitter {
this._missed = 0; this._missed = 0;
if (buffer.length !== bufferLength) {
const newBuffer = Buffer.alloc(bufferLength).fill(0);
buffer.copy(newBuffer);
buffer = newBuffer;
}
let packetMatrix = {}; let packetMatrix = {};
const getOpusPacket = (volume) => { const getOpusPacket = (volume) => {
@@ -336,10 +311,13 @@ class VoiceBroadcast extends EventEmitter {
}; };
for (const dispatcher of this.dispatchers) { for (const dispatcher of this.dispatchers) {
if (opus) {
dispatcher.processPacket(buffer);
continue;
}
const volume = dispatcher.volume; const volume = dispatcher.volume;
setImmediate(() => { dispatcher.processPacket(getOpusPacket(volume));
dispatcher.process(buffer, true, getOpusPacket(volume));
});
} }
const next = 20 + (this._startTime + this._pausedTime + (this._count * 20) - Date.now()); const next = 20 + (this._startTime + this._pausedTime + (this._count * 20) - Date.now());
@@ -347,6 +325,22 @@ class VoiceBroadcast extends EventEmitter {
setTimeout(() => this.tick(), next); setTimeout(() => this.tick(), next);
} }
readStreamBuffer() {
const opus = this.currentTranscoder.opus;
const bufferLength = (opus ? 80 : 1920) * 2;
let buffer = this._playableStream.read(bufferLength);
if (opus) return buffer;
if (!buffer) return null;
if (buffer.length !== bufferLength) {
const newBuffer = Buffer.alloc(bufferLength).fill(0);
buffer.copy(newBuffer);
buffer = newBuffer;
}
return buffer;
}
/** /**
* Stop the current stream from playing without unsubscribing dispatchers. * Stop the current stream from playing without unsubscribing dispatchers.
*/ */

View File

@@ -142,6 +142,7 @@ class VoiceConnection extends EventEmitter {
self_deaf: false, self_deaf: false,
}, },
}); });
this.player.destroy();
/** /**
* Emitted when the voice connection disconnects * Emitted when the voice connection disconnects
* @event VoiceConnection#disconnect * @event VoiceConnection#disconnect
@@ -236,8 +237,7 @@ class VoiceConnection extends EventEmitter {
* }) * })
* .catch(console.error); * .catch(console.error);
*/ */
playFile(file, { seek = 0, volume = 1, passes = 1 } = {}) { playFile(file, options) {
const options = { seek, volume, passes };
return this.player.playUnknownStream(`file:${file}`, options); return this.player.playUnknownStream(`file:${file}`, options);
} }
@@ -247,8 +247,7 @@ class VoiceConnection extends EventEmitter {
* @param {StreamOptions} [options] Options for playing the stream * @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher} * @returns {StreamDispatcher}
*/ */
playArbitraryInput(input, { seek = 0, volume = 1, passes = 1 } = {}) { playArbitraryInput(input, options) {
const options = { seek, volume, passes };
return this.player.playUnknownStream(input, options); return this.player.playUnknownStream(input, options);
} }
@@ -268,22 +267,31 @@ class VoiceConnection extends EventEmitter {
* }) * })
* .catch(console.error); * .catch(console.error);
*/ */
playStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { playStream(stream, options) {
const options = { seek, volume, passes };
return this.player.playUnknownStream(stream, options); return this.player.playUnknownStream(stream, options);
} }
/** /**
* Plays a stream of 16-bit signed stereo PCM at 48KHz. * Plays a stream of 16-bit signed stereo PCM at 48KHz.
* @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}
*/ */
playConvertedStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { playConvertedStream(stream, options) {
const options = { seek, volume, passes };
return this.player.playPCMStream(stream, options); return this.player.playPCMStream(stream, options);
} }
/**
* Plays an Opus encoded stream at 48KHz.
* <warn>Note that inline volume is not compatible with this method.</warn>
* @param {ReadableStream} stream The Opus audio stream to play
* @param {StreamOptions} [options] Options for playing the stream
* @returns {StreamDispatcher}
*/
playOpusStream(stream, options) {
return this.player.playOpusStream(stream, options);
}
/** /**
* Plays a voice broadcast * Plays a voice broadcast
* @param {VoiceBroadcast} broadcast the broadcast to play * @param {VoiceBroadcast} broadcast the broadcast to play

View File

@@ -1,4 +1,4 @@
const EventEmitter = require('events').EventEmitter; const VolumeInterface = require('../util/VolumeInterface');
const NaCl = require('tweetnacl'); const NaCl = require('tweetnacl');
const VoiceBroadcast = require('../VoiceBroadcast'); const VoiceBroadcast = require('../VoiceBroadcast');
@@ -16,9 +16,9 @@ nonce.fill(0);
* ``` * ```
* @extends {EventEmitter} * @extends {EventEmitter}
*/ */
class StreamDispatcher extends EventEmitter { class StreamDispatcher extends VolumeInterface {
constructor(player, stream, streamOptions) { constructor(player, stream, streamOptions) {
super(); super(streamOptions);
/** /**
* The Audio Player that controls this dispatcher * The Audio Player that controls this dispatcher
* @type {AudioPlayer} * @type {AudioPlayer}
@@ -31,7 +31,6 @@ class StreamDispatcher extends EventEmitter {
this.stream = stream; this.stream = stream;
if (!(this.stream instanceof VoiceBroadcast)) this.startStreaming(); if (!(this.stream instanceof VoiceBroadcast)) this.startStreaming();
this.streamOptions = streamOptions; this.streamOptions = streamOptions;
this.streamOptions.volume = this.streamOptions.volume || 0;
const data = this.streamingData; const data = this.streamingData;
data.length = 20; data.length = 20;
@@ -48,7 +47,7 @@ class StreamDispatcher extends EventEmitter {
*/ */
this.destroyed = false; this.destroyed = false;
this.setVolume(streamOptions.volume || 1); this._opus = streamOptions.opus;
} }
/** /**
@@ -86,46 +85,6 @@ class StreamDispatcher extends EventEmitter {
return this.time + this.streamingData.pausedTime; return this.time + this.streamingData.pausedTime;
} }
/**
* The volume of the stream, relative to the stream's input volume
* @type {number}
* @readonly
*/
get volume() {
return this.streamOptions.volume;
}
/**
* Sets the volume relative to the input stream - i.e. 1 is normal, 0.5 is half, 2 is double.
* @param {number} volume The volume that you want to set
*/
setVolume(volume) {
/**
* Emitted when the volume of this dispatcher changes
* @param {number} oldVolume the old volume
* @param {number} newVolume the new volume
* @event StreamDispatcher#volumeChange
*/
this.emit('volumeChange', this.streamOptions.volume, volume);
this.streamOptions.volume = volume;
}
/**
* Set the volume in decibels
* @param {number} db The decibels
*/
setVolumeDecibels(db) {
this.setVolume(Math.pow(10, db / 20));
}
/**
* Set the volume so that a perceived value of 0.5 is half the perceived volume etc.
* @param {number} value The value for the volume
*/
setVolumeLogarithmic(value) {
this.setVolume(Math.pow(value, 1.660964));
}
/** /**
* Stops sending voice packets to the voice connection (stream may still progress however) * Stops sending voice packets to the voice connection (stream may still progress however)
*/ */
@@ -196,20 +155,7 @@ class StreamDispatcher extends EventEmitter {
return packetBuffer; return packetBuffer;
} }
applyVolume(buffer) { processPacket(packet) {
if (this.volume === 1) return buffer;
const out = Buffer.alloc(buffer.length);
for (let i = 0; i < buffer.length; i += 2) {
if (i >= buffer.length - 1) break;
const uint = Math.min(32767, Math.max(-32767, Math.floor(this.volume * buffer.readInt16LE(i))));
out.writeInt16LE(uint, i);
}
return out;
}
process(buffer, controlled, packet) {
try { try {
if (this.destroyed) { if (this.destroyed) {
this.setSpeaking(false); this.setSpeaking(false);
@@ -218,7 +164,38 @@ class StreamDispatcher extends EventEmitter {
const data = this.streamingData; const data = this.streamingData;
if (data.missed >= 5 && !controlled) { if (this.paused) {
this.setSpeaking(false);
data.pausedTime = data.length * 10;
return;
}
if (!packet) {
data.missed++;
data.pausedTime += data.length * 10;
return;
}
this.started();
this.missed = 0;
this.stepStreamingData();
this.sendBuffer(null, data.sequence, data.timestamp, packet);
} catch (e) {
this.destroy('error', e);
}
}
process() {
try {
if (this.destroyed) {
this.setSpeaking(false);
return;
}
const data = this.streamingData;
if (data.missed >= 5) {
this.destroy('end', 'Stream is not generating quickly enough.'); this.destroy('end', 'Stream is not generating quickly enough.');
return; return;
} }
@@ -227,61 +204,30 @@ class StreamDispatcher extends EventEmitter {
this.setSpeaking(false); this.setSpeaking(false);
// data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0; // data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0;
data.pausedTime += data.length * 10; data.pausedTime += data.length * 10;
// if buffer is provided we are assuming a master process is controlling the dispatcher this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), data.length * 10);
if (!buffer) this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), data.length * 10);
return; return;
} }
if (!buffer && controlled) { this.started();
const buffer = this.readStreamBuffer();
if (!buffer) {
data.missed++; data.missed++;
data.pausedTime += data.length * 10; data.pausedTime += data.length * 10;
this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), data.length * 10);
return; return;
} }
if (!data.startTime) {
/**
* Emitted once the dispatcher starts streaming
* @event StreamDispatcher#start
*/
this.emit('start');
data.startTime = Date.now();
}
if (packet) {
data.count++;
data.sequence = data.sequence < 65535 ? data.sequence + 1 : 0;
data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0;
this.sendBuffer(null, data.sequence, data.timestamp, packet);
return;
}
const bufferLength = 1920 * data.channels;
if (!controlled) {
buffer = this.stream.read(bufferLength);
if (!buffer) {
data.missed++;
data.pausedTime += data.length * 10;
this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), data.length * 10);
return;
}
}
data.missed = 0; data.missed = 0;
if (buffer.length !== bufferLength) { this.stepStreamingData();
const newBuffer = Buffer.alloc(bufferLength).fill(0);
buffer.copy(newBuffer); if (this._opus) {
buffer = newBuffer; this.sendBuffer(null, data.sequence, data.timestamp, buffer);
} else {
this.sendBuffer(buffer, data.sequence, data.timestamp);
} }
buffer = this.applyVolume(buffer);
data.count++;
data.sequence = data.sequence < 65535 ? data.sequence + 1 : 0;
data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0;
this.sendBuffer(buffer, data.sequence, data.timestamp);
if (controlled) return;
const nextTime = data.length + (data.startTime + data.pausedTime + (data.count * data.length) - Date.now()); const nextTime = data.length + (data.startTime + data.pausedTime + (data.count * data.length) - Date.now());
this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), nextTime); this.player.voiceConnection.voiceManager.client.setTimeout(() => this.process(), nextTime);
} catch (e) { } catch (e) {
@@ -289,6 +235,43 @@ class StreamDispatcher extends EventEmitter {
} }
} }
readStreamBuffer() {
const data = this.streamingData;
const bufferLength = (this._opus ? 80 : 1920) * data.channels;
let buffer = this.stream.read(bufferLength);
if (this._opus) return buffer;
if (!buffer) return null;
if (buffer.length !== bufferLength) {
const newBuffer = Buffer.alloc(bufferLength).fill(0);
buffer.copy(newBuffer);
buffer = newBuffer;
}
buffer = this.applyVolume(buffer);
return buffer;
}
started() {
const data = this.streamingData;
if (!data.startTime) {
/**
* Emitted once the dispatcher starts streaming
* @event StreamDispatcher#start
*/
this.emit('start');
data.startTime = Date.now();
}
}
stepStreamingData() {
const data = this.streamingData;
data.count++;
data.sequence = data.sequence < 65535 ? data.sequence + 1 : 0;
data.timestamp = data.timestamp + 4294967295 ? data.timestamp + 960 : 0;
}
destroy(type, reason) { destroy(type, reason) {
if (this.destroyed) return; if (this.destroyed) return;
this.destroyed = true; this.destroyed = true;

View File

@@ -10,6 +10,10 @@ class BaseOpus {
decode(buffer) { decode(buffer) {
return buffer; return buffer;
} }
destroy() {
return;
}
} }
module.exports = BaseOpus; module.exports = BaseOpus;

View File

@@ -20,5 +20,9 @@ exports.fetch = () => {
const fetched = fetch(encoder); const fetched = fetch(encoder);
if (fetched) return fetched; if (fetched) return fetched;
} }
throw new Error('Couldn\'t find an Opus engine.'); return null;
};
exports.guaranteeOpusEngine = () => {
if (!this.opusEncoder) throw new Error('Couldn\'t find an Opus engine.');
}; };

View File

@@ -2,7 +2,7 @@ const OpusEngine = require('./BaseOpusEngine');
let OpusScript; let OpusScript;
class NodeOpusEngine extends OpusEngine { class OpusScriptEngine extends OpusEngine {
constructor(player) { constructor(player) {
super(player); super(player);
try { try {
@@ -22,6 +22,11 @@ class NodeOpusEngine extends OpusEngine {
super.decode(buffer); super.decode(buffer);
return this.encoder.decode(buffer); return this.encoder.decode(buffer);
} }
destroy() {
super.destroy();
this.encoder.delete();
}
} }
module.exports = NodeOpusEngine; module.exports = OpusScriptEngine;

View File

@@ -50,6 +50,10 @@ class AudioPlayer extends EventEmitter {
return this.streams.last().transcoder; return this.streams.last().transcoder;
} }
destroy() {
this.opusEncoder.destroy();
}
destroyStream(stream) { destroyStream(stream) {
const data = this.streams.get(stream); const data = this.streams.get(stream);
if (!data) return; if (!data) return;
@@ -69,6 +73,7 @@ class AudioPlayer extends EventEmitter {
} }
playUnknownStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { playUnknownStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
OpusEncoders.guaranteeOpusEngine();
const options = { seek, volume, passes }; const options = { seek, volume, passes };
const transcoder = this.prism.transcode({ const transcoder = this.prism.transcode({
type: 'ffmpeg', type: 'ffmpeg',
@@ -85,28 +90,39 @@ class AudioPlayer extends EventEmitter {
} }
playPCMStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { playPCMStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
OpusEncoders.guaranteeOpusEngine();
const options = { seek, volume, passes }; const options = { seek, volume, passes };
this.destroyAllStreams(stream); this.destroyAllStreams(stream);
const dispatcher = new StreamDispatcher(this, stream, options); const dispatcher = this.createDispatcher(stream, options);
dispatcher.on('speaking', value => this.voiceConnection.setSpeaking(value));
if (!this.streams.has(stream)) this.streams.set(stream, { dispatcher, input: stream }); if (!this.streams.has(stream)) this.streams.set(stream, { dispatcher, input: stream });
this.streams.get(stream).dispatcher = dispatcher; this.streams.get(stream).dispatcher = dispatcher;
dispatcher.on('end', () => this.destroyStream(stream)); return dispatcher;
dispatcher.on('error', () => this.destroyStream(stream)); }
playOpusStream(stream, { seek = 0, passes = 1 } = {}) {
const options = { seek, passes, opus: true };
this.destroyAllStreams(stream);
const dispatcher = this.createDispatcher(stream, options);
this.streams.set(stream, { dispatcher, input: stream });
return dispatcher; return dispatcher;
} }
playBroadcast(broadcast, { volume = 1, passes = 1 } = {}) { playBroadcast(broadcast, { volume = 1, passes = 1 } = {}) {
const options = { volume, passes }; const options = { volume, passes };
this.destroyAllStreams(); this.destroyAllStreams();
const dispatcher = new StreamDispatcher(this, broadcast, options); const dispatcher = this.createDispatcher(broadcast, options);
dispatcher.on('end', () => this.destroyStream(broadcast));
dispatcher.on('error', () => this.destroyStream(broadcast));
dispatcher.on('speaking', value => this.voiceConnection.setSpeaking(value));
this.streams.set(broadcast, { dispatcher, input: broadcast }); this.streams.set(broadcast, { dispatcher, input: broadcast });
broadcast.registerDispatcher(dispatcher); broadcast.registerDispatcher(dispatcher);
return dispatcher; return dispatcher;
} }
createDispatcher(stream, options) {
const dispatcher = new StreamDispatcher(this, stream, options);
dispatcher.on('end', () => this.destroyStream(stream));
dispatcher.on('error', () => this.destroyStream(stream));
dispatcher.on('speaking', value => this.voiceConnection.setSpeaking(value));
return dispatcher;
}
} }
module.exports = AudioPlayer; module.exports = AudioPlayer;

View File

@@ -84,7 +84,8 @@ class VoiceReceiver extends EventEmitter {
stream._push(null); stream._push(null);
this.opusStreams.delete(id); this.opusStreams.delete(id);
} }
for (const [id] of this.opusEncoders) { for (const [id, encoder] of this.opusEncoders) {
encoder.destroy();
this.opusEncoders.delete(id); this.opusEncoders.delete(id);
} }
this.destroyed = true; this.destroyed = true;

View File

@@ -0,0 +1,57 @@
const EventEmitter = require('events');
class VolumeInterface extends EventEmitter {
constructor({ volume = 0 } = {}) {
super();
this.setVolume(volume || 1);
}
applyVolume(buffer, volume) {
volume = volume || this._volume;
if (volume === 1) return buffer;
const out = new Buffer(buffer.length);
for (let i = 0; i < buffer.length; i += 2) {
if (i >= buffer.length - 1) break;
const uint = Math.min(32767, Math.max(-32767, Math.floor(volume * buffer.readInt16LE(i))));
out.writeInt16LE(uint, i);
}
return out;
}
/**
* Sets the volume relative to the input stream - i.e. 1 is normal, 0.5 is half, 2 is double.
* @param {number} volume The volume that you want to set
*/
setVolume(volume) {
this._volume = volume;
}
/**
* Set the volume in decibels
* @param {number} db The decibels
*/
setVolumeDecibels(db) {
this.setVolume(Math.pow(10, db / 20));
}
/**
* Set the volume so that a perceived value of 0.5 is half the perceived volume etc.
* @param {number} value The value for the volume
*/
setVolumeLogarithmic(value) {
this.setVolume(Math.pow(value, 1.660964));
}
/**
* The current volume of the broadcast
* @readonly
* @type {number}
*/
get volume() {
return this._volume;
}
}
module.exports = VolumeInterface;