From c92631a33090c4003f5e914dc9b4d80be070ea82 Mon Sep 17 00:00:00 2001 From: Jordan Williams Date: Wed, 13 Nov 2024 07:47:10 -0600 Subject: [PATCH 01/19] Ignore lychee errors from Raspberry Pi's website --- lychee.toml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lychee.toml b/lychee.toml index bad0945..4f982a6 100644 --- a/lychee.toml +++ b/lychee.toml @@ -1,5 +1,9 @@ -# Due to rate limiting from gnu.org -accept = [429] +# accept = [429] + +exclude = [ + # Raspberry Pi's website returns 403 forbidden codes for GitHub runners. + '^https://www.raspberrypi.com', +] cache = true exclude_path = [".direnv/", "result/"] From 562e25e95377bc520a9d4a2675f347a4530a5a1d Mon Sep 17 00:00:00 2001 From: Jordan Williams Date: Wed, 13 Nov 2024 07:47:26 -0600 Subject: [PATCH 02/19] Note the reason for the long compilation time --- README.adoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index 55218bb..0efb26d 100644 --- a/README.adoc +++ b/README.adoc @@ -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 @@ -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. From 3ac4a566bf65b1e9f04c21f8baa7fb8d94a9a724 Mon Sep 17 00:00:00 2001 From: Jordan Williams Date: Wed, 13 Nov 2024 07:47:35 -0600 Subject: [PATCH 03/19] Reduce the number of tags fro the project --- .github/settings.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/settings.yml b/.github/settings.yml index 09baae2..ed3c5be 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -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. From 63356923856c13af0579e3313b1f2e13da5269d7 Mon Sep 17 00:00:00 2001 From: Jordan Williams Date: Wed, 13 Nov 2024 07:54:02 -0600 Subject: [PATCH 04/19] Reduce latency for the Snapcast client and Mopidy --- home-manager/_mixins/services/mopidy/default.nix | 2 +- home-manager/_mixins/services/snapclient/default.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/home-manager/_mixins/services/mopidy/default.nix b/home-manager/_mixins/services/mopidy/default.nix index 01baf6f..bdf37bb 100644 --- a/home-manager/_mixins/services/mopidy/default.nix +++ b/home-manager/_mixins/services/mopidy/default.nix @@ -18,7 +18,7 @@ lib.mkIf (lib.elem username installFor && role == "piceiver") { settings = { audio = { # todo Investigate buffer time. - # buffer_time = 200; + buffer_time = 400; mixer = "software"; mixer_volume = 50; output = "pipewiresink client-name=Mopidy target-object=snapserver"; diff --git a/home-manager/_mixins/services/snapclient/default.nix b/home-manager/_mixins/services/snapclient/default.nix index e775eef..69ec3b2 100644 --- a/home-manager/_mixins/services/snapclient/default.nix +++ b/home-manager/_mixins/services/snapclient/default.nix @@ -11,7 +11,7 @@ let [ "--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=75" ] ++ lib.optionals (role == "piceiver") [ "--host ::1" From b1ddf569f390c00e881e506781fc34983a08520d Mon Sep 17 00:00:00 2001 From: Jordan Williams Date: Wed, 13 Nov 2024 15:50:20 -0600 Subject: [PATCH 05/19] Update shairport-sync to fix AirPlay 1 support for Music Assistant --- README.adoc | 62 ++++++++++++++++++++++++++++++++++++++------ flake.nix | 3 ++- overlays/default.nix | 24 +++++++++++++---- 3 files changed, 75 insertions(+), 14 deletions(-) diff --git a/README.adoc b/README.adoc index 0efb26d..c190df8 100644 --- a/README.adoc +++ b/README.adoc @@ -320,10 +320,56 @@ 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}. 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 @@ -435,7 +481,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] @@ -484,11 +530,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 diff --git a/flake.nix b/flake.nix index e913f9a..fc788af 100644 --- a/flake.nix +++ b/flake.nix @@ -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; diff --git a/overlays/default.nix b/overlays/default.nix index 7db8531..cc25c3b 100644 --- a/overlays/default.nix +++ b/overlays/default.nix @@ -22,11 +22,6 @@ stoken = prev.stoken.override { withGTK3 = false; }; }; - # Enable AirPlay 2 support in shairport-sync. - shairport-sync-airplay2 = _final: prev: { - shairport-sync = prev.shairport-sync.override { enableAirplay2 = true; }; - }; - # Expose the PipeWire gstreamer plugin to Mopidy. mopidy-pipewire-gstreamer-plugin = _final: prev: { mopidy = prev.mopidy.overrideAttrs (_prevAttrs: { @@ -56,6 +51,25 @@ unstable = import inputs.nixpkgs-unstable { inherit (final) system; }; }; + # Use a newer version of shairport-sync to get the latest bug fixes. + # We need fixes for ffmpeg 7 which as-of-yet are unreleased. + unstable-shairport-sync = _final: prev: { + shairport-sync = prev.shairport-sync.overrideAttrs (_prevAttrs: { + version = "4.3.5-dev"; + src = prev.fetchFromGitHub { + repo = "shairport-sync"; + owner = "mikebrady"; + rev = "ab6225c1ac1c57f5af50890d722437ec8a921d0d"; + hash = "sha256-iwyIUUFA5DzTkm/DXvEa3buVX4Dje0P0svteRAKIS20="; + }; + }); + }; + + # Enable AirPlay 2 support in shairport-sync. + shairport-sync-airplay2 = _final: prev: { + shairport-sync = prev.shairport-sync.override { enableAirplay2 = true; }; + }; + realtime = _final: prev: { rpi-kernels.v6_6_54.bcm2711 = prev.rpi-kernels.v6_6_54.bcm2711.override (prevKernel: { modDirVersion = "6.6.54-rt44"; From d699a03781f35ebf6fb10281e0228a1a6f04adc7 Mon Sep 17 00:00:00 2001 From: Jordan Williams Date: Wed, 13 Nov 2024 15:53:11 -0600 Subject: [PATCH 06/19] Fix dependencies between audio services --- .../_mixins/services/rygel/default.nix | 24 +++++++++++++++---- .../services/shairport-sync/default.nix | 18 ++++++++++++-- .../_mixins/services/snapclient/default.nix | 13 ++++++++-- .../services/wireplumber-init/default.nix | 10 ++++++-- 4 files changed, 54 insertions(+), 11 deletions(-) diff --git a/home-manager/_mixins/services/rygel/default.nix b/home-manager/_mixins/services/rygel/default.nix index ca2ee5b..9eac4f1 100644 --- a/home-manager/_mixins/services/rygel/default.nix +++ b/home-manager/_mixins/services/rygel/default.nix @@ -1,6 +1,7 @@ { config, lib, + osConfig, pkgs, role, username, @@ -87,23 +88,36 @@ 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" ]; }; }; diff --git a/home-manager/_mixins/services/shairport-sync/default.nix b/home-manager/_mixins/services/shairport-sync/default.nix index a4b003c..6ca9347 100644 --- a/home-manager/_mixins/services/shairport-sync/default.nix +++ b/home-manager/_mixins/services/shairport-sync/default.nix @@ -1,5 +1,6 @@ { lib, + osConfig, pkgs, role, username, @@ -30,17 +31,30 @@ lib.mkIf (lib.elem username installFor && role == "piceiver") { "nqptp.service" "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" + ]; + # 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" - "pipewire.service" + "wireplumber-init.service" + ]; + Wants = [ "wireplumber.service" ]; - Wants = [ "wireplumber-init.service" ]; + X-Restart-Triggers = [ + "${osConfig.environment.etc."shairport-sync.conf".source}" + ]; }; Service = { ExecStart = "${pkgs.shairport-sync}/bin/shairport-sync"; Restart = "always"; + RestartSec = 10; }; Install = { WantedBy = [ "default.target" ]; diff --git a/home-manager/_mixins/services/snapclient/default.nix b/home-manager/_mixins/services/snapclient/default.nix index 69ec3b2..e094b70 100644 --- a/home-manager/_mixins/services/snapclient/default.nix +++ b/home-manager/_mixins/services/snapclient/default.nix @@ -26,22 +26,31 @@ 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 = [ + 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" ]; }; }; diff --git a/home-manager/_mixins/services/wireplumber-init/default.nix b/home-manager/_mixins/services/wireplumber-init/default.nix index 16c7100..d5a2f76 100644 --- a/home-manager/_mixins/services/wireplumber-init/default.nix +++ b/home-manager/_mixins/services/wireplumber-init/default.nix @@ -1,5 +1,6 @@ { lib, + osConfig, pkgs, role, username, @@ -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 = { @@ -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"; }; }; From 52dc20b8d9d9d1d834a6f27208ba5bac397da6e0 Mon Sep 17 00:00:00 2001 From: Jordan Williams Date: Wed, 13 Nov 2024 15:53:42 -0600 Subject: [PATCH 07/19] Reduce the buffer time for the Snapcast client --- home-manager/_mixins/services/snapclient/default.nix | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/home-manager/_mixins/services/snapclient/default.nix b/home-manager/_mixins/services/snapclient/default.nix index e094b70..3a41524 100644 --- a/home-manager/_mixins/services/snapclient/default.nix +++ b/home-manager/_mixins/services/snapclient/default.nix @@ -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:buffer_time=75" + "--player pulse:buffer_time=10" # Minimum is 10ms, default is 100ms ] ++ lib.optionals (role == "piceiver") [ "--host ::1" From 098aa6de6bfa702443a65aab40ecccc59c1e3ae5 Mon Sep 17 00:00:00 2001 From: Jordan Williams Date: Wed, 13 Nov 2024 15:54:02 -0600 Subject: [PATCH 08/19] Bump the default volume for the snapserver --- pkgs/initialize-wireplumber/initialize-wireplumber.nu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/initialize-wireplumber/initialize-wireplumber.nu b/pkgs/initialize-wireplumber/initialize-wireplumber.nu index 2be4d1b..4abb0bb 100755 --- a/pkgs/initialize-wireplumber/initialize-wireplumber.nu +++ b/pkgs/initialize-wireplumber/initialize-wireplumber.nu @@ -21,7 +21,7 @@ def main [] { let pw_dump = (^pw-dump) ^wpctl set-default ($pw_dump | get_node_object_id alsa_input.platform-1000110000.pcie-pci-0000_02_00.0.iec958-stereo Audio/Source) ^wpctl set-default ($pw_dump | get_node_object_id Combined_Stereo_Sink Audio/Sink) - ^wpctl set-volume ($pw_dump | get_node_object_id snapserver Audio/Sink) 60% + ^wpctl set-volume ($pw_dump | get_node_object_id snapserver Audio/Sink) 70% ^wpctl set-volume ($pw_dump | get_node_object_id alsa_output.platform-1000110000.pcie-pci-0000_02_00.0.iec958-stereo Audio/Sink) 40% ^wpctl set-volume ($pw_dump | get_node_object_id alsa_output.platform-soc_sound.stereo-fallback Audio/Sink) 85% ^wpctl set-volume ($pw_dump | get_node_object_id alsa_output.usb-C-Media_Electronics_Inc._USB_Audio_Device-00.analog-stereo Audio/Sink) 80% From 04382aeb299d799f0799dbad0705d80eb49607b1 Mon Sep 17 00:00:00 2001 From: Jordan Williams Date: Wed, 13 Nov 2024 15:54:18 -0600 Subject: [PATCH 09/19] Rename the Snapcast stream from Piceiver to default --- nixos/_mixins/services/snapserver/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/_mixins/services/snapserver/default.nix b/nixos/_mixins/services/snapserver/default.nix index c020cdc..d052c67 100644 --- a/nixos/_mixins/services/snapserver/default.nix +++ b/nixos/_mixins/services/snapserver/default.nix @@ -13,7 +13,7 @@ lib.mkIf (role == "piceiver") { openFirewall = true; sampleFormat = "48000:16:2"; streams = { - "Piceiver" = { + default = { location = "127.0.0.1:4711"; # todo Use IPv6 here when Snapcast supports it. query.mode = "client"; type = "tcp"; From c3fd806f1c56d6897174333333207cb10c6cb8e1 Mon Sep 17 00:00:00 2001 From: Jordan Williams Date: Wed, 13 Nov 2024 15:54:36 -0600 Subject: [PATCH 10/19] Tweak the default buffer time for the snapserver stream --- nixos/_mixins/services/snapserver/default.nix | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nixos/_mixins/services/snapserver/default.nix b/nixos/_mixins/services/snapserver/default.nix index d052c67..80bac08 100644 --- a/nixos/_mixins/services/snapserver/default.nix +++ b/nixos/_mixins/services/snapserver/default.nix @@ -6,7 +6,11 @@ }: lib.mkIf (role == "piceiver") { services.snapserver = { - buffer = 100; + # For responsiveness, the buffer needs to probably be as low as 100ms at least or you'll notice the pause while it buffers before playing a song. + # Wired-only: 50ms is too low, but 75ms works well. + # Wireless: 300ms seems to work pretty well for this case. + # I still might need to tweak it a bit. + buffer = 300; # Minimum is 20ms, default is 1000ms codec = "pcm"; enable = true; http.docRoot = pkgs.unstable.snapweb; # todo Remove this in 24.11 where it should be the default. From 27e5c843798b79eb14a7cc3888d7cd67fd354390 Mon Sep 17 00:00:00 2001 From: Jordan Williams Date: Wed, 13 Nov 2024 15:54:54 -0600 Subject: [PATCH 11/19] Reduce the buffer time for Mopidy --- home-manager/_mixins/services/mopidy/default.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/home-manager/_mixins/services/mopidy/default.nix b/home-manager/_mixins/services/mopidy/default.nix index bdf37bb..7d52d90 100644 --- a/home-manager/_mixins/services/mopidy/default.nix +++ b/home-manager/_mixins/services/mopidy/default.nix @@ -17,8 +17,8 @@ lib.mkIf (lib.elem username installFor && role == "piceiver") { ]; settings = { audio = { - # todo Investigate buffer time. - buffer_time = 400; + # 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"; From 676b959a4b45180f8db7bfedddfb98e0df654d7b Mon Sep 17 00:00:00 2001 From: Jordan Williams Date: Wed, 13 Nov 2024 16:14:56 -0600 Subject: [PATCH 12/19] Tune shairport-sync --- nixos/_mixins/services/shairport-sync/default.nix | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/nixos/_mixins/services/shairport-sync/default.nix b/nixos/_mixins/services/shairport-sync/default.nix index c61dbf8..7c22ce3 100644 --- a/nixos/_mixins/services/shairport-sync/default.nix +++ b/nixos/_mixins/services/shairport-sync/default.nix @@ -4,12 +4,19 @@ lib.mkIf (role == "piceiver") { boot.kernel.sysctl = { "net.ipv4.ip_unprivileged_port_start" = 319; }; + # I'm still tweaking the volume controls to get things just right. + # Because this sends audio to Snapcast, the volume is generously scaled up so as not to be too quiet. environment.etc."shairport-sync.conf".text = '' general = { - default_airplay_volume = -14.0; - high_threshold_airplay_volume = -6.0; + // Seems like 50ms works fine here. + // 0.35 is the default for the pa backend. + audio_backend_buffer_desired_length_in_seconds = 0.05; + default_airplay_volume = -12.0; + high_threshold_airplay_volume = -8.0; + high_volume_idle_timeout_in_minutes = 180; mdns_backend = "avahi"; output_backend = "pw"; + volume_control_profile = "dasl_tapered"; }; pw = { sink_target = "snapserver"; From c0a338443830cb8b6263bc6e7adbb538f98a37f2 Mon Sep 17 00:00:00 2001 From: Jordan Williams Date: Wed, 13 Nov 2024 16:15:14 -0600 Subject: [PATCH 13/19] Increase the default volume of the Snapserver a bit more --- pkgs/initialize-wireplumber/initialize-wireplumber.nu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/initialize-wireplumber/initialize-wireplumber.nu b/pkgs/initialize-wireplumber/initialize-wireplumber.nu index 4abb0bb..35271fb 100755 --- a/pkgs/initialize-wireplumber/initialize-wireplumber.nu +++ b/pkgs/initialize-wireplumber/initialize-wireplumber.nu @@ -21,7 +21,7 @@ def main [] { let pw_dump = (^pw-dump) ^wpctl set-default ($pw_dump | get_node_object_id alsa_input.platform-1000110000.pcie-pci-0000_02_00.0.iec958-stereo Audio/Source) ^wpctl set-default ($pw_dump | get_node_object_id Combined_Stereo_Sink Audio/Sink) - ^wpctl set-volume ($pw_dump | get_node_object_id snapserver Audio/Sink) 70% + ^wpctl set-volume ($pw_dump | get_node_object_id snapserver Audio/Sink) 85% ^wpctl set-volume ($pw_dump | get_node_object_id alsa_output.platform-1000110000.pcie-pci-0000_02_00.0.iec958-stereo Audio/Sink) 40% ^wpctl set-volume ($pw_dump | get_node_object_id alsa_output.platform-soc_sound.stereo-fallback Audio/Sink) 85% ^wpctl set-volume ($pw_dump | get_node_object_id alsa_output.usb-C-Media_Electronics_Inc._USB_Audio_Device-00.analog-stereo Audio/Sink) 80% From ab23873db15f482e39d205e65c10a5cafcf193d1 Mon Sep 17 00:00:00 2001 From: Jordan Williams Date: Wed, 13 Nov 2024 16:35:37 -0600 Subject: [PATCH 14/19] Document Shairport-Sync limitation to only run one protocal at a time --- README.adoc | 6 +++++- nixos/_mixins/services/shairport-sync/default.nix | 9 +++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/README.adoc b/README.adoc index c190df8..cd47a52 100644 --- a/README.adoc +++ b/README.adoc @@ -322,7 +322,9 @@ To make it possible to switch between outputs, I'd need to add a button and some https://www.apple.com/airplay/[AirPlay] 1 and 2 are supported via {Shairport-Sync}. It works very nicely. -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 one thing to be aware of is that it doesn't support both 1 and 2 simultaneously. +It will effectively be set based on the initial connection. +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, using AirPlay 1. The following instructions document how to accomplish this. [NOTE] @@ -531,6 +533,7 @@ systemctl --user restart pipewire === Music Assistant The Piceiver may be integrated with {Music-Assistant} as an external Snapcast server, an AirPlay playback provider, and a UPnP/DLNA player provider. +Only AirPlay 1 is supported. 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. @@ -651,6 +654,7 @@ Using a new image every time isn't gonna fly with flash storage or the value of 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. .Todo +* Run two instances of Shairport-Sync simultaneously, one for AirPlay 1 and one for AirPlay 2. * 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. diff --git a/nixos/_mixins/services/shairport-sync/default.nix b/nixos/_mixins/services/shairport-sync/default.nix index 7c22ce3..0fc7953 100644 --- a/nixos/_mixins/services/shairport-sync/default.nix +++ b/nixos/_mixins/services/shairport-sync/default.nix @@ -4,13 +4,14 @@ lib.mkIf (role == "piceiver") { boot.kernel.sysctl = { "net.ipv4.ip_unprivileged_port_start" = 319; }; - # I'm still tweaking the volume controls to get things just right. # Because this sends audio to Snapcast, the volume is generously scaled up so as not to be too quiet. environment.etc."shairport-sync.conf".text = '' general = { - // Seems like 50ms works fine here. - // 0.35 is the default for the pa backend. - audio_backend_buffer_desired_length_in_seconds = 0.05; + // Seems like 500ms works well here when I stream using PipeWire's RAOP module from my laptop. + // When streaming from an iPhone, it seemed like I could get down to 50ms without a problem. + // I didn't try lower. + // The default for the PulseAudio backend is 350ms. + audio_backend_buffer_desired_length_in_seconds = 0.5; default_airplay_volume = -12.0; high_threshold_airplay_volume = -8.0; high_volume_idle_timeout_in_minutes = 180; From d7e54db6c33db12f5e31daaff42db387990c5cd2 Mon Sep 17 00:00:00 2001 From: Jordan Williams Date: Wed, 13 Nov 2024 17:34:09 -0600 Subject: [PATCH 15/19] Support AirPlay 1 and AirPlay 2 simultaneously --- README.adoc | 7 +-- .../services/shairport-sync/default.nix | 45 +++++++++++++++++-- .../services/shairport-sync/default.nix | 31 ++++++++++++- overlays/default.nix | 2 +- 4 files changed, 74 insertions(+), 11 deletions(-) diff --git a/README.adoc b/README.adoc index cd47a52..cc098bc 100644 --- a/README.adoc +++ b/README.adoc @@ -321,10 +321,9 @@ To make it possible to switch between outputs, I'd need to add a button and some === AirPlay 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. -The one thing to be aware of is that it doesn't support both 1 and 2 simultaneously. -It will effectively be set based on the initial connection. -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, using AirPlay 1. +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] @@ -533,7 +532,6 @@ systemctl --user restart pipewire === Music Assistant The Piceiver may be integrated with {Music-Assistant} as an external Snapcast server, an AirPlay playback provider, and a UPnP/DLNA player provider. -Only AirPlay 1 is supported. 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. @@ -654,7 +652,6 @@ Using a new image every time isn't gonna fly with flash storage or the value of 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. .Todo -* Run two instances of Shairport-Sync simultaneously, one for AirPlay 1 and one for AirPlay 2. * 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. diff --git a/home-manager/_mixins/services/shairport-sync/default.nix b/home-manager/_mixins/services/shairport-sync/default.nix index 6ca9347..6a95344 100644 --- a/home-manager/_mixins/services/shairport-sync/default.nix +++ b/home-manager/_mixins/services/shairport-sync/default.nix @@ -14,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"; @@ -24,12 +24,14 @@ 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. @@ -45,6 +47,7 @@ lib.mkIf (lib.elem username installFor && role == "piceiver") { "wireplumber-init.service" ]; Wants = [ + "shairport-sync-airplay-1.service" "wireplumber.service" ]; X-Restart-Triggers = [ @@ -52,7 +55,41 @@ lib.mkIf (lib.elem username installFor && role == "piceiver") { ]; }; Service = { - ExecStart = "${pkgs.shairport-sync}/bin/shairport-sync"; + 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" + ]; + # 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 --configfile=${osConfig.environment.etc."shairport-sync-airplay-1.conf".source}"; Restart = "always"; RestartSec = 10; }; diff --git a/nixos/_mixins/services/shairport-sync/default.nix b/nixos/_mixins/services/shairport-sync/default.nix index 0fc7953..04ed723 100644 --- a/nixos/_mixins/services/shairport-sync/default.nix +++ b/nixos/_mixins/services/shairport-sync/default.nix @@ -7,6 +7,33 @@ lib.mkIf (role == "piceiver") { # Because this sends audio to Snapcast, the volume is generously scaled up so as not to be too quiet. environment.etc."shairport-sync.conf".text = '' general = { + // Seems like 500ms works well here when I stream using PipeWire's RAOP module from my laptop. + // The default for the PulseAudio backend is 350ms. + audio_backend_buffer_desired_length_in_seconds = 0.5; + default_airplay_volume = -12.0; + high_threshold_airplay_volume = -8.0; + high_volume_idle_timeout_in_minutes = 180; + mdns_backend = "avahi"; + output_backend = "pw"; + // Use the AirPlay 2 port only. + port = 7000; + // Only advertise AirPlay 2. + regtype = "_airplay._tcp"; + volume_control_profile = "dasl_tapered"; + }; + pw = { + sink_target = "snapserver"; + }; + sessioncontrol = { + session_timeout = 20; + }; + diagnostics = { + log_verbosity = 1; + }; + ''; + environment.etc."shairport-sync-airplay-1.conf".text = '' + general = { + name = "%H (AirPlay 1️⃣)"; // Seems like 500ms works well here when I stream using PipeWire's RAOP module from my laptop. // When streaming from an iPhone, it seemed like I could get down to 50ms without a problem. // I didn't try lower. @@ -20,13 +47,15 @@ lib.mkIf (role == "piceiver") { volume_control_profile = "dasl_tapered"; }; pw = { + application_name = "Shairport Sync AirPlay 1"; + node_name = "Shairport Sync AirPlay 1"; sink_target = "snapserver"; }; sessioncontrol = { session_timeout = 20; }; diagnostics = { - log_verbosity = 1; + log_verbosity = 2; }; ''; networking.firewall = { diff --git a/overlays/default.nix b/overlays/default.nix index cc25c3b..d289ab2 100644 --- a/overlays/default.nix +++ b/overlays/default.nix @@ -67,7 +67,7 @@ # Enable AirPlay 2 support in shairport-sync. shairport-sync-airplay2 = _final: prev: { - shairport-sync = prev.shairport-sync.override { enableAirplay2 = true; }; + shairport-sync-airplay-2 = prev.shairport-sync.override { enableAirplay2 = true; }; }; realtime = _final: prev: { From 8a7e4efe2608f1ddf59f9600cd9ba614d112a053 Mon Sep 17 00:00:00 2001 From: Jordan Williams Date: Thu, 14 Nov 2024 06:37:58 -0600 Subject: [PATCH 16/19] Fix Lychee? --- lychee.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lychee.toml b/lychee.toml index 4f982a6..5dd7d26 100644 --- a/lychee.toml +++ b/lychee.toml @@ -1,8 +1,9 @@ # accept = [429] exclude = [ + '^https://www\.digikey\.com', # Raspberry Pi's website returns 403 forbidden codes for GitHub runners. - '^https://www.raspberrypi.com', + '^https://www\.raspberrypi\.com', ] cache = true From 5dffa0ce2b724a62f0dd60d8ccd724dd33dac302 Mon Sep 17 00:00:00 2001 From: Jordan Williams Date: Thu, 14 Nov 2024 06:42:01 -0600 Subject: [PATCH 17/19] Format files --- .../_mixins/services/rygel/default.nix | 16 +++-------- .../services/shairport-sync/default.nix | 28 ++++++------------- .../_mixins/services/snapclient/default.nix | 12 ++------ 3 files changed, 16 insertions(+), 40 deletions(-) diff --git a/home-manager/_mixins/services/rygel/default.nix b/home-manager/_mixins/services/rygel/default.nix index 9eac4f1..d3534b3 100644 --- a/home-manager/_mixins/services/rygel/default.nix +++ b/home-manager/_mixins/services/rygel/default.nix @@ -93,16 +93,10 @@ lib.mkIf (lib.elem username installFor && role == "piceiver") { "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" ]; + PartOf = [ "pipewire.service" ]; + Requires = [ "wireplumber-init.service" ]; + Wants = [ "wireplumber.service" ]; X-Restart-Triggers = [ "${osConfig.environment.etc."rygel.conf".source}" "${config.home.file."${config.xdg.configHome}/rygel.conf".source}" @@ -116,9 +110,7 @@ lib.mkIf (lib.elem username installFor && role == "piceiver") { Type = "dbus"; }; Install = { - WantedBy = [ - "default.target" - ]; + WantedBy = [ "default.target" ]; }; }; }; diff --git a/home-manager/_mixins/services/shairport-sync/default.nix b/home-manager/_mixins/services/shairport-sync/default.nix index 6a95344..814aaba 100644 --- a/home-manager/_mixins/services/shairport-sync/default.nix +++ b/home-manager/_mixins/services/shairport-sync/default.nix @@ -39,9 +39,7 @@ lib.mkIf (lib.elem username installFor && role == "piceiver") { ]; # 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" - ]; + PartOf = [ "pipewire.service" ]; Requires = [ "nqptp.service" "wireplumber-init.service" @@ -50,9 +48,7 @@ lib.mkIf (lib.elem username installFor && role == "piceiver") { "shairport-sync-airplay-1.service" "wireplumber.service" ]; - X-Restart-Triggers = [ - "${osConfig.environment.etc."shairport-sync.conf".source}" - ]; + X-Restart-Triggers = [ "${osConfig.environment.etc."shairport-sync.conf".source}" ]; }; Service = { ExecStart = "${pkgs.shairport-sync-airplay-2}/bin/shairport-sync"; @@ -75,21 +71,15 @@ lib.mkIf (lib.elem username installFor && role == "piceiver") { ]; # 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}" - ]; + 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 --configfile=${osConfig.environment.etc."shairport-sync-airplay-1.conf".source}"; + ExecStart = "${pkgs.shairport-sync}/bin/shairport-sync --configfile=${ + osConfig.environment.etc."shairport-sync-airplay-1.conf".source + }"; Restart = "always"; RestartSec = 10; }; diff --git a/home-manager/_mixins/services/snapclient/default.nix b/home-manager/_mixins/services/snapclient/default.nix index 3a41524..e6e75af 100644 --- a/home-manager/_mixins/services/snapclient/default.nix +++ b/home-manager/_mixins/services/snapclient/default.nix @@ -31,12 +31,8 @@ lib.mkIf (lib.elem username installFor) { # The wireplumber-init service ensures the volume is set correctly before playback starts. "wireplumber-init.service" ]; - PartOf = [ - "pipewire-pulse.service" - ]; - Requires = [ - "wireplumber-init.service" - ]; + PartOf = [ "pipewire-pulse.service" ]; + Requires = [ "wireplumber-init.service" ]; Wants = [ "pipewire.service" "wireplumber.service" @@ -48,9 +44,7 @@ lib.mkIf (lib.elem username installFor) { RestartSec = 10; }; Install = { - WantedBy = [ - "default.target" - ]; + WantedBy = [ "default.target" ]; }; }; }; From dbbe33a8a9ea025318fe9899d7e1414d0a7c77fd Mon Sep 17 00:00:00 2001 From: Jordan Williams Date: Thu, 14 Nov 2024 06:43:02 -0600 Subject: [PATCH 18/19] Fix Lychee --- lychee.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lychee.toml b/lychee.toml index 5dd7d26..b7bd8e6 100644 --- a/lychee.toml +++ b/lychee.toml @@ -1,8 +1,6 @@ -# accept = [429] - exclude = [ + # These websites return 403 forbidden codes for GitHub runners. '^https://www\.digikey\.com', - # Raspberry Pi's website returns 403 forbidden codes for GitHub runners. '^https://www\.raspberrypi\.com', ] From 09144cc16c751b5b2a7c3870c84ccd782f6ae27e Mon Sep 17 00:00:00 2001 From: Jordan Williams Date: Thu, 14 Nov 2024 06:49:31 -0600 Subject: [PATCH 19/19] Remove build that is too much for GitHub hosted runners --- .github/workflows/build.yaml | 37 ------------------------------------ 1 file changed, 37 deletions(-) delete mode 100644 .github/workflows/build.yaml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml deleted file mode 100644 index 3d9da47..0000000 --- a/.github/workflows/build.yaml +++ /dev/null @@ -1,37 +0,0 @@ -name: Build -"on": - pull_request: - paths: - - "**.nix" - - .github/workflows/build.yaml - - flake.lock - push: - branches: - - main - paths: - - "**.nix" - - .github/workflows/build.yaml - - flake.lock - schedule: - # Run at 04:10 on Sunday - - cron: "10 4 * * 0" - -jobs: - build: - name: Build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - # - name: Set up QEMU - # uses: docker/setup-qemu-action@v3 - - uses: DeterminateSystems/nix-installer-action@v15 - # with: - # extra-conf: | - # extra-platforms = aarch64-linux - - uses: DeterminateSystems/magic-nix-cache-action@v8 - - name: Build the image - run: nix build '.#piceiver-sd-image' - - uses: actions/upload-artifact@v4 - with: - name: piceiver-sd-image - path: result/sd-image/nixos-sd-image-*-aarch64-linux.img.zst