audio_switch 0.0.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 +7 -0
- data/bin/audio_switch +9 -0
- data/lib/audio_switch.rb +15 -0
- data/lib/audio_switch/model.rb +89 -0
- data/lib/audio_switch/pactl.rb +181 -0
- data/lib/audio_switch/ui.rb +92 -0
- data/lib/audio_switch/version.rb +3 -0
- metadata +150 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5505186d9284e215a4f09800accbb314901e1ed6
|
4
|
+
data.tar.gz: 5dabdbaecb7a30cd81e8e8a34f55ee4e79470c72
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6b8862b2f907915db9e3c62b6de64b857d95c05f4d41bab8ab9a734a80b4556d110c0c19a38a50260ce8994b6696973b0b4cf0614363756670d45da74b8f3f62
|
7
|
+
data.tar.gz: dfa7d089d74ca9e8e22ae16e77c6cfff6fdca2991734811050f408e7db2571f777a232840ce97a9ead825f22bcfda6f7348e7c262af1ee4377e85c8734ed9b51
|
data/bin/audio_switch
ADDED
data/lib/audio_switch.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative 'audio_switch/pactl.rb'
|
2
|
+
require_relative 'audio_switch/model.rb'
|
3
|
+
require_relative 'audio_switch/ui.rb'
|
4
|
+
require_relative 'audio_switch/version.rb'
|
5
|
+
|
6
|
+
module AudioSwitch
|
7
|
+
module App
|
8
|
+
def self.start
|
9
|
+
pactl = AudioSwitch::Pactl.new
|
10
|
+
model = AudioSwitch::Model.new(pactl)
|
11
|
+
ui = AudioSwitch::UI.new(model)
|
12
|
+
ui.start
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module AudioSwitch
|
2
|
+
class Model
|
3
|
+
MODULE_RTP_SEND = 'module-rtp-send'.freeze
|
4
|
+
MODULE_NULL_SINK = 'module-null-sink'.freeze
|
5
|
+
RTP = 'rtp'.freeze
|
6
|
+
|
7
|
+
def initialize(pactl)
|
8
|
+
@pactl = pactl
|
9
|
+
end
|
10
|
+
|
11
|
+
def watch(&block)
|
12
|
+
@pactl.subscribe { |event| handle(event, block) }
|
13
|
+
yield
|
14
|
+
end
|
15
|
+
|
16
|
+
def select_sink(sink_id)
|
17
|
+
@pactl.default_sink = sink_id
|
18
|
+
@pactl.inputs.each do |input|
|
19
|
+
@pactl.move_input(input[:id], sink_id)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def sinks
|
24
|
+
@pactl.sinks
|
25
|
+
end
|
26
|
+
|
27
|
+
def rtp_on?
|
28
|
+
return false unless @pactl.sinks.any? { |sink| sink[:name] == RTP }
|
29
|
+
return false unless @pactl.modules.any? { |mod| mod[:name] == MODULE_RTP_SEND }
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
def rtp_on
|
34
|
+
# prevent positive feedback loop
|
35
|
+
mute_sources
|
36
|
+
# see https://cgit.freedesktop.org/pulseaudio/paprefs/tree/src/paprefs.cc
|
37
|
+
@pactl.load_module(
|
38
|
+
MODULE_NULL_SINK,
|
39
|
+
'sink_name' => 'rtp',
|
40
|
+
'format' => 's16be',
|
41
|
+
'channels' => '2',
|
42
|
+
'rate' => '44100',
|
43
|
+
'sink_properties' => {
|
44
|
+
'device.description' => 'RTP Multicast',
|
45
|
+
'device.bus' => 'network',
|
46
|
+
'device.icon_name' => 'network-server'
|
47
|
+
}
|
48
|
+
)
|
49
|
+
@pactl.load_module(MODULE_RTP_SEND, 'source' => 'rtp.monitor')
|
50
|
+
select_sink(default_sink)
|
51
|
+
end
|
52
|
+
|
53
|
+
def rtp_off
|
54
|
+
@pactl.unload_module(MODULE_RTP_SEND)
|
55
|
+
@pactl.unload_module(MODULE_NULL_SINK)
|
56
|
+
end
|
57
|
+
|
58
|
+
def mute_sources
|
59
|
+
sources.each do |source|
|
60
|
+
@pactl.mute_source(source[:id])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def unmute_sources
|
65
|
+
sources.each do |source|
|
66
|
+
@pactl.unmute_source(source[:id])
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def sources_mute?
|
71
|
+
sources.all? { |source| source[:mute] }
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def sources
|
77
|
+
@pactl.sources.select { |source| source[:name] != 'rtp.monitor' }
|
78
|
+
end
|
79
|
+
|
80
|
+
def handle(event, block)
|
81
|
+
block.call if event[:object] == :sink ||
|
82
|
+
event[:object] == :source
|
83
|
+
end
|
84
|
+
|
85
|
+
def default_sink
|
86
|
+
(@pactl.sinks.find { |sink| sink[:default] } || {})[:id]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
require 'pty'
|
2
|
+
|
3
|
+
module AudioSwitch
|
4
|
+
class Pactl
|
5
|
+
def move_input(input_id, sink_id)
|
6
|
+
`pactl move-sink-input #{input_id} #{sink_id}`
|
7
|
+
end
|
8
|
+
|
9
|
+
def default_sink=(sink_id)
|
10
|
+
# pactl doesn't have this command
|
11
|
+
`pacmd set-default-sink #{sink_id}`
|
12
|
+
end
|
13
|
+
|
14
|
+
def sinks
|
15
|
+
default_sink_name = self.class.parse_default_sink(`pacmd stat`)
|
16
|
+
self.class.parse_sinks(`pactl list sinks`, default_sink_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def inputs
|
20
|
+
self.class.parse_inputs(`pactl list sink-inputs`)
|
21
|
+
end
|
22
|
+
|
23
|
+
def modules
|
24
|
+
self.class.parse_modules(`pactl list modules`)
|
25
|
+
end
|
26
|
+
|
27
|
+
def sources
|
28
|
+
self.class.parse_sources(`pactl list sources`)
|
29
|
+
end
|
30
|
+
|
31
|
+
def load_module(mod, options = {})
|
32
|
+
`pactl load-module #{mod} #{ModuleOptions.new(options)}`
|
33
|
+
end
|
34
|
+
|
35
|
+
def unload_module(mod)
|
36
|
+
`pactl unload-module #{mod}`
|
37
|
+
end
|
38
|
+
|
39
|
+
def subscribe(command = 'pactl subscribe')
|
40
|
+
Thread.start do
|
41
|
+
@pactl_sub = PTY.spawn(command)[0]
|
42
|
+
begin
|
43
|
+
@pactl_sub.each do |line|
|
44
|
+
yield(Out.new(line).parse_event)
|
45
|
+
end
|
46
|
+
rescue Errno::EIO, IOError
|
47
|
+
return
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def mute_source(source_id)
|
53
|
+
`pactl set-source-mute #{source_id} true`
|
54
|
+
end
|
55
|
+
|
56
|
+
def unmute_source(source_id)
|
57
|
+
`pactl set-source-mute #{source_id} false`
|
58
|
+
end
|
59
|
+
|
60
|
+
def dispose
|
61
|
+
@pactl_sub.close
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.parse_sinks(out, default_sink_name)
|
65
|
+
Out.new(out).parse_objects(
|
66
|
+
[
|
67
|
+
{ marker: 'Sink #', property: :id },
|
68
|
+
{ marker: 'Name:', property: :name },
|
69
|
+
{ marker: 'Description:', property: :description }
|
70
|
+
]
|
71
|
+
).each { |sink| sink[:default] = true if sink[:name] == default_sink_name }
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.parse_inputs(out)
|
75
|
+
Out.new(out).parse_objects(
|
76
|
+
[
|
77
|
+
{ marker: 'Sink Input #', property: :id }
|
78
|
+
]
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.parse_modules(out)
|
83
|
+
Out.new(out).parse_objects(
|
84
|
+
[
|
85
|
+
{ marker: 'Module #' },
|
86
|
+
{ marker: 'Name:', property: :name }
|
87
|
+
]
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.parse_sources(out)
|
92
|
+
Out.new(out).parse_objects(
|
93
|
+
[
|
94
|
+
{ marker: 'Source #', property: :id },
|
95
|
+
{ marker: 'Name:', property: :name },
|
96
|
+
{ marker: 'Mute:', property: :mute }
|
97
|
+
]
|
98
|
+
).each { |source| source[:mute] = source[:mute] == 'yes' }
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.parse_default_sink(out)
|
102
|
+
Out.new(out).parse_property('Default sink name:')
|
103
|
+
end
|
104
|
+
|
105
|
+
class Out
|
106
|
+
def initialize(string)
|
107
|
+
@string = string
|
108
|
+
end
|
109
|
+
|
110
|
+
def parse_objects(fields)
|
111
|
+
objects = []
|
112
|
+
field_id = 0
|
113
|
+
object = nil
|
114
|
+
|
115
|
+
@string.each_line do |line|
|
116
|
+
field = fields[field_id]
|
117
|
+
next unless line =~ Regexp.new(field[:marker])
|
118
|
+
|
119
|
+
object = {} if field_id == 0
|
120
|
+
update(object, line, field)
|
121
|
+
|
122
|
+
field_id += 1
|
123
|
+
next unless field_id == fields.size
|
124
|
+
|
125
|
+
objects << object
|
126
|
+
object = nil
|
127
|
+
field_id = 0
|
128
|
+
end
|
129
|
+
|
130
|
+
objects
|
131
|
+
end
|
132
|
+
|
133
|
+
def parse_event
|
134
|
+
parts = @string.split(' ')
|
135
|
+
{
|
136
|
+
type: parts[1].delete('\'').to_sym,
|
137
|
+
object: parts[3].to_sym,
|
138
|
+
id: parts[4].sub('#', '')
|
139
|
+
}
|
140
|
+
end
|
141
|
+
|
142
|
+
def parse_property(marker)
|
143
|
+
read_property(@string, marker)
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def update(object, line, field)
|
149
|
+
property = field[:property]
|
150
|
+
object[property] = read_property(line, field[:marker]) if field[:property]
|
151
|
+
end
|
152
|
+
|
153
|
+
def read_property(line, marker)
|
154
|
+
line.match(Regexp.new("#{marker}\\s*(.*?)\\s*$"))[1]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
class ModuleOptions
|
159
|
+
def initialize(options)
|
160
|
+
@options = options
|
161
|
+
end
|
162
|
+
|
163
|
+
def to_s
|
164
|
+
format(@options)
|
165
|
+
end
|
166
|
+
|
167
|
+
def format(opts, quote = '')
|
168
|
+
result = ''
|
169
|
+
opts.each_pair do |key, value|
|
170
|
+
result += ' ' unless result.empty?
|
171
|
+
result += if value.is_a? Hash
|
172
|
+
"#{key}=\\\"#{format(value, '\\\'')}\\\""
|
173
|
+
else
|
174
|
+
"#{key}=#{quote}#{value}#{quote}"
|
175
|
+
end
|
176
|
+
end
|
177
|
+
result
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'gtk2'
|
2
|
+
require 'ruby-libappindicator'
|
3
|
+
|
4
|
+
module AudioSwitch
|
5
|
+
class UI
|
6
|
+
def initialize(model)
|
7
|
+
@model = model
|
8
|
+
@menu = Gtk::Menu.new
|
9
|
+
@items = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def start
|
13
|
+
add_to_tray
|
14
|
+
subscribe
|
15
|
+
Gtk.main
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def add_to_tray
|
21
|
+
indicator = AppIndicator::AppIndicator.new(
|
22
|
+
self.class.name,
|
23
|
+
'multimedia-volume-control',
|
24
|
+
AppIndicator::Category::HARDWARE
|
25
|
+
)
|
26
|
+
indicator.set_menu(@menu)
|
27
|
+
indicator.set_status(AppIndicator::Status::ACTIVE)
|
28
|
+
end
|
29
|
+
|
30
|
+
def subscribe
|
31
|
+
@model.watch { draw(@model.sinks) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def draw(sinks)
|
35
|
+
clear
|
36
|
+
sinks.each { |sink| add new_item(sink) }
|
37
|
+
add new_separator
|
38
|
+
add new_rtp_toggle
|
39
|
+
add new_mute_toggle
|
40
|
+
end
|
41
|
+
|
42
|
+
def add(item)
|
43
|
+
@menu.append item
|
44
|
+
@items.push item
|
45
|
+
item.show
|
46
|
+
end
|
47
|
+
|
48
|
+
def new_item(sink)
|
49
|
+
item = Gtk::RadioMenuItem.new(@items, sink[:description])
|
50
|
+
item.signal_connect('toggled') do
|
51
|
+
@model.select_sink(sink[:id]) if item.active?
|
52
|
+
end
|
53
|
+
item.set_active(sink[:default])
|
54
|
+
item
|
55
|
+
end
|
56
|
+
|
57
|
+
def new_rtp_toggle
|
58
|
+
toggle = Gtk::CheckMenuItem.new('Enable RTP')
|
59
|
+
toggle.active = @model.rtp_on?
|
60
|
+
toggle.signal_connect('toggled') do
|
61
|
+
if toggle.active?
|
62
|
+
@model.rtp_on
|
63
|
+
else
|
64
|
+
@model.rtp_off
|
65
|
+
end
|
66
|
+
end
|
67
|
+
toggle
|
68
|
+
end
|
69
|
+
|
70
|
+
def new_mute_toggle
|
71
|
+
toggle = Gtk::CheckMenuItem.new('Mute Inputs')
|
72
|
+
toggle.active = @model.sources_mute?
|
73
|
+
toggle.sensitive = !@model.rtp_on?
|
74
|
+
toggle.signal_connect('toggled') do
|
75
|
+
if toggle.active?
|
76
|
+
@model.mute_sources
|
77
|
+
else
|
78
|
+
@model.unmute_sources
|
79
|
+
end
|
80
|
+
end
|
81
|
+
toggle
|
82
|
+
end
|
83
|
+
|
84
|
+
def new_separator
|
85
|
+
Gtk::SeparatorMenuItem.new
|
86
|
+
end
|
87
|
+
|
88
|
+
def clear
|
89
|
+
@menu.remove(@items.pop) until @items.empty?
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
metadata
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: audio_switch
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Anatolii Saienko
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-10-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: gtk2
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 3.0.8
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 3.0.8
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: ruby-libappindicator
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.1.5
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.1.5
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.12'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.12'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '11.2'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '11.2'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.41'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.41'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.5'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.5'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec-wait
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.0.9
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.0.9
|
111
|
+
description: Ubuntu applet that lets you switch audio sinks, toggle RTP sender and
|
112
|
+
inputs. Depends on PulseAudio and libappindicator-dev.
|
113
|
+
email:
|
114
|
+
- anatoly.sayenko@gmail.com
|
115
|
+
executables:
|
116
|
+
- audio_switch
|
117
|
+
extensions: []
|
118
|
+
extra_rdoc_files: []
|
119
|
+
files:
|
120
|
+
- bin/audio_switch
|
121
|
+
- lib/audio_switch.rb
|
122
|
+
- lib/audio_switch/model.rb
|
123
|
+
- lib/audio_switch/pactl.rb
|
124
|
+
- lib/audio_switch/ui.rb
|
125
|
+
- lib/audio_switch/version.rb
|
126
|
+
homepage: https://github.com/tsayen/audio-switch
|
127
|
+
licenses:
|
128
|
+
- MIT
|
129
|
+
metadata: {}
|
130
|
+
post_install_message:
|
131
|
+
rdoc_options: []
|
132
|
+
require_paths:
|
133
|
+
- lib
|
134
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '2.3'
|
139
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
140
|
+
requirements:
|
141
|
+
- - ">="
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: '0'
|
144
|
+
requirements: []
|
145
|
+
rubyforge_project:
|
146
|
+
rubygems_version: 2.5.1
|
147
|
+
signing_key:
|
148
|
+
specification_version: 4
|
149
|
+
summary: Sink switch for PulseAudio
|
150
|
+
test_files: []
|