3 # hwmixvolume - ALSA hardware mixer volume control applet
4 # Copyright (c) 2009-2010 Clemens Ladisch
5 # Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
7 # Permission to use, copy, modify, and/or distribute this software for any
8 # purpose with or without fee is hereby granted, provided that the above
9 # copyright notice and this permission notice appear in all copies.
11 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
12 # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
13 # AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
14 # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
15 # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
16 # OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
17 # PERFORMANCE OF THIS SOFTWARE.
20 gi.require_version('GLib', '2.0')
21 gi.require_version('Gtk', '3.0')
22 from gi.repository import GLib, Gtk
23 from pyalsa import alsacard, alsahcontrol
25 INTF_PCM = alsahcontrol.interface_id['PCM']
26 INTF_MIXER = alsahcontrol.interface_id['MIXER']
27 TYPE_INTEGER = alsahcontrol.element_type['INTEGER']
28 EVENT_VALUE = alsahcontrol.event_mask['VALUE']
29 EVENT_INFO = alsahcontrol.event_mask['INFO']
30 EVENT_REMOVE = alsahcontrol.event_mask_remove
33 def __init__(self, element, parent):
34 self.element = element
35 self.element.set_callback(self)
40 self.callback(self.element, EVENT_INFO)
45 def callback(self, e, mask):
46 if mask == EVENT_REMOVE:
48 elif (mask & EVENT_INFO) != 0:
49 info = alsahcontrol.Info(self.element)
54 elif (mask & EVENT_VALUE) != 0:
55 self.update_scales_from_ctl()
60 info = alsahcontrol.Info(self.element)
61 value = alsahcontrol.Value(self.element)
63 values = value.get_tuple(TYPE_INTEGER, info.count)
64 self.label = Gtk.Label.new(self.get_label(info))
65 self.label.set_single_line_mode(True)
66 self.parent.scales_vbox.add(self.label)
67 for i in range(info.count):
68 adj = Gtk.Adjustment(value=values[i],
69 lower=info.min, upper=info.max,
71 page_incr=(info.max-info.min+1)/8)
72 adj.connect('value-changed', self.update_ctl_from_scale, i)
73 scale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL, adjustment=adj)
74 scale.set_draw_value(False)
75 self.parent.scales_vbox.add(scale)
76 self.scales.append(scale)
77 self.adjustments.append(adj)
78 self.parent.scales_vbox.show_all()
79 self.parent.update_msg_label()
90 self.parent.update_msg_label()
92 def update_scales_from_ctl(self):
95 count = len(self.adjustments)
96 value = alsahcontrol.Value(self.element)
98 values = value.get_tuple(TYPE_INTEGER, count)
99 for i in range(count):
100 self.adjustments[i].set_value(values[i])
102 def update_ctl_from_scale(self, adj, index):
103 scale_value = adj.get_value()
104 value_to_set = int(round(adj.get_value()))
105 count = len(self.adjustments)
106 value = alsahcontrol.Value(self.element)
107 if self.parent.lock_check.get_active():
108 values = [value_to_set for i in range(count)]
111 values = value.get_array(TYPE_INTEGER, count)
112 values[index] = value_to_set
113 value.set_array(TYPE_INTEGER, values)
115 if value_to_set != scale_value:
116 adj.set_value(value_to_set)
118 def get_label(self, info):
119 pid = self.get_pid(info)
121 cmdline = self.get_pid_cmdline(pid)
125 return "PID %d" % pid
128 if name[-7:] == " Volume":
130 if name[-9:] == " Playback":
134 def get_pid(self, info):
135 card = self.parent.current_card
137 subdevice = info.subdevice
139 subdevice = info.index
140 filename = "/proc/asound/card%d/pcm%dp/sub%d/status" % (card, device, subdevice)
142 with open(filename, "r") as f:
144 if line[:9] == "owner_pid":
145 return int(line.split(':')[1].strip())
150 def get_pid_cmdline(self, pid):
152 with open("/proc/%d/cmdline" % pid, "r") as f:
156 return cmdline.replace('\x00', ' ').strip()
158 class MixerWindow(Gtk.Window):
159 card_numbers = alsacard.card_list()
168 Gtk.Window.__init__(self)
169 self.connect('destroy', lambda w: Gtk.main_quit())
170 self.set_title("Hardware Mixer Volumes")
173 vbox.set_orientation(Gtk.Orientation.VERTICAL)
179 label = Gtk.Label.new_with_mnemonic("_Sound Card: ")
182 combo = Gtk.ComboBoxText()
183 combo.set_hexpand(True)
184 for i in self.card_numbers:
185 str = "%d: %s" % (i, alsacard.card_get_name(i))
186 combo.append_text(str)
187 if len(self.card_numbers) > 0:
189 combo.connect('changed', lambda c: self.change_card(self.card_numbers[combo.get_active()]))
191 label.set_mnemonic_widget(combo)
193 self.lock_check = Gtk.CheckButton.new_with_mnemonic(label="_Lock Channels")
194 self.lock_check.set_active(True)
195 vbox.add(self.lock_check)
197 scrollwin = Gtk.ScrolledWindow()
198 scrollwin.set_policy(hscrollbar_policy=Gtk.PolicyType.NEVER, vscrollbar_policy=Gtk.PolicyType.AUTOMATIC)
199 scrollwin.set_shadow_type(Gtk.ShadowType.NONE)
200 scrollwin.set_vexpand(True)
203 self.scales_vbox = Gtk.Grid()
204 self.scales_vbox.set_orientation(Gtk.Orientation.VERTICAL)
205 scrollwin.add(self.scales_vbox)
208 label.set_single_line_mode(True)
209 line_height = max(label.get_size_request().height, 0)
211 scale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL)
212 scale.set_draw_value(False)
213 line_height += max(scale.get_size_request().height, 0)
215 # always have space for at least four sliders
216 scrollwin.set_size_request(width=-1, height=line_height*4+4)
218 # TODO: select the default card or the first card with stream controls
219 if len(self.card_numbers) > 0:
220 self.change_card(self.card_numbers[0])
221 self.update_msg_label()
225 def change_card(self, cardnum):
226 for s in self.hctl_sources:
227 GLib.source_remove(s)
228 self.hctl_sources = []
230 self.hcontrol = self.open_hcontrol_for_card(cardnum)
232 for s in self.streams:
236 self.current_card = cardnum
238 if not self.hcontrol:
239 self.update_msg_label()
242 for id in self.hcontrol.list():
243 if not self.is_stream_elem(id):
245 elem = alsahcontrol.Element(self.hcontrol, id[0])
246 info = alsahcontrol.Info(elem)
247 if not self.is_stream_info(info):
249 stream = Stream(elem, self)
250 self.streams.append(stream)
252 for fd,condition in self.hcontrol.poll_fds:
253 self.hctl_sources.append(GLib.io_add_watch(fd, 0, GLib.IOCondition(condition), self.hctl_io_callback))
255 self.update_msg_label()
257 self.scales_vbox.show_all()
259 def update_msg_label(self):
260 needs_msg = len(self.scales_vbox.get_children()) < 2
261 has_msg = self.msg_label
262 if has_msg and not needs_msg:
263 self.msg_label.destroy()
264 self.msg_label = None
266 if len(self.streams) > 0:
267 msg = "There are no open streams."
269 msg = "This card does not have stream controls."
271 self.msg_label = Gtk.Label.new(msg)
272 self.msg_label.set_vexpand(True)
273 self.scales_vbox.add(self.msg_label)
274 self.scales_vbox.show_all()
275 elif self.msg_label.get_text() != msg:
276 self.msg_label.set_text(msg)
278 def open_hcontrol_for_card(self, cardnum):
279 devname = "hw:CARD=" + str(cardnum)
281 hc = alsahcontrol.HControl(name=devname,
282 mode=alsahcontrol.open_mode['NONBLOCK'])
284 # TODO: alsa error msg
285 dlg = Gtk.MessageDialog(self,
286 Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
287 Gtk.MessageType.ERROR, Gtk.ButtonsType.OK,
288 "Cannot open sound card control device.")
294 def is_stream_elem(self, id):
295 return ((id[1] == INTF_PCM and
296 id[4] in ("PCM Playback Volume", "EMU10K1 PCM Volume")) or
297 (id[1] == INTF_MIXER and
298 id[4] == "VIA DXS Playback Volume"))
300 def is_stream_info(self, info):
301 return info.is_readable and info.is_writable and info.type == TYPE_INTEGER
303 def hctl_io_callback(self, source, condition):
304 self.hcontrol.handle_events()