Release v1.1.7
[alsa-tools.git] / hwmixvolume / hwmixvolume
1 #!/usr/bin/python2
2
3 # hwmixvolume - ALSA hardware mixer volume control applet
4 # Copyright (c) 2009-2010 Clemens Ladisch
5 #
6 # Permission to use, copy, modify, and/or distribute this software for any
7 # purpose with or without fee is hereby granted, provided that the above
8 # copyright notice and this permission notice appear in all copies.
9 #
10 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
11 # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
12 # AND FITNESS.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
13 # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
14 # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
15 # OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
16 # PERFORMANCE OF THIS SOFTWARE.
17
18 import gobject, gtk
19 from pyalsa import alsacard, alsahcontrol
20
21 INTF_PCM = alsahcontrol.interface_id['PCM']
22 INTF_MIXER = alsahcontrol.interface_id['MIXER']
23 TYPE_INTEGER = alsahcontrol.element_type['INTEGER']
24 EVENT_VALUE = alsahcontrol.event_mask['VALUE']
25 EVENT_INFO = alsahcontrol.event_mask['INFO']
26 EVENT_REMOVE = alsahcontrol.event_mask_remove
27
28 class Stream:
29         def __init__(self, element, parent):
30                 self.element = element
31                 self.element.set_callback(self)
32                 self.parent = parent
33                 self.label = None
34                 self.scales = []
35                 self.adjustments = []
36                 self.callback(self.element, EVENT_INFO)
37
38         def destroy(self):
39                 self.deactivate()
40
41         def callback(self, e, mask):
42                 if mask == EVENT_REMOVE:
43                         self.deactivate()
44                 elif (mask & EVENT_INFO) != 0:
45                         info = alsahcontrol.Info(self.element)
46                         if info.is_inactive:
47                                 self.deactivate()
48                         else:
49                                 self.activate()
50                 elif (mask & EVENT_VALUE) != 0:
51                         self.update_scales_from_ctl()
52
53         def activate(self):
54                 if self.label:
55                         return
56                 info = alsahcontrol.Info(self.element)
57                 value = alsahcontrol.Value(self.element)
58                 value.read()
59                 values = value.get_tuple(TYPE_INTEGER, info.count)
60                 self.label = gtk.Label(self.get_label(info))
61                 self.label.set_single_line_mode(True)
62                 self.parent.scales_vbox.pack_start(self.label, expand=False)
63                 for i in range(info.count):
64                         adj = gtk.Adjustment(value=values[i],
65                                         lower=info.min, upper=info.max,
66                                         step_incr=1,
67                                         page_incr=(info.max-info.min+1)/8)
68                         adj.connect('value-changed', self.update_ctl_from_scale, i)
69                         scale = gtk.HScale(adj)
70                         scale.set_draw_value(False)
71                         self.parent.scales_vbox.pack_start(scale, expand=False)
72                         self.scales.append(scale)
73                         self.adjustments.append(adj)
74                 self.parent.scales_vbox.show_all()
75                 self.parent.update_msg_label()
76
77         def deactivate(self):
78                 if not self.label:
79                         return
80                 self.label.destroy()
81                 for s in self.scales:
82                         s.destroy()
83                 self.label = None
84                 self.scales = []
85                 self.adjustments = []
86                 self.parent.update_msg_label()
87
88         def update_scales_from_ctl(self):
89                 if not self.label:
90                         return
91                 count = len(self.adjustments)
92                 value = alsahcontrol.Value(self.element)
93                 value.read()
94                 values = value.get_tuple(TYPE_INTEGER, count)
95                 for i in range(count):
96                         self.adjustments[i].set_value(values[i])
97
98         def update_ctl_from_scale(self, adj, index):
99                 scale_value = adj.get_value()
100                 value_to_set = int(round(adj.get_value()))
101                 count = len(self.adjustments)
102                 value = alsahcontrol.Value(self.element)
103                 if self.parent.lock_check.get_active():
104                         values = [value_to_set  for i in range(count)]
105                 else:
106                         value.read()
107                         values = value.get_array(TYPE_INTEGER, count)
108                         values[index] = value_to_set
109                 value.set_array(TYPE_INTEGER, values)
110                 value.write()
111                 if value_to_set != scale_value:
112                         adj.set_value(value_to_set)
113
114         def get_label(self, info):
115                 pid = self.get_pid(info)
116                 if pid:
117                         cmdline = self.get_pid_cmdline(pid)
118                         if cmdline:
119                                 return cmdline
120                         else:
121                                 return "PID %d" % pid
122                 else:
123                         name = info.name
124                         if name[-7:] == " Volume":
125                                 name = name[:-7]
126                         if name[-9:] == " Playback":
127                                 name = name[:-9]
128                         return name
129
130         def get_pid(self, info):
131                 card = self.parent.current_card
132                 device = info.device
133                 subdevice = info.subdevice
134                 if subdevice == 0:
135                         subdevice = info.index
136                 filename = "/proc/asound/card%d/pcm%dp/sub%d/status" % (card, device, subdevice)
137                 try:
138                         f = open(filename, "r")
139                 except IOError:
140                         return None
141                 try:
142                         for line in f.readlines():
143                                 if line[:9] == "owner_pid":
144                                         return int(line.split(':')[1].strip())
145                 finally:
146                         f.close()
147                 return None
148
149         def get_pid_cmdline(self, pid):
150                 try:
151                         f = open("/proc/%d/cmdline" % pid, "r")
152                 except IOError:
153                         return None
154                 try:
155                         cmdline = f.read()
156                 finally:
157                         f.close()
158                 return cmdline.replace('\x00', ' ').strip()
159
160 class MixerWindow(gtk.Window):
161         card_numbers = alsacard.card_list()
162         current_card = -1
163         hcontrol = None
164         scales_vbox = None
165         msg_label = None
166         streams = []
167         hctl_sources = []
168
169         def __init__(self):
170                 gtk.Window.__init__(self)
171                 self.connect('destroy', lambda w: gtk.main_quit())
172                 self.set_title("Hardware Mixer Volumes")
173
174                 vbox = gtk.VBox()
175                 self.add(vbox)
176
177                 hbox = gtk.HBox()
178                 vbox.pack_start(hbox, expand=False)
179
180                 label = gtk.Label("_Sound Card: ")
181                 label.set_use_underline(True)
182                 hbox.pack_start(label, expand=False)
183
184                 combo = gtk.combo_box_new_text()
185                 for i in self.card_numbers:
186                         str = "%d: %s" % (i, alsacard.card_get_name(i))
187                         combo.append_text(str)
188                 if len(self.card_numbers) > 0:
189                         combo.set_active(0)
190                 combo.connect('changed', lambda c: self.change_card(self.card_numbers[combo.get_active()]))
191                 hbox.pack_start(combo)
192                 label.set_mnemonic_widget(combo)
193
194                 self.lock_check = gtk.CheckButton(label="_Lock Channels")
195                 self.lock_check.set_active(True)
196                 vbox.pack_start(self.lock_check, expand=False)
197
198                 scrollwin = gtk.ScrolledWindow()
199                 scrollwin.set_policy(hscrollbar_policy=gtk.POLICY_NEVER, vscrollbar_policy=gtk.POLICY_AUTOMATIC)
200                 scrollwin.set_shadow_type(gtk.SHADOW_NONE)
201                 vbox.pack_start(scrollwin)
202
203                 self.scales_vbox = gtk.VBox()
204                 scrollwin.add_with_viewport(self.scales_vbox)
205
206                 label = gtk.Label()
207                 label.set_single_line_mode(True)
208                 line_height = label.size_request()[1]
209                 label.destroy()
210                 scale = gtk.HScale()
211                 scale.set_draw_value(False)
212                 line_height += scale.size_request()[1]
213                 scale.destroy()
214                 # always have space for at least four sliders
215                 scrollwin.set_size_request(width=-1, height=line_height*4+4)
216
217                 # TODO: select the default card or the first card with stream controls
218                 if len(self.card_numbers) > 0:
219                         self.change_card(self.card_numbers[0])
220                 self.update_msg_label()
221
222                 self.show_all()
223
224         def change_card(self, cardnum):
225                 for s in self.hctl_sources:
226                         gobject.source_remove(s)
227                 self.hctl_sources = []
228
229                 self.hcontrol = self.open_hcontrol_for_card(cardnum)
230
231                 for s in self.streams:
232                         s.destroy()
233                 self.streams = []
234
235                 self.current_card = cardnum
236
237                 if not self.hcontrol:
238                         self.update_msg_label()
239                         return
240
241                 for id in self.hcontrol.list():
242                         if not self.is_stream_elem(id):
243                                 continue
244                         elem = alsahcontrol.Element(self.hcontrol, id[0])
245                         info = alsahcontrol.Info(elem)
246                         if not self.is_stream_info(info):
247                                 continue
248                         stream = Stream(elem, self)
249                         self.streams.append(stream)
250
251                 for fd,condition in self.hcontrol.poll_fds:
252                         self.hctl_sources.append(gobject.io_add_watch(fd, condition, self.hctl_io_callback))
253
254                 self.update_msg_label()
255
256                 self.scales_vbox.show_all()
257
258         def update_msg_label(self):
259                 needs_msg = len(self.scales_vbox.get_children()) < 2
260                 has_msg = self.msg_label
261                 if has_msg and not needs_msg:
262                         self.msg_label.destroy()
263                         self.msg_label = None
264                 elif needs_msg:
265                         if len(self.streams) > 0:
266                                 msg = "There are no open streams."
267                         else:
268                                 msg = "This card does not have stream controls."
269                         if not has_msg:
270                                 self.msg_label = gtk.Label(msg)
271                                 self.scales_vbox.pack_start(self.msg_label)
272                                 self.scales_vbox.show_all()
273                         elif self.msg_label.get_text() != msg:
274                                 self.msg_label.set_text(msg)
275
276         def open_hcontrol_for_card(self, cardnum):
277                 devname = "hw:CARD=" + str(cardnum)
278                 try:
279                         hc = alsahcontrol.HControl(name=devname,
280                                         mode=alsahcontrol.open_mode['NONBLOCK'])
281                 except:
282                         # TODO: alsa error msg
283                         dlg = gtk.MessageDialog(self,
284                                         gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
285                                         gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
286                                         "Cannot open sound card control device.")
287                         dlg.run()
288                         dlg.destroy()
289                         return None
290                 return hc
291
292         def is_stream_elem(self, id):
293                 return ((id[1] == INTF_PCM and
294                          id[4] in ("PCM Playback Volume", "EMU10K1 PCM Volume")) or
295                         (id[1] == INTF_MIXER and
296                          id[4] == "VIA DXS Playback Volume"))
297
298         def is_stream_info(self, info):
299                 return info.is_readable and info.is_writable and info.type == TYPE_INTEGER
300
301         def hctl_io_callback(self, source, condition):
302                 self.hcontrol.handle_events()
303                 return True
304
305 def main():
306         MixerWindow()
307         gtk.main()
308
309 main()
310