audio_switch 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ require 'audio_switch'
3
+
4
+ begin
5
+ puts "audio_switch version #{AudioSwitch::VERSION}"
6
+ AudioSwitch::App.start
7
+ rescue Interrupt
8
+ puts "\nexit"
9
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module AudioSwitch
2
+ VERSION = '0.0.1'.freeze
3
+ 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: []