discordrb 1.6.6 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of discordrb might be problematic. Click here for more details.

@@ -1,5 +1,5 @@
1
1
  # Discordrb and all its functionality, in this case only the version.
2
2
  module Discordrb
3
3
  # The current version of discordrb.
4
- VERSION = '1.6.6'.freeze
4
+ VERSION = '1.7.0'.freeze
5
5
  end
@@ -11,19 +11,8 @@ module Discordrb::Voice
11
11
  # This class conveniently abstracts opus and ffmpeg/avconv, for easy implementation of voice sending. It's not very
12
12
  # useful for most users, but I guess it can be useful sometimes.
13
13
  class Encoder
14
- # The volume that should be used with future ffmpeg conversions. If ffmpeg is used, this can be specified as:
15
- #
16
- # * A number, where `1` is no change in volume, `0` is completely silent, `0.5` is half the default volume and `2` is twice the default.
17
- # * A string representation of the above number.
18
- # * A string representing a change in gain given in decibels, in the format `-6dB` or `6dB`.
19
- #
20
- # If avconv is used (see #use_avconv) then it can only be given as a number from `0` to `1`, where `1` is no change
21
- # and `0` is completely silent.
22
- # @return [String, Number] the volume for future playbacks, `1.0` by default.
23
- attr_accessor :volume
24
-
25
14
  # Whether or not avconv should be used instead of ffmpeg. If possible, it is recommended to use ffmpeg instead,
26
- # as it is better supported and has a wider range of possible volume settings (see #volume).
15
+ # as it is better supported.
27
16
  # @return [true, false] whether avconv should be used instead of ffmpeg.
28
17
  attr_accessor :use_avconv
29
18
 
@@ -32,7 +21,6 @@ module Discordrb::Voice
32
21
  @sample_rate = 48_000
33
22
  @frame_size = 960
34
23
  @channels = 2
35
- @volume = 1.0
36
24
 
37
25
  if OPUS_AVAILABLE
38
26
  @opus = Opus::Encoder.new(@sample_rate, @frame_size, @channels)
@@ -41,6 +29,12 @@ module Discordrb::Voice
41
29
  end
42
30
  end
43
31
 
32
+ # Set the opus encoding bitrate
33
+ # @param value [Integer] The new bitrate to use, in bits per second (so 64000 if you want 64 kbps)
34
+ def bitrate=(value)
35
+ @opus.bitrate = value
36
+ end
37
+
44
38
  # Encodes the given buffer using opus.
45
39
  # @param buffer [String] An unencoded PCM (s16le) buffer.
46
40
  # @return [String] A buffer encoded using opus.
@@ -53,13 +47,33 @@ module Discordrb::Voice
53
47
  @opus.destroy
54
48
  end
55
49
 
50
+ # Adjusts the volume of a given buffer of s16le PCM data.
51
+ # @param buf [String] An unencoded PCM (s16le) buffer.
52
+ # @param mult [Float] The volume multiplier, 1 for same volume.
53
+ # @return [String] The buffer with adjusted volume, s16le again
54
+ def adjust_volume(buf, mult)
55
+ # We don't need to adjust anything if the buf is nil so just return in that case
56
+ return unless buf
57
+
58
+ # buf is s16le so use 's<' for signed, 16 bit, LE
59
+ result = buf.unpack('s<*').map do |sample|
60
+ sample *= mult
61
+
62
+ # clamp to s16 range
63
+ [32_767, [-32_768, sample].max].min
64
+ end
65
+
66
+ # After modification, make it s16le again
67
+ result.pack('s<*')
68
+ end
69
+
56
70
  # Encodes a given file (or rather, decodes it) using ffmpeg. This accepts pretty much any format, even videos with
57
71
  # an audio track. For a list of supported formats, see https://ffmpeg.org/general.html#Audio-Codecs. It even accepts
58
72
  # URLs, though encoding them is pretty slow - I recommend to make a stream of it and then use {#encode_io} instead.
59
73
  # @param file [String] The path or URL to encode.
60
74
  # @return [IO] the audio, encoded as s16le PCM
61
75
  def encode_file(file)
62
- command = "#{ffmpeg_command} -loglevel 0 -i \"#{file}\" -f s16le -ar 48000 -ac 2 #{ffmpeg_volume} pipe:1"
76
+ command = "#{ffmpeg_command} -loglevel 0 -i \"#{file}\" -f s16le -ar 48000 -ac 2 pipe:1"
63
77
  IO.popen(command)
64
78
  end
65
79
 
@@ -69,7 +83,7 @@ module Discordrb::Voice
69
83
  # @return [IO] the audio, encoded as s16le PCM
70
84
  def encode_io(io)
71
85
  ret_io, writer = IO.pipe
72
- command = "#{ffmpeg_command} -loglevel 0 -i - -f s16le -ar 48000 -ac 2 #{ffmpeg_volume} pipe:1"
86
+ command = "#{ffmpeg_command} -loglevel 0 -i - -f s16le -ar 48000 -ac 2 pipe:1"
73
87
  spawn(command, in: io, out: writer)
74
88
  ret_io
75
89
  end
@@ -79,9 +93,5 @@ module Discordrb::Voice
79
93
  def ffmpeg_command
80
94
  @use_avconv ? 'avconv' : 'ffmpeg'
81
95
  end
82
-
83
- def ffmpeg_volume
84
- @use_avconv ? "-vol #{(@volume * 256).ceil}" : "-af volume=#{@volume}"
85
- end
86
96
  end
87
97
  end
@@ -5,20 +5,19 @@ require 'json'
5
5
 
6
6
  begin
7
7
  require 'rbnacl'
8
- $rbnacl_available = true
8
+ RBNACL_AVAILABLE = true
9
9
  rescue LoadError
10
10
  puts "libsodium not available! You can continue to use discordrb as normal but voice support won't work.
11
11
  Read https://github.com/meew0/discordrb/wiki/Installing-libsodium for more details."
12
- $rbnacl_available = false
12
+ RBNACL_AVAILABLE = false
13
13
  end
14
14
 
15
-
16
15
  module Discordrb::Voice
17
16
  # Signifies to Discord that encryption should be used
18
- ENCRYPTED_MODE = 'xsalsa20_poly1305'
17
+ ENCRYPTED_MODE = 'xsalsa20_poly1305'.freeze
19
18
 
20
19
  # Signifies to Discord that no encryption should be used
21
- PLAIN_MODE = 'plain'
20
+ PLAIN_MODE = 'plain'.freeze
22
21
 
23
22
  # Represents a UDP connection to a voice server. This connection is used to send the actual audio data.
24
23
  class VoiceUDP
@@ -70,9 +69,7 @@ module Discordrb::Voice
70
69
  header = [0x80, 0x78, sequence, time, @ssrc].pack('CCnNN')
71
70
 
72
71
  # Encrypt data, if necessary
73
- if encrypted?
74
- buf = encrypt_audio(header, buf)
75
- end
72
+ buf = encrypt_audio(header, buf) if encrypted?
76
73
 
77
74
  send_packet(header + buf)
78
75
  end
@@ -122,7 +119,7 @@ module Discordrb::Voice
122
119
  # @param session [String] The voice session ID Discord sends over the regular websocket
123
120
  # @param endpoint [String] The endpoint URL to connect to
124
121
  def initialize(channel, bot, token, session, endpoint)
125
- fail 'RbNaCl is unavailable - unable to create voice bot! Please read https://github.com/meew0/discordrb/wiki/Installing-libsodium' unless $rbnacl_available
122
+ fail 'RbNaCl is unavailable - unable to create voice bot! Please read https://github.com/meew0/discordrb/wiki/Installing-libsodium' unless RBNACL_AVAILABLE
126
123
 
127
124
  @channel = channel
128
125
  @bot = bot
@@ -223,8 +220,6 @@ module Discordrb::Voice
223
220
  @ws_data = packet['d']
224
221
  @ready = true
225
222
  @udp.secret_key = @ws_data['secret_key'].pack('C*')
226
- else
227
- # irrelevant opcode, ignore
228
223
  end
229
224
  end
230
225
 
@@ -48,6 +48,25 @@ module Discordrb::Voice
48
48
  # @return [true, false] whether adjustment lengths should be averaged with the respective previous value.
49
49
  attr_accessor :adjust_average
50
50
 
51
+ # Disable the debug message for length adjustment specifically, as it can get quite spammy with very low intervals
52
+ # @see #adjust_interval
53
+ # @return [true, false] whether length adjustment debug messages should be printed
54
+ attr_accessor :adjust_debug
55
+
56
+ # If this value is set, no length adjustments will ever be done and this value will always be used as the length
57
+ # (i. e. packets will be sent every N seconds). Be careful not to set it too low as to not spam Discord's servers.
58
+ # The ideal length is 20 ms (accessible by the {Discordrb::Voice::IDEAL_LENGTH} constant), this value should be
59
+ # slightly lower than that because encoding + sending takes time. Note that sending DCA files is significantly
60
+ # faster than sending regular audio files (usually about four times as fast), so you might want to set this value
61
+ # to something else if you're sending a DCA file.
62
+ # @return [Float] the packet length that should be used instead of calculating it during the adjustments, in ms.
63
+ attr_accessor :length_override
64
+
65
+ # The factor the audio's volume should be multiplied with. `1` is no change in volume, `0` is completely silent,
66
+ # `0.5` is half the default volume and `2` is twice the default.
67
+ # @return [Float] the volume for audio playback, `1.0` by default.
68
+ attr_accessor :volume
69
+
51
70
  def initialize(channel, bot, token, session, endpoint, encrypted)
52
71
  @bot = bot
53
72
  @ws = VoiceWS.new(channel, bot, token, session, endpoint)
@@ -55,27 +74,19 @@ module Discordrb::Voice
55
74
  @udp.encrypted = encrypted
56
75
 
57
76
  @sequence = @time = 0
77
+ @skips = 0
58
78
 
59
79
  @adjust_interval = 100
60
80
  @adjust_offset = 10
61
81
  @adjust_average = false
82
+ @adjust_debug = true
83
+
84
+ @volume = 1.0
62
85
 
63
86
  @encoder = Encoder.new
64
87
  @ws.connect
65
88
  end
66
89
 
67
- # Set the volume. Only applies to future playbacks
68
- # @see Encoder#volume=
69
- def volume=(value)
70
- @encoder.volume = value
71
- end
72
-
73
- # @see Encoder#volume
74
- # @return [Integer, String] the current encoder volume.
75
- def volume
76
- @encoder.volume
77
- end
78
-
79
90
  # @return [true, false] whether audio data sent will be encrypted.
80
91
  def encrypted?
81
92
  @udp.encrypted?
@@ -92,6 +103,13 @@ module Discordrb::Voice
92
103
  @paused = false
93
104
  end
94
105
 
106
+ # Skips to a later time in the song. It's impossible to go back without replaying the song.
107
+ # @param secs [Float] How many seconds to skip forwards. Skipping will always be done in discrete intervals of
108
+ # 0.05 seconds, so if the given amount is smaller than that, it will be rounded up.
109
+ def skip(secs)
110
+ @skips += (secs * (1000 / IDEAL_LENGTH)).ceil
111
+ end
112
+
95
113
  # Sets whether or not the bot is speaking (green circle around user).
96
114
  # @param value [true, false] whether or not the bot should be speaking.
97
115
  def speaking=(value)
@@ -103,8 +121,7 @@ module Discordrb::Voice
103
121
  def stop_playing
104
122
  @was_playing_before = @playing
105
123
  @speaking = false
106
- @io.close if @io
107
- @io = nil
124
+ @playing = false
108
125
  sleep IDEAL_LENGTH / 1000.0 if @was_playing_before
109
126
  end
110
127
 
@@ -122,8 +139,34 @@ module Discordrb::Voice
122
139
  # @param encoded_io [IO] A stream of raw PCM data (s16le)
123
140
  def play(encoded_io)
124
141
  stop_playing if @playing
125
- @io = encoded_io
126
- play_internal
142
+ @retry_attempts = 3
143
+
144
+ play_internal do
145
+ buf = nil
146
+
147
+ # Read some data from the buffer
148
+ begin
149
+ buf = encoded_io.readpartial(DATA_LENGTH) if encoded_io
150
+ rescue EOFError
151
+ @bot.debug('EOF while reading, breaking immediately')
152
+ break
153
+ end
154
+
155
+ # Check whether the buffer has enough data
156
+ if !buf || buf.length != DATA_LENGTH
157
+ @bot.debug("No data is available! Retrying #{@retry_attempts} more times")
158
+ break if @retry_attempts == 0
159
+
160
+ @retry_attempts -= 1
161
+ next
162
+ end
163
+
164
+ # Adjust volume
165
+ buf = @encoder.adjust_volume(buf, @volume) if @volume != 1.0
166
+
167
+ # Encode data
168
+ @encoder.encode(buf)
169
+ end
127
170
  end
128
171
 
129
172
  # Plays an encoded audio file of arbitrary format to the channel.
@@ -140,6 +183,31 @@ module Discordrb::Voice
140
183
  play @encoder.encode_io(io)
141
184
  end
142
185
 
186
+ # Plays a stream of audio data in the DCA format. This format has the advantage that no recoding has to be
187
+ # done - the file contains the data exactly as Discord needs it.
188
+ # @see https://github.com/bwmarrin/dca
189
+ # @see #play
190
+ def play_dca(file)
191
+ stop_playing if @playing
192
+
193
+ @bot.debug "Reading DCA file #{file}"
194
+ input_stream = open(file)
195
+
196
+ # Play the data, without re-encoding it to opus
197
+ play_internal do
198
+ begin
199
+ # Read header
200
+ header = input_stream.read(2).unpack('S')[0]
201
+ rescue EOFError
202
+ @bot.debug 'Finished DCA parsing'
203
+ break
204
+ end
205
+
206
+ # Read bytes
207
+ input_stream.read(header)
208
+ end
209
+ end
210
+
143
211
  alias_method :play_stream, :play_io
144
212
 
145
213
  private
@@ -148,36 +216,24 @@ module Discordrb::Voice
148
216
  def play_internal
149
217
  count = 0
150
218
  @playing = true
151
- @retry_attempts = 3
152
219
 
153
220
  # Default play length (ms), will be adjusted later
154
221
  @length = IDEAL_LENGTH
155
222
 
156
223
  self.speaking = true
157
224
  loop do
158
- if count % @adjust_interval == @adjust_offset
159
- # Starting from the tenth packet, perform length adjustment every 100 packets (2 seconds)
160
- @length_adjust = Time.now.nsec
161
- end
225
+ # Starting from the tenth packet, perform length adjustment every 100 packets (2 seconds)
226
+ should_adjust_this_packet = (count % @adjust_interval == @adjust_offset)
162
227
 
163
- break unless @playing
164
- break unless @io
228
+ # If we should adjust, start now
229
+ @length_adjust = Time.now.nsec if should_adjust_this_packet
165
230
 
166
- # Read some data from the buffer
167
- buf = nil
168
- begin
169
- buf = @io.readpartial(DATA_LENGTH) if @io
170
- rescue EOFError
171
- @bot.debug('EOF while reading, breaking immediately')
172
- break
173
- end
174
-
175
- # Check whether the buffer has enough data
176
- if !buf || buf.length != DATA_LENGTH
177
- @bot.debug("No data is available! Retrying #{@retry_attempts} more times")
178
- break if @retry_attempts == 0
231
+ break unless @playing
179
232
 
180
- @retry_attempts -= 1
233
+ # If we should skip, get some data, discard it and go to the next iteration
234
+ if @skips > 0
235
+ @skips -= 1
236
+ yield
181
237
  next
182
238
  end
183
239
 
@@ -186,16 +242,27 @@ module Discordrb::Voice
186
242
  (@sequence + 10 < 65_535) ? @sequence += 1 : @sequence = 0
187
243
  (@time + 9600 < 4_294_967_295) ? @time += 960 : @time = 0
188
244
 
189
- # Encode the packet and send it
190
- @udp.send_audio(@encoder.encode(buf), @sequence, @time)
245
+ # Get packet data
246
+ buf = yield
247
+ next unless buf
248
+
249
+ # Track intermediate adjustment so we can measure how much encoding contributes to the total time
250
+ @intermediate_adjust = Time.now.nsec if should_adjust_this_packet
251
+
252
+ # Send the packet
253
+ @udp.send_audio(buf, @sequence, @time)
191
254
 
192
255
  # Set the stream time (for tracking how long we've been playing)
193
256
  @stream_time = count * @length / 1000
194
257
 
195
- # Perform length adjustment
196
- if @length_adjust
258
+ if @length_override # Don't do adjustment because the user has manually specified an override value
259
+ @length = @length_override
260
+ elsif @length_adjust # Perform length adjustment
261
+ # Define the time once so it doesn't get inaccurate
262
+ now = Time.now.nsec
263
+
197
264
  # Difference between length_adjust and now in ms
198
- ms_diff = (Time.now.nsec - @length_adjust) / 1_000_000.0
265
+ ms_diff = (now - @length_adjust) / 1_000_000.0
199
266
  if ms_diff >= 0
200
267
  @length = if @adjust_average
201
268
  (IDEAL_LENGTH - ms_diff + @length) / 2.0
@@ -203,7 +270,9 @@ module Discordrb::Voice
203
270
  IDEAL_LENGTH - ms_diff
204
271
  end
205
272
 
206
- @bot.debug("Length adjustment: new length #{@length}")
273
+ # Track the time it took to encode
274
+ encode_ms = (@intermediate_adjust - @length_adjust) / 1_000_000.0
275
+ @bot.debug("Length adjustment: new length #{@length} (measured #{ms_diff}, #{(100 * encode_ms) / ms_diff}% encoding)") if @adjust_debug
207
276
  end
208
277
  @length_adjust = nil
209
278
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: discordrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.6
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - meew0
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-02-13 00:00:00.000000000 Z
11
+ date: 2016-02-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faye-websocket
@@ -136,6 +136,34 @@ dependencies:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
138
  version: 0.8.7.6
139
+ - !ruby/object:Gem::Dependency
140
+ name: rspec
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 3.4.0
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 3.4.0
153
+ - !ruby/object:Gem::Dependency
154
+ name: rubocop
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - '='
158
+ - !ruby/object:Gem::Version
159
+ version: 0.36.0
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - '='
165
+ - !ruby/object:Gem::Version
166
+ version: 0.36.0
139
167
  description: A Ruby implementation of the Discord (https://discordapp.com) API.
140
168
  email:
141
169
  - ''
@@ -164,9 +192,13 @@ files:
164
192
  - lib/discordrb/await.rb
165
193
  - lib/discordrb/bot.rb
166
194
  - lib/discordrb/commands/command_bot.rb
195
+ - lib/discordrb/commands/container.rb
167
196
  - lib/discordrb/commands/events.rb
168
197
  - lib/discordrb/commands/parser.rb
198
+ - lib/discordrb/commands/rate_limiter.rb
199
+ - lib/discordrb/container.rb
169
200
  - lib/discordrb/data.rb
201
+ - lib/discordrb/errors.rb
170
202
  - lib/discordrb/events/await.rb
171
203
  - lib/discordrb/events/bans.rb
172
204
  - lib/discordrb/events/channel_create.rb
@@ -183,7 +215,6 @@ files:
183
215
  - lib/discordrb/events/presence.rb
184
216
  - lib/discordrb/events/typing.rb
185
217
  - lib/discordrb/events/voice_state_update.rb
186
- - lib/discordrb/exceptions.rb
187
218
  - lib/discordrb/logger.rb
188
219
  - lib/discordrb/permissions.rb
189
220
  - lib/discordrb/token_cache.rb