This project is a build of a 2.1 channel receiver based on the Raspberry Pi. Out-of-the-box, it takes in audio from the digital optical input. The receiver leverages PipeWire and WirePlumber for the audio routing and session management. The software and configuration are built on NixOS and managed through a Nix flake in this repository. Nix has quite up-to-date software, is extensively customizable, and ensures that builds are reproducible.
-
2.1 channel output
-
Digital optical audio input
-
Low-latency
-
Realtime - for the lowest possible latency
-
AirPlay
-
Bluetooth
-
DLNA Digital Media Renderer
-
Cast directly from Jellyfin
-
Multi-room audio
-
Integrates with Music Assistant and Home Assistant
-
MIDI Synthesizer
-
Raspberry Pi DigiAMP+ HAT $30.00
-
Power cord $2.70
-
A sufficiently large and performant microSD card can be used instead of NVMe storage. I recommend using larger capacity micro SD cards for increased lifespan, especially when building code on the Pi itself. Depending on your situation, you might want to do this. If you just need enough space to fit the image on the SD card, 16 GiB should be ample. The 128GB Samsung Pro Ultimate and 128GB Samsung Pro Endurance are two good options. $25
-
Extended M2.5 Standoffs for Pi HATs (Raspberry Pi 5) - Pack of 4 $3.18 This is 4 17mm M2.5 Hex socket-socket standoffs and 8 6mm M2.5 screws.
-
Booster Header $2.22 This header has a standard 8.5mm height with 5mm tall pins.
-
Bluetooth USB Adapter such as this TP-Link USB Bluetooth Adapter $15.
The total price comes to a whopping $249.02.
That’s still a bit pricey, so I’d love to get the cost down to below $200 at the very least.
However, I’m very much inclined for everything to be supported in the Linux kernel.
It’s bad enough that I have to make a concession for the Amp HAT, which requires using Raspberry Pi’s fork of the Linux kernel.
If you’re reading this Raspberry Pi, please upstream your audio HATs.
I’d like to find a cheaper, newer PCIe sound card with in-kernel Linux support to provide the digital optical input and, potentially, an LFE output.
The current Sound Blaster card requires enabling extra configuration options, i.e. CONFIG_SND_HDA_INTEL
, in the kernel config as well as adding a special config.txt
overlay.
As for using HATs to provide these features, I’ll gladly switch over if and when they are supported upstream.
Since kernel has to be compiled anyways, realtime preemption is enabled to further reduce latency.
Assembling the Piceiver is very straightforward. In fact, I’m not going to go in to much detail. There are a couple of important things that require explanation.
It’s expected that the Piceiver will be connected to the network via ethernet and not through WiFi. To use WiFi, the image will need to be customized.
Do not supply power through the USB-C connector on the Raspberry Pi. I recommend covering the USB-C connector on the Raspberry Pi with a small strip of electrical tape so that you don’t forget.
The digital optical input is the bottom digital optical connector on the Sound Blaster card.
If you use a reasonably large gauge of speaker wire, it will take some work to fit the wire into the screw terminals on the Amp HAT. Take some time to shrink the wire a smidgen by twisting the ends.
Here’s a picture showing all of the cables attached except for the barrel power plug.
Installation is done by building a system image which is flashed directly to an SD card. This can all be built and customized locally with Nix. Unfortunately, unless you’re building this on an aarch64 machine, it will take a significant amount of time to build. The initial build probably took about two full days for me. I need to tweak the kernel config to avoid building a bunch of unnecessary things, like drivers for AMD and Nouveau GPUs.
-
Install an implementation of Nix, such as Lix demonstrated in the following command. Enable support for flakes when prompted.
curl -sSf -L https://install.lix.systems/lix | sh -s -- install
-
Clone this repository.
git clone [email protected]:jwillikers/piceiver.git
-
Change into the project directory.
cd piceiver
-
Configure ccache support for Nix. These instructions are for non-NixOS. See the NixOS Wiki CCache page for details, including how to configure ccache for NixOS.
-
Create the ccache cache directory.
sudo mkdir --mode=0770 --parents /nix/var/cache/ccache
-
Set ownership of the ccache cache directory.
sudo chown root:nixbld /nix/var/cache/ccache
-
Configure the ccache directory as an extra sandbox path for Nix.
/etc/nix/nix.confextra-sandbox-paths = /nix/var/cache/ccache
-
Restart the Nix daemon for the change to take effect.
sudo systemctl restart nix-daemon.service
-
-
Build the SD card image. Prefix the command with
systemd-inhibit
to prevent your computer from sleeping. This will take a long time. Like, two days a day in my case. The default,basic-sd-image
package, produces a minimal image that requires no extra configuration. There is an alternative package,full-sd-image
, which is more fully-featured, including more integrations, but requires customization and additional set up.systemd-inhibit nix build --accept-flake-config
💡If for any reason the build fails or your computer locks up, there’s a good chance that it’s related to Nix attempting to build too many jobs simultaneously or not having adequate RAM space to hold the build directory for a package. These issues can be fixed with configuration options for the Nix daemon in
/etc/nix/nix.conf
. Use themax-jobs
option to limit the number of simultaneous jobs. To build only a single job at a time, this would look likemax-jobs = 1
in the config file.To prevent running out space in RAM, set the
build-dir
option to a path that is located on disk. The defaulttmp
directory is usually stored in a special filesystem backed by RAM. To set this to/var/tmp/nix-daemon
, the line in the config will look likebuild-dir = /var/tmp/nix-daemon
. Be sure to create this directory.sudo mkdir --parents /var/tmp/nix-daemon
To apply changes in
/etc/nix/nix.conf
, restart the Nix daemon.sudo systemctl restart nix-daemon.service
-
Once the image is ready, insert the SD card into your computer.
-
Use
lsblk
to find the SD card. This will probably be a device like/dev/mmcblkX
or possibly/dev/sdX
.lsblk
-
Flash the SD card with the image. Replace the
/dev/mmcblkX
device path with yours.🔥Using the wrong device path could wreck your entire computer or precious data on an attached disk, so be careful to use the right path. Or just use a safe graphical application to flash the image to your SD card.
nix develop --command bash -c 'sudo env "PATH=$PATH" zstdcat result/sd-image/nixos-sd-image-*-aarch64-linux.img.zst | dd bs=1M status=progress of=/dev/mmcblkX'
When booting the Piceiver for the first time, give it a few extra minutes to start working as it has to resize the filesystem.
Key-based authentication is required for the root
user.
So, unless you’ve configured that, log in as the user jordan
with the default password opW6&Aa
.
The root
password is V2psT!t0
.
I recommend configuring the authorized keys for the root
user as well as your own user in the NixOS configuration.
This is done for the jordan
user here.
With SSH keys configured, I recommend completely disabling password authentication for security.
Also, you should change the default passwords for the users.
See the Deploy section for how to deploy such configuration changes to a Piceiver that’s already running.
You may want to update or make changes to an existing Piceiver instance. Such changes might include supplying your own SSH keys for authentication, altering the default user, changing passwords, or applying credentials for certain services. It is possible to apply such changes as well as updates to an already running instance by using deploy-rs. This should save your microSD cards from an tortured and all too brief existence. The instructions here describe how to deploy updates to an existing Piceiver server. It is assumed that you’ve already cloned the repository and changed to its directory.
-
First, make your desired modifications to the configuration.
-
Activate the development environment with Nix to pull in the correct version of
deploy-rs
.nix develop
-
Deploy. This will prompt for the
sudo
password of the userjordan
, which isopW6&Aa
by default.systemd-inhibit deploy --interactive-sudo true --ssh-user jordan .#piceiver
💡After deploying your own SSH key for authentication of the
root
user, the--interactive-sudo true
and--ssh-user jordan
options can be omitted.
The PipeWire and WirePlumber sessions run under the dedicated core
user account.
Almost all audio-related services run under this user’s account because they need to interact with the PipeWire daemon.
The exception is the Snapcast server, which runs as a system service under a dedicated user because it only handles audio over the network.
The PipeWire configuration creates a virtual sink that forwards audio to both the DigiAmp+ HAT and the USB audio interface.
A loopback device is created which connects the digital optical input on the Sound Blaster card to this sink.
To reduce latency, I’ve lowered the quantum as low as possible until just before audio begins to stutter.
The WirePlumber configuration sets the correct device profile for the Sound Blaster card in addition to several other important tweaks like optimizations for the USB output and preventing the digital optical input from being suspended.
The default sink, default source, plus initial volume levels are configured for WirePlumber by a systemd service which runs a few seconds after the WirePlumber service starts.
Most audio applications interact directly with PipeWire, but a single holdout, the Snapcast client, is only capable of using PulseAudio’s API.
Thus, the PipeWire’s PulseAudio daemon is also running.
The audio routing is pretty much hard-coded for everything. Audio from the digital optical input is assumed to require low latency and high reliability, and thus is routed directly to the combined stereo and sub output. The digital optical input is connected to my TV, which is why it’s configured this way. The synthesizer is also routed to the combined output because that also requires low latency All other inputs are over the network and audio only, so they are all connected to Snapcast. The hard-coded behavior is great when you know exactly how you want everything to be routed, so this setup works really well for me. Plus, it’s one less thing I need to think about or troubleshoot. To make it possible to switch between outputs, I’d need to add a button and some kind of indicator to the Piceiver so you could properly switch between them on the device.
AirPlay 1 and 2 are supported via Shairport Sync. Two instances of Shairport-Sync run simultaneously to provide support for both AirPlay 1 and AirPlay 2. It works very nicely. PipeWire’s RAOP Discover module can be used to automatically discover and stream directly to the Piceiver. The following instructions document how to accomplish this.
ℹ️
|
Make sure that the ephemeral port range is open in the firewall on the device from which you are streaming. |
-
Create the configuration directory for PipeWire for your user.
mkdir --parents ~/.config/pipewire/pipewire.conf.d
-
Configure the RAOP Discover module in a config file fragment.
~/.config/pipewire/pipewire.conf.d/raop-discover.confcontext.modules = [ { name = libpipewire-module-raop-discover args = { stream.rules = [ { matches = [ { raop.ip = "~.*" } ] actions = { create-stream = { stream.props = { media.class = "Audio/Sink" } } } } ] } } ]
-
Restart PipeWire.
systemctl --user restart pipewire
Bluetooth streaming is supported.
Just pair your device with the receiver.
The Piceiver is only discoverable for the first five minutes after it boots.
Since it has no way to either display a pin or enter one, it accepts connections from anyone.
The timeout limits the window where an unwanted guest may hijack your receiver.
Only one device may be connected at a time.
If you get a prompt for a pin code for some reason, try entering 0000
.
It can be a bit finicky pairing my Android phone, so just give it a couple minutes after it disconnects to reconnect and get everything figured out.
My wife’s iPhone paired much more easily over Bluetooth.
A dedicated button to enter Bluetooth pairing mode would be really helpful.
I’ve not yet tested whether Bluetooth MIDI works.
Rygel provides a DLNA/UPnP Digital Media Renderer which can be used to playback audio from services that support the protocol.
If you have a Jellyfin media server, you can cast directly to the Piceiver via Mopidy and the Mopidy-Jellyfin plugin. This requires the user credentials and the address of your Jellyfin server. Once configured, Jellyfin’s web interface can be used to cast directly to the device. I’m planning on adding support for using secrets to populate credentials like this in the image. That could well end up being super complicated and not be worth it if you just want to get things set up. It’s possible to configure credentials locally in the repository and deploy them to your server by following the instructions in the Deploy section.
💡
|
A Mopidy web server is available at |
Multi-room audio is handy feature, it’s been incorporated in the Piceiver thanks to the Snapcast project.
I haven’t found anything to package up something to manage multi-room audio via PipeWire, although I’m certain it’s possible.
Until someone makes something like that, Snapcast is a great open-source solution for multi-room audio.
Since it doesn’t integrate directly with PipeWire, there will likely be an additional level of latency introduced by PipeWire.
The Snapcast control webserver is accessible at piceiver.local:1780
.
ℹ️
|
Snapcast introduces a substantial amount of latency in order to synchronize playback between the various playback clients. This isn’t much of a problem when playing music, audio books, or podcasts. However, you’ll want to avoid using it as the sink for video playback or any kind of realtime audio interactions such as calls, Mumble, etc. |
A Raspberry Zero 2W and DAC Pro HAT would make a great combination for creating a remote playback satellite that you can attach to a set of speakers in another location. Alas, it’s been a couple of years at this point where I can get the darn thing to not kernel panic when using a USB ethernet adapter. So, I recommend a Raspberry PI 4 B instead at this point. If you opt for a USB audio device instead of the DAC Pro, you can even use a mainline kernel! I call it Snappellite for Snapcast Satellite.
-
Raspberry Pi DAC Pro HAT $25 or, alternatively, a USB audio adapter $15
-
32 GB microSD Card $8.99
- Total Cost
-
$91.94 - 101.94 USD
Oh, I really need to stop doing the math on how much these component costs. I’ve spent way too much on all of this.
I’ve configured an SD image target for it, snappellite-sd-image
.
Build it with Nix build.
nix build .#snappellite-sd-image
It still takes forever to build, so feel free to grab some of your favorite on-brand iced tea while you wait.
PipeWire 1.2.0 added the Snapcast Discover module. This module makes it really easy to set up a stream from any device running PipeWire, like your laptop. Or maybe your phone. I don’t want to assume anything about your sanity or lack thereof. To actually configure the Snapcast server to use the input stream, you’ll probably want to use Snapdroid or alternatively Snapweb directly from your browser. There’s also a bunch of other third-party integrations available. To use this module, configure PipeWire to load it with the appropriate settings on your device. The steps here walk through how to do this.
-
Create the configuration directory for PipeWire for your user.
mkdir --parents ~/.config/pipewire/pipewire.conf.d
-
Drop in and configure the Snapcast Discover module in a config file fragment.
~/.config/pipewire/pipewire.conf.d/51-snapcast-discover.confcontext.modules = [ { name = libpipewire-module-snapcast-discover args = { stream.rules = [ { matches = [ { snapcast.ip = "~.*" } ] actions = { create-stream = { audio.rate = 48000 audio.format = S32LE audio.channels = 2 audio.position = [ FL FR ] node.name = "Piceiver Snapcast Sink" # If your firewall blocks ephemeral ports, open those ports or open the specific port in the following line and uncomment it. # Only after considering the security implications, of course. # server.address = [ "tcp:4711" ] snapcast.stream-name = "My Laptop" capture = true capture.props = { media.class = "Audio/Sink" } } } } ] } } ]
-
Restart PipeWire to load the module.
systemctl --user restart pipewire
-
Now you should be able see an additional stream available for Snapcast in the app, web interface, or what have you.
The Piceiver may be integrated with Music Assistant as an external Snapcast server, an AirPlay playback provider, and a UPnP/DLNA player provider.
The external Snapcast server option will create a Snapcast stream specific to Music Assistant.
This requires manually switching the Snapcast stream back to the default default
stream after playing anything through Music Assistant, otherwise you’ll hear nothing.
This is a pain, so I recommend using the AirPlay or UPnP/DLNA player providers instead unless you stream everything through Music Assistant.
The Piceiver can likewise be incorporated directly in Home Assistant using either the Snapcast or DLNA integrations or directly via Music Assistant.
USB MIDI keyboards are plug-and-play with the Piceiver thanks to the FluidSynth software synthesizer.
Just plug in the keyboard and FluidSynth will translate the MIDI messages and output the audio through the stereo.
A systemd service for the core
user runs FluidSynth in the background.
The command-line flags to the service can be configured via Nix or on the Pi itself by running the following command-line as the core
user.
systemctl --user edit fluidsynth.service
After making modifications, be sure to restart the service.
systemctl --user restart fluidsynth.service
The Piceiver is admittedly, not the most secure thing out-of-the-box. It’s running services listening on several ports, including open web interfaces for controlling audio streaming and accessing your media. The Bluetooth is not particularly secure either, since nothing prevents someone from pairing. This device is intended for use in a private network, like a home network, and even there it is still important to consider access and if anyone on your network should be able to control the receiver server.
It’s not possible to log in to the core
user account, but is possible to use sudo
to switch to it.
This isn’t possible when using the basic image because that doesn’t have an account with which to log in.
Add a root password or another user account, like in the custom image, to be able to log in.
The following command can be used to switch to the core
user account.
I use the fish shell, by the way.
sudo -H -u core fish -c 'cd; fish'
The following table shows some performance benchmarks which were obtained using pw-top
.
This table includes my original prototype based off of the Raspberry Pi Compute Module 4, which used Raspberry Pi OS 5 based on Debian Bookworm.
Raspberry Pi Model |
OS |
Kernel |
PipeWire Version |
WirePlumber Version |
Quantum |
Rate |
Active CPU Usage |
Idle CPU Usage |
RAM Usage |
Latency (μs) |
Notes |
CM4 8GiB RAM, no WiFi |
Raspberry Pi OS 5 (Debian Bookworm) |
Linux 6.1.54-rt15 |
0.82.0 |
0.4.15 |
128 |
48,000 |
10-20% |
5-10% |
0.3% |
100-400 |
Without Snapcast and Jellyfin MPV Shim. No LFE or upmixing. |
Pi 5 Model B, 8GiB RAM |
Raspberry Pi OS 5 (Debian Bookworm) |
Linux 6.1.54-rt15 |
1.0.6 |
0.5.2 |
512 |
48,000 |
10-20% |
5-10% |
0.3% |
100-200 |
There’s a lot left I need to complete. The custom image, which is tailored for my personal usage, still has many outstanding tasks. First, the Nix stuff needs cleaned up. Significantly. My primary focus now is to add support for secrets handling via sops-nix in the configuration to allow me to set things like passwords as well as credentials for my Jellyfin server. Then, there’s still the fact there’s not a proper plan in place for managing and updating the installation. Using a new image every time isn’t gonna fly with flash storage or the value of anybody’s time, so figuring out deployment is a high priority. After that, I need to figure out how to configure Net-SNMP in NixOS, as monitoring is a really nice feature to have in place.
-
Use a reverse-proxy for the Snapcast and Mopidy servers?
-
Fix the sub flipping on and off when idle.
-
Auto-mute speakers and subwoofer when nothing is being output. I think that the constant input from the digital optical input causes this. However, I have to disable suspend for that node otherwise nothing ever comes through.
-
FCast for streaming, but right now I’d have to write a receiver for audio only myself and then I’d have to write integrations and apps that actually use the protocol.
-
Snapcast microcontroller for playback
-
Copy nixos configuration or flake to /etc/ in the image?
-
Add a button to trigger Bluetooth pairing.
-
Test how well the onboard Bluetooth works for the Pi 5.
-
Use nix-sops for secret management
-
Configure monitoring over Net-SNMP
-
A mechanism for switching the output, so as to choose between the lower latency stereo output or the Snapcast output.
-
Automatic updates?
-
Better filesystem such as Bcachefs or Btrfs
-
Automatically log in to Tailscale
-
Remove a bunch of extra dependencies that nixpkgs pulls in but that isn’t necessary.
-
Script for collecting performance metrics?
-
LFE
-
Bluetooth MIDI
-
SELinux
-
Case
-
Low-cost
Contributions in the form of issues, feedback, and even pull requests are welcome. Make sure to adhere to the project’s Code of Conduct.
This project is built on the hard work of countless open source contributors. A few of these projects are enumerated below.
The project’s Code of Conduct is available in the Code of Conduct file.
This repository is licensed under the MIT license.
© 2024 Jordan Williams