├── .gitignore ├── README.md ├── pavolume ├── pavolume.conf └── pulseaudio.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | 37 | # vim 38 | .*.swp 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pavolume is a simple python PulseAudio volume control for the command line. It is designed to be bound to your XF86Audio* keys so you can comfortably control PulseAudio volume. 2 | 3 | ![pavolume notifications](http://i.imgur.com/SnVxL.png) 4 | 5 | # Usage 6 | 7 | pavolume show # show volume and mute status as notification 8 | pavolume volup # increase volume 9 | pavolume voldown # decrease volume 10 | pavolume volset 50% # set volume to 50% 11 | pavolume volset 200% # boost volume to 200% 12 | pavolume muteon # mute audio output 13 | pavolume muteoff # un-mute audio output 14 | pavolume mutetoggle # toggle mute 15 | 16 | 17 | You can use the --quiet switch to not play a blip sound and the --noshow switch to not show notifications. If you want to allow volup to go over 100%, you can use the --nolimit switch: 18 | 19 | pavolume volup --nolimit # turn it to 11! 20 | 21 | Pressing the volume up/down keys normally will increase/decrease volume, if you hold shift, you can increase the volume over 100%. 22 | 23 | # Dependencies 24 | 25 | * pacmd 26 | * Python 3 with packages: 27 | * docopt 28 | * pygobject 29 | 30 | # Installation 31 | Clone the git repo somewhere and put pavolume on your $PATH. The pavolume.conf can go in one of these places: 32 | 33 | * The same directory as pavolume 34 | * $XDG_CONFIG_HOME/pavolume/pavolume.conf, i.e. $HOME/.config/pavolume/pavolume.conf 35 | * /etc/pavolume/pavolume.conf 36 | 37 | # Configuration 38 | 39 | To see running sinks you can use pactl list short sinks and copy sink from there. Otherwise, pavolume will use the first sink registered in your system. 40 | 41 | There is an option to specify minimum and maximum volumes and steps for one command. 42 | 43 | You can also point to a valid blip sound file (it uses the Ubuntu message sound by default). 44 | 45 | ## Using pavolume with [Awesome](http://awesome.naquadah.org/) and [Qtile](http://www.qtile.org/) 46 | 47 | * If you are using [Awesome](http://awesome.naquadah.org/), you can use the following key bindings to control pavolume: 48 | 49 | ``` 50 | awful.key({ }, "XF86AudioRaiseVolume", function() awful.util.spawn("pavolume volup") end), 51 | awful.key({"Shift"}, "XF86AudioRaiseVolume", function() awful.util.spawn("pavolume volup --nolimit") end), 52 | awful.key({ }, "XF86AudioLowerVolume", function() awful.util.spawn("pavolume voldown") end), 53 | awful.key({ }, "XF86AudioMute", function() awful.util.spawn("pavolume mutetoggle") end), 54 | ``` 55 | 56 | * If you are using [Qtile](http://www.qtile.org/), you can use the following key bindings to control pavolume: 57 | 58 | ``` 59 | Key([ ], "XF86AudioRaiseVolume", lazy.spawn("pavolume volup")), 60 | Key(["Shift"], "XF86AudioRaiseVolume", lazy.spawn("pavolume volup --nolimit")), 61 | Key([ ], "XF86AudioLowerVolume", lazy.spawn("pavolume voldown")), 62 | Key([ ], "XF86AudioMute", lazy.spawn("pavolume mutetoggle")), 63 | ``` 64 | 65 | ## Troubleshooting 66 | 67 | If these bindings don't work for some reason, try calling the commands from the command line directly. If this works, then check whether the pavolume script is also visible from awesome's $PATH. If its still not working, you can insert full path to your pavolume folder and pavolume file e.g. /home/user/.config/pavolume/pavolume. 68 | 69 | If it always showing 100% despite changing volume, you should change sink. 70 | 71 | # Questions, Comments, Code 72 | If you have questions or comments, drop me a mail on GitHub! Code contributions are always welcome, too! 73 | -------------------------------------------------------------------------------- /pavolume: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """pavolume - Control PulseAudio Volume from the commandline 3 | 4 | Usage: 5 | pavolume show 6 | pavolume volup [--nolimit] [--nounmute] [--noshow] [--quiet] 7 | pavolume volup [--nolimit] [--nounmute] [--noshow] [--quiet] 8 | pavolume voldown [--noshow] [--quiet] 9 | pavolume voldown [--noshow] [--quiet] 10 | pavolume volset [--nounmute] [--noshow] [--quiet] 11 | pavolume muteon [--noshow] [--quiet] 12 | pavolume muteoff [--noshow] [--quiet] 13 | pavolume mutetoggle [--noshow] [--quiet] 14 | pavolume -h | --help 15 | pavolume --version 16 | 17 | Options: 18 | --h --help Show this help. 19 | --version Show program version. 20 | --quiet Don't play blip sound 21 | --noshow Don't show notifications 22 | --nolimit Allow increasing the volume over 100% 23 | --nounmute Disable the default behavior of un-muting when increasing the volume 24 | 25 | """ 26 | 27 | from pulseaudio import PulseAudio 28 | from docopt import docopt 29 | from configparser import ConfigParser 30 | from gi.repository import Notify 31 | import os 32 | import math 33 | import subprocess 34 | 35 | square_full = "\u25a0" 36 | square_empty = "\u25a1" 37 | square_cross = "\u25a8" 38 | 39 | def progress_bar(v_min, v_max, v_current, increment, full_char=square_full, empty_char=square_empty): 40 | """Render a progress bar out of characters""" 41 | v_range = (v_max - v_min) 42 | filled = math.floor((v_current - v_min) / v_range * increment) 43 | empty = increment - filled 44 | 45 | overfilled = 0 46 | if empty < 0: 47 | empty = 0 48 | overfilled = filled - increment 49 | filled = increment 50 | 51 | if overfilled: 52 | return (full_char * filled) + "|" + (full_char * overfilled) 53 | else: 54 | return (full_char * filled) + (empty_char * empty) 55 | 56 | 57 | scriptdir = os.path.dirname(os.path.realpath(__file__)) 58 | 59 | # read config 60 | config = ConfigParser() 61 | config.read([ 62 | os.path.join(scriptdir, "pavolume.conf"), 63 | os.path.expanduser("$XDG_CONFIG_HOME/pavolume/pavolume.conf"), 64 | os.path.expanduser("$HOME/.config/pavolume/pavolume.conf"), 65 | "/etc/pavolume/pavolume.conf" 66 | ]) 67 | 68 | sink = config.get("Sink", "default_sink") 69 | if sink == "None": sink = None 70 | volume_max = int(config.get("Sink", "volume_max")) 71 | volume_min = int(config.get("Sink", "volume_min")) 72 | volume_increment = int(config.get("Sink", "volume_increment")) 73 | blip_sound = config.get("Sounds", "blip") 74 | 75 | Notify.init("pavolume") 76 | 77 | def show(): 78 | percent = (pa.get_volume(sink) - volume_min) / (volume_max - volume_min) * 100 79 | 80 | if pa.get_mute(sink): 81 | title = "Muted %2.0f%%" % percent 82 | pb = progress_bar(volume_min, volume_max, pa.get_volume(sink), volume_increment, full_char=square_cross) 83 | else: 84 | title = "Volume %2.0f%%" % percent 85 | pb = progress_bar(volume_min, volume_max, pa.get_volume(sink), volume_increment) 86 | 87 | Notify.Notification.new(title, pb, "dialog-information").show() 88 | 89 | def volume_set(new_volume, noshow=False, quiet=False): 90 | pa.set_volume(int(new_volume), sink) 91 | if not noshow: show() 92 | if not quiet: blip() 93 | 94 | def volume_mod(increments, noshow=False, nolimit=False, quiet=False): 95 | volume = pa.get_volume(sink) 96 | volume = volume + increments * ((volume_max - volume_min) / volume_increment) 97 | 98 | if volume < volume_min: volume = volume_min 99 | if increments > 0 and volume > volume_max and not nolimit: volume = volume_max 100 | 101 | volume_set(volume, noshow, quiet) 102 | 103 | def volume_inc(increments, noshow=False, nolimit=False, quiet=False): 104 | volume = pa.get_volume(sink) 105 | volume = (volume + (volume_max - volume_min) * (increments / 100)) 106 | 107 | if volume < volume_min: volume = volume_min 108 | if increments > 0 and volume > volume_max and not nolimit: volume = volume_max 109 | 110 | volume_set(volume, noshow, quiet) 111 | 112 | def blip(): 113 | subprocess.Popen(["paplay", blip_sound]) 114 | 115 | 116 | 117 | if __name__ == "__main__": 118 | 119 | args = docopt(__doc__, version="pavolume 0.1") 120 | 121 | pa = PulseAudio() 122 | pa.update() 123 | 124 | if args['show']: 125 | show() 126 | 127 | elif args['volup']: 128 | if not args['--nounmute']: pa.set_mute(False, sink) 129 | if args['']: 130 | volume = args[''] 131 | if '%' in volume: 132 | volume = int(volume[:volume.find("%")]) 133 | else: 134 | volume = int(volume) 135 | volume_inc(volume, args['--noshow'], args['--nolimit'], args['--quiet']) 136 | else: 137 | volume_mod(1, args['--noshow'], args['--nolimit'], args['--quiet']) 138 | 139 | elif args['voldown']: 140 | if args['']: 141 | volume = args[''] 142 | if '%' in volume: 143 | volume = int(volume[:volume.find("%")]) 144 | else: 145 | volume = int(volume) 146 | volume_inc(-volume, args['--noshow'], args['--nolimit'], args['--quiet']) 147 | else: 148 | volume_mod(-1, args['--noshow'], args['--nolimit'], args['--quiet']) 149 | 150 | elif args['volset']: 151 | volume = args[''] 152 | if '%' in volume: 153 | volume = int(volume[:volume.find("%")]) 154 | volume = int(volume_min + (volume_max - volume_min) * (volume / 100)) 155 | else: 156 | volume = int(volume) 157 | 158 | if not args['--nounmute']: pa.set_mute(False, sink) 159 | volume_set(volume, args['--noshow'], args['--quiet']) 160 | 161 | elif args['muteon']: 162 | pa.set_mute(True, sink) 163 | if not args['--noshow']: show() 164 | 165 | elif args['muteoff']: 166 | pa.set_mute(False, sink) 167 | if not args['--noshow']: show() 168 | if not args['--quiet']: blip() 169 | 170 | elif args['mutetoggle']: 171 | pa.set_mute(not pa.get_mute(sink), sink) 172 | if not args['--noshow']: show() 173 | if not args['--quiet']: blip() 174 | 175 | #vim: set filetype=python 176 | -------------------------------------------------------------------------------- /pavolume.conf: -------------------------------------------------------------------------------- 1 | [Sink] 2 | ## Specify the name of the default sink to use or None to use the first sink 3 | default_sink = None 4 | 5 | ## Specify the maximum and minimum volumes for the sink, plus in how many steps we should increment volume. 6 | ## Please make sure that (volume_max - volume_min) will be divisible by volume_steps, for best results. 7 | volume_max = 65530 8 | volume_min = 0 9 | volume_increment = 10 10 | 11 | [Sounds] 12 | ## Blip sound for showing the current volume 13 | blip = /usr/share/sounds/ubuntu/stereo/message.ogg 14 | -------------------------------------------------------------------------------- /pulseaudio.py: -------------------------------------------------------------------------------- 1 | """Query PulseAudio status using pacmd commandline tool""" 2 | 3 | import subprocess 4 | import re 5 | import collections 6 | 7 | class PulseAudio(object): 8 | 9 | volume_re = re.compile('^set-sink-volume ([^ ]+) (.*)') 10 | mute_re = re.compile('^set-sink-mute ([^ ]+) ((?:yes)|(?:no))') 11 | 12 | def __init__(self): 13 | self._mute = collections.OrderedDict() 14 | self._volume = collections.OrderedDict() 15 | 16 | def update(self): 17 | proc = subprocess.Popen(['pacmd','dump'], stdout=subprocess.PIPE) 18 | 19 | for line in proc.stdout: 20 | line = line.decode("utf-8") 21 | volume_match = PulseAudio.volume_re.match(line) 22 | mute_match = PulseAudio.mute_re.match(line) 23 | 24 | if volume_match: 25 | self._volume[volume_match.group(1)] = int(volume_match.group(2),16) 26 | elif mute_match: 27 | self._mute[mute_match.group(1)] = mute_match.group(2).lower() == "yes" 28 | 29 | 30 | def get_mute(self, sink=None): 31 | if not sink: 32 | sink = list(self._mute.keys())[0] 33 | 34 | return self._mute[sink] 35 | 36 | def get_volume(self, sink=None): 37 | if not sink: 38 | sink = list(self._volume.keys())[0] 39 | 40 | return self._volume[sink] 41 | 42 | def set_mute(self, mute, sink=None): 43 | if not sink: 44 | sink = list(self._mute.keys())[0] 45 | 46 | subprocess.Popen(['pacmd', 'set-sink-mute', sink, 'yes' if mute else 'no'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 47 | self._mute[sink] = mute 48 | 49 | def set_volume(self, volume, sink=None): 50 | if not sink: 51 | sink = list(self._volume.keys())[0] 52 | 53 | subprocess.Popen(['pacmd', 'set-sink-volume', sink, hex(volume)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 54 | self._volume[sink] = volume 55 | 56 | if __name__ == "__main__": 57 | pa = PulseAudio() 58 | pa.update() 59 | print(pa.get_mute(), pa.get_volume()) 60 | 61 | --------------------------------------------------------------------------------