Jan 9, 2023 Tags: howto, music
This is just a brief followup to my November post, in which I used a Raspberry Pi and HiFiBerryOS to rejuvenate an old sound system I inherited.
While trying to get AirPlay working on Linux (more on that in a moment) I realized that I had accumulated a (small) variety of PulseAudio tweaks over the years, and figured I’d bundle them all together in a post to share. Hopefully they save you (dear reader) some time1 and stress.
Everything below goes in your default PulseAudio configuration file which,
for me, was at /etc/pulse/default.pa
. They should all take effect
with systemctl --user restart pulseaudio.service
(or your equivalent
service management tool).
This didn’t ultimately work for me (see directly below), but it’ll probably work for 95% of people:
1
load-module module-raop-discover
(you’ll need this module installed, which for me was present in the pulseaudio-module-raop
package in Ubuntu’s index).
With PulseAudio restarted, each of your AirPlay devices should be automatically discovered (including, if I understand correctly, in real time as they join your network) and available as audio sinks.
The above worked too well for me: not only did it discover my HiFiBerry, but it discovered it twice: once over IPv4, and again over IPv6. This is a known issue.
On top of that, it discovered every other AirPlay-aware device on my network, including my laptop, TV, &c. Each of these also received two unique sinks, which would then be ordered inconsistently in the applet I use (pasystray) to manage my inputs and outputs.
I found this very annoying, especially since I couldn’t figure out how to change each sink’s assigned name: I had to visually scan a list of IP-address-containing names to find the correct sink each time, and would regularly click the wrong one.
As it turns out, like the other *-discover
plugins in Pulse,
module-raop-discover
is only responsible for the discovery logic,
and delegates all actual AirPlay sink management to a separate module,
one that gets loaded once per discovered sink.
That module is module-raop-sink
, and can be configured entirely separately
(with module-raop-discover
removed completely, to prevent the sink spam):
1
load-module module-raop-sink server=ADDRESS:PORT protocol=PROTO codec=CODEC sink_name=NAME
…where ADDRESS:PORT
is the IP address and port for your AirPlay device,
PROTO
is either TCP
or UDP
depending on what your device speaks,
CODEC
is PCM
or ALAC
depending on what your device supports,
and sink_name
is any name you choose to give your device2.
Thus, for my particular HiFiBerry, I needed the following (with some IP octets censored):
1
load-module module-raop-sink server=192.168.XX.XX:7000 protocol=UDP codec=ALAC sink_name=hifi
(If you’re using a HiFiBerry, I do not recommend trying codec=PCM
— the HiFiBerry
advertises support for it over mDNS, but actually trying to stream with it just produced
terrible noise for me.)
PulseAudio’s documentation for this is really terrible: following it only
produced obscure errors on the command line and marginally more helpful syslog messages,
so I ended up having to
read through raop-sink.c
’s actual source
to figure out which parameters the module was actually expecting.
Here are some other parameters that, while not necessary for me, may be helpful to you:
autoreconnect
: This seems to control whether PulseAudio attempts to automatically
reconnect to the AirPlay device on disconnect. The default seems to be not to attempt
to reconnect, but I haven’t experienced enough instability to need to experiment with this
yet.latency_msec
: This appears to default to
2000 milliseconds.
I think this is meant to be roughly how long Pulse waits before actually sending the
“play” command to the AirPlay device, probably to give it time to buffer. Experimentally,
setting it much lower (500
) doesn’t change anything for me.encryption
: This can apparently be either none
or RSA
; the meaning of these
is presumably defined more precisely by AirPlay itself.password
: This is presumably the password used to connect to the AirPlay device,
if it’s password protected. There’s also a username
setting commented out in the code.name
: This seems to behave similarly to sink_name
, except that it ends up being formatted
as raop_client.${name}
. When I tried to set this, I couldn’t get the sink to play correctly
(even though the logs indicated that it was connected to the HiFiBerry just fine). YMMV.sink_properties
: This seems to be some kind of nested configuration format, that may
just override the previous settings. Why does this exist?Out of the box, PulseAudio (on Ubuntu) tries to be “clever” about your default
source and sink. For me, that cleverness would almost never be correct:
I use a USB DAC for my headphones, and PulseAudio would regularly default
to one of my monitors’ audio interfaces (which aren’t actually connected),
presumably based on them appearing to udev
or something similar first.
Similarly, I have both a USB microphone and a microphone built into my webcam, and I almost never want the latter. Despite that, PulseAudio regularly defaults to it.
Fixing these required just two additional directives:
1
2
set-default-sink alsa_output.LONG_SINK_NAME_HERE
set-default-source alsa_input.LONG_SOURCE_NAME_HERE
…where the source and sink names come from pactl list sources
and
pactl list sinks
, respectively. These appear to be very stable for the
same hardware, so I’ve never needed to update them.
Related to the default sources-and-sinks problem: on Ubuntu (and maybe Debian),
PulseAudio has been
“helpfully”
configured with module-switch-on-connect
:
1
2
3
4
### Use hot-plugged devices like Bluetooth or USB automatically (LP: #1702794)
.ifexists module-switch-on-connect.so
load-module module-switch-on-connect
.endif
What module-switch-on-connect
does is simple and, in my experience, almost always
incorrect: it immediately switches the source and/or sink to the latest hotplugged
device. In practice, this means suddenly losing your sink if your hotplugged
microphone happens to have a headphone port on it (like mine does), or having your
audio go to a monitor’s unconnected sink because of faulty wakeup handling.
To “fix” it, I just disabled it outright:
1
2
3
4
### Use hot-plugged devices like Bluetooth or USB automatically (LP: #1702794)
# .ifexists module-switch-on-connect.so
# load-module module-switch-on-connect
# .endif
Of note: module-switch-on-connect
is completely different from
module-switch-on-port-available
, which is enabled by default without
Ubuntu/Debian’s patches. module-switch-on-port-available
has much more
reasonable behavior (it only switches the active source/sink on plug/unplug
changes from things like 3.5mm jacks). I haven’t had a reason to disable
that.