This mini-tutorial is for Linux newbies. It shows how to automate a simple shell script in systemd's non-privileged user domain. Any desktop user can use systemd to run a script on a scheduled timer and get all the benefits of systemd's task scheduling framework, including dependencies, logging, and more.
I have two Pulseaudio outputs that I use daily. The first is the jack on my primary monitor which I feed into a desktop amplifier and surround sound speaker set. This is what I use during the day for normal use.
At night, I prefer to use the headphones plugged into my motherboard to keep the noise level down. I also do some gaming, so I'd prefer to make the headphones the default output after 7pm.
First we'll take a look at some bash scripts that I use to switch between the two sinks. After that, I'll show two systemd user files: a service and a timer. These will be configured to call the script in the background automatically.
Pulseaudio sinks have names that are long and automatically-generated from an ALSA backend driver. When you select a sink, you need to specify this name or specify the sink index that is generated by Pulseaudio.
Since the selectors are both generated, it isn't a good idea to put these names explicitly into a script. The underlying software could easily change and it would break the script.
We want to find some attribute of the sink that is not likely to change with software updates and find the sink's index based on that attribute. Then we can change the default sink using the index once it is found. The idea here is that when automating, you want to identify your software dependencies so that you don't end up with a solution that silently breaks later on.
Try the command pacmd list-sinks and gaze on in awe at the plethora of attributes available to each sink. The attribute device.product_name looks like best choice since it seems to correspond to the description string that comes from firmware. I would use something like this if you can.
In my case, my motherboard provides two different sinks for digital and analog output that have the same device.product_name. So, I'll take a risk and use device.profile.name. This is "analog-stereo" for my motherboard and headphones and "hdmi-stereo" for my monitor and surround sound system.
Choose the attribute that works best for your needs and hardware.
Clone/checkout the code at [1]. Open the file pa-functions and look at pa-get-index().
# Get the index of a pulseaudio sink using a regex function pa-get-index () { cmd=$1 export attr=$2 export regexp=$3 pacmd $cmd | perl -ne ' $re = $ENV{regexp}; $attr = $ENV{attr}; if (/index: (\d+)/) { $index = $1; } elsif (/$attr = \"([^\"]+)\"/) { if (/$re/) { print "$index\n" } } ' }
For bash newbies: a bash function basically works like a mini-executable. You give it a name and thereafter you can just call it like you would run an executable. It takes parameters just like an executable takes parameters. It doesn't return anything, but its output is sent to stdout.
To capture its output to a variable, you just use the $(...) construct:
$(pa-get-index list-sinks "attribute.name" "pattern")
In this case, the output is the found index that preceded the descriptor pattern in the output.
I like to use perl to capture/parse text out of command output like this. We can't simply use grep to get the information because it's grouped into sections and so an isolated line that reads device.profile.name = "..." could correspond to any device. Perl's strength here is that we can use variables to capture context of the output like the "current device index". When we actually arrive at the attribute we want, the context variable will contain the index we want.
The -ne option to the perl interpreter says, "put a while loop around my script so that $_ always refers to the variable describing the current line of text". Now we can match on regexps that detect the index and the attribute we're selecting on.
The actual script pasink-alternator.sh just invokes this function to get the index corresponding to the attribute and regex match that's being asked for, then makes the call to pacmd to set the default sink. Simple:
#!/bin/bash source /home/joya/bin/pulseaudio/pa-functions # Get time of day hour=$(date "+%H") if [[ hour -gt 10 && hour -lt 19 ]]; then device_pattern="hdmi-stereo" else device_pattern="analog-stereo" fi # Get the index of the sink with this attribute and value pattern. index=$(pa-get-index list-sinks "device.profile.name" "$device_pattern") # Set the default sink to the index we found. pacmd set-default-sink $index
There's not much else going on here. Now let's take a look at how to hook this into systemd.
It's a little-known feature that you can invoke systemd as a non-root user. It's so handy and has become my defacto replacement for the old crontab mechanism.
First, let's take a look at pasink-alternator.service. The important part is that this is a Type=oneshot which means that the "service" is really just something that runs once and exits. I use RemainAfterExit=no with this service so that after running, the service goes into a deactivating followed by a dead state. We need this otherwise the service will "look active" each time the timer wants to run it, and it won't bother if the service is already started. It's also important to set the WorkingDirectory= as our bash script will source functions from outside of itself and will use a relative path to do so.
[Unit] Description=Alternating the default audio sink between main speakers during the day and the headphones at night. Requires=pulseaudio.service After=pulseaudio.service [Install] WantedBy=multi-user.target [Service] Type=oneshot # Service is 'active' after after process exits RemainAfterExit=yes ExecStart=/bin/bash /home/joya/bin/pulseaudio/pasink-alternator.sh WorkingDirectory=/home/joya/bin/pulseaudio
Finally, notice that when invoking a bash script like a service, you can't use the normal hash-bang (#!/bin/bash) inside an executable script and run it directly with systemd's ExecStart directive. You need to tell it to invoke the interpreter with ExecStart=/bin/bash <script name>.
Also important is the WantedBy=multi-user.target. This just tells systemd where in its dependency graph framework the service will need to be activated. What you need to know here is that multi-user.target is a target and can be thought of as a system-wide mode that systemd has put your computer in. Another possible value might have been graphical.target which would mean that this unit would only activate when your system's graphical desktop is started up and running.
Now look at the smaller systemd file is the pasink-alternator.timer file that defines the schedule for running the script.
[Unit] Description=pasink-alternator timer for 10am and 7pm. [Install] WantedBy=multi-user.target [Timer] OnCalendar=*-*-* 10:00:00 OnCalendar=*-*-* 19:00:00 Unit=pasink-alternator.service
This one is fairly self-explanatory. It creates a timer that will fire the above pasink-alternator.service The two OnCalendar specifications make sure that the timer is triggered at 10am and 7pm.
It's worthwhile here to look at man systemd.timer and man systemd.time and in particular the OnCalendar attribute in the latter. In my case, my bash script is using one sink between the hours of 10am-7pm and the other sink the rest of the time. If I schedule this script to be run at 10:00 and at 19:00, then it will catch these "edges" and do its thing at the right times.
The two systemd files should go under your home directory at .config/systemd/user. Don't forget to change ExecStart to point at the chosen location of the bash scripts.
Once they're there, just run:
$ systemctl --user enable pasink-alternator.service $ systemctl --user enable pasink-alternator.timer
...and both service and timer should be active. Finally, you can check their status and details with:
$ systemctl --user status pasink-alternator.service $ systemctl --user status pasink-alternator.timer
If this was useful to you, buy me a cup of tea or a slice of vegan pizza via PayPal: