├── .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 | 
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 |
--------------------------------------------------------------------------------