Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support AirPlay 1 and 2 simultaneously and fix systemd service dependencies #2

Merged
merged 19 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ repository:
# A URL with more information about the repository
# homepage: ""
# A comma-separated list of topics to set on the repository
topics: airplay, audio, bluetooth, deploy-rs, dlna, fluidsynth, home-assistant, image, jellyfin, low-latency, music-assistant, midi, multi-room, nix, nixos, pi, pipewire, raspberry, realtime, receiver, rygel, sbc, sd, shairport-sync, snapcast, stereo, synth, synthesizer, upnp, wireplumber,
topics: airplay, audio, bluetooth, dlna-upnp, home-assistant, jellyfin, low-latency, music-assistant, midi, multi-room, nixos, pipewire, raspberry-pi, realtime, receiver, snapcast, stereo, synthesizer, wireplumber,
# Either `true` to make the repository private, or `false` to make it public.
private: false
# Either `true` to enable issues for this repository, `false` to disable them.
Expand Down
37 changes: 0 additions & 37 deletions .github/workflows/build.yaml

This file was deleted.

66 changes: 57 additions & 9 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ endif::[]
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 is built on NixOS and managed through a Nix flake in this repository.
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.

.Features
Expand Down Expand Up @@ -163,6 +163,7 @@ Installation is done by building a system image which is flashed directly to an
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 https://lix.systems[Lix] demonstrated in the following command.
Enable support for flakes when prompted.
Expand Down Expand Up @@ -319,10 +320,57 @@ To make it possible to switch between outputs, I'd need to add a button and some

=== AirPlay

https://www.apple.com/airplay/[AirPlay], specifically Airplay 2, is supported via {Shairport-Sync}.
https://www.apple.com/airplay/[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.
AirPlay 1 doesn't appear to work at all, however.
This may be fixed when `shairport-sync` is updated in NixOS 24.11.
PipeWire's https://docs.pipewire.org/page_module_raop_discover.html[RAOP Discover module] can be used to automatically discover and stream directly to the Piceiver.
The following instructions document how to accomplish this.

[NOTE]
====
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.
+
[,sh]
----
mkdir --parents ~/.config/pipewire/pipewire.conf.d
----

. Configure the RAOP Discover module in a config file fragment.
+
.~/.config/pipewire/pipewire.conf.d/raop-discover.conf
[,lua]
----
context.modules = [
{ name = libpipewire-module-raop-discover
args = {
stream.rules = [
{ matches = [
{ raop.ip = "~.*"
}
]
actions = {
create-stream = {
stream.props = {
media.class = "Audio/Sink"
}
}
}
}
]
}
}
]
----

. Restart PipeWire.
+
[,sh]
----
systemctl --user restart pipewire
----

=== Bluetooth

Expand Down Expand Up @@ -434,7 +482,7 @@ The steps here walk through how to do this.
mkdir --parents ~/.config/pipewire/pipewire.conf.d
----

. Drop in and configure the the Snapcast Discover module in a config file fragment.
. Drop in and configure the Snapcast Discover module in a config file fragment.
+
.~/.config/pipewire/pipewire.conf.d/51-snapcast-discover.conf
[,lua]
Expand Down Expand Up @@ -483,11 +531,11 @@ systemctl --user restart pipewire

=== Music Assistant

The Piceiver may be integrated with {Music-Assistant} as an external Snapcast server as well as a UPnP/DLNA player provider.
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.
It may be less disruptive to your Snapcast configuration to use the UPnP/DLNA player provider instead.
The AirPlay player provider relies on AirPlay 1 and will not work with AirPlay 2 support in shairport-sync enabled.
It can likewise be incorporated directly in {Home-Assistant} using either the Snapcast or DLNA integrations or directly via 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.

=== Synthesizer

Expand Down
3 changes: 2 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@
mopidy-pipewire-gstreamer-plugin
rygel-pipewire-gstreamer-plugin
realtime
shairport-sync-airplay2
unstablePackages
unstable-shairport-sync
shairport-sync-airplay2
];
pkgsArmCross = import nixpkgs {
# inherit system;
Expand Down
4 changes: 2 additions & 2 deletions home-manager/_mixins/services/mopidy/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ lib.mkIf (lib.elem username installFor && role == "piceiver") {
];
settings = {
audio = {
# todo Investigate buffer time.
# buffer_time = 200;
# I'm surprised I don't need a value larger than 1ms here.
buffer_time = 1; # Must be greater than 0, default from GStreamer is 1000ms
mixer = "software";
mixer_volume = 50;
output = "pipewiresink client-name=Mopidy target-object=snapserver";
Expand Down
22 changes: 14 additions & 8 deletions home-manager/_mixins/services/rygel/default.nix
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
config,
lib,
osConfig,
pkgs,
role,
username,
Expand Down Expand Up @@ -87,24 +88,29 @@ lib.mkIf (lib.elem username installFor && role == "piceiver") {
"rygel" = {
Unit = {
Description = "Rygel DLNA/UPnP Digital Media Renderer";
After = [ "wireplumber.service" ];
Requires = [ "wireplumber.service" ];
After = [
"pipewire.service"
"wireplumber.service"
# The delay from the wireplumber-init service provides enough time for all of the PipeWire nodes to become available.
"wireplumber-init.service"
];
PartOf = [ "pipewire.service" ];
Requires = [ "wireplumber-init.service" ];
Wants = [ "wireplumber.service" ];
X-Restart-Triggers = [
"/etc/rygel.conf"
"${config.xdg.configHome}/rygel.conf"
"${osConfig.environment.etc."rygel.conf".source}"
"${config.home.file."${config.xdg.configHome}/rygel.conf".source}"
];
};
Service = {
BusName = "org.gnome.Rygel1";
ExecStart = "${pkgs.gnome.rygel}/bin/rygel";
Restart = "always";
RestartSec = 10;
Type = "dbus";
};
Install = {
WantedBy = [
"default.target"
"wireplumber.service"
];
WantedBy = [ "default.target" ];
};
};
};
Expand Down
51 changes: 46 additions & 5 deletions home-manager/_mixins/services/shairport-sync/default.nix
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
lib,
osConfig,
pkgs,
role,
username,
Expand All @@ -13,7 +14,7 @@ lib.mkIf (lib.elem username installFor && role == "piceiver") {
nqptp = {
Unit = {
Description = "shairport-sync AirPlay 2 NQPTP server";
Before = [ "shairport-sync.service" ];
Before = [ "shairport-sync-airplay-2.service" ];
};
Service = {
ExecStart = "${pkgs.nqptp}/bin/nqptp -v";
Expand All @@ -23,24 +24,64 @@ lib.mkIf (lib.elem username installFor && role == "piceiver") {
WantedBy = [ "default.target" ];
};
};
shairport-sync = {
shairport-sync-airplay-2 = {
Unit = {
Description = "shairport-sync AirPlay server";
Description = "shairport-sync AirPlay 2 server";
After = [
"nqptp.service"
"pipewire.service"
# Start Shairport-Sync for AirPlay 1 first to avoid contention of the same network ports.
"shairport-sync-airplay-1.service"
"wireplumber.service"
# The delay from the wireplumber-init service provides enough time for all of the PipeWire nodes to become available.
# todo I should probably use a proper `pipewire-ready.target` instead.
"wireplumber-init.service"
];
# This service needs to be restarted whenever PipeWire is.
# If it isn't restarted, it will fallback to the combined stereo sink.
PartOf = [ "pipewire.service" ];
Requires = [
"nqptp.service"
"wireplumber-init.service"
];
Wants = [
"shairport-sync-airplay-1.service"
"wireplumber.service"
];
X-Restart-Triggers = [ "${osConfig.environment.etc."shairport-sync.conf".source}" ];
};
Service = {
ExecStart = "${pkgs.shairport-sync-airplay-2}/bin/shairport-sync";
Restart = "always";
RestartSec = 10;
};
Install = {
WantedBy = [ "default.target" ];
};
};
shairport-sync-airplay-1 = {
Unit = {
Description = "shairport-sync AirPlay 1 server";
After = [
"pipewire.service"
"wireplumber.service"
# The delay from the wireplumber-init service provides enough time for all of the PipeWire nodes to become available.
# todo I should probably use a proper `pipewire-ready.target` instead.
"wireplumber-init.service"
];
Wants = [ "wireplumber-init.service" ];
# This service needs to be restarted whenever PipeWire is.
# If it isn't restarted, it will fallback to the combined stereo sink.
PartOf = [ "pipewire.service" ];
Requires = [ "wireplumber-init.service" ];
Wants = [ "wireplumber.service" ];
X-Restart-Triggers = [ "${osConfig.environment.etc."shairport-sync-airplay-1.conf".source}" ];
};
Service = {
ExecStart = "${pkgs.shairport-sync}/bin/shairport-sync";
ExecStart = "${pkgs.shairport-sync}/bin/shairport-sync --configfile=${
osConfig.environment.etc."shairport-sync-airplay-1.conf".source
}";
Restart = "always";
RestartSec = 10;
};
Install = {
WantedBy = [ "default.target" ];
Expand Down
18 changes: 10 additions & 8 deletions home-manager/_mixins/services/snapclient/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ let
snapcastFlags =
[
"--logsink system"
# todo I'm not sure the best buffer time here, but it may need tweaked. pulse:buffer_time=100
"--player pulse"
"--player pulse:buffer_time=10" # Minimum is 10ms, default is 100ms
]
++ lib.optionals (role == "piceiver") [
"--host ::1"
Expand All @@ -26,23 +25,26 @@ lib.mkIf (lib.elem username installFor) {
Unit = {
Description = "Snapcast client";
After = [
"pipewire.service"
"pipewire-pulse.service"
"wireplumber.service"
# The wireplumber-init service ensures the volume is set correctly before playback starts.
"wireplumber-init.service"
];
Requires = [
"pipewire-pulse.service"
PartOf = [ "pipewire-pulse.service" ];
Requires = [ "wireplumber-init.service" ];
Wants = [
"pipewire.service"
"wireplumber.service"
];
};
Service = {
ExecStart = "${pkgs.snapcast}/bin/snapclient " + builtins.toString snapcastFlags;
Restart = "always";
RestartSec = 10;
};
Install = {
WantedBy = [
"default.target"
"wireplumber.service"
];
WantedBy = [ "default.target" ];
};
};
};
Expand Down
10 changes: 8 additions & 2 deletions home-manager/_mixins/services/wireplumber-init/default.nix
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
lib,
osConfig,
pkgs,
role,
username,
Expand All @@ -11,8 +12,7 @@ let
if role == "piceiver" then
"${pkgs.initialize-wireplumber}/bin/initialize-wireplumber.nu"
else
# todo Keep in sync with the version in NixOs PipeWire config.
"${pkgs.unstable.wireplumber}/bin/wpctl set-volume @DEFAULT_AUDIO_SINK@ 80%";
"${osConfig.services.pipewire.wireplumber.package}/bin/wpctl set-volume @DEFAULT_AUDIO_SINK@ 80%";
in
lib.mkIf (lib.elem username installFor) {
systemd.user = {
Expand All @@ -22,9 +22,15 @@ lib.mkIf (lib.elem username installFor) {
Description = "Initialize WirePlumber default devices and volumes";
After = [ "wireplumber.service" ];
Requires = [ "wireplumber.service" ];
X-Restart-Triggers = lib.optionals (role == "piceiver") [
"${pkgs.initialize-wireplumber}/bin/initialize-wireplumber.nu"
];
};
Service = {
# Sleep for 5 seconds to give PipeWire a chance to initialize?
ExecStartPre = "${pkgs.coreutils}/bin/sleep 5";
ExecStart = script;
# RemainAfterExit = true;
Type = "oneshot";
};
};
Expand Down
7 changes: 5 additions & 2 deletions lychee.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Due to rate limiting from gnu.org
accept = [429]
exclude = [
# These websites return 403 forbidden codes for GitHub runners.
'^https://www\.digikey\.com',
'^https://www\.raspberrypi\.com',
]

cache = true
exclude_path = [".direnv/", "result/"]
Expand Down
Loading
Loading