discorb-voice 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,64 +1,79 @@
1
- # frozen_string_literal: true
2
-
3
- module Discorb::Voice
4
- # @private
5
- module ClientVoicePrepend
6
- attr_reader :voice_conditions
7
-
8
- def initialize(*, **)
9
- super
10
- @voice_clients = Discorb::Dictionary.new
11
- @voice_conditions = {}
12
- end
13
-
14
- def event_voice_server_update(data)
15
- @log.debug("Received VOICE_SERVER_UPDATE")
16
- client = Discorb::Voice::Client.new(self, data)
17
- @voice_clients[data[:guild_id]] = client
18
- client.connect_condition.wait
19
- @voice_conditions[data[:guild_id]].signal client
20
- end
21
-
22
- def disconnect_voice(guild_id)
23
- send_gateway(
24
- 4,
25
- guild_id: guild_id,
26
- channel_id: nil,
27
-
28
- )
29
- end
30
-
31
- def connect_to(channel)
32
- Async do
33
- @log.debug("Connecting to #{channel.id}")
34
- send_gateway(
35
- 4,
36
- guild_id: channel.guild.id.to_s,
37
- channel_id: channel.id.to_s,
38
- self_mute: channel.guild.me.voice_state&.self_mute?,
39
- self_deaf: channel.guild.me.voice_state&.self_deaf?,
40
-
41
- )
42
- condition = Async::Condition.new
43
- @voice_conditions[channel.guild.id.to_s] = condition
44
- condition.wait
45
- end
46
- end
47
- end
48
- end
49
-
50
- class Discorb::Client
51
- # @return [Discorb::Dictionary{String => Discorb::Voice::Client}] The voice clients.
52
- attr_accessor :voice_clients
53
-
54
- prepend Discorb::Voice::ClientVoicePrepend
55
- end
56
-
57
- class Discorb::Guild
58
- # @!attribute [r] voice_client
59
- # @return [Discorb::Voice::Client] The voice client.
60
- # @return [nil] If the client is not connected to the voice server.
61
- def voice_client
62
- @client.voice_clients[@id.to_s]
63
- end
64
- end
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
@@ -1,120 +1,121 @@
1
- module Discorb::Voice
2
- #
3
- # Represents a Ogg stream.
4
- #
5
- class OggStream
6
- #
7
- # Creates a new Ogg stream.
8
- #
9
- # @param [IO] io The audio ogg stream.
10
- #
11
- def initialize(io)
12
- @io = io
13
- end
14
-
15
- #
16
- # Enumerates the packets of the Ogg stream.
17
- #
18
- # @return [Enumerator<Discorb::Voice::OggStream::Page::Packet>] The packets of the Ogg stream.
19
- #
20
- def packets
21
- Enumerator.new do |enum|
22
- part = +""
23
- raw_packets.each do |packet|
24
- part << packet.data
25
- unless packet.partial
26
- enum << part
27
- part = +""
28
- end
29
- end
30
- end
31
- end
32
-
33
- #
34
- # Enumerates the raw packets of the Ogg stream.
35
- # This may include partial packets.
36
- #
37
- # @return [Enumerator<Discorb::Voice::OggStream::Page::Packet>] The raw packets of the Ogg stream.
38
- #
39
- def raw_packets
40
- Enumerator.new do |enum|
41
- loop do
42
- # p c += 1
43
- if @io.read(4) != "OggS"
44
- break
45
- end
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
@@ -1,110 +1,113 @@
1
- require "open3"
2
- require "tmpdir"
3
-
4
- module Discorb::Voice
5
- #
6
- # The source of audio data.
7
- # @abstract
8
- #
9
- class Source
10
- #
11
- # The audio ogg stream. This MUST be implemented by subclasses.
12
- #
13
- # @return [#read] The audio ogg stream.
14
- #
15
- def io
16
- raise NotImplementedError
17
- end
18
-
19
- #
20
- # Clean up the source.
21
- # This does nothing by default.
22
- #
23
- def cleanup
24
- # noop
25
- end
26
- end
27
-
28
- #
29
- # Plays audio from a source, using FFmpeg.
30
- # @note You must install FFmpeg and should be on the PATH.
31
- #
32
- class FFmpegAudio
33
- attr_reader :stdin, :stdout, :stderr, :process
34
-
35
- #
36
- # Creates a new FFmpegAudio.
37
- #
38
- # @param [String, IO] source The source of audio data.
39
- # @param [Integer] bitrate The bitrate of the audio.
40
- # @param [{String => String}] extra_options Extra options for FFmpeg. This will be passed before `-i`.
41
- # @param [{String => String}] extra_options2 Extra options for FFmpeg. This will be passed after `-i`.
42
- #
43
- def initialize(source, bitrate: 128, extra_options: {}, extra_options2: {})
44
- if source.is_a?(String)
45
- source_path = source
46
- @tmp_path = nil
47
- else
48
- source_path = "#{Dir.tmpdir}/#{Process.pid}.#{source.object_id}"
49
- @tmp_path = source_path
50
- File.open(source_path, "wb") do |f|
51
- while chunk = source.read(4096)
52
- f.write(chunk)
53
- end
54
- end
55
- end
56
- args = %W[ffmpeg]
57
- extra_options.each do |key, value|
58
- args += ["-#{key}", "#{value}"]
59
- end
60
- args += %W[
61
- -i #{source_path}
62
- -f opus
63
- -c:a libopus
64
- -ar 48000
65
- -ac 2
66
- -b:a #{bitrate}k
67
- -loglevel warning
68
- -map_metadata -1]
69
- extra_options2.each do |key, value|
70
- args += ["-#{key}", "#{value}"]
71
- end
72
- args += %w[pipe:1]
73
- @stdin, @stdout, @process = Open3.popen2(*args)
74
- end
75
-
76
- def io
77
- @stdout
78
- end
79
-
80
- #
81
- # Kills the FFmpeg process.
82
- #
83
- def cleanup
84
- @process.kill
85
- if @tmp_path
86
- File.delete(@tmp_path)
87
- end
88
- end
89
- end
90
-
91
- #
92
- # Plays audio from a ogg file.
93
- #
94
- class OggAudio
95
- attr_reader :io
96
-
97
- #
98
- # Opens an ogg file.
99
- #
100
- # @param [String, IO] src The ogg file to open, or an IO object.
101
- #
102
- def initialize(src)
103
- if src.is_a?(String)
104
- @io = File.open(src, "rb")
105
- else
106
- @io = src
107
- end
108
- end
109
- end
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
@@ -3,6 +3,6 @@
3
3
  module Discorb
4
4
  module Voice
5
5
  # @return [String] The version of the library.
6
- VERSION = "0.1.0"
6
+ VERSION = "0.1.1"
7
7
  end
8
8
  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, <<~EOS, cause: nil
8
- Could not load libsodium library.
9
- Follow the instructions at https://github.com/discorb-lib/discorb-voice#install-libsodium
10
- EOS
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, <<~EOS, cause: nil
17
- Could not find ffmpeg.
18
- Follow the instructions at https://github.com/discorb-lib/discorb-voice#install-ffmpeg
19
- EOS
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 { |line| line.start_with?("configuration: ") }
21
+ line = ffmpeg_version.split("\n").find { |l| l.start_with?("configuration: ") }
22
22
  unless line.include? "--enable-libopus"
23
- raise LoadError, <<~EOS, cause: nil
24
- Your ffmpeg version does not support opus.
25
- Install ffmpeg with opus support.
26
- EOS
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
- require "discorb/voice"
1
+ # frozen_string_literal: true
2
+ require "discorb/voice"