diff --git a/README.md b/README.md
index 23015ac..f6ab645 100644
--- a/README.md
+++ b/README.md
@@ -1,83 +1,186 @@
# RaspberryPi-AirPlay-Installer π»
-Turn any Raspberry Pi (Zero 2 W, 3, 4, 5) into a modern, high-quality AirPlay 2 receiver in just 5 minutes. This project uses a set of robust scripts to automate the entire installation process, making it incredibly easy to revive your old home theater or favorite speakers.
+Turn any Raspberry Pi (Zero 2 W, 3, 4, 5) into a modern, high-quality **AirPlay 2** receiver β and, optionally, a **Spotify Connect** endpoint β in just a few minutes. This project automates the entire build of [Shairport-Sync](https://github.com/mikebrady/shairport-sync) + [NQPTP](https://github.com/mikebrady/nqptp) and the install/configuration of [raspotify](https://github.com/dtcooper/raspotify) with a set of robust, interactive scripts.
-> **If you find this project helpful, please consider giving it a β star on GitHub!** It helps others discover it and shows your appreciation for the work. Also, please like the video and **[subscribe to the channel](https://www.youtube.com/@ravis1ngh)**. It helps us create more content like this.
+> **If you find this project helpful, please consider giving it a β star on GitHub!**
-The goal of this project was to simplify the manual installation process, making it accessible to everyone.
+---
+
+## β¨ Features
-| The Old, Manual Way (40+ Minutes) | The New, Automated Way (5 Minutes!) |
-| :---: | :---: |
-| [](https://www.youtube.com/watch?v=WeibcfMywXU) | **[Link to New Video Coming Soon!]**
*(Placeholder for your new, shorter video)* |
+* **π Fast setup** β From a fresh Raspberry Pi OS to a working AirPlay 2 speaker in minutes.
+* **π€ Fully automated** β Handles system update, dependencies, compiling and configuration.
+* **β
Smart pre-flight checks** β Validates internet, disk space, memory and detected hardware before changing anything.
+* **π Flexible audio** β Works with **USB DAC**, **audio HAT**, or the **Raspberry Pi's built-in audio** (3.5mm jack / HDMI). All detected devices are listed and labelled `[built-in]` / `[external/DAC]`.
+* **π§ Optional Spotify Connect** β Installer can also set up `raspotify` (librespot) so the same Pi appears as a Spotify Connect endpoint. Coexists with AirPlay; one source plays at a time.
+* **π οΈ Idempotent management** β Dedicated scripts to **modify** or **uninstall** an existing installation without reinstalling from scratch.
+* **ποΈ Volume control aware** β Auto-selects the best ALSA mixer (`PCM`, `Master`, `Speaker`, ...) and falls back to software volume if no hardware mixer is available.
+* **π Rollback on failure** β Backs up configuration files and cleans up on failed installs.
+* **π Detailed logging** β Every installation writes a timestamped log under `/tmp/airplay_install_*.log`.
---
-### β¨ Features
+## π§° Hardware Requirements
+
+| Component | Recommended |
+| --- | --- |
+| Raspberry Pi | Zero 2 W, 3, 4 or 5 |
+| MicroSD card | Quality card, β₯ 8 GB |
+| Power supply | Official PSU for your Pi |
+| Audio output | USB DAC, audio HAT **or** built-in 3.5mm / HDMI |
-* **π 5-Minute Setup:** Go from a fresh Raspberry Pi OS to a working AirPlay 2 speaker in minutes.
-* **π€ Fully Automated:** The script handles system updates, dependency installation, compiling, and configuration.
-* **β
Smart Pre-Checks:** A pre-installation script verifies your system is ready, checking for internet, disk space, and audio devices to prevent errors.
-* **π USB DAC Auto-Detection:** Intelligently finds your external USB sound card and lets you choose the correct one if you have multiple.
-* **βοΈ Optimized for Performance:** Automatically configures settings for the best audio quality and prompts to disable Wi-Fi power saving to prevent dropouts.
-* **π οΈ Robust & Reliable:** Includes error handling and detailed logging for easy troubleshooting.
+> The older Pi 1 / Pi Zero W are not officially supported β they typically lack the CPU headroom for AirPlay 2.
---
-### Hardware Requirements
+## π¦ What's in the box
-* **Raspberry Pi:** A Pi Zero 2 W, 3, 4, or 5 is recommended.
-* **MicroSD Card:** A quality card with at least 8GB.
-* **Power Supply:** The official power supply for your Pi model.
-* **Audio Output:**
- * For Pi Zero: An **OTG cable** and a **USB DAC** with a 3.5mm output.
- * For Pi 3/4/5: The built-in 3.5mm jack or an optional USB DAC.
+All scripts live under `RaspberryPi-AirPlay-Installer-Scripts/`:
+
+| Script | Purpose |
+| --- | --- |
+| `pre_check_airplay_on_pi.sh` | Read-only system check before installing. |
+| `install_airplay_v3.sh` | Main installer: deps, build, service, config. |
+| `modify_airplay.sh` | Edit an existing install (name, audio device, mixer, volume...). |
+| `uninstall_airplay.sh` | Cleanly remove Shairport-Sync, NQPTP, services and config. |
+| `airplay_manager.sh` | Unified menu that dispatches to the three scripts above. |
---
-### π Quick Start Installation
+## π Quick Start
+
+After flashing **Raspberry Pi OS** (Lite is fine) and connecting via SSH, you have two options.
-After installing Raspberry Pi OS Lite and connecting to your Pi via SSH, run this single command. It will download the pre-check script and, if successful, automatically launch the main installer.
+### Option A β Run from this repo (recommended for development)
+
+```bash
+git clone https://github.com/Techposts/RaspberryPi-AirPlay-Installer.git
+or git clone https://github.com/ermanno00/RaspberryPi-AirPlay-Installer.git
+cd RaspberryPi-AirPlay-Installer/RaspberryPi-AirPlay-Installer-Scripts
+bash airplay_manager.sh # unified menu
+```
+
+The menu lets you install, modify or uninstall, and tail live service logs.
+
+### Option B β One-shot install from upstream
```bash
curl -sSL https://raw.githubusercontent.com/Techposts/RaspberryPi-AirPlay-Installer/main/RaspberryPi-AirPlay-Installer-Scripts/pre_check_airplay_on_pi.sh | bash
-curl -sSl https://raw.githubusercontent.com/Techposts/RaspberryPi-AirPlay-Installer/main/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh | bash
+curl -sSL https://raw.githubusercontent.com/Techposts/RaspberryPi-AirPlay-Installer/main/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh | bash
```
-The script is interactive and will guide you through the process. Once finished, it will reboot, and your AirPlay 2 receiver will be ready to use!
+The installer is interactive: it will ask you to pick the audio device, give your AirPlay endpoint a name and decide on Wi-Fi power management. When it's done, the Pi will offer to reboot and your speaker is ready.
+
+> **Do not run any of these scripts with `sudo`.** They invoke `sudo` only where needed and will refuse to start as `root`.
---
-### β
The Final Result
+## ποΈ Modifying an existing installation
-When you're done, your setup will be seamless. Your Raspberry Pi will appear as a native AirPlay device on your network, ready to stream from any Apple device.
+Need to rename the speaker, change the audio output or adjust volume limits? You don't have to reinstall.
-| Mobile Screenshot | Hardware Setup |
-| :---: | :---: |
-| ****
*Your new device, ready to connect.* | ****
*The simple and clean hardware setup.* |
+```bash
+bash modify_airplay.sh
+```
+
+The interactive menu provides:
+
+1. Change AirPlay name
+2. Change audio output device (USB DAC / HAT / built-in)
+3. Change mixer / hardware volume control (or disable it)
+4. Change volume limits (`volume_max_db`, `default_airplay_volume`)
+5. Test audio output
+6. View current configuration
+7. Show service status
+8. Restart service
+9. Edit `/etc/shairport-sync.conf` manually (+ auto restart)
+
+All changes are written to `/etc/shairport-sync.conf` and the service is restarted automatically.
+
+The Spotify Connect section of the same menu lets you:
+
+* Install / reconfigure Spotify Connect (raspotify)
+* Change the Spotify device name (independent from the AirPlay one)
+* Sync the Spotify audio device to the current AirPlay one
+* Uninstall Spotify Connect
+
+> **Spotify Premium is required** on the controller device.
+> Shairport-Sync and raspotify share the same ALSA card, so only one source can play at a time β the inactive one releases the device automatically, no extra config needed.
---
-### How It Works
+## π§Ή Uninstalling
+
+```bash
+bash uninstall_airplay.sh
+```
+
+Removes:
+
+* `shairport-sync` and `nqptp` binaries
+* `/etc/shairport-sync.conf` and sample
+* systemd units (`shairport-sync.service`, `nqptp.service`)
+* The `shairport-sync` user and group
+* UFW firewall rules added during install (`5353/udp`, `319/udp`, `320/udp`, `7000/tcp`)
+* `raspotify` package + apt repository, if Spotify Connect was installed
-This project uses a two-script system for a safe and reliable installation:
+A backup of the current config is saved under `/tmp/airplay_uninstall_backup_/` before anything is deleted.
-1. **`pre_check_airplay_on_pi.sh`:** A non-invasive script that checks your system for common issues without making any changes. If all checks pass, it automatically downloads and runs the main installer.
-2. **`install_airplay_v3.sh`:** The powerful main installer that performs all the required actions to build and configure the AirPlay 2 software (`Shairport-Sync` and `nqptp`).
+> APT build dependencies (`libsoxr-dev`, `libplist-dev`, ...) are intentionally **not** removed β other software on your system may rely on them. The uninstaller prints the `apt-get` command to remove them manually if you want a fully clean state.
---
-### β€οΈ Support the Project
+## π Troubleshooting
+
+**`configure: error: plistutil can not be found`** (Debian 13 "Trixie" / Pi OS Bookworm successor)
+
+On recent releases the `plistutil` binary moved from `libplist-dev` to a separate package `libplist-utils`. The installer in this repo already pulls it in. If you hit this on an older copy:
+
+```bash
+sudo apt-get install -y libplist-utils
+bash install_airplay_v3.sh
+```
+
+**The Pi doesn't appear in the AirPlay picker**
+
+* Make sure iPhone/iPad and Pi are on the **same Wi-Fi network and same subnet**.
+* Check that `avahi-daemon` is running: `systemctl status avahi-daemon`.
+* Tail the service: `sudo journalctl -u shairport-sync -f`.
+
+**Audio stutters / drops out**
-If this installer helped you bring your old speakers back to life, please consider showing your support!
+* Disable Wi-Fi power management: `sudo raspi-config` β Performance β Wireless LAN β Power Management β Disable.
+* On Pi Zero 2, prefer a wired ethernet adapter or stay close to the access point.
-* **β Star the Repository:** Starring this project on GitHub is a great way to show your appreciation and helps others find it.
-* **π Like & Subscribe:** If you came from the video tutorial, please **like the video** and **[subscribe to the channel](https://www.youtube.com/@ravis1ngh)**. It helps us create more content like this.
+**Useful one-liners**
+
+```bash
+sudo systemctl status shairport-sync # service health
+sudo journalctl -u shairport-sync -f # live logs
+sudo nano /etc/shairport-sync.conf # manual edit (then restart)
+sudo systemctl restart shairport-sync
+```
---
+## βοΈ How it works
-### License
+1. **`pre_check_airplay_on_pi.sh`** β non-invasive system check (no changes made).
+2. **`install_airplay_v3.sh`** β installs build deps, clones and compiles `nqptp` and `shairport-sync` with `--with-airplay-2`, writes `/etc/shairport-sync.conf`, creates a systemd service and a dedicated user, configures UFW if active.
+3. **`modify_airplay.sh`** β edits `/etc/shairport-sync.conf` in place via targeted `sed` rules and restarts the service.
+4. **`uninstall_airplay.sh`** β reverses everything the installer did, in dependency-safe order.
+5. **`airplay_manager.sh`** β thin wrapper that picks the right script based on what's currently installed.
-This project is licensed under the MIT License. See the `LICENSE` file for details.
+---
+## π Credits
+* [Mike Brady](https://github.com/mikebrady) β author of Shairport-Sync and NQPTP, the upstream projects that make all of this possible.
+* [David Cooper](https://github.com/dtcooper) β author of [raspotify](https://github.com/dtcooper/raspotify), used here for optional Spotify Connect support.
+* Original installer scripts: [Techposts/RaspberryPi-AirPlay-Installer](https://github.com/Techposts/RaspberryPi-AirPlay-Installer).
+
+---
+
+## π License
+
+This project is licensed under the MIT License. See the `LICENSE` file for details.
diff --git a/RaspberryPi-AirPlay-Installer-Scripts/airplay_manager.sh b/RaspberryPi-AirPlay-Installer-Scripts/airplay_manager.sh
new file mode 100755
index 0000000..56b3f2c
--- /dev/null
+++ b/RaspberryPi-AirPlay-Installer-Scripts/airplay_manager.sh
@@ -0,0 +1,141 @@
+#!/bin/bash
+
+# ===================================================================================
+# AirPlay 2 Manager β Unified menu for install / modify / uninstall
+#
+# Dispatches to the dedicated scripts in the same directory:
+# install_airplay_v3.sh β First-time installation
+# modify_airplay.sh β Modify existing installation
+# uninstall_airplay.sh β Remove the installation
+# ===================================================================================
+
+set -eo pipefail
+IFS=$'\n\t'
+
+SCRIPT_VERSION="1.0"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+cecho() {
+ local code="\033["
+ local color
+ case "$1" in
+ "red") color="${code}1;31m" ;;
+ "green") color="${code}1;32m" ;;
+ "yellow") color="${code}1;33m" ;;
+ "blue") color="${code}1;34m" ;;
+ "magenta") color="${code}1;35m" ;;
+ "cyan") color="${code}1;36m" ;;
+ *) color="${code}0m" ;;
+ esac
+ echo -e "${color}$2\033[0m"
+}
+
+run_script() {
+ local script_name="$1"
+ local script_path="$SCRIPT_DIR/$script_name"
+ if [ ! -f "$script_path" ]; then
+ cecho "red" "β Script not found: $script_path"
+ read -p "Press Enter to continue..." || true
+ return 1
+ fi
+ echo
+ bash "$script_path" || true
+ echo
+ read -p "Press Enter to return to the menu..." || true
+}
+
+is_installed() {
+ [ -f /etc/shairport-sync.conf ] && command -v shairport-sync >/dev/null 2>&1
+}
+
+service_status_line() {
+ if systemctl is-active --quiet shairport-sync 2>/dev/null; then
+ echo "active"
+ elif systemctl list-unit-files 2>/dev/null | grep -q '^shairport-sync\.service'; then
+ echo "inactive"
+ else
+ echo "not registered"
+ fi
+}
+
+current_name() {
+ [ -f /etc/shairport-sync.conf ] || { echo ""; return; }
+ grep -oE '^[[:space:]]*name[[:space:]]*=[[:space:]]*"[^"]*"' /etc/shairport-sync.conf 2>/dev/null \
+ | head -1 | sed -E 's/.*"([^"]*)".*/\1/' || true
+}
+
+spotify_installed() {
+ dpkg -l raspotify 2>/dev/null | grep -q '^ii'
+}
+
+spotify_status_line() {
+ if ! spotify_installed; then
+ echo "not installed"
+ elif systemctl is-active --quiet raspotify 2>/dev/null; then
+ echo "active"
+ else
+ echo "inactive"
+ fi
+}
+
+while true; do
+ clear
+ cecho "green" "βββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
+ cecho "green" "β AirPlay 2 Manager (Raspberry Pi) v$SCRIPT_VERSION β"
+ cecho "green" "βββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
+ echo
+ if is_installed; then
+ cecho "green" " β Shairport-Sync installed"
+ cecho "blue" " AirPlay service: $(service_status_line)"
+ cecho "blue" " AirPlay name: $(current_name)"
+ cecho "blue" " Spotify Connect: $(spotify_status_line)"
+ else
+ cecho "yellow" " β Shairport-Sync NOT installed."
+ fi
+ echo
+ echo " 1) Install AirPlay 2"
+ echo " 2) Modify existing installation"
+ echo " 3) Uninstall"
+ echo " 4) Show service logs (live, Ctrl+C to exit)"
+ echo " 5) Rebuild / repair (fixes crashes after 'apt upgrade')"
+ echo " 0) Exit"
+ echo
+ read -p "Choose: " choice || true
+ case "$choice" in
+ 1) run_script "install_airplay_v3.sh" ;;
+ 2)
+ if ! is_installed; then
+ cecho "red" "β No installation detected. Install first."
+ read -p "Press Enter..." || true
+ continue
+ fi
+ run_script "modify_airplay.sh"
+ ;;
+ 3)
+ if ! is_installed; then
+ cecho "red" "β No installation detected to uninstall."
+ read -p "Press Enter..." || true
+ continue
+ fi
+ run_script "uninstall_airplay.sh"
+ ;;
+ 4)
+ if ! is_installed; then
+ cecho "red" "β No installation detected."
+ read -p "Press Enter..." || true
+ continue
+ fi
+ sudo journalctl -u shairport-sync -f || true
+ ;;
+ 5)
+ if ! is_installed; then
+ cecho "red" "β No installation detected. Install first."
+ read -p "Press Enter..." || true
+ continue
+ fi
+ run_script "repair_airplay.sh"
+ ;;
+ 0|q|Q|"") cecho "blue" "Bye!"; exit 0 ;;
+ *) cecho "red" "Invalid choice."; sleep 1 ;;
+ esac
+done
diff --git a/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh b/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh
old mode 100644
new mode 100755
index 6b9cbee..93e8381
--- a/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh
+++ b/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh
@@ -3,7 +3,9 @@
# ===================================================================================
# Shairport-Sync AirPlay 2 ROBUST Installer - ENHANCED VERSION 3.0
#
-# Tailored for: Raspberry Pi (Zero 2/3/4/5) with USB DAC
+# Tailored for: Raspberry Pi (Zero 2/3/4/5) with USB DAC, audio HAT
+# or built-in audio (3.5mm jack / HDMI)
+# Optional: Spotify Connect support via the raspotify package
# Version: 3.0 - Production Ready
# Features:
# - Comprehensive error handling with rollback capability
@@ -24,6 +26,17 @@ LOG_FILE="/tmp/airplay_install_$(date +%Y%m%d_%H%M%S).log"
BACKUP_DIR="/tmp/airplay_backup_$(date +%Y%m%d_%H%M%S)"
INSTALLATION_FAILED=0
+# Pinned upstream versions.
+# We deliberately build from stable release tags instead of the `master`
+# branches. Shairport-Sync 5.0 is a major rewrite that introduced a new
+# "encoded output format" engine; recent dev builds crash on some DACs with
+# "fatal error: Unexpected SPS_FORMAT_* with index N while outputting silence".
+# 5.0 also changed the config-file format, which our sed edits below do not
+# target. The 4.3.x line is the long-proven AirPlay 2 stable line and uses the
+# config keys this installer writes. nqptp 1.2.8 is the matching stable timer.
+SHAIRPORT_VERSION="4.3.7"
+NQPTP_VERSION="1.2.8"
+
# Audio configuration variables
audio_device=""
audio_device_plug=""
@@ -34,6 +47,12 @@ selected_device=""
airplay_name=""
disable_wifi_pm=false
+# Spotify Connect configuration
+install_spotify=false
+spotify_name=""
+spotify_bitrate="320"
+SPOTIFY_ZEROCONF_PORT="5354"
+
# --- Cleanup Handler ---
cleanup() {
local exit_code=$?
@@ -45,6 +64,7 @@ cleanup() {
# Stop services if they were started
sudo systemctl stop shairport-sync 2>/dev/null || true
sudo systemctl stop nqptp 2>/dev/null || true
+ sudo systemctl stop raspotify 2>/dev/null || true
# Restore backups if they exist
if [ -d "$BACKUP_DIR" ]; then
@@ -153,6 +173,74 @@ safe_cd() {
}
}
+# Pin the card's hardware mixer to 100% and keep it there across reboots.
+# We drive AirPlay and Spotify with SOFTWARE volume (so each source has its own
+# independent volume), which means the DAC's hardware mixer must stay at maximum
+# as a common ceiling. How we keep it there depends on the audio stack:
+# - PipeWire/WirePlumber (Pi OS Desktop, Debian 12/13): WirePlumber owns the
+# ALSA mixer and restores its OWN saved level at session start, overriding
+# anything an early boot service sets. The reliable way is to set it through
+# PipeWire (wpctl), which WirePlumber then persists across reboots.
+# - Plain ALSA (Pi OS Lite): no session manager, so a boot-time oneshot that
+# forces every control to 100% on each boot is enough.
+pin_hardware_volume_max() {
+ local card="$1"
+ [ -z "$card" ] && return 0
+
+ # Always raise every control now and snapshot the ALSA state.
+ local ctl
+ while IFS= read -r ctl; do
+ [ -z "$ctl" ] && continue
+ amixer -c "$card" set "$ctl" 100% unmute > /dev/null 2>&1 || true
+ done < <(amixer -c "$card" scontrols 2>/dev/null | grep -oP "Simple mixer control '\K[^']+" || true)
+ sudo alsactl store > /dev/null 2>&1 || true
+
+ if command_exists wpctl; then
+ cecho "blue" "PipeWire detected β setting default sink to 100% (persisted by WirePlumber)..."
+ wpctl set-volume @DEFAULT_AUDIO_SINK@ 1.0 >/dev/null 2>&1 || true
+ # An amixer boot service is pointless on PipeWire (runs too early, gets
+ # overridden) β remove any left over from an older install.
+ if [ -f /lib/systemd/system/airplay-volume.service ]; then
+ sudo systemctl disable airplay-volume.service >/dev/null 2>&1 || true
+ sudo rm -f /lib/systemd/system/airplay-volume.service
+ sudo systemctl daemon-reload >/dev/null 2>&1 || true
+ fi
+ cecho "green" "β Hardware volume pinned to maximum via PipeWire"
+ return 0
+ fi
+
+ # Plain ALSA: install a boot-time oneshot that forces 100% on every boot.
+ cecho "blue" "Installing boot-time max-volume service..."
+ local amixer_bin exec_lines=""
+ amixer_bin="$(command -v amixer || echo /usr/bin/amixer)"
+ while IFS= read -r ctl; do
+ [ -z "$ctl" ] && continue
+ exec_lines+="ExecStart=-$amixer_bin -c $card set \"$ctl\" 100% unmute"$'\n'
+ done < <(amixer -c "$card" scontrols 2>/dev/null | grep -oP "Simple mixer control '\K[^']+" || true)
+ [ -z "$exec_lines" ] && { cecho "yellow" "β No mixer controls on card $card."; return 0; }
+
+ sudo tee /lib/systemd/system/airplay-volume.service > /dev/null </dev/null 2>&1 || true
+ if sudo systemctl enable airplay-volume.service >/dev/null 2>&1; then
+ sudo systemctl start airplay-volume.service >/dev/null 2>&1 || true
+ cecho "green" "β Max volume will be re-applied on every boot"
+ else
+ cecho "yellow" "β Could not enable boot-time volume service"
+ fi
+}
+
# --- Pre-flight Checks ---
pre_flight_checks() {
cecho "blue" "βββββββββββββββββββββββββββββββββββββββ"
@@ -299,60 +387,55 @@ select_audio_device() {
if [ -z "$all_cards" ]; then
cecho "red" "β No audio devices detected at all!"
- cecho "yellow" " Make sure your USB DAC is properly connected."
- cecho "yellow" " Try: lsusb (to check if USB device is recognized)"
+ cecho "yellow" " Make sure your audio output (USB DAC, HAT or built-in) is enabled."
+ cecho "yellow" " Try: lsusb (to check if a USB device is recognized)"
+ cecho "yellow" " Or: sudo raspi-config (to enable built-in audio)"
exit 1
fi
- # Show all detected devices
- cecho "green" "Found these audio devices:"
- echo "$all_cards" | nl -w2 -s'. '
- echo
-
- # Filter out built-in audio (bcm2835, Headphones, vc4-hdmi)
- mapfile -t external_devices < <(echo "$all_cards" | grep -iv 'bcm2835\|Headphones\|vc4-hdmi' || true)
+ # Build the full list of available devices (built-in audio included)
+ mapfile -t all_devices < <(echo "$all_cards")
- if [ ${#external_devices[@]} -eq 0 ]; then
- cecho "yellow" "β No external USB DAC detected!"
- cecho "yellow" " Only built-in audio found."
- echo
- read -p "Do you want to use built-in audio? (y/N): " use_builtin || true
-
- if [[ "$use_builtin" =~ ^[Yy]$ ]]; then
- mapfile -t external_devices < <(echo "$all_cards")
+ # Mark built-in devices so the user can recognise them in the menu
+ local device_labels=()
+ local i
+ for i in "${!all_devices[@]}"; do
+ local label="${all_devices[$i]}"
+ if echo "$label" | grep -qi 'bcm2835\|Headphones\|vc4-hdmi'; then
+ label="$label [built-in]"
else
- cecho "yellow" " Please:"
- cecho "yellow" " 1. Connect your USB DAC"
- cecho "yellow" " 2. Wait 5 seconds"
- cecho "yellow" " 3. Run this script again"
- exit 1
+ label="$label [external/DAC]"
fi
- fi
+ device_labels+=("$label")
+ done
- # Auto-select if only one device
- if [ ${#external_devices[@]} -eq 1 ]; then
+ # Auto-select if only one device is available
+ if [ ${#all_devices[@]} -eq 1 ]; then
cecho "green" "β Found one audio device, auto-selecting:"
- cecho "magenta" " β ${external_devices[0]}"
- selected_device="${external_devices[0]}"
+ cecho "magenta" " β ${device_labels[0]}"
+ selected_device="${all_devices[0]}"
else
- # Multiple devices - let user choose
- cecho "yellow" "Found ${#external_devices[@]} audio devices:"
- for i in "${!external_devices[@]}"; do
- echo " [$i] ${external_devices[$i]}"
+ cecho "yellow" "Found ${#all_devices[@]} audio devices:"
+ for i in "${!device_labels[@]}"; do
+ echo " [$i] ${device_labels[$i]}"
done
echo
+ cecho "blue" "You can select either a USB DAC / HAT or the Raspberry Pi's built-in audio"
+ cecho "blue" "(3.5mm jack / HDMI). Pick the one connected to your speakers/amplifier."
+ echo
local device_choice
while true; do
- read -p "Enter the number [0-$((${#external_devices[@]}-1))]: " device_choice || true
+ read -p "Enter the number [0-$((${#all_devices[@]}-1))]: " device_choice || true
- if [[ "$device_choice" =~ ^[0-9]+$ ]] && [ "$device_choice" -lt "${#external_devices[@]}" ]; then
+ if [[ "$device_choice" =~ ^[0-9]+$ ]] && [ "$device_choice" -lt "${#all_devices[@]}" ]; then
break
fi
cecho "red" "Invalid selection. Please try again."
done
- selected_device="${external_devices[$device_choice]}"
+ selected_device="${all_devices[$device_choice]}"
+ cecho "green" "β Selected: ${device_labels[$device_choice]}"
fi
# Extract card and device numbers more reliably
@@ -478,6 +561,142 @@ configure_wifi() {
echo
}
+# --- Spotify Connect Configuration Prompt ---
+configure_spotify() {
+ echo
+ cecho "yellow" "ββββββββββββββββββββββββββββββββββββ"
+ cecho "yellow" " Step 4: Spotify Connect (optional)"
+ cecho "yellow" "ββββββββββββββββββββββββββββββββββββ"
+ echo
+ cecho "cyan" "βΈ PLEASE RESPOND TO THIS PROMPT βΈ"
+ echo
+
+ cecho "blue" "Spotify Connect lets you stream from the Spotify app to this Pi."
+ cecho "blue" "It is installed via the raspotify package (requires Spotify Premium)"
+ cecho "blue" "and coexists with AirPlay: only one source plays at a time."
+ echo
+ cecho "green" ">>> "
+ read -p "Install Spotify Connect as well? (Y/n): " spotify_choice || true
+
+ if [[ -z "$spotify_choice" || "$spotify_choice" =~ ^[Yy]$ ]]; then
+ install_spotify=true
+ echo
+ read -p "Spotify device name (Enter for '$airplay_name'): " spotify_name || true
+ [ -z "$spotify_name" ] && spotify_name="$airplay_name"
+ spotify_name=$(echo "$spotify_name" | sed 's/[^a-zA-Z0-9 _-]//g')
+ cecho "green" "β Spotify Connect will be installed as '$spotify_name'"
+ else
+ install_spotify=false
+ cecho "yellow" "β Spotify Connect will NOT be installed"
+ fi
+ echo
+}
+
+# --- Spotify Connect Installation (raspotify) ---
+install_spotify_connect() {
+ [ "$install_spotify" != true ] && return 0
+
+ cecho "blue" "ββββββββββββββββββββββββββββββββββββ"
+ cecho "blue" " Installing Spotify Connect (raspotify)..."
+ cecho "blue" "ββββββββββββββββββββββββββββββββββββ"
+ log "Setting up raspotify..."
+
+ # Add raspotify apt repo if not already configured
+ if [ ! -f /etc/apt/sources.list.d/raspotify.list ]; then
+ cecho "yellow" "Adding raspotify apt repository..."
+ if ! curl -fsSL https://dtcooper.github.io/raspotify/key.asc \
+ | sudo tee /usr/share/keyrings/raspotify_key.asc > /dev/null; then
+ cecho "red" "β Failed to fetch raspotify repository key"
+ cecho "yellow" " Skipping Spotify Connect installation, continuing..."
+ install_spotify=false
+ return 0
+ fi
+ sudo chmod 644 /usr/share/keyrings/raspotify_key.asc
+ echo 'deb [signed-by=/usr/share/keyrings/raspotify_key.asc] https://dtcooper.github.io/raspotify raspotify main' \
+ | sudo tee /etc/apt/sources.list.d/raspotify.list > /dev/null
+ sudo apt-get update -qq 2>&1 | tee -a "$LOG_FILE" || true
+ fi
+
+ log "Installing raspotify package..."
+ if ! sudo apt-get install -y raspotify 2>&1 | tee -a "$LOG_FILE"; then
+ cecho "red" "β Failed to install raspotify"
+ cecho "yellow" " Skipping Spotify Connect installation, continuing..."
+ install_spotify=false
+ return 0
+ fi
+
+ # Configure raspotify via /etc/raspotify/conf
+ local raspotify_conf="/etc/raspotify/conf"
+ if [ ! -f "$raspotify_conf" ]; then
+ cecho "yellow" "β $raspotify_conf not found after install β skipping config"
+ return 0
+ fi
+
+ # Defensive: never write an empty name. raspotify.service ships a default
+ # Environment="LIBRESPOT_NAME=raspotify (%H)"; an empty value here would let
+ # that default win and the speaker would show the wrong name.
+ if [ -z "$spotify_name" ]; then
+ spotify_name="${airplay_name:-$(hostname)}"
+ log "spotify_name was empty β falling back to '$spotify_name'"
+ fi
+
+ log "Configuring raspotify: name='$spotify_name' device='$audio_device_plug'"
+ sudo cp "$raspotify_conf" "$BACKUP_DIR/raspotify.conf" 2>/dev/null || true
+
+ # Replace any prior managed block, then append our settings
+ sudo sed -i '/^# >>> airplay-installer >>>$/,/^# <<< airplay-installer <<<$/d' "$raspotify_conf"
+ sudo tee -a "$raspotify_conf" > /dev/null <>> airplay-installer >>>
+LIBRESPOT_NAME="$spotify_name"
+LIBRESPOT_DEVICE="$audio_device_plug"
+LIBRESPOT_BITRATE="$spotify_bitrate"
+LIBRESPOT_INITIAL_VOLUME="100"
+LIBRESPOT_ZEROCONF_PORT="$SPOTIFY_ZEROCONF_PORT"
+# <<< airplay-installer <<<
+EOF
+
+ # Belt-and-suspenders for the device name. The raspotify unit ships an inline
+ # default Environment=LIBRESPOT_NAME="%N (%H)" (i.e. "raspotify (hostname)").
+ # The conf above is loaded via EnvironmentFile, which should override that
+ # default β but to remove any ambiguity we also drop in a systemd override
+ # that sets the same name. A drop-in is merged after the main unit, so our
+ # value deterministically wins and the speaker advertises the chosen name.
+ sudo mkdir -p /etc/systemd/system/raspotify.service.d
+ sudo tee /etc/systemd/system/raspotify.service.d/airplay-installer.conf > /dev/null </dev/null || true
+ sudo systemctl daemon-reload 2>&1 | tee -a "$LOG_FILE" || true
+ sudo systemctl enable raspotify 2>&1 | tee -a "$LOG_FILE" || true
+ sudo systemctl stop raspotify 2>/dev/null || true
+ sleep 1
+ sudo systemctl start raspotify 2>&1 | tee -a "$LOG_FILE" || true
+ sleep 3
+
+ # Verify the name actually landed in the conf (catches a silently-failed write)
+ if grep -qE "^LIBRESPOT_NAME=\"?${spotify_name}\"?$" "$raspotify_conf" 2>/dev/null; then
+ log "Verified LIBRESPOT_NAME='$spotify_name' in $raspotify_conf"
+ else
+ cecho "yellow" "β Could not verify the Spotify name in $raspotify_conf"
+ fi
+
+ if check_service "raspotify"; then
+ cecho "green" "β Spotify Connect (raspotify) is running as '$spotify_name'"
+ cecho "blue" " Note: if Spotify still shows the old name, fully close and"
+ cecho "blue" " reopen the Spotify app (it caches discovered devices)."
+ else
+ cecho "yellow" "β raspotify service is not active β check 'journalctl -u raspotify'"
+ fi
+ echo
+}
+
# --- Main Installation ---
main() {
# Initialize log file immediately
@@ -486,7 +705,7 @@ main() {
clear
cecho "green" "βββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
cecho "green" "β β"
- cecho "green" "β AirPlay 2 Installer for Raspberry Pi + DAC β"
+ cecho "green" "β AirPlay 2 Installer for Raspberry Pi β"
cecho "green" "β Version $SCRIPT_VERSION β"
cecho "green" "β β"
cecho "green" "βββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
@@ -518,6 +737,7 @@ main() {
select_audio_device
get_airplay_name
configure_wifi
+ configure_spotify
# --- Confirmation ---
echo
@@ -528,8 +748,13 @@ main() {
echo
cecho "yellow" " π± AirPlay Name: $airplay_name"
cecho "yellow" " π Audio Output: $audio_device_plug"
- cecho "yellow" " ποΈ Volume Control: ${mixer_control:-None (fixed volume)}"
+ cecho "yellow" " ποΈ Volume Control: Software (independent AirPlay/Spotify, DAC pinned 100%)"
cecho "yellow" " π‘ Disable Wi-Fi PM: $disable_wifi_pm"
+ if [ "$install_spotify" = true ]; then
+ cecho "yellow" " π§ Spotify Connect: yes ($spotify_name)"
+ else
+ cecho "yellow" " π§ Spotify Connect: no"
+ fi
echo
cecho "blue" "Installation will take 10-30 minutes depending on your Pi model."
cecho "blue" "(Pi Zero 2 will be slower, Pi 4/5 will be faster)"
@@ -580,7 +805,7 @@ main() {
build-essential git autoconf automake libtool pkg-config
libpopt-dev libconfig-dev libasound2-dev
avahi-daemon libavahi-client-dev libssl-dev
- libsoxr-dev libplist-dev libsodium-dev
+ libsoxr-dev libplist-dev libplist-utils libsodium-dev
libavutil-dev libavcodec-dev libavformat-dev
uuid-dev libgcrypt20-dev xxd alsa-utils
)
@@ -624,7 +849,9 @@ main() {
safe_cd /tmp
rm -rf nqptp 2>/dev/null || true
- if ! git clone https://github.com/mikebrady/nqptp.git 2>&1 | tee -a "$LOG_FILE"; then
+ log "Pinning NQPTP to release $NQPTP_VERSION"
+ if ! git clone --branch "$NQPTP_VERSION" --depth 1 \
+ https://github.com/mikebrady/nqptp.git 2>&1 | tee -a "$LOG_FILE"; then
cecho "red" "β Failed to clone NQPTP repository"
cecho "yellow" " Possible causes:"
cecho "yellow" " - No internet connection"
@@ -684,7 +911,9 @@ main() {
safe_cd /tmp
rm -rf shairport-sync 2>/dev/null || true
- if ! git clone https://github.com/mikebrady/shairport-sync.git 2>&1 | tee -a "$LOG_FILE"; then
+ log "Pinning Shairport-Sync to release $SHAIRPORT_VERSION"
+ if ! git clone --branch "$SHAIRPORT_VERSION" --depth 1 \
+ https://github.com/mikebrady/shairport-sync.git 2>&1 | tee -a "$LOG_FILE"; then
cecho "red" "β Failed to clone Shairport-Sync repository"
cecho "yellow" " Possible causes:"
cecho "yellow" " - No internet connection"
@@ -775,24 +1004,27 @@ FALLBACK_EOF
sudo sed -i "s|^[[:space:]]*output_device = .*| output_device = \"$audio_device_plug\";|" /etc/shairport-sync.conf
sudo sed -i "s|^//[[:space:]]*output_device = .*| output_device = \"$audio_device_plug\";|" /etc/shairport-sync.conf
- # Set mixer control if available
- if [ -n "$mixer_control" ]; then
- log "Configuring mixer control: $mixer_control on hw:$card_number"
- sudo sed -i "s|^//[[:space:]]*mixer_control_name = .*| mixer_control_name = \"$mixer_control\";|" /etc/shairport-sync.conf
- sudo sed -i "s|^[[:space:]]*mixer_control_name = .*| mixer_control_name = \"$mixer_control\";|" /etc/shairport-sync.conf
- # Also set mixer_device if needed (usually commented out by default)
- sudo sed -i "s|^//[[:space:]]*mixer_device = .*| mixer_device = \"hw:$card_number\";|" /etc/shairport-sync.conf
- sudo sed -i "s|^[[:space:]]*mixer_device = .*| mixer_device = \"hw:$card_number\";|" /etc/shairport-sync.conf
- fi
-
- # Set output format
+ # Use SOFTWARE volume β do NOT bind a hardware mixer.
+ # If shairport drives the hardware mixer (mixer_control_name), the AirPlay
+ # volume from your phone/Mac moves the shared DAC control and LEAVES it there
+ # when you disconnect, so a later Spotify session inherits that low ceiling.
+ # With software volume, AirPlay and Spotify each attenuate their own stream
+ # and the hardware PCM stays pinned at 100% (see pin_hardware_volume_max).
+ # Force these to commented regardless of any previous run's state.
+ log "Using software volume (no hardware mixer binding)"
+ sudo sed -i -E "s|^[[:space:]]*(//[[:space:]]*)?mixer_control_name[[:space:]]*=.*|// mixer_control_name = \"PCM\";|" /etc/shairport-sync.conf
+ sudo sed -i -E "s|^[[:space:]]*(//[[:space:]]*)?mixer_device[[:space:]]*=.*|// mixer_device = \"default\";|" /etc/shairport-sync.conf
+
+ # Set output format. S32 gives software volume the most headroom so low
+ # volumes don't lose audible detail (plughw converts to the DAC's real depth).
sudo sed -i "s|^//[[:space:]]*output_rate = .*| output_rate = \"auto\";|" /etc/shairport-sync.conf
sudo sed -i "s|^[[:space:]]*output_rate = .*| output_rate = \"auto\";|" /etc/shairport-sync.conf
- sudo sed -i "s|^//[[:space:]]*output_format = .*| output_format = \"S16\";|" /etc/shairport-sync.conf
- sudo sed -i "s|^[[:space:]]*output_format = .*| output_format = \"S16\";|" /etc/shairport-sync.conf
+ sudo sed -i "s|^//[[:space:]]*output_format = .*| output_format = \"S32\";|" /etc/shairport-sync.conf
+ sudo sed -i "s|^[[:space:]]*output_format = .*| output_format = \"S32\";|" /etc/shairport-sync.conf
- # Set volume settings
- sudo sed -i "s|^//[[:space:]]*volume_max_db = .*| volume_max_db = 4.0;|" /etc/shairport-sync.conf
+ # Set volume settings. With software volume, volume_max_db = 0 means unity
+ # gain at the top of the dial (no digital amplification β no clipping).
+ sudo sed -i "s|^//[[:space:]]*volume_max_db = .*| volume_max_db = 0.0;|" /etc/shairport-sync.conf
sudo sed -i "s|^//[[:space:]]*default_airplay_volume = .*| default_airplay_volume = -6.0;|" /etc/shairport-sync.conf
sudo sed -i "s|^//[[:space:]]*high_volume_idle_timeout_in_minutes = .*| high_volume_idle_timeout_in_minutes = 1;|" /etc/shairport-sync.conf
@@ -804,16 +1036,9 @@ FALLBACK_EOF
cecho "green" "β Configuration file created and customized"
- # Set mixer volume to maximum if available
- if [ -n "$mixer_control" ]; then
- cecho "blue" "Setting mixer volume to 100%..."
- if amixer -c "$card_number" set "$mixer_control" 100% unmute > /dev/null 2>&1; then
- sudo alsactl store > /dev/null 2>&1 || true
- cecho "green" "β Mixer volume set to maximum"
- else
- cecho "yellow" "β Could not set mixer volume (may not be supported)"
- fi
- fi
+ # Pin the DAC hardware mixer to 100% (and keep it there) so the software
+ # volumes of AirPlay and Spotify both have full range. PipeWire-aware.
+ pin_hardware_volume_max "$card_number"
echo
# --- Create/Update Systemd Service ---
@@ -879,6 +1104,9 @@ EOF
fi
echo
+ # --- Spotify Connect (optional) ---
+ install_spotify_connect
+
# --- Wi-Fi Power Management Instructions ---
if [ "$disable_wifi_pm" = true ]; then
cecho "blue" "ββββββββββββββββββββββββββββββββββββ"
@@ -919,6 +1147,11 @@ EOF
# AirPlay ports
sudo ufw allow 7000/tcp comment 'AirPlay' 2>&1 | tee -a "$LOG_FILE"
+ # Spotify Connect (librespot zeroconf) port if installed
+ if [ "$install_spotify" = true ]; then
+ sudo ufw allow "$SPOTIFY_ZEROCONF_PORT"/tcp comment 'librespot/Spotify Connect' 2>&1 | tee -a "$LOG_FILE"
+ fi
+
cecho "green" "β Firewall rules added"
echo
fi
@@ -974,7 +1207,10 @@ EOF
echo
cecho "yellow" " π± Device Name: $airplay_name"
cecho "yellow" " π Audio Output: $audio_device_plug"
- cecho "yellow" " ποΈ Volume: ${mixer_control:-Fixed (no hardware control)}"
+ cecho "yellow" " ποΈ Volume: Software (independent AirPlay/Spotify, DAC pinned 100%)"
+ if [ "$install_spotify" = true ]; then
+ cecho "yellow" " π§ Spotify Connect: $spotify_name (Premium account required)"
+ fi
echo
cecho "blue" "βββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
cecho "blue" "β How to use: β"
diff --git a/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh b/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh
new file mode 100755
index 0000000..4cf4a35
--- /dev/null
+++ b/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh
@@ -0,0 +1,657 @@
+#!/bin/bash
+
+# ===================================================================================
+# Shairport-Sync AirPlay 2 - Configuration Modifier
+#
+# Modify an existing AirPlay 2 installation (name, audio device, mixer, volume...)
+# without reinstalling. Designed to work with installs done by install_airplay_v3.sh.
+# ===================================================================================
+
+set -eo pipefail
+IFS=$'\n\t'
+
+SCRIPT_VERSION="1.0"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+CONFIG_FILE="/etc/shairport-sync.conf"
+SERVICE_NAME="shairport-sync"
+RASPOTIFY_CONF="/etc/raspotify/conf"
+SPOTIFY_ZEROCONF_PORT="5354"
+
+# --- Helpers ---
+cecho() {
+ local code="\033["
+ local color
+ case "$1" in
+ "red") color="${code}1;31m" ;;
+ "green") color="${code}1;32m" ;;
+ "yellow") color="${code}1;33m" ;;
+ "blue") color="${code}1;34m" ;;
+ "magenta") color="${code}1;35m" ;;
+ "cyan") color="${code}1;36m" ;;
+ *) color="${code}0m" ;;
+ esac
+ echo -e "${color}$2\033[0m"
+}
+
+require_install() {
+ if [ ! -f "$CONFIG_FILE" ]; then
+ cecho "red" "β Configuration file $CONFIG_FILE not found."
+ cecho "yellow" " Shairport-Sync does not appear to be installed."
+ cecho "yellow" " Run install_airplay_v3.sh first."
+ exit 1
+ fi
+ if ! command -v shairport-sync >/dev/null 2>&1; then
+ cecho "red" "β shairport-sync binary not found in PATH."
+ cecho "yellow" " Run install_airplay_v3.sh first."
+ exit 1
+ fi
+}
+
+require_sudo() {
+ if [ "$EUID" -eq 0 ]; then
+ cecho "red" "β Don't run this script with sudo or as root."
+ cecho "yellow" " Just run: bash modify_airplay.sh"
+ exit 1
+ fi
+ if ! sudo -n true 2>/dev/null; then
+ cecho "yellow" "Checking sudo access..."
+ sudo true || { cecho "red" "Sudo required."; exit 1; }
+ fi
+}
+
+restart_service() {
+ cecho "blue" "Restarting $SERVICE_NAME..."
+ if sudo systemctl restart "$SERVICE_NAME"; then
+ sleep 2
+ if systemctl is-active --quiet "$SERVICE_NAME"; then
+ cecho "green" "β $SERVICE_NAME is running"
+ else
+ cecho "red" "β $SERVICE_NAME is not active after restart"
+ sudo systemctl status "$SERVICE_NAME" --no-pager -l | tail -20
+ fi
+ else
+ cecho "red" "β Failed to restart $SERVICE_NAME (check config syntax)"
+ sudo systemctl status "$SERVICE_NAME" --no-pager -l | tail -20
+ fi
+}
+
+# --- Current value readers (best-effort, tolerate missing keys) ---
+current_name() {
+ grep -oE '^[[:space:]]*name[[:space:]]*=[[:space:]]*"[^"]*"' "$CONFIG_FILE" 2>/dev/null \
+ | head -1 | sed -E 's/.*"([^"]*)".*/\1/' || true
+}
+
+current_output_device() {
+ grep -oE '^[[:space:]]*output_device[[:space:]]*=[[:space:]]*"[^"]*"' "$CONFIG_FILE" 2>/dev/null \
+ | head -1 | sed -E 's/.*"([^"]*)".*/\1/' || true
+}
+
+current_mixer() {
+ grep -oE '^[[:space:]]*mixer_control_name[[:space:]]*=[[:space:]]*"[^"]*"' "$CONFIG_FILE" 2>/dev/null \
+ | head -1 | sed -E 's/.*"([^"]*)".*/\1/' || true
+}
+
+# --- Spotify helpers ---
+spotify_installed() {
+ dpkg -l raspotify 2>/dev/null | grep -q '^ii'
+}
+
+spotify_current_name() {
+ [ -f "$RASPOTIFY_CONF" ] || { echo ""; return; }
+ grep -oE '^[[:space:]]*LIBRESPOT_NAME[[:space:]]*=[[:space:]]*"[^"]*"' "$RASPOTIFY_CONF" 2>/dev/null \
+ | head -1 | sed -E 's/.*"([^"]*)".*/\1/' || true
+}
+
+spotify_current_device() {
+ [ -f "$RASPOTIFY_CONF" ] || { echo ""; return; }
+ grep -oE '^[[:space:]]*LIBRESPOT_DEVICE[[:space:]]*=[[:space:]]*"[^"]*"' "$RASPOTIFY_CONF" 2>/dev/null \
+ | head -1 | sed -E 's/.*"([^"]*)".*/\1/' || true
+}
+
+write_spotify_managed_block() {
+ # Args: name, device
+ local name="$1" device="$2"
+ sudo sed -i '/^# >>> airplay-installer >>>$/,/^# <<< airplay-installer <<<$/d' "$RASPOTIFY_CONF"
+ sudo tee -a "$RASPOTIFY_CONF" > /dev/null <>> airplay-installer >>>
+LIBRESPOT_NAME="$name"
+LIBRESPOT_DEVICE="$device"
+LIBRESPOT_BITRATE="320"
+LIBRESPOT_INITIAL_VOLUME="100"
+LIBRESPOT_ZEROCONF_PORT="$SPOTIFY_ZEROCONF_PORT"
+# <<< airplay-installer <<<
+EOF
+
+ # Keep the systemd drop-in in sync so our name always overrides the unit's
+ # inline default Environment=LIBRESPOT_NAME="%N (%H)". See install script.
+ sudo mkdir -p /etc/systemd/system/raspotify.service.d
+ sudo tee /etc/systemd/system/raspotify.service.d/airplay-installer.conf > /dev/null </dev/null || true
+}
+
+restart_spotify() {
+ cecho "blue" "Restarting raspotify..."
+ if sudo systemctl restart raspotify 2>/dev/null; then
+ sleep 2
+ if systemctl is-active --quiet raspotify; then
+ cecho "green" "β raspotify is running"
+ else
+ cecho "red" "β raspotify is not active after restart"
+ sudo systemctl status raspotify --no-pager -l | tail -15
+ fi
+ else
+ cecho "red" "β Failed to restart raspotify"
+ fi
+}
+
+# --- Actions ---
+action_change_name() {
+ local cur new_name
+ cur=$(current_name)
+ cecho "blue" "Current AirPlay name: ${cur:-}"
+ echo
+ read -p "Enter new name (empty to cancel): " new_name || true
+ if [ -z "$new_name" ]; then
+ cecho "yellow" "Cancelled."
+ return
+ fi
+ new_name=$(echo "$new_name" | sed 's/[^a-zA-Z0-9 _-]//g')
+ if [ -z "$new_name" ]; then
+ cecho "red" "Name became empty after sanitization. Cancelled."
+ return
+ fi
+ sudo sed -i -E "s|^[[:space:]]*(//[[:space:]]*)?name[[:space:]]*=.*| name = \"$new_name\";|" "$CONFIG_FILE"
+ cecho "green" "β AirPlay name updated to '$new_name'"
+ restart_service
+}
+
+action_change_audio_device() {
+ cecho "blue" "Scanning for audio devices..."
+ local all_cards
+ all_cards=$(aplay -l 2>/dev/null | grep '^card' || true)
+ if [ -z "$all_cards" ]; then
+ cecho "red" "β No audio devices detected."
+ return
+ fi
+
+ local all_devices device_labels=() i
+ mapfile -t all_devices < <(echo "$all_cards")
+ for i in "${!all_devices[@]}"; do
+ local label="${all_devices[$i]}"
+ if echo "$label" | grep -qi 'bcm2835\|Headphones\|vc4-hdmi'; then
+ label="$label [built-in]"
+ else
+ label="$label [external/DAC]"
+ fi
+ device_labels+=("$label")
+ done
+
+ cecho "yellow" "Available audio devices:"
+ for i in "${!device_labels[@]}"; do
+ echo " [$i] ${device_labels[$i]}"
+ done
+ echo
+ cecho "blue" "Current output_device: $(current_output_device)"
+ echo
+
+ local choice
+ while true; do
+ read -p "Enter number [0-$((${#all_devices[@]}-1))] (empty=cancel): " choice || true
+ [ -z "$choice" ] && { cecho "yellow" "Cancelled."; return; }
+ if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -lt "${#all_devices[@]}" ]; then
+ break
+ fi
+ cecho "red" "Invalid selection."
+ done
+
+ local selected="${all_devices[$choice]}"
+ local card_number device_number
+ card_number=$(echo "$selected" | grep -oE 'card [0-9]+' | grep -oE '[0-9]+')
+ device_number=$(echo "$selected" | grep -oE 'device [0-9]+' | grep -oE '[0-9]+')
+ [ -z "$device_number" ] && device_number=0
+ local audio_device_plug="plughw:$card_number,$device_number"
+
+ sudo sed -i -E "s|^[[:space:]]*(//[[:space:]]*)?output_device[[:space:]]*=.*| output_device = \"$audio_device_plug\";|" "$CONFIG_FILE"
+ cecho "green" "β output_device set to $audio_device_plug"
+
+ # Refresh mixer config to match the new card
+ local mixers=()
+ mapfile -t mixers < <(amixer -c "$card_number" scontrols 2>/dev/null | grep -oP "Simple mixer control '\K[^']+" || true)
+ if [ ${#mixers[@]} -eq 0 ]; then
+ cecho "yellow" "β No mixer controls on card $card_number β disabling hardware mixer in config."
+ sudo sed -i -E "s|^[[:space:]]*(//[[:space:]]*)?mixer_control_name[[:space:]]*=.*|// mixer_control_name = \"PCM\";|" "$CONFIG_FILE"
+ sudo sed -i -E "s|^[[:space:]]*(//[[:space:]]*)?mixer_device[[:space:]]*=.*|// mixer_device = \"default\";|" "$CONFIG_FILE"
+ else
+ local mixer_control="" preferred m
+ for preferred in "PCM" "Master" "Speaker" "Headphone" "Digital"; do
+ for m in "${mixers[@]}"; do
+ if [[ "$m" == "$preferred" ]]; then
+ mixer_control="$m"; break 2
+ fi
+ done
+ done
+ [ -z "$mixer_control" ] && mixer_control="${mixers[0]}"
+ cecho "green" "β mixer_control_name = $mixer_control (on hw:$card_number)"
+ sudo sed -i -E "s|^[[:space:]]*(//[[:space:]]*)?mixer_control_name[[:space:]]*=.*| mixer_control_name = \"$mixer_control\";|" "$CONFIG_FILE"
+ sudo sed -i -E "s|^[[:space:]]*(//[[:space:]]*)?mixer_device[[:space:]]*=.*| mixer_device = \"hw:$card_number\";|" "$CONFIG_FILE"
+ fi
+
+ restart_service
+}
+
+action_change_mixer() {
+ local cur_out
+ cur_out=$(current_output_device)
+ if [ -z "$cur_out" ]; then
+ cecho "yellow" "No output_device configured yet. Change the audio device first."
+ return
+ fi
+ local card_number
+ card_number=$(echo "$cur_out" | grep -oE '[0-9]+' | head -1)
+ if [ -z "$card_number" ]; then
+ cecho "red" "Could not parse card number from current output_device: $cur_out"
+ return
+ fi
+
+ local mixers=()
+ mapfile -t mixers < <(amixer -c "$card_number" scontrols 2>/dev/null | grep -oP "Simple mixer control '\K[^']+" || true)
+ cecho "blue" "Current mixer_control_name: $(current_mixer)"
+ echo
+
+ if [ ${#mixers[@]} -eq 0 ]; then
+ cecho "yellow" "No mixer controls available on card $card_number."
+ local ans
+ read -p "Disable hardware mixer in config (use software volume)? (y/N): " ans || true
+ if [[ "$ans" =~ ^[Yy]$ ]]; then
+ sudo sed -i -E "s|^[[:space:]]*(//[[:space:]]*)?mixer_control_name[[:space:]]*=.*|// mixer_control_name = \"PCM\";|" "$CONFIG_FILE"
+ cecho "green" "β Hardware mixer disabled."
+ restart_service
+ fi
+ return
+ fi
+
+ cecho "yellow" "Available mixer controls on card $card_number:"
+ local i
+ for i in "${!mixers[@]}"; do
+ echo " [$i] ${mixers[$i]}"
+ done
+ echo " [d] Disable hardware mixer (software volume only)"
+ echo
+ local choice
+ read -p "Choose [0-$((${#mixers[@]}-1))] / d / empty=cancel: " choice || true
+ if [ -z "$choice" ]; then
+ cecho "yellow" "Cancelled."; return
+ fi
+ if [ "$choice" = "d" ] || [ "$choice" = "D" ]; then
+ sudo sed -i -E "s|^[[:space:]]*(//[[:space:]]*)?mixer_control_name[[:space:]]*=.*|// mixer_control_name = \"PCM\";|" "$CONFIG_FILE"
+ cecho "green" "β Hardware mixer disabled."
+ restart_service
+ return
+ fi
+ if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -lt "${#mixers[@]}" ]; then
+ local mixer_control="${mixers[$choice]}"
+ sudo sed -i -E "s|^[[:space:]]*(//[[:space:]]*)?mixer_control_name[[:space:]]*=.*| mixer_control_name = \"$mixer_control\";|" "$CONFIG_FILE"
+ sudo sed -i -E "s|^[[:space:]]*(//[[:space:]]*)?mixer_device[[:space:]]*=.*| mixer_device = \"hw:$card_number\";|" "$CONFIG_FILE"
+ cecho "green" "β Mixer set to $mixer_control"
+ restart_service
+ else
+ cecho "red" "Invalid selection."
+ fi
+}
+
+pin_hardware_volume_max() {
+ # Args: card_number. Raise every control to 100% now, then keep it there.
+ # - PipeWire (Desktop): WirePlumber owns the mixer and restores its own saved
+ # level, so we set it via wpctl (which WirePlumber then persists) and drop
+ # any stale amixer boot service.
+ # - Plain ALSA (Lite): install a boot-time oneshot that forces 100% each boot.
+ local card_number="$1"
+ [ -z "$card_number" ] && { cecho "red" "No card number."; return 1; }
+
+ local ctl
+ while IFS= read -r ctl; do
+ [ -z "$ctl" ] && continue
+ amixer -c "$card_number" set "$ctl" 100% unmute > /dev/null 2>&1 || true
+ done < <(amixer -c "$card_number" scontrols 2>/dev/null | grep -oP "Simple mixer control '\K[^']+" || true)
+ sudo alsactl store > /dev/null 2>&1 || true
+
+ if command -v wpctl >/dev/null 2>&1; then
+ cecho "blue" "PipeWire detected β setting default sink to 100% (persisted by WirePlumber)..."
+ wpctl set-volume @DEFAULT_AUDIO_SINK@ 1.0 >/dev/null 2>&1 || true
+ if [ -f /lib/systemd/system/airplay-volume.service ]; then
+ sudo systemctl disable airplay-volume.service >/dev/null 2>&1 || true
+ sudo rm -f /lib/systemd/system/airplay-volume.service
+ sudo systemctl daemon-reload >/dev/null 2>&1 || true
+ fi
+ return 0
+ fi
+
+ local amixer_bin exec_lines=""
+ amixer_bin="$(command -v amixer || echo /usr/bin/amixer)"
+ while IFS= read -r ctl; do
+ [ -z "$ctl" ] && continue
+ exec_lines+="ExecStart=-$amixer_bin -c $card_number set \"$ctl\" 100% unmute"$'\n'
+ done < <(amixer -c "$card_number" scontrols 2>/dev/null | grep -oP "Simple mixer control '\K[^']+" || true)
+ if [ -z "$exec_lines" ]; then
+ cecho "yellow" "β No mixer controls on card $card_number β nothing to do."
+ return 1
+ fi
+
+ sudo tee /lib/systemd/system/airplay-volume.service > /dev/null </dev/null || true
+ sudo systemctl enable airplay-volume.service >/dev/null 2>&1 || true
+ sudo systemctl start airplay-volume.service >/dev/null 2>&1 || true
+ return 0
+}
+
+# Switch shairport-sync to SOFTWARE volume: unbind the hardware mixer so AirPlay
+# stops driving the shared DAC control, and give software volume full headroom.
+set_software_volume_config() {
+ sudo sed -i -E "s|^[[:space:]]*(//[[:space:]]*)?mixer_control_name[[:space:]]*=.*|// mixer_control_name = \"PCM\";|" "$CONFIG_FILE"
+ sudo sed -i -E "s|^[[:space:]]*(//[[:space:]]*)?mixer_device[[:space:]]*=.*|// mixer_device = \"default\";|" "$CONFIG_FILE"
+ sudo sed -i -E "s|^[[:space:]]*(//[[:space:]]*)?output_format[[:space:]]*=.*| output_format = \"S32\";|" "$CONFIG_FILE"
+ sudo sed -i -E "s|^[[:space:]]*(//[[:space:]]*)?volume_max_db[[:space:]]*=.*| volume_max_db = 0.0;|" "$CONFIG_FILE"
+}
+
+action_max_volume() {
+ local cur_out card_number
+ cur_out=$(current_output_device)
+ if [ -z "$cur_out" ]; then
+ cecho "yellow" "No output_device configured yet. Change the audio device first."
+ return
+ fi
+ card_number=$(echo "$cur_out" | grep -oE '[0-9]+' | head -1)
+ if [ -z "$card_number" ]; then
+ cecho "red" "Could not parse card number from: $cur_out"
+ return
+ fi
+
+ cecho "blue" "This sets up INDEPENDENT volumes for AirPlay and Spotify:"
+ cecho "blue" " β’ shairport-sync switched to software volume (stops driving the DAC mixer)"
+ cecho "blue" " β’ DAC hardware volume pinned at 100% (now and persisted across reboots)"
+ echo
+ set_software_volume_config
+ cecho "green" "β shairport-sync set to software volume (output_format S32, volume_max_db 0)"
+
+ if pin_hardware_volume_max "$card_number"; then
+ cecho "green" "β Hardware volume pinned to maximum"
+ fi
+ restart_service
+ cecho "blue" " Current level on card $card_number:"
+ amixer -c "$card_number" sget PCM 2>/dev/null | grep -E '\[[0-9]+%\]' | head -2 || true
+}
+
+action_change_volume_limits() {
+ cecho "blue" "Volume limits are expressed in dB (0 = max, negative attenuates)."
+ cecho "blue" "Examples: volume_max_db = 0 | -10 to cap max output"
+ cecho "blue" " default_airplay_volume = -6 (volume at first connection)"
+ echo
+ local vmax vdef
+ read -p "Enter volume_max_db (empty = skip): " vmax || true
+ read -p "Enter default_airplay_volume (empty = skip): " vdef || true
+ local changed=0
+ if [ -n "$vmax" ]; then
+ if [[ "$vmax" =~ ^-?[0-9]+(\.[0-9]+)?$ ]]; then
+ sudo sed -i -E "s|^[[:space:]]*(//[[:space:]]*)?volume_max_db[[:space:]]*=.*| volume_max_db = ${vmax};|" "$CONFIG_FILE"
+ cecho "green" "β volume_max_db = $vmax"
+ changed=1
+ else
+ cecho "red" "β '$vmax' is not a valid number, skipped."
+ fi
+ fi
+ if [ -n "$vdef" ]; then
+ if [[ "$vdef" =~ ^-?[0-9]+(\.[0-9]+)?$ ]]; then
+ sudo sed -i -E "s|^[[:space:]]*(//[[:space:]]*)?default_airplay_volume[[:space:]]*=.*| default_airplay_volume = ${vdef};|" "$CONFIG_FILE"
+ cecho "green" "β default_airplay_volume = $vdef"
+ changed=1
+ else
+ cecho "red" "β '$vdef' is not a valid number, skipped."
+ fi
+ fi
+ if [ "$changed" -eq 1 ]; then
+ restart_service
+ else
+ cecho "yellow" "Nothing changed."
+ fi
+}
+
+action_test_audio() {
+ local dev
+ dev=$(current_output_device)
+ if [ -z "$dev" ]; then
+ cecho "red" "No output_device configured."
+ return
+ fi
+ cecho "blue" "Stopping shairport-sync to free the audio device..."
+ sudo systemctl stop "$SERVICE_NAME" 2>/dev/null || true
+ sleep 1
+ cecho "yellow" "Playing test sound on $dev..."
+ timeout 10 speaker-test -D "$dev" -c 2 -t wav -l 1 || true
+ cecho "blue" "Restarting service..."
+ sudo systemctl start "$SERVICE_NAME" || true
+}
+
+action_view_config() {
+ cecho "blue" "Current configuration ($CONFIG_FILE):"
+ echo
+ cecho "yellow" " AirPlay name: $(current_name)"
+ cecho "yellow" " Output device: $(current_output_device)"
+ cecho "yellow" " Mixer control: $(current_mixer)"
+ echo
+ cecho "blue" "Active uncommented settings (first 50 lines):"
+ grep -vE '^[[:space:]]*//|^[[:space:]]*$|^[[:space:]]*#' "$CONFIG_FILE" | head -50
+}
+
+action_service_status() {
+ cecho "blue" "βββ shairport-sync βββ"
+ sudo systemctl status shairport-sync --no-pager -l | head -20 || true
+ echo
+ cecho "blue" "βββ nqptp βββ"
+ sudo systemctl status nqptp --no-pager -l | head -10 || true
+}
+
+action_rebuild() {
+ local repair="$SCRIPT_DIR/repair_airplay.sh"
+ if [ ! -f "$repair" ]; then
+ cecho "red" "β repair_airplay.sh not found next to this script."
+ return
+ fi
+ cecho "blue" "Launching rebuild / repair..."
+ echo
+ bash "$repair" || true
+}
+
+# --- Spotify actions ---
+action_install_spotify() {
+ if spotify_installed; then
+ cecho "yellow" "raspotify is already installed."
+ read -p "Reinstall / refresh configuration? (y/N): " ans || true
+ [[ ! "$ans" =~ ^[Yy]$ ]] && { cecho "yellow" "Cancelled."; return; }
+ fi
+
+ local cur_dev cur_name spotify_name
+ cur_dev=$(current_output_device)
+ cur_name=$(current_name)
+ if [ -z "$cur_dev" ]; then
+ cecho "red" "β No AirPlay output_device configured. Configure AirPlay audio first."
+ return
+ fi
+
+ echo
+ read -p "Spotify device name (Enter for '$cur_name'): " spotify_name || true
+ [ -z "$spotify_name" ] && spotify_name="$cur_name"
+ spotify_name=$(echo "$spotify_name" | sed 's/[^a-zA-Z0-9 _-]//g')
+ if [ -z "$spotify_name" ]; then
+ cecho "red" "Name became empty after sanitization. Cancelled."
+ return
+ fi
+
+ if [ ! -f /etc/apt/sources.list.d/raspotify.list ]; then
+ cecho "yellow" "Adding raspotify apt repository..."
+ if ! curl -fsSL https://dtcooper.github.io/raspotify/key.asc \
+ | sudo tee /usr/share/keyrings/raspotify_key.asc > /dev/null; then
+ cecho "red" "β Failed to fetch raspotify repository key. Cancelled."
+ return
+ fi
+ sudo chmod 644 /usr/share/keyrings/raspotify_key.asc
+ echo 'deb [signed-by=/usr/share/keyrings/raspotify_key.asc] https://dtcooper.github.io/raspotify raspotify main' \
+ | sudo tee /etc/apt/sources.list.d/raspotify.list > /dev/null
+ sudo apt-get update -qq || true
+ fi
+
+ cecho "blue" "Installing raspotify..."
+ if ! sudo apt-get install -y raspotify; then
+ cecho "red" "β Failed to install raspotify."
+ return
+ fi
+
+ if [ ! -f "$RASPOTIFY_CONF" ]; then
+ cecho "red" "β $RASPOTIFY_CONF not found after install."
+ return
+ fi
+
+ write_spotify_managed_block "$spotify_name" "$cur_dev"
+ cecho "green" "β raspotify configured: '$spotify_name' on $cur_dev"
+
+ # Defensive unmask in case a previous installer run left it masked.
+ sudo systemctl unmask raspotify 2>/dev/null || true
+ sudo systemctl enable raspotify >/dev/null 2>&1 || true
+ restart_spotify
+}
+
+action_uninstall_spotify() {
+ if ! spotify_installed; then
+ cecho "yellow" "raspotify is not installed."
+ return
+ fi
+ read -p "Remove Spotify Connect (raspotify)? (y/N): " ans || true
+ [[ ! "$ans" =~ ^[Yy]$ ]] && { cecho "yellow" "Cancelled."; return; }
+
+ sudo systemctl stop raspotify 2>/dev/null || true
+ sudo systemctl disable raspotify 2>/dev/null || true
+ sudo apt-get remove --purge -y raspotify || true
+ sudo rm -f /etc/apt/sources.list.d/raspotify.list
+ sudo rm -f /usr/share/keyrings/raspotify_key.asc
+ cecho "green" "β Spotify Connect removed"
+}
+
+action_change_spotify_name() {
+ if ! spotify_installed; then
+ cecho "yellow" "raspotify is not installed."
+ return
+ fi
+ local cur new_name
+ cur=$(spotify_current_name)
+ cecho "blue" "Current Spotify name: ${cur:-}"
+ echo
+ read -p "Enter new name (empty to cancel): " new_name || true
+ [ -z "$new_name" ] && { cecho "yellow" "Cancelled."; return; }
+ new_name=$(echo "$new_name" | sed 's/[^a-zA-Z0-9 _-]//g')
+ [ -z "$new_name" ] && { cecho "red" "Name became empty after sanitization."; return; }
+
+ local cur_dev
+ cur_dev=$(spotify_current_device)
+ [ -z "$cur_dev" ] && cur_dev=$(current_output_device)
+ write_spotify_managed_block "$new_name" "$cur_dev"
+ cecho "green" "β Spotify name updated to '$new_name'"
+ restart_spotify
+}
+
+action_sync_spotify_to_airplay() {
+ if ! spotify_installed; then
+ cecho "yellow" "raspotify is not installed."
+ return
+ fi
+ local cur_dev cur_spo_name
+ cur_dev=$(current_output_device)
+ if [ -z "$cur_dev" ]; then
+ cecho "red" "No AirPlay output_device configured."
+ return
+ fi
+ cur_spo_name=$(spotify_current_name)
+ [ -z "$cur_spo_name" ] && cur_spo_name=$(current_name)
+ write_spotify_managed_block "$cur_spo_name" "$cur_dev"
+ cecho "green" "β Spotify audio device synced to $cur_dev"
+ restart_spotify
+}
+
+# --- Menu ---
+main() {
+ require_install
+ require_sudo
+ while true; do
+ echo
+ cecho "magenta" "βββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
+ cecho "magenta" " AirPlay 2 β Modify Existing Installation v$SCRIPT_VERSION"
+ cecho "magenta" "βββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
+ echo
+ cecho "yellow" " AirPlay name: $(current_name)"
+ cecho "yellow" " Audio device: $(current_output_device)"
+ cecho "yellow" " Mixer: $(current_mixer)"
+ if spotify_installed; then
+ cecho "yellow" " Spotify: installed β $(spotify_current_name)"
+ else
+ cecho "yellow" " Spotify: not installed"
+ fi
+ echo
+ cecho "cyan" " AirPlay:"
+ echo " 1) Change AirPlay name"
+ echo " 2) Change audio output device"
+ echo " 3) Change mixer / hardware volume control"
+ echo " 4) Change volume limits (volume_max_db, default_airplay_volume)"
+ echo " 15) Independent volumes: software volume + pin DAC at MAX (recommended)"
+ echo " 5) Test audio output"
+ echo " 6) View configuration"
+ echo " 7) Show service status"
+ echo " 8) Restart service"
+ echo " 9) Edit configuration file manually (nano)"
+ echo " 14) Rebuild / repair (fixes crashes after 'apt upgrade')"
+ echo
+ cecho "cyan" " Spotify Connect:"
+ echo " 10) Install / reconfigure Spotify Connect"
+ echo " 11) Change Spotify device name"
+ echo " 12) Sync Spotify audio device to AirPlay one"
+ echo " 13) Uninstall Spotify Connect"
+ echo
+ echo " 0) Exit"
+ echo
+ local choice
+ read -p "Choose: " choice || true
+ case "$choice" in
+ 1) action_change_name ;;
+ 2) action_change_audio_device ;;
+ 3) action_change_mixer ;;
+ 4) action_change_volume_limits ;;
+ 15) action_max_volume ;;
+ 5) action_test_audio ;;
+ 6) action_view_config ;;
+ 7) action_service_status ;;
+ 8) restart_service ;;
+ 9) sudo nano "$CONFIG_FILE" && restart_service ;;
+ 10) action_install_spotify ;;
+ 11) action_change_spotify_name ;;
+ 12) action_sync_spotify_to_airplay ;;
+ 13) action_uninstall_spotify ;;
+ 14) action_rebuild ;;
+ 0|q|Q|"") cecho "blue" "Bye!"; return 0 ;;
+ *) cecho "red" "Invalid choice." ;;
+ esac
+ done
+}
+
+main "$@"
diff --git a/RaspberryPi-AirPlay-Installer-Scripts/repair_airplay.sh b/RaspberryPi-AirPlay-Installer-Scripts/repair_airplay.sh
new file mode 100755
index 0000000..378b78e
--- /dev/null
+++ b/RaspberryPi-AirPlay-Installer-Scripts/repair_airplay.sh
@@ -0,0 +1,312 @@
+#!/bin/bash
+
+# ===================================================================================
+# Shairport-Sync AirPlay 2 - Rebuild / Repair
+#
+# Recompiles NQPTP and Shairport-Sync from source against the libraries currently
+# installed on the system, WITHOUT touching the configuration.
+#
+# Why this exists:
+# This installer builds nqptp and shairport-sync from source and links them
+# dynamically against system libraries (libplist, libsodium, libsoxr, libavcodec,
+# libssl, libasound, ...). After an "apt upgrade" that bumps one of those
+# libraries, the locally-compiled binary has a stale ABI and can read garbage
+# from internal structs. The classic symptom is a fatal crash on connection:
+#
+# fatal error: Unexpected SPS_FORMAT_* with index 52 while outputting silence
+# shairport-sync.service: Main process exited, code=killed, status=6/ABRT
+#
+# "index 52" is not a real audio format β it is uninitialized memory. Rebuilding
+# against the upgraded libraries restores a coherent ABI and fixes the crash.
+#
+# The existing /etc/shairport-sync.conf is preserved (and backed up).
+# ===================================================================================
+
+set -eo pipefail
+IFS=$'\n\t'
+
+SCRIPT_VERSION="1.0"
+CONFIG_FILE="/etc/shairport-sync.conf"
+SERVICE_NAME="shairport-sync"
+
+# Pinned stable upstream versions β must match install_airplay_v3.sh.
+# Building from `master` pulls Shairport-Sync 5.0-dev, which crashes on some
+# DACs with "Unexpected SPS_FORMAT_* with index N while outputting silence".
+SHAIRPORT_VERSION="4.3.7"
+NQPTP_VERSION="1.2.8"
+BACKUP_DIR="$HOME/airplay-repair-backup-$(date +%Y%m%d-%H%M%S)"
+LOG_FILE="/tmp/airplay-repair-$(date +%Y%m%d-%H%M%S).log"
+
+# --- Helpers ---
+cecho() {
+ local code="\033["
+ local color
+ case "$1" in
+ "red") color="${code}1;31m" ;;
+ "green") color="${code}1;32m" ;;
+ "yellow") color="${code}1;33m" ;;
+ "blue") color="${code}1;34m" ;;
+ "magenta") color="${code}1;35m" ;;
+ "cyan") color="${code}1;36m" ;;
+ *) color="${code}0m" ;;
+ esac
+ echo -e "${color}$2\033[0m"
+}
+
+log() { echo "[$(date '+%H:%M:%S')] $*" >> "$LOG_FILE" 2>/dev/null || true; }
+
+command_exists() { command -v "$1" >/dev/null 2>&1; }
+
+safe_cd() {
+ cd "$1" || { cecho "red" "β Cannot enter directory: $1"; exit 1; }
+}
+
+require_sudo() {
+ if [ "$EUID" -eq 0 ]; then
+ cecho "red" "β Don't run this script with sudo or as root."
+ cecho "yellow" " Just run: bash repair_airplay.sh"
+ exit 1
+ fi
+ if ! sudo -n true 2>/dev/null; then
+ cecho "yellow" "Checking sudo access..."
+ sudo true || { cecho "red" "Sudo required."; exit 1; }
+ fi
+}
+
+check_service() {
+ local service_name="$1"
+ local wait_time="${2:-5}"
+ sleep "$wait_time"
+ systemctl is-active --quiet "$service_name"
+}
+
+# --- Steps ---
+backup_config() {
+ if [ -f "$CONFIG_FILE" ]; then
+ mkdir -p "$BACKUP_DIR"
+ cp "$CONFIG_FILE" "$BACKUP_DIR/" 2>/dev/null || true
+ cecho "green" "β Config backed up to $BACKUP_DIR"
+ log "Backed up $CONFIG_FILE to $BACKUP_DIR"
+ else
+ cecho "yellow" "β $CONFIG_FILE not found β nothing to back up (a fresh install may be needed instead)."
+ fi
+}
+
+install_dependencies() {
+ cecho "blue" "ββββββββββββββββββββββββββββββββββββ"
+ cecho "blue" " Refreshing build dependencies..."
+ cecho "blue" "ββββββββββββββββββββββββββββββββββββ"
+ log "Installing build dependencies..."
+
+ # Same set used by install_airplay_v3.sh β ensures the -dev headers match the
+ # libraries that 'apt upgrade' just installed.
+ local dependencies=(
+ build-essential git autoconf automake libtool pkg-config
+ libpopt-dev libconfig-dev libasound2-dev
+ avahi-daemon libavahi-client-dev libssl-dev
+ libsoxr-dev libplist-dev libplist-utils libsodium-dev
+ libavutil-dev libavcodec-dev libavformat-dev
+ uuid-dev libgcrypt20-dev xxd alsa-utils
+ )
+
+ if ! sudo apt-get install -y "${dependencies[@]}" 2>&1 | tee -a "$LOG_FILE"; then
+ cecho "red" "β Failed to install/refresh build dependencies"
+ exit 1
+ fi
+ cecho "green" "β Dependencies up to date"
+ echo
+}
+
+rebuild_nqptp() {
+ cecho "blue" "ββββββββββββββββββββββββββββββββββββ"
+ cecho "blue" " Rebuilding NQPTP (Timing System)..."
+ cecho "blue" "ββββββββββββββββββββββββββββββββββββ"
+ log "Rebuilding NQPTP..."
+
+ sudo systemctl stop nqptp 2>/dev/null || true
+
+ safe_cd /tmp
+ rm -rf nqptp 2>/dev/null || true
+ log "Pinning NQPTP to release $NQPTP_VERSION"
+ if ! git clone --branch "$NQPTP_VERSION" --depth 1 \
+ https://github.com/mikebrady/nqptp.git 2>&1 | tee -a "$LOG_FILE"; then
+ cecho "red" "β Failed to clone NQPTP repository (check your internet connection)"
+ exit 1
+ fi
+
+ safe_cd nqptp
+ if ! autoreconf -fi 2>&1 | tee -a "$LOG_FILE" \
+ || ! ./configure --with-systemd-startup 2>&1 | tee -a "$LOG_FILE" \
+ || ! make -j"$(nproc)" 2>&1 | tee -a "$LOG_FILE" \
+ || ! sudo make install 2>&1 | tee -a "$LOG_FILE"; then
+ cecho "red" "β NQPTP rebuild failed (see $LOG_FILE)"
+ exit 1
+ fi
+
+ if ! command_exists nqptp; then
+ cecho "red" "β NQPTP binary not found after rebuild"
+ exit 1
+ fi
+
+ sudo systemctl daemon-reload
+ sudo systemctl enable nqptp >/dev/null 2>&1 || true
+ sudo systemctl restart nqptp 2>&1 | tee -a "$LOG_FILE"
+
+ if ! check_service "nqptp" 3; then
+ cecho "red" "β NQPTP service failed to start after rebuild"
+ sudo systemctl status nqptp --no-pager -l | tail -20
+ exit 1
+ fi
+ cecho "green" "β NQPTP rebuilt and running"
+ echo
+}
+
+rebuild_shairport() {
+ cecho "blue" "ββββββββββββββββββββββββββββββββββββ"
+ cecho "blue" " Rebuilding Shairport-Sync..."
+ cecho "blue" " (This takes 10-20 mins on slower Pis)"
+ cecho "blue" "ββββββββββββββββββββββββββββββββββββ"
+ log "Rebuilding Shairport-Sync..."
+
+ sudo systemctl stop "$SERVICE_NAME" 2>/dev/null || true
+
+ safe_cd /tmp
+ rm -rf shairport-sync 2>/dev/null || true
+ log "Pinning Shairport-Sync to release $SHAIRPORT_VERSION"
+ if ! git clone --branch "$SHAIRPORT_VERSION" --depth 1 \
+ https://github.com/mikebrady/shairport-sync.git 2>&1 | tee -a "$LOG_FILE"; then
+ cecho "red" "β Failed to clone Shairport-Sync repository (check your internet connection)"
+ exit 1
+ fi
+
+ safe_cd shairport-sync
+ if ! autoreconf -fi 2>&1 | tee -a "$LOG_FILE"; then
+ cecho "red" "β Shairport-Sync autoreconf failed"
+ exit 1
+ fi
+
+ cecho "yellow" "Configuring build (same flags as the installer)..."
+ # IMPORTANT: must match install_airplay_v3.sh so behaviour is identical.
+ if ! ./configure --sysconfdir=/etc --with-alsa --with-avahi \
+ --with-ssl=openssl --with-soxr --with-systemd \
+ --with-airplay-2 2>&1 | tee -a "$LOG_FILE"; then
+ cecho "red" "β Shairport-Sync configure failed"
+ exit 1
+ fi
+
+ cecho "yellow" "Compiling (be patient)..."
+ if ! make -j"$(nproc)" 2>&1 | tee -a "$LOG_FILE"; then
+ cecho "red" "β Shairport-Sync compilation failed"
+ cecho "yellow" "Last 20 lines of build log:"
+ tail -20 "$LOG_FILE" 2>/dev/null || true
+ exit 1
+ fi
+
+ cecho "yellow" "Installing..."
+ # make install can fail on the systemd unit step β that's fine, the binary is
+ # what matters and we recreate the unit below if needed.
+ sudo make install 2>&1 | tee -a "$LOG_FILE" || true
+
+ if ! command_exists shairport-sync; then
+ cecho "red" "β Shairport-Sync binary not found after rebuild"
+ exit 1
+ fi
+ cecho "green" "β Shairport-Sync rebuilt and installed"
+ echo
+}
+
+ensure_service_unit() {
+ # Preserve an existing unit; only recreate it if it disappeared.
+ if systemctl list-unit-files 2>/dev/null | grep -q '^shairport-sync\.service'; then
+ return
+ fi
+ cecho "yellow" "systemd unit missing β recreating it..."
+ log "Recreating shairport-sync.service unit"
+
+ if ! getent group shairport-sync >/dev/null 2>&1; then
+ sudo groupadd -r shairport-sync
+ fi
+ if ! getent passwd shairport-sync >/dev/null 2>&1; then
+ sudo useradd -r -M -g shairport-sync -s /usr/sbin/nologin -G audio shairport-sync
+ fi
+
+ sudo tee /lib/systemd/system/shairport-sync.service > /dev/null </dev/null 2>&1 || true
+}
+
+restart_and_verify() {
+ cecho "blue" "Restarting services..."
+ sudo systemctl daemon-reload
+ ensure_service_unit
+ sudo systemctl restart nqptp 2>/dev/null || true
+ sudo systemctl restart "$SERVICE_NAME" 2>&1 | tee -a "$LOG_FILE"
+
+ if check_service "$SERVICE_NAME" 5; then
+ cecho "green" "β $SERVICE_NAME is running"
+ else
+ cecho "red" "β $SERVICE_NAME is NOT active after rebuild."
+ cecho "yellow" "Recent logs:"
+ sudo journalctl -u "$SERVICE_NAME" --no-pager -n 25 || true
+ cecho "yellow" "Your previous config is backed up in: $BACKUP_DIR"
+ exit 1
+ fi
+
+ if ! systemctl is-active --quiet avahi-daemon; then
+ cecho "yellow" "β avahi-daemon not running β starting it (needed for discovery)..."
+ sudo systemctl start avahi-daemon || true
+ fi
+}
+
+main() {
+ clear
+ cecho "magenta" "βββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
+ cecho "magenta" " AirPlay 2 β Rebuild / Repair v$SCRIPT_VERSION"
+ cecho "magenta" "βββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
+ echo
+ cecho "yellow" "This recompiles NQPTP and Shairport-Sync from source against the"
+ cecho "yellow" "libraries currently on your system. Use it when AirPlay broke after"
+ cecho "yellow" "an 'apt upgrade' (e.g. the 'Unexpected SPS_FORMAT_* / status=6/ABRT'"
+ cecho "yellow" "crash). Your configuration is preserved."
+ echo
+ cecho "blue" "Log file: $LOG_FILE"
+ echo
+
+ if [ ! -f "$CONFIG_FILE" ]; then
+ cecho "red" "β No existing configuration found at $CONFIG_FILE."
+ cecho "yellow" " This looks like a fresh system β a full install is more appropriate."
+ read -p "Continue with rebuild anyway? (y/N): " ans || true
+ [[ ! "$ans" =~ ^[Yy]$ ]] && { cecho "yellow" "Cancelled."; exit 0; }
+ fi
+
+ read -p "Proceed with the rebuild? (y/N): " ans || true
+ [[ ! "$ans" =~ ^[Yy]$ ]] && { cecho "yellow" "Cancelled."; exit 0; }
+ echo
+
+ require_sudo
+ backup_config
+ install_dependencies
+ rebuild_nqptp
+ rebuild_shairport
+ restart_and_verify
+
+ echo
+ cecho "green" "βββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
+ cecho "green" "β β Rebuild complete β AirPlay should work again. β"
+ cecho "green" "βββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
+ cecho "blue" "Try connecting from your Mac/iPhone now."
+ cecho "blue" "If issues persist, check: sudo journalctl -u shairport-sync -f"
+}
+
+main "$@"
diff --git a/RaspberryPi-AirPlay-Installer-Scripts/uninstall_airplay.sh b/RaspberryPi-AirPlay-Installer-Scripts/uninstall_airplay.sh
new file mode 100755
index 0000000..875afaf
--- /dev/null
+++ b/RaspberryPi-AirPlay-Installer-Scripts/uninstall_airplay.sh
@@ -0,0 +1,195 @@
+#!/bin/bash
+
+# ===================================================================================
+# Shairport-Sync AirPlay 2 - Uninstaller
+#
+# Completely removes Shairport-Sync, NQPTP, configuration files, systemd services,
+# the dedicated user/group and UFW firewall rules added by install_airplay_v3.sh.
+#
+# APT build dependencies are left installed (they may be in use by other software).
+# ===================================================================================
+
+set -eo pipefail
+IFS=$'\n\t'
+
+SCRIPT_VERSION="1.0"
+
+cecho() {
+ local code="\033["
+ local color
+ case "$1" in
+ "red") color="${code}1;31m" ;;
+ "green") color="${code}1;32m" ;;
+ "yellow") color="${code}1;33m" ;;
+ "blue") color="${code}1;34m" ;;
+ "magenta") color="${code}1;35m" ;;
+ "cyan") color="${code}1;36m" ;;
+ *) color="${code}0m" ;;
+ esac
+ echo -e "${color}$2\033[0m"
+}
+
+if [ "$EUID" -eq 0 ]; then
+ cecho "red" "β Don't run this script with sudo or as root."
+ cecho "yellow" " Just run: bash uninstall_airplay.sh"
+ exit 1
+fi
+
+if ! sudo -n true 2>/dev/null; then
+ cecho "yellow" "Sudo access is required for uninstallation."
+ sudo true || { cecho "red" "Sudo required."; exit 1; }
+fi
+
+cecho "magenta" "βββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
+cecho "magenta" "β AirPlay 2 / Shairport-Sync Uninstaller v$SCRIPT_VERSION β"
+cecho "magenta" "βββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
+echo
+cecho "yellow" "This will REMOVE:"
+cecho "yellow" " β’ shairport-sync and nqptp binaries (/usr/local/bin)"
+cecho "yellow" " β’ /etc/shairport-sync.conf and sample"
+cecho "yellow" " β’ systemd services (shairport-sync, nqptp)"
+cecho "yellow" " β’ shairport-sync user and group"
+cecho "yellow" " β’ UFW firewall rules for AirPlay (5353/udp, 319/udp, 320/udp, 7000/tcp)"
+if dpkg -l raspotify 2>/dev/null | grep -q '^ii'; then
+ cecho "yellow" " β’ raspotify (Spotify Connect) package + apt repo"
+fi
+echo
+cecho "blue" "APT build dependencies (libsoxr-dev, libplist-dev, ...) are NOT removed."
+cecho "blue" "Other software on your system may rely on them."
+echo
+read -p "Type 'yes' to confirm uninstall: " confirm || true
+if [ "$confirm" != "yes" ]; then
+ cecho "yellow" "Cancelled."
+ exit 0
+fi
+
+# Backup current configuration (best effort)
+BACKUP_DIR="/tmp/airplay_uninstall_backup_$(date +%Y%m%d_%H%M%S)"
+mkdir -p "$BACKUP_DIR"
+[ -f /etc/shairport-sync.conf ] && sudo cp /etc/shairport-sync.conf "$BACKUP_DIR/" 2>/dev/null || true
+[ -f /etc/shairport-sync.conf.sample ] && sudo cp /etc/shairport-sync.conf.sample "$BACKUP_DIR/" 2>/dev/null || true
+cecho "blue" "Config backup saved to: $BACKUP_DIR"
+echo
+
+# --- Stop services ---
+cecho "blue" "Stopping services..."
+sudo systemctl stop shairport-sync 2>/dev/null || true
+sudo systemctl stop nqptp 2>/dev/null || true
+sudo systemctl stop raspotify 2>/dev/null || true
+
+cecho "blue" "Disabling services..."
+sudo systemctl stop airplay-volume 2>/dev/null || true
+sudo systemctl disable airplay-volume 2>/dev/null || true
+sudo systemctl disable shairport-sync 2>/dev/null || true
+sudo systemctl disable nqptp 2>/dev/null || true
+sudo systemctl disable raspotify 2>/dev/null || true
+
+# --- Remove raspotify (Spotify Connect) ---
+if dpkg -l raspotify 2>/dev/null | grep -q '^ii'; then
+ cecho "blue" "Removing raspotify package..."
+ sudo cp /etc/raspotify/conf "$BACKUP_DIR/raspotify.conf" 2>/dev/null || true
+ sudo apt-get remove --purge -y raspotify 2>/dev/null || true
+fi
+if [ -f /etc/apt/sources.list.d/raspotify.list ]; then
+ cecho "blue" "Removing raspotify apt repository..."
+ sudo rm -f /etc/apt/sources.list.d/raspotify.list
+ sudo rm -f /usr/share/keyrings/raspotify_key.asc
+fi
+
+# --- Remove systemd service files ---
+cecho "blue" "Removing systemd service files..."
+sudo rm -f /lib/systemd/system/shairport-sync.service
+sudo rm -f /etc/systemd/system/shairport-sync.service
+sudo rm -f /usr/local/lib/systemd/system/shairport-sync.service
+sudo rm -f /lib/systemd/system/nqptp.service
+sudo rm -f /etc/systemd/system/nqptp.service
+sudo rm -f /usr/local/lib/systemd/system/nqptp.service
+sudo rm -f /lib/systemd/system/airplay-volume.service
+sudo rm -rf /etc/systemd/system/raspotify.service.d
+sudo systemctl daemon-reload
+sudo systemctl reset-failed 2>/dev/null || true
+
+# --- Remove binaries ---
+cecho "blue" "Removing binaries..."
+sudo rm -f /usr/local/bin/shairport-sync
+sudo rm -f /usr/local/bin/nqptp
+
+# --- Remove configuration files ---
+cecho "blue" "Removing configuration files..."
+sudo rm -f /etc/shairport-sync.conf
+sudo rm -f /etc/shairport-sync.conf.sample
+
+# --- Remove ancillary files ---
+cecho "blue" "Removing ancillary files (man pages, shared data)..."
+sudo rm -rf /usr/local/share/shairport-sync 2>/dev/null || true
+sudo rm -rf /etc/shairport-sync 2>/dev/null || true
+sudo rm -f /usr/local/share/man/man7/shairport-sync.7 2>/dev/null || true
+sudo rm -f /usr/local/share/man/man7/nqptp.7 2>/dev/null || true
+
+# --- Remove user and group ---
+cecho "blue" "Removing shairport-sync user and group..."
+if getent passwd shairport-sync >/dev/null 2>&1; then
+ sudo userdel shairport-sync 2>/dev/null || true
+fi
+if getent group shairport-sync >/dev/null 2>&1; then
+ sudo groupdel shairport-sync 2>/dev/null || true
+fi
+
+# --- Remove firewall rules ---
+if command -v ufw >/dev/null 2>&1; then
+ cecho "blue" "Removing UFW firewall rules..."
+ sudo ufw delete allow 5353/udp 2>/dev/null || true
+ sudo ufw delete allow 319/udp 2>/dev/null || true
+ sudo ufw delete allow 320/udp 2>/dev/null || true
+ sudo ufw delete allow 7000/tcp 2>/dev/null || true
+fi
+
+# --- Verify ---
+echo
+cecho "blue" "Verifying removal..."
+failures=0
+if command -v shairport-sync >/dev/null 2>&1; then
+ cecho "yellow" "β shairport-sync still present at $(command -v shairport-sync)"
+ failures=$((failures+1))
+fi
+if command -v nqptp >/dev/null 2>&1; then
+ cecho "yellow" "β nqptp still present at $(command -v nqptp)"
+ failures=$((failures+1))
+fi
+if [ -f /etc/shairport-sync.conf ]; then
+ cecho "yellow" "β /etc/shairport-sync.conf still present"
+ failures=$((failures+1))
+fi
+if systemctl list-unit-files 2>/dev/null | grep -qE '^(shairport-sync|nqptp|raspotify)\.service'; then
+ cecho "yellow" "β Some systemd unit files are still registered"
+ failures=$((failures+1))
+fi
+if dpkg -l raspotify 2>/dev/null | grep -q '^ii'; then
+ cecho "yellow" "β raspotify package still installed"
+ failures=$((failures+1))
+fi
+
+echo
+if [ "$failures" -eq 0 ]; then
+ cecho "green" "βββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
+ cecho "green" "β β
UNINSTALL COMPLETE β
β"
+ cecho "green" "βββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
+else
+ cecho "yellow" "β Uninstall finished with $failures leftover item(s) β see warnings above."
+fi
+echo
+cecho "blue" "Config backup (if any): $BACKUP_DIR"
+echo
+cecho "blue" "To also remove the APT build dependencies (only if not used by anything else):"
+cecho "blue" " sudo apt-get remove --purge libsoxr-dev libplist-dev libplist-utils libsodium-dev \\"
+cecho "blue" " libavutil-dev libavcodec-dev libavformat-dev libpopt-dev libconfig-dev \\"
+cecho "blue" " libgcrypt20-dev libavahi-client-dev libssl-dev"
+cecho "blue" " sudo apt-get autoremove"
+echo
+
+read -p "Reboot now to ensure a clean state? (y/N): " do_reboot || true
+if [[ "$do_reboot" =~ ^[Yy]$ ]]; then
+ cecho "yellow" "Rebooting in 3 seconds..."
+ sleep 3
+ sudo reboot
+fi