Skip to content

Commit

Permalink
Merge pull request #2 from jwillikers/fix-lychee
Browse files Browse the repository at this point in the history
Support AirPlay 1 and 2 simultaneously and fix systemd service dependencies
  • Loading branch information
jwillikers authored Nov 14, 2024
2 parents 30ec9ec + 09144cc commit d7bbcea
Show file tree
Hide file tree
Showing 14 changed files with 210 additions and 85 deletions.
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

0 comments on commit d7bbcea

Please sign in to comment.