Using a Raspberry Pi as a Sound Server

Introduction

I have in my office a workstation, a PC running Linux (Manjaro at the time of this writing). I also have a fairly nice stereo system that consists of the following:

The Raspberry Pi (RPi) is connected to the Topping D70 via USB. The RPi runs mpd and is intended to be my reference playback interface. That is, MPD is configured to use ALSA directly, with no sample rate conversion, for true bitperfect playback.

The PC and the D70 aren't physically close to each other. So if I want to use this system for playback from my PC, I have to either run a long cable (further cluttering a room that already has too many cables), or use the D70's Bluetooth interface. I've been doing the latter for the better part of the year, and it works reasonably well.

However, I also dabble in DIY audio electronics. I recently discovered the Infineon MA12070P, an audio amplifier with excellent audio performance measurements and digital (I2S) inputs. (There is a related chip, the MA12070, which has analog inputs.) An audio amplifier with digital inputs (sometimes called a "power DAC" or "digital amplifier") isn't anything new. For example, the Texas Instrument TAS-series amplifiers have been around for some time. But the Infineon chip offers a fairly simple implementation, class-leading power saving features, and surprisingly good performance measurements. Furthermore, the ideal implementation of these MA12070(p) chips includes a microcontroller, since some features and functionality are controlled by a serial (I2C) protocol. Given that the Raspberry Pi supports both I2S audio and I2C serial communication, it seems like a natural pairing to the MA12070P chip to create a true sound/music server.

But, before I start working the MA12070P-based music/sound server hardware, I wanted to make sure I could eliminate the need for Bluetooth, and rely entirely on my Raspberry Pi for my workstation's audio needs. Thus I started on a project to create a reliable network audio setup between my PC and Raspberry Pi using PulseAudio. It turned out to be non-trivial to get everything working reliably, so I tried to document my notes with this blog post.

User vs System Mode

  • User generally recommended
  • System mode seems "easier"

To me, system-mode is simpler, and in some ways more intuitive, as it looks like a traditional Unix daemon: a single instance, launched at start time, servicing all users. Here's the overview from the PulseAudio documentation: Running PulseAudio as System-Wide Daemon. But at the same time, they discourage using this mode, see What is wrong with system mode?. And even if you accept all this, and run system mode anyway, the you'll get warnings in the logs, reminding you that this mode is discouraged.

Also, it seems all the onlnie examples I found of people doing something similar just use system mode. For example:

For better or worse, I decided to do it the "hard way" and create some documentation on running in user mode on a headless server.

HOWTO User Mode

  • In systemd, under the "[Service]" section, do this to run as a non-root user:

    User=pulse
    Group=pulse
    
  • Need to give pulse user real-time priority via /etc/security/limits.conf:

    pulse   -       rtprio 95
    pulse   -       nice    -19
    
  • Works when run from the CLI:

    $ runuser -u pulse -- /usr/bin/pulseaudio --daemonize=no --disallow-module-loading --disallow-exit=yes --disable-shm=no --verbose
    
  • But when I try to start in user mode from systemd, when running as user "pulse" and group "pulse" I get this, even after reboot:

    Oct 30 16:52:44 dietpi-music pulseaudio[994]: I: [pulseaudio] main.c: setrlimit(RLIMIT_NICE, (31, 31)) failed: Operation not permitted
    Oct 30 16:52:44 dietpi-music pulseaudio[994]: I: [pulseaudio] main.c: setrlimit(RLIMIT_RTPRIO, (9, 9)) failed: Operation not permitted
    
  • Solution: if started from systemd, additionally need these corresponding entries in the service file, under the [Service] section:

    LimitRTPRIO=95
    LimitNICE=-19
    
  • Now, how do local clients use PulseAudio? In system mode, local clients seemed to magically "just work". Not sure how. But in user mode, I had to explicitly connect a local socket (i.e. Unix domain socket). In /etc/pulse/default.pa:

    load-module module-native-protocol-unix auth-group=pulse-access socket=/tmp/pulseaudio-socket
    

    And in /etc/pulse/client.conf:

    default-server = unix:/tmp/pulseaudio-socket
    

    Note also I have specified auth-group=pulse-access. In my specific case, the only local PulseAudio client is the mpd daemon. My mpd daemon runs as user "mpd", and I have added that user to the pulse-access group. Not sure if that's necessary (need to test if mpd still works even if mpd is not a member of pulse-access).

    See also: [SOLVED] PulseAudio: sharing audio devices between different users

    See also: PulseAudio: Modules documentation

DietPi Setup

Need to install pulseaudio packages. These don't appear to be exposed by dietpi-software, but you can install directly via apt:

# apt install pulseaudio pulseaudio-module-zeroconf

Systemd service file

Current /etc/systemd/system/pulseaudio.service file:

[Unit]
Description=PulseAudio Sound System
Before=sound.target
Wants=network-online.target
After=network-online.target

[Service]
LimitRTPRIO=99
LimitNICE=-19
CPUSchedulingPolicy=fifo
CPUSchedulingPriority=50
IOSchedulingClass=realtime
Nice=-19
User=pulse
Group=pulse
Type=simple
BusName=org.pulseaudio.Server
RuntimeDirectory=pulse
ExecStart=/usr/bin/pulseaudio --daemonize=no --disallow-module-loading --disallow-exit=yes --disable-shm=no --verbose
#ExecReload=/bin/kill -HUP $MAINPID
#ExecReload=/bin/pkill pulseaudio
Restart=always

[Install]
WantedBy=default.target

/etc/pulse/client.conf file

Current /etc/pulse/client.conf file:

default-server = unix:/tmp/pulseaudio-socket

/etc/pulse/default.pa file

Current /etc/pulse/default.pa file. Note if you're using system mode, by default, PulseAudio won't read dafault.conf, but will instead read system.conf. This is basically the stock default.pa file as provided by my distribution (DietPi v6.33.3 at the time of this writing), with some tweaks. I tried to remove everything that wasn't needed, since my goal for this Raspberry Pi is to be like an appliance, i.e. do one specific thing and nothing more. I think more can likely be removed from this file to further simplify and fine-tune it.

The key lines, for my specific config, are as follows:

load-module module-alsa-sink control='D70 '
load-module module-native-protocol-unix auth-group=pulse-access socket=/tmp/pulseaudio-socket
load-module module-native-protocol-tcp auth-ip-acl=127.0.0.1;10.18.51.0/24

The first line allows me to use my DAC's built-in hardware volume control. The second line is described above, basically it allows local users (mpd in my case) to use the daemon via Unix domain sockets. And the last line enables the networking component, using IP address as an authentication method.

Current /etc/pulse/default.pa file:

.fail

load-module module-device-restore
load-module module-stream-restore
load-module module-card-restore

load-module module-augment-properties

load-module module-switch-on-port-available

load-module module-alsa-sink control='D70 '

load-module module-native-protocol-unix auth-group=pulse-access socket=/tmp/pulseaudio-socket

load-module module-native-protocol-tcp auth-ip-acl=127.0.0.1;10.18.51.0/24

load-module module-zeroconf-publish

load-module module-default-device-restore

load-module module-rescue-streams

load-module module-always-sink

load-module module-intended-roles

load-module module-suspend-on-idle

.ifexists module-console-kit.so
load-module module-console-kit
.endif
.ifexists module-systemd-login.so
load-module module-systemd-login
.endif

/etc/pulse/daemon.conf file

Many (all?) of these options can also be set from the commandline (and I believe in most (probably all) the commandline parameters take precedence). Note that I disable flat-volumes, which (at the time of this writing) is enabled by default. Volume behaves in a very non-intuitive way when enabled (for me, anyway!).

Current /etc/pulse/daemon.conf file:

allow-module-loading = no
daemonize = no
avoid-resampling = yes
enable-shm = yes
allow-exit = no
realtime-scheduling = yes
realtime-priority = 10
resample-method = soxr-mq

flat-volumes = no

Time Synchronization Between Server and Client

I had all kinds of hair-pulling problems getting tunnel mode to work reliably. It seems to have been solved by ensuring time synchronization was happening between my RPI server and my PC client. This post from the mpv GitHub was helpful: Video freezes when audio sent over network with Pulseaudio #959.

In the Arch Linux Wiki, they do make a mention of this requirement in the PulseAudio over network section: "Ensure that client and server systems agree on the time (i.e., use NTP), or audio streams may be choppy or may not work at all."

On both my Raspberry Pi server, and my Linux workstation, I use systemd-timesyncd to synchronize time. I'm actually running an NTP server in my home. So for both systems, my /etc/systemd/timesyncd.conf file looks like this:

[Time]
NTP=<ip address of my NTP server>

Once again, the Arch Linux Wiki appears to have more detailed documentation about this: Arch Wiki: systemd-timesyncd.

mpd config

The main thing here is the "audio_output" section in /etc/mpd.conf, and it's basically trivial:

audio_output {
    type            "alsa"
    name            "My ALSA Device"
    device          "pulse"
    mixer_control   "Master"
}

Note that I'm using "Master" instead of "D70 " for mixer_control. But that's not entirely surprising:

# amixer --device pulse scontrols
Simple mixer control 'Master',0
Simple mixer control 'Capture',0

See also: Music Player Daemon Wiki: Setting up PulseAudio

Useful Links

PulseAudio: The Perfect Setup

Some useful nuggets of information in PulseAudio's documentation, The Perfect Setup.

PulseAudio: Network Setup

Official PulseAudio documentation on Network Setup.

ArchLinux Forum Post

ArchLinux forum post, [Solved] PulseAudio does not use hardware volume control, suggested something like the following needed to be added to /etc/pulse/default.pa (actually /etc/pulse/system.pa in this case):

load-module module-alsa-card device_id='Groove' control='Groove Output' ignore_dB=1 deferred_volume=1

And also remarked that instead of specifying "module-alsa-card", but instead specifying, "module-alsa-sink", pulse audio would immediate crash.

However, this is inconsistent with the PulseAudio Modules Documentation, as the "module-alsa-card" does not appear to support the "control" parameter. The "module-alsa-sink" modules does support the "control" parameter.

Volumio GitHub

Volumio GitHub, Send audio from main computer (pulseaudio) to Volumio #159, gave me the key insight that "module-udev-detect" apparently needs to be disabled. Also, I didn't realize it, but in the ArchLinux discussion (above), the module-udev-detect module was not explicitly loaded (the complete file contents were shown, my intial too-quick read thought it was just a snippet).

ArchLinux Wiki - PulseAudio

The ArchLinux Wiki is a great general resource, not just for ArchLinux. The page PulseAudio/Examples, in particular the PulseAudio over network section, was my starting point for this.

The overengineering of ALSA userland

This blog post by Flameeyes, The overengineering of ALSA userland, covers some of the history of ALSA and PulseAudio.

Manjaro Forum Post - Apps using both alsa and pulseaudio, headless and multi-user desktop

I made this post on the Manjaro Forums, Apps using both alsa and pulseaudio, headless and multi-user desktop. The context was a little different, as at the time I was trying to set up another PC as sort of a family media hub. But the concepts are related, so I'm including it here for reference.

PulseAudio systemd service files

Some discussion about what is appropriate for a systemd service file for PulseAudio at awidegreen github.

AudioScienceReview Post - Topping D70 Bluetooth on Linux

I initially struggled to get Bluetooth working between my Linux workstation and the Topping D70 DAC. I finally figured it out, and made this post on ASR to describe my solution. Basically: It was just a matter of installing the additional Pulseaudio modules that provide additional codec support. On Manjaro, the needed packages are available in the AUR: pulseaudio-modules-bt-git and libldac. I installed, restarted Pulseaudio, and can see see that I'm now using LDAC as my Bluetooth Audio codec. And no more hiss!

That post also references this incredibly helpful blog post by Jiri Eischmann: Better Bluetooth sound quality on Linux. One of the most helpful parts of this page was illustrating that the "pactl list" command can be used:

pactl list | grep codec

Manjaro Forums Post - Tip: Improved Bluetooth Audio Quality

I posted this to the Manjaro Forums on July 2, 2020. The Manjaro Forums had a technical issue, and they basically started out fresh. The old forums are frozen in read-only mode under the "archived" subdomain. For whatever reason, standard search engines don't seem to index the archived forums very well. Here is my post: Tip: Improved Bluetooth Audio Quality

Just in case the archived forums go away, I made a copy of that post here for safe-keeping: Improved Bluetooth Audio Quality

Victor Gaydov: PulseAudio under the hood

Here is an immensely useful document which goes into detail about the architecture of PulseAudio: Victor Gaydov: PulseAudio under the hood.

My posts to the pulseaudio mailing list

I made a number of posts to the pulseaudio-discuss mailing list for help. Thank you to all the friendly people who offered help and suggestions!