#!/usr/bin/env python3 '''Manages PulseAudio volume for a sink (output) by percentage Run by Sway on XF86Audio{Raise,Lower}Volume''' import argparse import pulsectl from pydbus import SessionBus def get_pulse(): '''Returns a Pulse object for inspection/modification''' return pulsectl.Pulse('volume-changer') def get_sink(pulse, sink_name=None): '''Get / return a PulseAudio sink (output) object by name. Used to query and set the volume. If `sink_name` is omitted, return *default* Args: pulse (Pulse): The Pulse object to query, see `get_pulse()` sink_name (str, optional): The name of the PulseAudio sink to look for. If omitted the default sink is assumed Returns: pulsectl.PulseSinkInfo''' if sink_name is None: return pulse.get_sink_by_name(pulse.server_info().default_sink_name) return pulse.get_sink_by_name(sink_name) def get_volume_rounded(pulse, sink): '''Return the volume of the provided sink Returned as a rounded int averaged across channels, assumed linked''' return round(pulse.volume_get_all_chans(sink) * 100) def set_volume(pulse, sink, adjustment): '''Changes the PulseAudio `sink` volume by the given `adjustment` `sink` should be a pactl object; see `get_sink` `adjustment` should be a float, ie 0.01 for *raising* 1% Invert (* -1) to lower''' pulse.volume_change_all_chans(sink, adjustment) # Create argument parser parser = argparse.ArgumentParser(description='Change audio volume.') parser.add_argument('direction', choices=['raise', 'lower'], help='The direction to change the volume.') parser.add_argument('percentage', nargs='?', default=1, type=int, help='The percentage to change the volume.') parser.add_argument('--sink', default=None, help='The PulseAudio sink (name) to manage.') # Parse arguments args = parser.parse_args() # Calculate the volume change as a float, inverse it if *lowering* # used as a multiplier change = args.percentage / 100 if args.direction == 'lower': change = change * -1 # construct empty dict for JSON output/data # interesting info is appended later data = {"sink": "", "change": "", "start_vol": "", "new_vol": ""} # connect to the notification bus notifications = SessionBus().get('.Notifications') # get pulse / connect try: with get_pulse() as _p: # query the default sink sink_def = get_sink(pulse=_p) # get the starting vol start_vol = get_volume_rounded(pulse=_p, sink=sink_def) # change the volume set_volume(pulse=_p, sink=sink_def, adjustment=change) # query the volume again new_volume = get_volume_rounded(pulse=_p, sink=sink_def) # construct data dict for CLI output/reference data['sink'] = sink_def.name data['change'] = change data['start_vol'] = start_vol data['new_vol'] = new_volume # Create a desktop notification notification_id = notifications.Notify( 'volume-changer', 0, 'dialog-information', 'Volume Change', f"Now {data['new_vol']}%, was {data['start_vol']}%", [], {}, 1000) except pulsectl.PulseError as e: data['sink'] = None data['change'] = 'Impossible, exception: {e}' # notify that we couldn't work with pulseaudio/compatible daemon notification_id = notifications.Notify( 'volume-changer', 0, 'dialog-error', 'Volume Change', f"Exception: {e}", [], {}, 1000) print(data, flush=True)