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.
@@ -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"