Skip to content

Instantly share code, notes, and snippets.

@gadtfly
Last active June 23, 2023 17:41
Show Gist options
  • Select an option

  • Save gadtfly/a9519ca6347d9be88c3450e59adce564 to your computer and use it in GitHub Desktop.

Select an option

Save gadtfly/a9519ca6347d9be88c3450e59adce564 to your computer and use it in GitHub Desktop.
Automatically switch Linux PulseAudio output between an HDMI output and a headphone jack (apparently necessary when they are on seperate cards)
event=jack/headphone
action=/etc/acpi/headphone-plug.py %e
#!/usr/bin/python
from syslog import syslog
import subprocess
import sys
# Key is matched against 3rd word in acpid event (can be checked via `acpi_listen`)
# Value will be loosely matched against sinks' `active-port` field
config = {'plug': 'headphone',
'unplug': 'hdmi'}
syslog('ACPI Headphone Plug Switcher: received %s' % ' '.join(sys.argv))
target = config[sys.argv[3]]
syslog('Switching to %s' % target)
# Since PulseAudio is per-user and acpid scripts run as root
# We need to pose as users
users = [line.split()[0].strip() for line in subprocess.check_output('who').splitlines()]
syslog('Users logged in: %s' % users)
for user in users:
syslog('Switching for %s' % user)
try:
subprocess.check_output('sudo -u %s XDG_RUNTIME_DIR=/run/user/%s pa_change_sink %s' %
(user, subprocess.check_output(['id', '-u', user]).strip(), target),
shell=True)
except Exception as e:
syslog('ERROR %s: %s' % (type(e), str(e)))
exit(1)

In order for any of this to not reset constantly:

  • In /etc/pulse/default.pa, change the line:
    load-module module-stream-restore
    to
    load-module module-stream-restore restore_device=false

To use pa_change_sink as a standalone command:

  • Copy pa_change_sink to /usr/local/bin/pa_change_sink
  • chmod +x /usr/local/bin/pa_change_sink

To automate switching outputs on plug/unplug:

  • sudo apt install acpid
  • Copy headphone-plug to /etc/acpi/events/headphone-plug
  • Copy headphone-plug.py to /etc/acpi/headphone-plug.py
  • chmod +x /etc/acpi/headphone-plug.py
  • sudo systemctl enable acpid.service
  • sudo systemctl start acpid.service
    • (must restart acpid.service after any changes to /etc/acpi/events/headphone-plug, but restart not neccesary for changes to /etc/acpi/headphone-plug.py)
#!/usr/bin/python
# Change PulseAudio's default-sink + move all sink-inputs
# Target sink is selected by loosely matching against sinks' `active-port` field
# See `pacmd list-sinks` for `active-port` options
#
# For proper function:
# in: `/etc/pulse/default.pa`
# at the end of the line: `load-module module-stream-restore`
# add: ` restore_device=false`
# then restart PulseAudio (or reboot)
#
# Usage: pa_change_sink <~sink active port>
# Examples: pa_change_sink headphone
# pa_change_sink hdmi
from syslog import syslog
import subprocess
import sys
import re
# Utility functions for consuming `pacmd list-X` output
def pacmd(*args, **kwargs):
if kwargs.get('log'): syslog('Running pacmd %s' % ' '.join(map(str, list(args))))
result = subprocess.check_output(['pacmd'] + list(args))
if kwargs.get('log') and result: syslog('Got back %s' % result)
return result
def all_group_dicts(pattern, string, flags):
return [match.groupdict() for match in re.finditer(pattern, string, flags)]
def get_sinks():
return all_group_dicts(r'(?P<default>\*?) '
'index: (?P<index>\d+).+?'
'active port: <(?P<active_port>.+?)>',
pacmd('list-sinks'),
re.DOTALL)
def get_inputs():
return all_group_dicts(r'index: (?P<index>\d+).+?'
'sink: (?P<sink_index>\d+) <.+?'
'application.process.binary = "(?P<application>.+?)"',
pacmd('list-sink-inputs'),
re.DOTALL)
# Script-specific utility functions
def find_default_sink(sinks):
return next(sink for sink in sinks if sink['default'] == '*')
def find_sink_for_input(sinks, input):
return next(sink for sink in sinks if sink['index'] == input['sink_index'])
def refresh_input(input):
return next(_input for _input in get_inputs() if _input['index'] == input['index'])
# Script
def change_sink(target):
syslog('Changing PulseAudio sink to %s' % target)
sinks = get_sinks()
inputs = get_inputs()
target_sink = next(sink for sink in sinks if target in sink['active_port'])
pacmd('set-default-sink', target_sink['index'], log=True)
syslog('Changed default sink from %s to %s' % (
find_default_sink(sinks)['active_port'],
find_default_sink(get_sinks())['active_port']))
for input in inputs:
pacmd('move-sink-input', input['index'], target_sink['index'], log=True)
syslog('Changed sink for %s from %s to %s'% (
input['application'],
find_sink_for_input(sinks, input)['active_port'],
find_sink_for_input(sinks, refresh_input(input))['active_port']))
# Ugly but simple way to notify the user within the most appropriate UI
subprocess.check_output(['xdotool', 'key', 'XF86AudioMute'])
subprocess.check_output(['xdotool', 'key', 'XF86AudioMute'])
if __name__ == '__main__':
try:
change_sink(sys.argv[1])
except Exception as e:
print('ERROR %s: %s' % (type(e), e))
exit(1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment