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:



55
56
57
# File 'lib/discordrb/voice/voice_bot.rb', line 55

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:



60
61
62
# File 'lib/discordrb/voice/voice_bot.rb', line 60

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 (20 ms).



40
41
42
# File 'lib/discordrb/voice/voice_bot.rb', line 40

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 = 20 ms) at which length adjustments should start.

See Also:



48
49
50
# File 'lib/discordrb/voice/voice_bot.rb', line 48

def adjust_offset
  @adjust_offset
end

#channelChannel (readonly)

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.



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

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 20 ms (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.



69
70
71
# File 'lib/discordrb/voice/voice_bot.rb', line 69

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.



28
29
30
# File 'lib/discordrb/voice/voice_bot.rb', line 28

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.



74
75
76
# File 'lib/discordrb/voice/voice_bot.rb', line 74

def volume
  @volume
end

Instance Method Details

#continueObject

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



136
137
138
# File 'lib/discordrb/voice/voice_bot.rb', line 136

def continue
  @paused = false
end

#destroyObject

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



171
172
173
174
175
# File 'lib/discordrb/voice/voice_bot.rb', line 171

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

#encrypted?true, false

Returns whether audio data sent will be encrypted.

Returns:

  • (true, false)

    whether audio data sent will be encrypted.



104
105
106
# File 'lib/discordrb/voice/voice_bot.rb', line 104

def encrypted?
  @udp.encrypted?
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:



117
118
119
# File 'lib/discordrb/voice/voice_bot.rb', line 117

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



111
112
113
# File 'lib/discordrb/voice/voice_bot.rb', line 111

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.)



123
124
125
# File 'lib/discordrb/voice/voice_bot.rb', line 123

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)



182
183
184
185
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
# File 'lib/discordrb/voice/voice_bot.rb', line 182

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

    @first_packet = false

    # Encode data
    @encoder.encode(buf)
  end

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

    begin
      Process.kill('TERM', encoded_io.pid)
    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:



255
256
257
258
259
260
261
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
# File 'lib/discordrb/voice/voice_bot.rb', line 255

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).unpack('l<')[0]
  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.unpack('s<')[0]

      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.



237
238
239
# File 'lib/discordrb/voice/voice_bot.rb', line 237

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.



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

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:



129
130
131
# File 'lib/discordrb/voice/voice_bot.rb', line 129

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.



143
144
145
# File 'lib/discordrb/voice/voice_bot.rb', line 143

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

#speaking=(value) ⇒ Object

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

Parameters:

  • value (true, false)

    whether or not the bot should be speaking.



149
150
151
152
# File 'lib/discordrb/voice/voice_bot.rb', line 149

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.



157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/discordrb/voice/voice_bot.rb', line 157

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