The Adventures of Joshua Judson Rosen
(action man)

[ sections: VisualIDs | art | movies | everything ]

Tue, 26 Jul 2011

01:32: Whole-Home Audio with MPD + PulseAudio

I have a whole-home audio system that I cobbled together out of spare PCs and low-cost, low-power plug-computers running a mostly-stock Debian 6.0 with MPD and PulseAudio, and which works better than any of the prefabricated solutions that I could find. Not only is it better, but it deployed cheaper, and it was actually quicker to build it than it would have been to finish evaluating the canned solutions and buy one. And, since the protocols (and software) in use are all open standards and Open Source, there's no danger of vendor-lock.

This setup uses multicast RTP with latency-matching across all nodes in the network; I have only one `channel' right now (in the radio-tuner or input-switching sense, not in the `mono vs. stereo' sense), but it's possible to define multiple channels (or `sources', in proper home-audio vocabulary) by giving them separate multicast addresses, and then a given receiver can be siwtched to another channel just by changing the multicast address on which that receiver listens.

Because of the latency-matching, multiple receiver-nodes (to the extent that the ear can tell) all have their playback perfectly time-synced such that there's no `echo' or `reverb' effect when standing between two adjacent rooms with separate receivers. I actually compared this against how well two different FM radios in adjacent rooms sync to the same radio station, and the RTP system did better. Really, multicast RTP over ethernet + dynamic resampling-algorithms with latency-analysis on pentium-class CPUs with network time-sync... provides quite a convincing emulation of speaker-wire. Of course, it also provides a lot of dreamy features that speaker-wire... can't even dream about.

That it's MPD-based means that I can have a single playback- and playlist-control server for the entire house, accessible from anywhere on the network; in other words, I have multiple `single points of control': client UIs are available for desktop and laptop computers of all OSes, Android devices, my friends' Apple iThings (which can automagically discover the MPD server via ZeroConf), etc.--there's even an adaptor that enables `dumb phones' with Java ME to control MPD via bluetooth (or Wi-Fi, if they have it).

MPD manages playlists on the server, and can be told by the clients to load them or to create and store new ones--and playlists can include not just any of the tracks stored on the server, but also anything available via an HTTP URL (e.g.: Internet radio, or an audio-file shared by a laptop or other system on the LAN).

Of course, Using MPD also means that I can use all of the tools and plugins that exist for MPD--like support, and my `mpdjay' autojockey script (which elicited such a positive response from my wife--along the lines of `OMG it's so awesome! Can you make me a portable version‽'--that we now both have NanoNote units running a pocket-sized setup similar to this, but without the multicast).

And MPD does gapless playback, and it can use ReplayGain to automatically adjust for differences in overall volume between different tracks or albums--or even do dynamicrange compression when you want background-music in a relatively noisy environment. And, on the subject of volume-control: there's even a central volume-control--so that you can turn the whole house's volume up or down, after you've got the individual output-volumes set relative to each other.

We generally run GMPC or Sonata on our desktop and laptop systems (though I sometimes like ncmpc for its extremem keyboard-friendliness--and it's what I run on my NanoNote..., but that's mostly another story), my wife runs the Remuco JME app on her Nokia (Symbian) phone, we use PMix on our Android tablet, and our iFriends use something called "MPoD", when they're visiting. And there are plenty of other clients available.

How does it all work?

MPD runs on the machine hosting all of the audio-files, which is set-up as an RTP sender--and, in my case, without any local speakers: all of the `actual audio output' is done on `receiver' nodes elsewhere on the network (my MPD server is a noisy old machine that we keep down in the oubliette/basement).

In /etc/mpd.conf, MPD can be told to use PulseAudio for output with a stanza like:

audio_output {
    type            "pulse"
    name            "MPD Stream"
    sink            "rtp"
    description     "what's playing on the stereo"
    mixer_type      "software"

... where "rtp" is the name of a sink defined in /etc/pulse/ via:

load-module module-null-sink sink_name=rtp format=s16be channels=2 rate=44100
load-module module-rtp-send source=rtp.monitor

That last line is the one that actually makes the RTP multicasting happen.

Now, keep all of the system clocks tightly synchronised so that PulseAudio can actually determine the network latency (by comparing the source timestamp on the RTP packets to the system time on the receiver), and then it's just a matter of also running Pulseaudio on the other hosts--but with those PulseAudio instances setup as receivers, which can be done in the respective /etc/pulseaudio/ files with...:

load-module module-rtp-recv

... or, if you have a GUI running on the receiver machine(s), you can just toggle-on the `enable Multicast/RTP receiver' setting in paprefs.


Actually, it can be a little more complicated, because:

  • PulseAudio defaults to doing something clever that should make playback better but which, as I understand it, tends to trigger bugs in ALSA; so any "load-module module-udev-detect" directives in may need to be modified to be "load-module module-udev-detect tsched=0" on each host.

  • There were major problems in PulseAudio's latency-matching code that weren't resolved until this past January; so you want PulseAudio 0.9.23 or later, but that didn't actually exist yet when Debian 6.0 was released. I just grabbed src/modules/rtp/module-rtp-recv.c from git and spliced it into the version of PulseAudio that Debian already had (there were a couple of minor wrinkles, there, that I needed to smooth out--nothing hard, though). Figuring out that this module was just buggy and needed to be updated was probably the biggest problem that I ran into.

  • The ARM-based plug-computers that I'm using have no floating-point units, which means that PulseAudio needs a little bit of additional configuration before it will work entirely right on them and produce sound reliably through the USB audio-adaptor.

  • If your RTP-transmitter machine has multiple network interfaces, make sure that you're routing the multicast packets out over the correct one--I did run into that problem!

  • If you're using DHCP, the system may hiccough when the hosts go through DHCP cycles, so you'll want to find a way of setting your network up such that your audio-system doesn't decide to renew its DHCP leases in the middle of a party.


I initially installed PTPd for super-accurate clock-sync between hosts, but ntpd should actually provide sufficient precision. You can use PTP to ensure that all of the nodes on a LAN are kept in very tight sync, even if their collective idea of `what time is it?' isn't actually accurate. If your LAN is connected to the Internet, then it's probably sufficient to just use NTP--which will allow your clocks to also be accurate in addition to being reasonably precise.

I also installed rtkit, which PulseAudio can use to get realtime scheduling; in Debian, I had to get this from Wheezy/testing, but it installed fine on 6.0/Squeeze.

Under GNOME on my normal PCs, PulseAudio starts automatically; on the plug-computers, I had to find another way to make PulseAudio run automatically, and the most sensible thing was to hook into Debian's `ifupdown' system by adding `up' and `down' options to the definition of the plug's ethernet interface in /etc/network/interfaces, such that the complete definition of eth0 looks like this:

# The primary network interface
allow-hotplug eth0
iface eth0 inet dhcp
        up su pulse --login --shell /bin/sh \
                    --command 'pulseaudio --exit-idle-time=-1 --daemonize'
        down killall pulseaudio

The day after I got everything working, the hard disk in one of my `speaker-wire emulator' nodes died....