add hwmixvolume
authorClemens Ladisch <clemens@ladisch.de>
Mon, 18 Jan 2010 14:37:41 +0000 (15:37 +0100)
committerClemens Ladisch <clemens@ladisch.de>
Mon, 18 Jan 2010 14:37:41 +0000 (15:37 +0100)
Add a tool to control the volume of individual streams on sound cards
that use hardware mixing.

Signed-off-by: Clemens Ladisch <clemens@ladisch.de>

.gitignore
Makefile
hwmixvolume/Makefile.am [new file with mode: 0644]
hwmixvolume/README [new file with mode: 0644]
hwmixvolume/configure.ac [new file with mode: 0644]
hwmixvolume/gitcompile [new file with mode: 0644]
hwmixvolume/hwmixvolume [new file with mode: 0644]

index 98a34f3..cdaa897 100644 (file)
@@ -53,6 +53,8 @@ hdspmixer/src/hdspmixer
 hdspmixer/configure
 hdsploader/Makefile
 hdsploader/configure
+hwmixvolume/Makefile
+hwmixvolume/configure
 ld10k1/Makefile
 ld10k1/compile
 ld10k1/ld10k1d
index 3668cd4..f237653 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,8 @@ VERSION = 1.0.22
 TOP = .
 SUBDIRS = ac3dec as10k1 envy24control hdsploader hdspconf hdspmixer \
          mixartloader pcxhrloader rmedigicontrol sb16_csp seq sscape_ctl \
-         us428control usx2yloader vxloader echomixer ld10k1 qlo10k1
+         us428control usx2yloader vxloader echomixer ld10k1 qlo10k1 \
+         hwmixvolume
 
 all:
        @for i in $(SUBDIRS); do \
diff --git a/hwmixvolume/Makefile.am b/hwmixvolume/Makefile.am
new file mode 100644 (file)
index 0000000..abc7996
--- /dev/null
@@ -0,0 +1,11 @@
+# # Process this file with automake to procude Makefile.in.
+bin_SCRIPTS = hwmixvolume
+#man_MANS =
+EXTRA_DIST = gitcompile
+AUTOMAKE_OPTIONS = foreign
+
+alsa-dist: distdir
+       @rm -rf ../distdir/hwmixvolume
+       @mkdir -p ../distdir/hwmixvolume
+       @cp -RLpv $(distdir)/* ../distdir/hwmixvolume
+       @rm -rf $(distdir)
diff --git a/hwmixvolume/README b/hwmixvolume/README
new file mode 100644 (file)
index 0000000..96bb0b4
--- /dev/null
@@ -0,0 +1,17 @@
+hwmixvolume
+===========
+
+This tool allows you to control the volume of individual streams on sound cards
+that use hardware mixing, i.e., those based on the following chips:
+* Creative Emu10k1 (SoundBlaster Live!) (driver: snd-emu10k1)
+* VIA VT823x southbridge (driver: snd-via82xx)
+* Yamaha DS-1 (YMF-724/740/744/754) (driver: snd-ymfpci)
+
+
+This tool requires Python, pygtk, and alsa-pyton 1.0.22 or later.
+
+It is recommended to use at least Linux kernel 2.6.32 or alsa-driver 1.0.22;
+otherwise, the name of the program that is using a stream cannot be shown.
+
+
+Author: Clemens Ladisch <clemens@ladisch.de>
diff --git a/hwmixvolume/configure.ac b/hwmixvolume/configure.ac
new file mode 100644 (file)
index 0000000..ad318ad
--- /dev/null
@@ -0,0 +1,6 @@
+dnl Process this file with autoconf to produce a configure script.
+AC_INIT([hwmixvolume], [0.9])
+AM_INIT_AUTOMAKE
+AC_CONFIG_SRCDIR([hwmixvolume])
+AC_PROG_INSTALL
+AC_OUTPUT([Makefile])
diff --git a/hwmixvolume/gitcompile b/hwmixvolume/gitcompile
new file mode 100644 (file)
index 0000000..46977b7
--- /dev/null
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+aclocal $ACLOCAL_FLAGS || exit 1
+automake --foreign --add-missing || exit 1
+autoconf || exit 1
+echo "./configure $@"
+./configure $@ || exit 1
+if [ -z "$GITCOMPILE_NO_MAKE" ]; then
+  make || exit 1
+fi
diff --git a/hwmixvolume/hwmixvolume b/hwmixvolume/hwmixvolume
new file mode 100644 (file)
index 0000000..f25062b
--- /dev/null
@@ -0,0 +1,310 @@
+#!/usr/bin/env python
+
+# hwmixvolume - ALSA hardware mixer volume control applet
+# Copyright (c) 2009-2010 Clemens Ladisch
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+import gobject, gtk
+from pyalsa import alsacard, alsahcontrol
+
+INTF_PCM = alsahcontrol.interface_id['PCM']
+INTF_MIXER = alsahcontrol.interface_id['MIXER']
+TYPE_INTEGER = alsahcontrol.element_type['INTEGER']
+EVENT_VALUE = alsahcontrol.event_mask['VALUE']
+EVENT_INFO = alsahcontrol.event_mask['INFO']
+EVENT_REMOVE = alsahcontrol.event_mask_remove
+
+class Stream:
+       def __init__(self, element, parent):
+               self.element = element
+               self.element.set_callback(self)
+               self.parent = parent
+               self.label = None
+               self.scales = []
+               self.adjustments = []
+               self.callback(self.element, EVENT_INFO)
+
+       def destroy(self):
+               self.deactivate()
+
+       def callback(self, e, mask):
+               if mask == EVENT_REMOVE:
+                       self.deactivate()
+               elif (mask & EVENT_INFO) != 0:
+                       info = alsahcontrol.Info(self.element)
+                       if info.is_inactive:
+                               self.deactivate()
+                       else:
+                               self.activate()
+               elif (mask & EVENT_VALUE) != 0:
+                       self.update_scales_from_ctl()
+
+       def activate(self):
+               if self.label:
+                       return
+               info = alsahcontrol.Info(self.element)
+               value = alsahcontrol.Value(self.element)
+               value.read()
+               values = value.get_tuple(TYPE_INTEGER, info.count)
+               self.label = gtk.Label(self.get_label(info))
+               self.label.set_single_line_mode(True)
+               self.parent.scales_vbox.pack_start(self.label, expand=False)
+               for i in range(info.count):
+                       adj = gtk.Adjustment(value=values[i],
+                                       lower=info.min, upper=info.max,
+                                       step_incr=1,
+                                       page_incr=(info.max-info.min+1)/8)
+                       adj.connect('value-changed', self.update_ctl_from_scale, i)
+                       scale = gtk.HScale(adj)
+                       scale.set_draw_value(False)
+                       self.parent.scales_vbox.pack_start(scale, expand=False)
+                       self.scales.append(scale)
+                       self.adjustments.append(adj)
+               self.parent.scales_vbox.show_all()
+               self.parent.update_msg_label()
+
+       def deactivate(self):
+               if not self.label:
+                       return
+               self.label.destroy()
+               for s in self.scales:
+                       s.destroy()
+               self.label = None
+               self.scales = []
+               self.adjustments = []
+               self.parent.update_msg_label()
+
+       def update_scales_from_ctl(self):
+               if not self.label:
+                       return
+               count = len(self.adjustments)
+               value = alsahcontrol.Value(self.element)
+               value.read()
+               values = value.get_tuple(TYPE_INTEGER, count)
+               for i in range(count):
+                       self.adjustments[i].set_value(values[i])
+
+       def update_ctl_from_scale(self, adj, index):
+               scale_value = adj.get_value()
+               value_to_set = int(round(adj.get_value()))
+               count = len(self.adjustments)
+               value = alsahcontrol.Value(self.element)
+               if self.parent.lock_check.get_active():
+                       values = [value_to_set  for i in range(count)]
+               else:
+                       value.read()
+                       values = value.get_array(TYPE_INTEGER, count)
+                       values[index] = value_to_set
+               value.set_array(TYPE_INTEGER, values)
+               value.write()
+               if value_to_set != scale_value:
+                       adj.set_value(value_to_set)
+
+       def get_label(self, info):
+               pid = self.get_pid(info)
+               if pid:
+                       cmdline = self.get_pid_cmdline(pid)
+                       if cmdline:
+                               return cmdline
+                       else:
+                               return "PID %d" % pid
+               else:
+                       name = info.name
+                       if name[-7:] == " Volume":
+                               name = name[:-7]
+                       if name[-9:] == " Playback":
+                               name = name[:-9]
+                       return name
+
+       def get_pid(self, info):
+               card = self.parent.current_card
+               device = info.device
+               subdevice = info.subdevice
+               if subdevice == 0:
+                       subdevice = info.index
+               filename = "/proc/asound/card%d/pcm%dp/sub%d/status" % (card, device, subdevice)
+               try:
+                       f = open(filename, "r")
+               except IOError:
+                       return None
+               try:
+                       for line in f.readlines():
+                               if line[:9] == "owner_pid":
+                                       return int(line.split(':')[1].strip())
+               finally:
+                       f.close()
+               return None
+
+       def get_pid_cmdline(self, pid):
+               try:
+                       f = open("/proc/%d/cmdline" % pid, "r")
+               except IOError:
+                       return None
+               try:
+                       cmdline = f.read()
+               finally:
+                       f.close()
+               return cmdline.replace('\x00', ' ').strip()
+
+class MixerWindow(gtk.Window):
+       card_numbers = alsacard.card_list()
+       current_card = -1
+       hcontrol = None
+       scales_vbox = None
+       msg_label = None
+       streams = []
+       hctl_sources = []
+
+       def __init__(self):
+               gtk.Window.__init__(self)
+               self.connect('destroy', lambda w: gtk.main_quit())
+               self.set_title("Hardware Mixer Volumes")
+
+               vbox = gtk.VBox()
+               self.add(vbox)
+
+               hbox = gtk.HBox()
+               vbox.pack_start(hbox, expand=False)
+
+               label = gtk.Label("_Sound Card: ")
+               label.set_use_underline(True)
+               hbox.pack_start(label, expand=False)
+
+               combo = gtk.combo_box_new_text()
+               for i in self.card_numbers:
+                       str = "%d: %s" % (i, alsacard.card_get_name(i))
+                       combo.append_text(str)
+               if len(self.card_numbers) > 0:
+                       combo.set_active(0)
+               combo.connect('changed', lambda c: self.change_card(self.card_numbers[combo.get_active()]))
+               hbox.pack_start(combo)
+               label.set_mnemonic_widget(combo)
+
+               self.lock_check = gtk.CheckButton(label="_Lock Channels")
+               self.lock_check.set_active(True)
+               vbox.pack_start(self.lock_check, expand=False)
+
+               scrollwin = gtk.ScrolledWindow()
+               scrollwin.set_policy(hscrollbar_policy=gtk.POLICY_NEVER, vscrollbar_policy=gtk.POLICY_AUTOMATIC)
+               scrollwin.set_shadow_type(gtk.SHADOW_NONE)
+               vbox.pack_start(scrollwin)
+
+               self.scales_vbox = gtk.VBox()
+               scrollwin.add_with_viewport(self.scales_vbox)
+
+               label = gtk.Label()
+               label.set_single_line_mode(True)
+               line_height = label.size_request()[1]
+               label.destroy()
+               scale = gtk.HScale()
+               scale.set_draw_value(False)
+               line_height += scale.size_request()[1]
+               scale.destroy()
+               # always have space for at least four sliders
+               scrollwin.set_size_request(width=-1, height=line_height*4+4)
+
+               # TODO: select the default card or the first card with stream controls
+               if len(self.card_numbers) > 0:
+                       self.change_card(self.card_numbers[0])
+               self.update_msg_label()
+
+               self.show_all()
+
+       def change_card(self, cardnum):
+               for s in self.hctl_sources:
+                       gobject.source_remove(s)
+               self.hctl_sources = []
+
+               self.hcontrol = self.open_hcontrol_for_card(cardnum)
+
+               for s in self.streams:
+                       s.destroy()
+               self.streams = []
+
+               self.current_card = cardnum
+
+               if not self.hcontrol:
+                       self.update_msg_label()
+                       return
+
+               for id in self.hcontrol.list():
+                       if not self.is_stream_elem(id):
+                               continue
+                       elem = alsahcontrol.Element(self.hcontrol, id[0])
+                       info = alsahcontrol.Info(elem)
+                       if not self.is_stream_info(info):
+                               continue
+                       stream = Stream(elem, self)
+                       self.streams.append(stream)
+
+               for fd,condition in self.hcontrol.poll_fds:
+                       self.hctl_sources.append(gobject.io_add_watch(fd, condition, self.hctl_io_callback))
+
+               self.update_msg_label()
+
+               self.scales_vbox.show_all()
+
+       def update_msg_label(self):
+               needs_msg = len(self.scales_vbox.get_children()) < 2
+               has_msg = self.msg_label
+               if has_msg and not needs_msg:
+                       self.msg_label.destroy()
+                       self.msg_label = None
+               elif needs_msg:
+                       if len(self.streams) > 0:
+                               msg = "There are no open streams."
+                       else:
+                               msg = "This card does not have stream controls."
+                       if not has_msg:
+                               self.msg_label = gtk.Label(msg)
+                               self.scales_vbox.pack_start(self.msg_label)
+                               self.scales_vbox.show_all()
+                       elif self.msg_label.get_text() != msg:
+                               self.msg_label.set_text(msg)
+
+       def open_hcontrol_for_card(self, cardnum):
+               devname = "hw:CARD=" + str(cardnum)
+               try:
+                       hc = alsahcontrol.HControl(name=devname,
+                                       mode=alsahcontrol.open_mode['NONBLOCK'])
+               except:
+                       # TODO: alsa error msg
+                       dlg = gtk.MessageDialog(self,
+                                       gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
+                                       gtk.MESSAGE_ERROR, gtk.BUTTONS_OK,
+                                       "Cannot open sound card control device.")
+                       dlg.run()
+                       dlg.destroy()
+                       return None
+               return hc
+
+       def is_stream_elem(self, id):
+               return ((id[1] == INTF_PCM and
+                        id[4] in ("PCM Playback Volume", "EMU10K1 PCM Volume")) or
+                       (id[1] == INTF_MIXER and
+                        id[4] == "VIA DXS Playback Volume"))
+
+       def is_stream_info(self, info):
+               return info.is_readable and info.is_writable and info.type == TYPE_INTEGER
+
+       def hctl_io_callback(self, source, condition):
+               self.hcontrol.handle_events()
+               return True
+
+def main():
+       MixerWindow()
+       gtk.main()
+
+main()
+