Class: Discordrb::Voice::VoiceBot

Inherits:
Object
  • Object
show all
Defined in:
lib/discordrb/voice/voice_bot.rb

Overview

This class represents a connection to a Discord voice server and channel. It can be used to play audio files and streams and to control playback on currently playing tracks. The method Bot#voice_connect can be used to connect to a voice channel.

discordrb does latency adjustments every now and then to improve playback quality. I made sure to put useful defaults for the adjustment parameters, but if the sound is patchy or too fast (or the speed varies a lot) you should check the parameters and adjust them to your connection: #adjust_interval, #adjust_offset, and #adjust_average.

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#adjust_averagetrue, false

This value determines whether or not the adjustment length should be averaged with the previous value. This may be useful on slower connections where latencies vary a lot. In general, it will make adjustments more smooth, but whether that is desired behaviour should be tried on a case-by-case basis.

Returns:

  • (true, false)

    whether adjustment lengths should be averaged with the respective previous value.

See Also:



58
59
60
# File 'lib/discordrb/voice/voice_bot.rb', line 58

def adjust_average
  @adjust_average
end

#adjust_debugtrue, false

Disable the debug message for length adjustment specifically, as it can get quite spammy with very low intervals

Returns:

  • (true, false)

    whether length adjustment debug messages should be printed

See Also:



63
64
65
# File 'lib/discordrb/voice/voice_bot.rb', line 63

def adjust_debug
  @adjust_debug
end

#adjust_intervalInteger

discordrb will occasionally measure the time it takes to send a packet, and adjust future delay times based on that data. This makes voice playback more smooth, because if packets are sent too slowly, the audio will sound patchy, and if they're sent too quickly, packets will "pile up" and occasionally skip some data or play parts back too fast. How often these measurements should be done depends a lot on the system, and if it's done too quickly, especially on slow connections, the playback speed will vary wildly; if it's done too slowly however, small errors will cause quality problems for a longer time.

Returns:

  • (Integer)

    how frequently audio length adjustments should be done, in ideal packets (20ms).



43
44
45
# File 'lib/discordrb/voice/voice_bot.rb', line 43

def adjust_interval
  @adjust_interval
end

#adjust_offsetInteger

This particular value is also important because ffmpeg may take longer to process the first few packets. It is recommended to set this to 10 at maximum, otherwise it will take too long to make the first adjustment, but it shouldn't be any higher than #adjust_interval, otherwise no adjustments will take place. If #adjust_interval is at a value higher than 10, this value should not be changed at all.

Returns:

  • (Integer)

    the packet number (1 packet = 20ms) at which length adjustments should start.

See Also:



51
52
53
# File 'lib/discordrb/voice/voice_bot.rb', line 51

def adjust_offset
  @adjust_offset
end

#channelChannel

Returns the current voice channel.

Returns:

  • (Channel)

    the current voice channel



25
26
27
# File 'lib/discordrb/voice/voice_bot.rb', line 25

def channel
  @channel
end

#encoderEncoder (readonly)

Returns the encoder used to encode audio files into the format required by Discord.

Returns:

  • (Encoder)

    the encoder used to encode audio files into the format required by Discord.



34
35
36
# File 'lib/discordrb/voice/voice_bot.rb', line 34

def encoder
  @encoder
end

#length_overrideFloat

If this value is set, no length adjustments will ever be done and this value will always be used as the length (i.e. packets will be sent every N seconds). Be careful not to set it too low as to not spam Discord's servers. The ideal length is 20ms (accessible by the IDEAL_LENGTH constant), this value should be slightly lower than that because encoding + sending takes time. Note that sending DCA files is significantly faster than sending regular audio files (usually about four times as fast), so you might want to set this value to something else if you're sending a DCA file.

Returns:

  • (Float)

    the packet length that should be used instead of calculating it during the adjustments, in ms.



72
73
74
# File 'lib/discordrb/voice/voice_bot.rb', line 72

def length_override
  @length_override
end

#stream_timeInteger? (readonly)

Returns the amount of time the stream has been playing, or nil if nothing has been played yet.

Returns:

  • (Integer, nil)

    the amount of time the stream has been playing, or nil if nothing has been played yet.



31
32
33
# File 'lib/discordrb/voice/voice_bot.rb', line 31

def stream_time
  @stream_time
end

#volumeFloat

The factor the audio's volume should be multiplied with. 1 is no change in volume, 0 is completely silent, 0.5 is half the default volume and 2 is twice the default.

Returns:

  • (Float)

    the volume for audio playback, 1.0 by default.



77
78
79
# File 'lib/discordrb/voice/voice_bot.rb', line 77

def volume
  @volume
end

Instance Method Details

#continueObject

Continue playback. This change may take up to 100ms to take effect, which is usually negligible.



139
140
141
# File 'lib/discordrb/voice/voice_bot.rb', line 139

def continue
  @paused = false
end

#destroyObject

Permanently disconnects from the voice channel; to reconnect you will have to call Bot#voice_connect again.



175
176
177
178
179
# File 'lib/discordrb/voice/voice_bot.rb', line 175

def destroy
  stop_playing
  @bot.voice_destroy(@channel.server.id, false)
  @ws.destroy
end

#encrypted?true, false

Deprecated.

Discord no longer supports unencrypted voice communication.

Returns whether audio data sent will be encrypted.

Returns:

  • (true, false)

    whether audio data sent will be encrypted.



107
108
109
# File 'lib/discordrb/voice/voice_bot.rb', line 107

def encrypted?
  true
end

#filter_volumeInteger

Returns the volume used as a filter for ffmpeg/avconv.

Returns:

  • (Integer)

    the volume used as a filter for ffmpeg/avconv.

See Also:



120
121
122
# File 'lib/discordrb/voice/voice_bot.rb', line 120

def filter_volume
  @encoder.filter_volume
end

#filter_volume=(value) ⇒ Object

Set the filter volume. This volume is applied as a filter for decoded audio data. It has the advantage that using it is much faster than regular volume, but it can only be changed before starting to play something.

Parameters:

  • value (Integer)

    The value to set the volume to. For possible values, see #volume



114
115
116
# File 'lib/discordrb/voice/voice_bot.rb', line 114

def filter_volume=(value)
  @encoder.filter_volume = value
end

#pauseObject

Pause playback. This is not instant; it may take up to 20 ms for this change to take effect. (This is usually negligible.)



126
127
128
# File 'lib/discordrb/voice/voice_bot.rb', line 126

def pause
  @paused = true
end

#play(encoded_io) ⇒ Object

Plays a stream of raw data to the channel. All playback methods are blocking, i.e. they wait for the playback to finish before exiting the method. This doesn't cause a problem if you just use discordrb events/commands to play stuff, as these are fully threaded, but if you don't want this behaviour anyway, be sure to call these methods in separate threads.

Parameters:

  • encoded_io (IO)

    A stream of raw PCM data (s16le)



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/discordrb/voice/voice_bot.rb', line 186

def play(encoded_io)
  stop_playing(true) if @playing
  @retry_attempts = 3
  @first_packet = true

  play_internal do
    buf = nil

    # Read some data from the buffer
    begin
      buf = encoded_io.readpartial(DATA_LENGTH) if encoded_io
    rescue EOFError
      raise IOError, 'File or stream not found!' if @first_packet

      @bot.debug('EOF while reading, breaking immediately')
      next :stop
    end

    # Check whether the buffer has enough data
    if !buf || buf.length != DATA_LENGTH
      @bot.debug("No data is available! Retrying #{@retry_attempts} more times")
      next :stop if @retry_attempts.zero?

      @retry_attempts -= 1
      next
    end

    # Adjust volume
    buf = @encoder.adjust_volume(buf, @volume) if @volume != 1.0 # rubocop:disable Lint/FloatComparison

    @first_packet = false

    # Encode data
    @encoder.encode(buf)
  end

  # If the stream is a process, kill it
  if encoded_io&.pid
    Discordrb::LOGGER.debug("Killing ffmpeg process with pid #{encoded_io.pid.inspect}")

    begin
      pid = encoded_io.pid
      # Windows does not support TERM as a kill signal, so we use KILL. `Process.waitpid` verifies that our
      # child process has not already completed.
      Process.kill(Gem.win_platform? ? 'KILL' : 'TERM', pid) if Process.waitpid(pid, Process::WNOHANG).nil?
    rescue StandardError => e
      Discordrb::LOGGER.warn('Failed to kill ffmpeg process! You *might* have a process leak now.')
      Discordrb::LOGGER.warn("Reason: #{e}")
    end
  end

  # Close the stream
  encoded_io.close
end

#play_dca(file) ⇒ Object

Note:

DCA playback will not be affected by the volume modifier (#volume) because the modifier operates on raw PCM, not opus data. Modifying the volume of DCA data would involve decoding it, multiplying the samples and re-encoding it, which defeats its entire purpose (no recoding).

Plays a stream of audio data in the DCA format. This format has the advantage that no recoding has to be done - the file contains the data exactly as Discord needs it.

Raises:

  • (ArgumentError)

See Also:



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/discordrb/voice/voice_bot.rb', line 262

def play_dca(file)
  stop_playing(true) if @playing

  @bot.debug "Reading DCA file #{file}"
  input_stream = File.open(file)

  magic = input_stream.read(4)
  raise ArgumentError, 'Not a DCA1 file! The file might have been corrupted, please recreate it.' unless magic == 'DCA1'

  # Read the metadata header, then read the metadata and discard it as we don't care about it
   = input_stream.read(4).unpack1('l<')
  input_stream.read()

  # Play the data, without re-encoding it to opus
  play_internal do
    begin
      # Read header
      header_str = input_stream.read(2)

      unless header_str
        @bot.debug 'Finished DCA parsing (header is nil)'
        next :stop
      end

      header = header_str.unpack1('s<')

      raise 'Negative header in DCA file! Your file is likely corrupted.' if header.negative?
    rescue EOFError
      @bot.debug 'Finished DCA parsing (EOFError)'
      next :stop
    end

    # Read bytes
    input_stream.read(header)
  end
end

#play_file(file, options = '') ⇒ Object

Plays an encoded audio file of arbitrary format to the channel.



244
245
246
# File 'lib/discordrb/voice/voice_bot.rb', line 244

def play_file(file, options = '')
  play @encoder.encode_file(file, options)
end

#play_io(io, options = '') ⇒ Object Also known as: play_stream

Plays a stream of encoded audio data of arbitrary format to the channel.



251
252
253
# File 'lib/discordrb/voice/voice_bot.rb', line 251

def play_io(io, options = '')
  play @encoder.encode_io(io, options)
end

#playing?true, false Also known as: isplaying?

Returns Whether it is playing sound or not.

Returns:

  • (true, false)

    Whether it is playing sound or not.

See Also:



132
133
134
# File 'lib/discordrb/voice/voice_bot.rb', line 132

def playing?
  @playing
end

#skip(secs) ⇒ Object

Skips to a later time in the song. It's impossible to go back without replaying the song.

Parameters:

  • secs (Float)

    How many seconds to skip forwards. Skipping will always be done in discrete intervals of 0.05 seconds, so if the given amount is smaller than that, it will be rounded up.



146
147
148
# File 'lib/discordrb/voice/voice_bot.rb', line 146

def skip(secs)
  @skips += (secs * (1000 / IDEAL_LENGTH)).ceil
end

#speaking=(value) ⇒ Object

Note:

Sets whether or not the bot is speaking (green circle around user).

Parameters:

  • value (true, false, Integer)

    whether or not the bot should be speaking, or a bitmask denoting the audio type



153
154
155
156
# File 'lib/discordrb/voice/voice_bot.rb', line 153

def speaking=(value)
  @playing = value
  @ws.send_speaking(value)
end

#stop_playing(wait_for_confirmation = false) ⇒ Object

Stops the current playback entirely.

Parameters:

  • wait_for_confirmation (true, false) (defaults to: false)

    Whether the method should wait for confirmation from the playback method that the playback has actually stopped.



161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/discordrb/voice/voice_bot.rb', line 161

def stop_playing(wait_for_confirmation = false)
  @was_playing_before = @playing
  @speaking = false
  @playing = false
  sleep IDEAL_LENGTH / 1000.0 if @was_playing_before

  return unless wait_for_confirmation

  @has_stopped_playing = false
  sleep IDEAL_LENGTH / 1000.0 until @has_stopped_playing
  @has_stopped_playing = false
end