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:



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

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:



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

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



37
38
39
# File 'lib/discordrb/voice/voice_bot.rb', line 37

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:



45
46
47
# File 'lib/discordrb/voice/voice_bot.rb', line 45

def adjust_offset
  @adjust_offset
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.



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

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.



66
67
68
# File 'lib/discordrb/voice/voice_bot.rb', line 66

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.



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

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.



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

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.



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

def continue
  @paused = false
end

#destroyObject

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



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

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.



100
101
102
# File 'lib/discordrb/voice/voice_bot.rb', line 100

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:



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

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



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

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



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

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)



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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
# File 'lib/discordrb/voice/voice_bot.rb', line 162

def play(encoded_io)
  stop_playing 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')
      break
    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")
      break if @retry_attempts == 0

      @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.info("Killing ffmpeg process with pid #{encoded_io.pid.inspect}")

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



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/discordrb/voice/voice_bot.rb', line 235

def play_dca(file)
  stop_playing if @playing

  @bot.debug "Reading DCA file #{file}"
  input_stream = 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 = input_stream.read(2).unpack('s<')[0]

      raise 'Negative header in DCA file! Your file is likely corrupted.' if header < 0
    rescue EOFError
      @bot.debug 'Finished DCA parsing'
      break
    end

    # Read bytes
    input_stream.read(header)
  end
end

#play_file(file) ⇒ Object

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



217
218
219
# File 'lib/discordrb/voice/voice_bot.rb', line 217

def play_file(file)
  play @encoder.encode_file(file)
end

#play_io(io) ⇒ Object Also known as: play_stream

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



224
225
226
# File 'lib/discordrb/voice/voice_bot.rb', line 224

def play_io(io)
  play @encoder.encode_io(io)
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.



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

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.



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

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

#stop_playingObject

Stops the current playback entirely.



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

def stop_playing
  @was_playing_before = @playing
  @speaking = false
  @playing = false
  sleep IDEAL_LENGTH / 1000.0 if @was_playing_before
end