From 3496fa622a4b2688baeb2813980bae9a082a5884 Mon Sep 17 00:00:00 2001 From: Tobias Witek Date: Sun, 23 Feb 2020 13:45:42 +0100 Subject: [PATCH] [modules] Add preliminary version of pulseaudio module Add pasink/pasource and __pulseaudio (internal module, not intended for direct use). --- modules/__pulseaudio.py | 172 ++++++++++++++++++++++++++++++++++++++++ modules/pasink.py | 7 ++ modules/pasource.py | 7 ++ 3 files changed, 186 insertions(+) create mode 100644 modules/__pulseaudio.py create mode 100644 modules/pasink.py create mode 100644 modules/pasource.py diff --git a/modules/__pulseaudio.py b/modules/__pulseaudio.py new file mode 100644 index 0000000..9d8eadd --- /dev/null +++ b/modules/__pulseaudio.py @@ -0,0 +1,172 @@ +# pylint: disable=C0111,R0903 + +'''Displays volume and mute status and controls for PulseAudio devices. Use wheel up and down to change volume, left click mutes, right click opens pavucontrol. + +Aliases: pasink (use this to control output instead of input), pasource + +Parameters: + * pulseaudio.autostart: If set to 'true' (default is 'false'), automatically starts the pulseaudio daemon if it is not running + * pulseaudio.percent_change: How much to change volume by when scrolling on the module (default is 2%) + * pulseaudio.limit: Upper limit for setting the volume (default is 0%, which means 'no limit') + Note: If the left and right channels have different volumes, the limit might not be reached exactly. + * pulseaudio.showbars: 1 for showing volume bars, requires --markup=pango; + 0 for not showing volume bars (default) + +Requires the following executable: + * pulseaudio + * pactl + * pavucontrol +''' + +import re +import logging + +import core.module +import core.widget +import core.input + +import util.format +import util.cli + +class Module(core.module.Module): + def __init__(self, config, channel): + super().__init__(config, core.widget.Widget(self.volume)) + + if util.format.asbool(self.parameter('autostart', False)): + util.cli.execute('pulseaudio --start', ignore_errors=True) + + self._change = util.format.asint(self.parameter('percent_change', '2%').strip('%'), 0, 100) + self._limit = util.format.asint(self.parameter('limit', '0%').strip('%'), 0) + + self._left = 0 + self._right = 0 + self._mono = 0 + self._mute = False + self._failed = False + self._channel = channel + #self._showbars = core.fmt.bool(self.parameter('showbars', 0)) + + self._patterns = [ + {'expr': 'Name:', 'callback': (lambda line: False)}, + {'expr': 'Mute:', 'callback': (lambda line: self.mute(False if ' no' in line.lower() else True))}, + {'expr': 'Volume:', 'callback': self.getvolume}, + ] + + core.input.register(self, button=core.input.RIGHT_MOUSE, cmd='pavucontrol') + + events = [ + {'type': 'mute', 'action': self.toggle, 'button': core.input.LEFT_MOUSE}, + {'type': 'volume', 'action': self.increase_volume, 'button': core.input.WHEEL_UP}, + {'type': 'volume', 'action': self.decrease_volume, 'button': core.input.WHEEL_DOWN}, + ] + + for event in events: + core.input.register(self, button=event['button'], cmd=event['action']) + + def set_volume(self, amount): + util.cli.execute('pactl set-{}-{} @DEFAULT_{}@ {}'.format( + self._channel, 'volume', self._channel.upper(), amount)) + + def increase_volume(self, event): + if self._limit > 0: # we need to check the limit + left = int(self._left) + right = int(self._right) + if left + self._change >= self._limit or right + self._change >= self._limit: + if left == right: + # easy case, just set to limit + self.set_volume('{}%'.format(self._limit)) + return + else: + # don't adjust anymore, since i don't know how to update only one channel + return + + self.set_volume('+{}%'.format(self._change)) + + def decrease_volume(self, event): + self.set_volume('-{}%'.format(self._change)) + + def toggle(self, event): + util.cli.execute('pactl set-{}-mute @DEFAULT_{}@ toggle'.format( + self._channel, self._channel.upper())) + + def mute(self, value): + self._mute = value + + def getvolume(self, line): + if 'mono' in line: + m = re.search(r'mono:.*\s*\/\s*(\d+)%', line) + if m: + self._mono = m.group(1) + else: + m = re.search(r'left:.*\s*\/\s*(\d+)%.*right:.*\s*\/\s*(\d+)%', line) + if m: + self._left = m.group(1) + self._right = m.group(2) + + def _default_device(self): + output = util.cli.execute('pactl info') + pattern = 'Default {}: '.format('Sink' if self._channel == 'sink' else 'Source') + for line in output.split('\n'): + if line.startswith(pattern): + return line.replace(pattern, '') + logging.error('no pulseaudio device found') + return 'n/a' + + def volume(self): + if self._failed == True: + return 'n/a' + if int(self._mono) > 0: + vol = '{}%'.format(self._mono) + #if self._showbars: + # vol = '{} {}'.format( + # vol, bumblebee.output.hbar(float(self._mono))) + return vol + elif self._left == self._right: + vol = '{}%'.format(self._left) + #if self._showbars: + # vol = '{} {}'.format( + # vol, bumblebee.output.hbar(float(self._left))) + return vol + else: + #if self._showbars: + # vol = '{} {}{}'.format( + # vol, + # bumblebee.output.hbar(float(self._left)), + # bumblebee.output.hbar(float(self._right))) + vol = '{}%/{}%'.format(self._left, self._right) + return vol + + def update(self): + try: + self._failed = False + channel = 'sinks' if self._channel == 'sink' else 'sources' + device = self._default_device() + + result = util.cli.execute('pactl list {}'.format(channel)) + found = False + + for line in result.split('\n'): + if 'Name: {}'.format(device) in line: + found = True + continue + if found is False: + continue + for pattern in self._patterns: + if not pattern['expr'] in line: + continue + if pattern['callback'](line) is False and found == True: + return + except Exception as e: + self._failed = True + logging.exception(e) + if util.format.asbool(self.parameter('autostart', False)): + util.cli.execute('pulseaudio --start', ignore_errors=True) + + def state(self, widget): + if self._mute: + return ['warning', 'muted'] + if int(self._left) > int(100): + return ['critical', 'unmuted'] + return ['unmuted'] + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/modules/pasink.py b/modules/pasink.py new file mode 100644 index 0000000..423bcb9 --- /dev/null +++ b/modules/pasink.py @@ -0,0 +1,7 @@ +from .__pulseaudio import Module + +class Module(Module): + def __init__(self, config): + super().__init__(config, 'sink') + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/modules/pasource.py b/modules/pasource.py new file mode 100644 index 0000000..d3c9a2a --- /dev/null +++ b/modules/pasource.py @@ -0,0 +1,7 @@ +from .__pulseaudio import Module + +class Module(Module): + def __init__(self, config): + super().__init__(config, 'source') + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4