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 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: []