Pulseaudio: switch to headset automatically when it’s plugged in / docked

By | October 5, 2012

I recently wiped Windows 7 from my ThinkPad and replaced it with Linux (Fedora 17). One of the (few) things I missed about Windows was the fact that when I docked my laptop, it automatically switched from the laptop’s internal speakers and microphone to the USB headset plugged into my dock. Pulseaudio doesn’t seem to, nor could I seem to find any supported way to make it do so.

After some hacking around, here’s what I came up with to make it work. It’s not as generic as it could be, and I’m sure it could be better integrated into ConsoleKit or whatever, but it seems to do the job. Perhaps it will help you too.

UPDATE [2013-07-08]: Fixed to work with Fedora 19 (need to set XDG_RUNTIME_DIR environment variable for pacmd).

UPDATE [2013-02-03]: The script below was originally a shell script rather than a Perl script, and it was originally written only for USB headsets. I’ve converted it to Perl to make it more modular as well as to work around the fact that udevd for some reason felt compelled to kill the script before it was finished, and the Perl version prevents that by “daemonizing.” Also, it now supports Bluetooth as well as USB. Enjoy!

UPDATE [2013-02-16]: Apparently, changing the process group of the script is not enough to prevent udevd from killing it. You also need to change its control group. I’ve updated the script to do that as well.

UPDATE [2013-02-27]: Removed “wait” from parent process in the script so that it doesn’t cause udevd to wait too long to add the audio device to PulseAudio.

#!/usr/bin/perl

# ********************************************************************
#
# Script to automatically switch input/output to your headset when it
# is plugged in or paird or when you dock and it is plugged into the
# dock.
#
# By Jonathan Kamens <jik@kamens.us>. Share and enjoy!
#
# ********************************************************************
#
# How to use:
#
# 1. Disable "requiretty" in /etc/sudoers so sudo from udev script
#    will work.
#
# 2. Create /etc/udev/rules.d/99-headset.rules with this in it (fill
#    in vendor and product codes from lines in /var/log/messages when
#    headset is plugged in).
#
#      SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="0d8c",
#      ATTRS{idProduct}=="000c", RUN+="/lib/udev/switch_to_headset"
#
#    This (along with the example below) NEEDS TO BE ON ONE LINE in
#    the rules file; they are shown on two lines here only for
#    readability.
#
#    If you want to do this with a Bluetooth headset, it would look
#    something like this instead:
#
#      SUBSYSTEM=="input", ACTION=="add", ATTRS{name}=="00:12:3D:00:48:FD",
#      RUN+="/lib/udev/switch_to_headset"
#
#    To figure out what to put in the "name" attribute above, look for
#    a "kernel:" line in /var/log/messages shortly after you pair the
#    headset with your computer which says "input: [something] as
#    /devices/virtual/[more stuff]", and the "[something]" is the name
#    you want.
#
#    If you want to trigger off of multiple audio devices, you can put
#    multiple lines like this in 99-headset.rules.
#
# 3. Run "udevadm control --reload" to reload your rules.
#
# 4. Check /var/log/messages to make sure you didn't screw up the
#    syntax. If you did, then fix and run udevadm again until you
#    don't get any errors.
#
# 5. Run "pacmd list-sources" (input, i.e., microphones) and "pacmd
#    list-sinks" (output, i.e., speakers) as yourself, not root, and
#    look for the "name:" corresponding to the device you want to
#    activate automatically. Fill it in below in @input_devices (for
#    microphones) and @output_devices (for speakers). You can leave
#    either of these arrays empty if you don't want to automatically
#    switch to a microphone or speaker. You can specify multiple
#    (whitespace-separated) PulseAudio names to support multiple
#    devices (it's not good enough to have udev rules for each device;
#    you also need to have all of their names listed in this script).
#
# 6. Save this script as /lib/udev/switch_to_headset and make it
#    executable.
#
# 7. Unplug or unpaid your headset, wait a few seconds, plug it in or
#    pair it, and watch it get switched to automatically about five
#    seconds later! If not, look in /var/log/messages, or in the
#    recently created log file in /tmp/switch_to_headset.#.log, to
#    troubleshoot.
#
# ********************************************************************
#
# I tried to figure out how to make this generic and trigger when any
# audio device is plugged in, not just a specific audio device, but
# the udev documentation is sparse and unhelpful, so this is the best
# I was able to come up with.

use File::Basename;

$whoami = basename $0;
$logfile = "/tmp/$whoami.$$.log";

@input_devices = qw(bluez_source.00_12_3D_00_48_FD
                    bluez_source.00_12_3D_00_07_BA
                    alsa_input.usb-0d8c_C-Media_USB_Headphone_Set-00-Set.analog-mono);
@output_devices = qw(bluez_sink.00_12_3D_00_48_FD
                     bluez_sink.00_12_3D_00_07_BA
                     alsa_output.usb-0d8c_C-Media_USB_Headphone_Set-00-Set.analog-stereo);

if (! -t STDOUT) {
    open(STDOUT, '>', $logfile) or die;
    open(STDERR, '>&', STDOUT) or die;
}

# Not sure how portable this is, or if there's a better way to do it,
# but it seems to work at least on Fedora 18.

# First, use fgconsole to find out which VT is active.
if (! ($vt = `fgconsole`)) {
    die "Could not find foreground VT";
}
chomp $vt;
print "Using foreground VT $vt.\n";

# Now, figure out which X server is running on that VT.
logfile: foreach my $logfile (glob "/var/log/Xorg.*.log") {
    open(LOG, '<', $logfile) or die;
    while (<LOG>) {
	if (/using VT number \b$vt\b/o) {
	    ($server = $logfile) =~ s,/var/log/Xorg\.(.*)\.log$,:$1,;
	    close(LOG);
	    last logfile;
	}
    }
}
if (! defined $server) {
    die "Could not find X server on VT $vt";
}
print "Using X server $server.\n";

# Now figure out who is logged in on that VT.
open(WHO, '-|', 'who') or die;
while (<WHO>) {
    ($user, $display) = split;
    if ($display eq $server) {
	$user = $user;
	close(WHO);
	last;
    }
}
if (! $user) {
    die "Could not find user on display :$server";
}
print "Using user $user.\n";
my $xdg_env = '';
if (my $uid = getpwnam($user) and
    -d "/run/user/$uid") {
    $xdg_env = "XDG_RUNTIME_DIR=/run/user/$uid";
}

# Fork and change process groups so udevd won't kill us!

print "Daemonizing.\n";
if (! defined($pid = fork())) {
    die "fork: $!";
}
elsif ($pid) {
    exit;
}

if (-f '/sys/fs/cgroup/systemd/tasks') {
    print "Changing control group.\n";
    open(CGROUP, '>', '/sys/fs/cgroup/systemd/tasks') or die;
    print(CGROUP "$$\n") or die;
    close(CGROUP) or die;
}

print "Changing process group.\n";
setpgrp or die "setpgrp: $!";
print "PID: $$ Process group: ", getpgrp(), "\n";

print "Sleeping.\n";
sleep(5);

&do_pacmd('source', @input_devices);
&do_pacmd('sink', @output_devices);

sub do_pacmd {
    my($op, @names) = @_;

    if (! @names) {
	print "No $op names configured, skipping.\n";
	return;
    }

    print "Looking for $op in @names.\n";

    open(PACMD, "-|", "sudo -u $user -H $xdg_env /bin/pacmd list-${op}s") or die;
    my $got;
    while (<PACMD>) {
	if (/^\s*name:\s*<(.*)>\s*$/ and grep($_ eq $1, @names)) {
	    $got = $1;
	    close(PACMD);
	    last;
	}
    }
    if (! $got) {
	print "Found no matching $op devices.\n";
	return;
    }
    print "Setting default $op to $got.\n";
    system('sudo', '-u', $user, '-H', $xdg_env, '/bin/pacmd', "set-default-$op", $got)
	and warn "Failed to set-default-$op $got";
    print "Successfully set default $op to $got.\n";
}
Share

One thought on “Pulseaudio: switch to headset automatically when it’s plugged in / docked

  1. Sepero

    Hey thanks. I edited my script to look like this:

    whoami=$(basename $0)
    logfile=”/tmp/$whoami.$$.log”

    # There might be a “standard” way of figuring out who’s logged in on
    # the console and running a command on that user, but I’m not sure
    # what it is, and this works well enough.
    user=$(who | grep “:0:S.0” | awk ‘{print $1}’)
    if [ “$user” ]; then
    (
    sleep 1;
    sudo -u $user -H pacmd set-default-sink “alsa_output.usb-0d8c_C-Media_USB_Audio_Device-00-Device.iec958-stereo”
    sudo -u $user -H pacmd set-default-source “alsa_input.usb-0d8c_C-Media_USB_Audio_Device-00-Device.analog-mono”
    ) > “$logfile” 2>&1 &
    else
    echo “No user found” > “$logfile”
    fi

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *