discorb-voice 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/lint-push.yml +20 -0
- data/.github/workflows/lint.yml +16 -0
- data/.rubocop.yml +77 -0
- data/discorb-voice.gemspec +1 -1
- data/lib/discorb/voice/core.rb +326 -314
- data/lib/discorb/voice/error.rb +10 -7
- data/lib/discorb/voice/extend.rb +79 -64
- data/lib/discorb/voice/ogg.rb +121 -120
- data/lib/discorb/voice/source.rb +113 -110
- data/lib/discorb/voice/version.rb +1 -1
- data/lib/discorb/voice.rb +13 -13
- data/lib/discorb-voice.rb +2 -1
- metadata +7 -4
data/lib/discorb/voice/extend.rb
CHANGED
@@ -1,64 +1,79 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
module
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# rubocop:disable Style/Documentation
|
3
|
+
|
4
|
+
module Discorb
|
5
|
+
module Voice
|
6
|
+
# @private
|
7
|
+
module ClientVoicePrepend
|
8
|
+
attr_reader :voice_conditions
|
9
|
+
attr_reader :voice_mutexes
|
10
|
+
|
11
|
+
def initialize(*, **)
|
12
|
+
super
|
13
|
+
@voice_clients = Discorb::Dictionary.new
|
14
|
+
@voice_conditions = {}
|
15
|
+
@voice_mutexes = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def event_voice_server_update(data)
|
19
|
+
@log.debug("Received VOICE_SERVER_UPDATE")
|
20
|
+
client = Discorb::Voice::Client.new(self, data)
|
21
|
+
@voice_clients[data[:guild_id]] = client
|
22
|
+
client.connect_condition.wait
|
23
|
+
@voice_conditions[data[:guild_id]].signal client
|
24
|
+
end
|
25
|
+
|
26
|
+
def disconnect_voice(guild_id)
|
27
|
+
send_gateway(
|
28
|
+
4,
|
29
|
+
guild_id: guild_id,
|
30
|
+
channel_id: nil,
|
31
|
+
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def connect_to(channel)
|
36
|
+
Async do
|
37
|
+
@log.debug("Connecting to #{channel.id}")
|
38
|
+
send_gateway(
|
39
|
+
4,
|
40
|
+
guild_id: channel.guild.id.to_s,
|
41
|
+
channel_id: channel.id.to_s,
|
42
|
+
self_mute: channel.guild.me.voice_state&.self_mute?,
|
43
|
+
self_deaf: channel.guild.me.voice_state&.self_deaf?,
|
44
|
+
|
45
|
+
)
|
46
|
+
condition = Async::Condition.new
|
47
|
+
@voice_conditions[channel.guild.id.to_s] = condition
|
48
|
+
condition.wait
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Client
|
55
|
+
# @return [Discorb::Dictionary{String => Discorb::Voice::Client}] The voice clients.
|
56
|
+
attr_accessor :voice_clients
|
57
|
+
|
58
|
+
prepend Discorb::Voice::ClientVoicePrepend
|
59
|
+
end
|
60
|
+
|
61
|
+
class Guild
|
62
|
+
# @!attribute [r] voice_client
|
63
|
+
# @return [Discorb::Voice::Client] The voice client.
|
64
|
+
# @return [nil] If the client is not connected to the voice server.
|
65
|
+
def voice_client
|
66
|
+
@client.voice_clients[@id.to_s]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
module Connectable
|
71
|
+
def connect
|
72
|
+
Async do
|
73
|
+
@client.connect_to(self).wait
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# rubocop:enable Style/Documentation
|
data/lib/discorb/voice/ogg.rb
CHANGED
@@ -1,120 +1,121 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
#
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
pg = Page.new(@io)
|
47
|
-
pg.packets.each do |packet|
|
48
|
-
enum << packet
|
49
|
-
end
|
50
|
-
# enum << pg.packets.next
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
#
|
56
|
-
# Represents a page of the Ogg stream.
|
57
|
-
#
|
58
|
-
class Page
|
59
|
-
# @return [Struct] The struct of the packet
|
60
|
-
Packet = Struct.new(:data, :partial, :page)
|
61
|
-
# @return [Integer] The version of the page.
|
62
|
-
attr_reader :version
|
63
|
-
# @return [Integer] The header type of the page.
|
64
|
-
attr_reader :header_type
|
65
|
-
# @return [Integer] The granule position of the page.
|
66
|
-
attr_reader :granule_position
|
67
|
-
# @return [Integer] The bitstream serial number of the page.
|
68
|
-
attr_reader :bitstream_serial_number
|
69
|
-
# @return [Integer] The page sequence number of the page.
|
70
|
-
attr_reader :page_sequence_number
|
71
|
-
# @return [Integer] The CRC checksum of the page.
|
72
|
-
attr_reader :crc_checksum
|
73
|
-
# @return [Integer] The length of the page segment table.
|
74
|
-
attr_reader :page_segments
|
75
|
-
# @return [String] The body of the page.
|
76
|
-
attr_reader :body
|
77
|
-
|
78
|
-
#
|
79
|
-
# Creates a new page.
|
80
|
-
#
|
81
|
-
# @param [IO] io The audio ogg stream.
|
82
|
-
# @note This method will seek the io.
|
83
|
-
#
|
84
|
-
def initialize(io)
|
85
|
-
@version, @header_type, @granule_position, @bitstream_serial_number, @page_sequence_number, @crc_checksum, @page_segments =
|
86
|
-
io.read(23).unpack("CCQ<L<L<L<C")
|
87
|
-
@segtable = io.read(@page_segments)
|
88
|
-
len = @segtable.unpack("C*").sum
|
89
|
-
@body = io.read(len)
|
90
|
-
end
|
91
|
-
|
92
|
-
#
|
93
|
-
# Enumerates the packets of the page.
|
94
|
-
#
|
95
|
-
# @return [Enumerator<Discorb::Voice::OggStream::Page::Packet>] The packets of the page.
|
96
|
-
#
|
97
|
-
def packets
|
98
|
-
Enumerator.new do |enum|
|
99
|
-
offset = 0
|
100
|
-
length = 0
|
101
|
-
partial = true
|
102
|
-
@segtable.bytes.each do |seg|
|
103
|
-
if seg == 255
|
104
|
-
length += 255
|
105
|
-
partial = true
|
106
|
-
else
|
107
|
-
length += seg
|
108
|
-
partial = false
|
109
|
-
enum << Packet.new(@body[offset, length], partial, self)
|
110
|
-
offset += length
|
111
|
-
length = 0
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
enum << Packet.new(@body[offset, length], partial, self) if partial
|
116
|
-
end
|
117
|
-
end
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Discorb
|
3
|
+
module Voice
|
4
|
+
#
|
5
|
+
# Represents a Ogg stream.
|
6
|
+
#
|
7
|
+
class OggStream
|
8
|
+
#
|
9
|
+
# Creates a new Ogg stream.
|
10
|
+
#
|
11
|
+
# @param [IO] io The audio ogg stream.
|
12
|
+
#
|
13
|
+
def initialize(io)
|
14
|
+
@io = io
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Enumerates the packets of the Ogg stream.
|
19
|
+
#
|
20
|
+
# @return [Enumerator<Discorb::Voice::OggStream::Page::Packet>] The packets of the Ogg stream.
|
21
|
+
#
|
22
|
+
def packets
|
23
|
+
Enumerator.new do |enum|
|
24
|
+
part = +""
|
25
|
+
raw_packets.each do |packet|
|
26
|
+
part << packet.data
|
27
|
+
unless packet.partial
|
28
|
+
enum << part
|
29
|
+
part = +""
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Enumerates the raw packets of the Ogg stream.
|
37
|
+
# This may include partial packets.
|
38
|
+
#
|
39
|
+
# @return [Enumerator<Discorb::Voice::OggStream::Page::Packet>] The raw packets of the Ogg stream.
|
40
|
+
#
|
41
|
+
def raw_packets
|
42
|
+
Enumerator.new do |enum|
|
43
|
+
loop do
|
44
|
+
# p c += 1
|
45
|
+
break if @io.read(4) != "OggS"
|
46
|
+
pg = Page.new(@io)
|
47
|
+
pg.packets.each do |packet|
|
48
|
+
enum << packet
|
49
|
+
end
|
50
|
+
# enum << pg.packets.next
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# Represents a page of the Ogg stream.
|
57
|
+
#
|
58
|
+
class Page
|
59
|
+
# @return [Struct] The struct of the packet
|
60
|
+
Packet = Struct.new(:data, :partial, :page)
|
61
|
+
# @return [Integer] The version of the page.
|
62
|
+
attr_reader :version
|
63
|
+
# @return [Integer] The header type of the page.
|
64
|
+
attr_reader :header_type
|
65
|
+
# @return [Integer] The granule position of the page.
|
66
|
+
attr_reader :granule_position
|
67
|
+
# @return [Integer] The bitstream serial number of the page.
|
68
|
+
attr_reader :bitstream_serial_number
|
69
|
+
# @return [Integer] The page sequence number of the page.
|
70
|
+
attr_reader :page_sequence_number
|
71
|
+
# @return [Integer] The CRC checksum of the page.
|
72
|
+
attr_reader :crc_checksum
|
73
|
+
# @return [Integer] The length of the page segment table.
|
74
|
+
attr_reader :page_segments
|
75
|
+
# @return [String] The body of the page.
|
76
|
+
attr_reader :body
|
77
|
+
|
78
|
+
#
|
79
|
+
# Creates a new page.
|
80
|
+
#
|
81
|
+
# @param [IO] io The audio ogg stream.
|
82
|
+
# @note This method will seek the io.
|
83
|
+
#
|
84
|
+
def initialize(io)
|
85
|
+
@version, @header_type, @granule_position, @bitstream_serial_number, @page_sequence_number, @crc_checksum, @page_segments =
|
86
|
+
io.read(23).unpack("CCQ<L<L<L<C")
|
87
|
+
@segtable = io.read(@page_segments)
|
88
|
+
len = @segtable.unpack("C*").sum
|
89
|
+
@body = io.read(len)
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# Enumerates the packets of the page.
|
94
|
+
#
|
95
|
+
# @return [Enumerator<Discorb::Voice::OggStream::Page::Packet>] The packets of the page.
|
96
|
+
#
|
97
|
+
def packets
|
98
|
+
Enumerator.new do |enum|
|
99
|
+
offset = 0
|
100
|
+
length = 0
|
101
|
+
partial = true
|
102
|
+
@segtable.bytes.each do |seg|
|
103
|
+
if seg == 255
|
104
|
+
length += 255
|
105
|
+
partial = true
|
106
|
+
else
|
107
|
+
length += seg
|
108
|
+
partial = false
|
109
|
+
enum << Packet.new(@body[offset, length], partial, self)
|
110
|
+
offset += length
|
111
|
+
length = 0
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
enum << Packet.new(@body[offset, length], partial, self) if partial
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/lib/discorb/voice/source.rb
CHANGED
@@ -1,110 +1,113 @@
|
|
1
|
-
|
2
|
-
require "
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
#
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
#
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
args
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "open3"
|
3
|
+
require "tmpdir"
|
4
|
+
|
5
|
+
module Discorb
|
6
|
+
module Voice
|
7
|
+
#
|
8
|
+
# The source of audio data.
|
9
|
+
# @abstract
|
10
|
+
#
|
11
|
+
class Source
|
12
|
+
#
|
13
|
+
# The audio ogg stream. This MUST be implemented by subclasses.
|
14
|
+
#
|
15
|
+
# @return [#read] The audio ogg stream.
|
16
|
+
#
|
17
|
+
def io
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Clean up the source.
|
23
|
+
# This does nothing by default.
|
24
|
+
#
|
25
|
+
def cleanup
|
26
|
+
# noop
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Plays audio from a source, using FFmpeg.
|
32
|
+
# @note You must install FFmpeg and should be on the PATH.
|
33
|
+
#
|
34
|
+
class FFmpegAudio
|
35
|
+
attr_reader :stdin, :stdout, :stderr, :process
|
36
|
+
|
37
|
+
#
|
38
|
+
# Creates a new FFmpegAudio.
|
39
|
+
#
|
40
|
+
# @param [String, IO] source The source of audio data.
|
41
|
+
# @param [Integer] bitrate The bitrate of the audio.
|
42
|
+
# @param [{String => String}] extra_options Extra options for FFmpeg. This will be passed before `-i`.
|
43
|
+
# @param [{String => String}] extra_options2 Extra options for FFmpeg. This will be passed after `-i`.
|
44
|
+
#
|
45
|
+
def initialize(source, bitrate: 128, extra_options: {}, extra_options2: {})
|
46
|
+
if source.is_a?(String)
|
47
|
+
source_path = source
|
48
|
+
@tmp_path = nil
|
49
|
+
else
|
50
|
+
source_path = "#{Dir.tmpdir}/#{Process.pid}.#{source.object_id}"
|
51
|
+
@tmp_path = source_path
|
52
|
+
File.open(source_path, "wb") do |f|
|
53
|
+
while chunk = source.read(4096)
|
54
|
+
f.write(chunk)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
args = %w[ffmpeg]
|
59
|
+
extra_options.each do |key, value|
|
60
|
+
args += ["-#{key}", "#{value}"]
|
61
|
+
end
|
62
|
+
args += %W[
|
63
|
+
-i #{source_path}
|
64
|
+
-f opus
|
65
|
+
-c:a libopus
|
66
|
+
-ar 48000
|
67
|
+
-ac 2
|
68
|
+
-b:a #{bitrate}k
|
69
|
+
-loglevel warning
|
70
|
+
-map_metadata -1]
|
71
|
+
extra_options2.each do |key, value|
|
72
|
+
args += ["-#{key}", "#{value}"]
|
73
|
+
end
|
74
|
+
args += %w[pipe:1]
|
75
|
+
@stdin, @stdout, @process = Open3.popen2(*args)
|
76
|
+
end
|
77
|
+
|
78
|
+
def io
|
79
|
+
@stdout
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# Kills the FFmpeg process, and closes io.
|
84
|
+
#
|
85
|
+
def cleanup
|
86
|
+
@process.kill
|
87
|
+
@stdin.close
|
88
|
+
@stdout.close
|
89
|
+
File.delete(@tmp_path) if @tmp_path
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# Plays audio from a ogg file.
|
95
|
+
#
|
96
|
+
class OggAudio
|
97
|
+
attr_reader :io
|
98
|
+
|
99
|
+
#
|
100
|
+
# Opens an ogg file.
|
101
|
+
#
|
102
|
+
# @param [String, IO] src The ogg file to open, or an IO object.
|
103
|
+
#
|
104
|
+
def initialize(src)
|
105
|
+
@io = if src.is_a?(String)
|
106
|
+
File.open(src, "rb")
|
107
|
+
else
|
108
|
+
src
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/lib/discorb/voice.rb
CHANGED
@@ -4,26 +4,26 @@ require "discorb"
|
|
4
4
|
begin
|
5
5
|
require "rbnacl"
|
6
6
|
rescue LoadError
|
7
|
-
raise LoadError, <<~
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
raise LoadError, <<~ERROR, cause: nil
|
8
|
+
Could not load libsodium library.
|
9
|
+
Follow the instructions at https://github.com/discorb-lib/discorb-voice#install-libsodium
|
10
|
+
ERROR
|
11
11
|
end
|
12
12
|
require "open3"
|
13
13
|
begin
|
14
14
|
ffmpeg_version = Open3.capture2e("ffmpeg -version")[0]
|
15
15
|
rescue Errno::ENOENT
|
16
|
-
raise LoadError, <<~
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
raise LoadError, <<~ERROR, cause: nil
|
17
|
+
Could not find ffmpeg.
|
18
|
+
Follow the instructions at https://github.com/discorb-lib/discorb-voice#install-ffmpeg
|
19
|
+
ERROR
|
20
20
|
else
|
21
|
-
line = ffmpeg_version.split("\n").find { |
|
21
|
+
line = ffmpeg_version.split("\n").find { |l| l.start_with?("configuration: ") }
|
22
22
|
unless line.include? "--enable-libopus"
|
23
|
-
raise LoadError, <<~
|
24
|
-
|
25
|
-
|
26
|
-
|
23
|
+
raise LoadError, <<~ERROR, cause: nil
|
24
|
+
Your ffmpeg version does not support opus.
|
25
|
+
Install ffmpeg with opus support.
|
26
|
+
ERROR
|
27
27
|
end
|
28
28
|
end
|
29
29
|
require_relative "voice/version"
|
data/lib/discorb-voice.rb
CHANGED
@@ -1 +1,2 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "discorb/voice"
|