From 11a8d0e98c6bc170c8d402c44e613f30e7648884 Mon Sep 17 00:00:00 2001 From: ermannonicoletti Date: Mon, 18 May 2026 20:14:58 +0200 Subject: [PATCH 01/12] Expand audio device support: added compatibility for built-in audio and HATs, improved device selection UI. --- .../install_airplay_v3.sh | 68 +++++++++---------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh b/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh index 6b9cbee..63f9420 100644 --- a/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh +++ b/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh @@ -3,7 +3,8 @@ # =================================================================================== # 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) # Version: 3.0 - Production Ready # Features: # - Comprehensive error handling with rollback capability @@ -299,60 +300,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) - - 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 + # Build the full list of available devices (built-in audio included) + mapfile -t all_devices < <(echo "$all_cards") - 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 @@ -486,7 +482,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" "╚═════════════════════════════════════════════════════╝" From 9686254587eecd9c168b55f2af6491cc54444193 Mon Sep 17 00:00:00 2001 From: ermannonicoletti Date: Mon, 18 May 2026 20:27:05 +0200 Subject: [PATCH 02/12] Add uninstall, modify, and manager scripts for AirPlay setup. Added libraries for trixie --- README.md | 167 ++++++-- .../airplay_manager.sh | 117 ++++++ .../install_airplay_v3.sh | 2 +- .../modify_airplay.sh | 358 ++++++++++++++++++ .../uninstall_airplay.sh | 170 +++++++++ 5 files changed, 774 insertions(+), 40 deletions(-) create mode 100755 RaspberryPi-AirPlay-Installer-Scripts/airplay_manager.sh mode change 100644 => 100755 RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh create mode 100755 RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh create mode 100755 RaspberryPi-AirPlay-Installer-Scripts/uninstall_airplay.sh diff --git a/README.md b/README.md index 23015ac..143278f 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,172 @@ # 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 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) 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!) | -| :---: | :---: | -| [![Manual AirPlay 2 Pi Setup](https://img.youtube.com/vi/WeibcfMywXU/0.jpg)](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]`. +* **🛠️ 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. + +### Option A — Run from this repo (recommended for development) + +```bash +git clone https://github.com/Techposts/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. -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 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 + +Need to rename the speaker, change the audio output or adjust volume limits? You don't have to reinstall. + +```bash +bash modify_airplay.sh +``` + +The interactive menu provides: -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. +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) -| Mobile Screenshot | Hardware Setup | -| :---: | :---: | -| ****
*Your new device, ready to connect.* | ****
*The simple and clean hardware setup.* | +All changes are written to `/etc/shairport-sync.conf` and the service is restarted automatically. --- -### How It Works +## 🧹 Uninstalling + +```bash +bash uninstall_airplay.sh +``` + +Removes: -This project uses a two-script system for a safe and reliable installation: +* `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`) -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`). +A backup of the current config is saved under `/tmp/airplay_uninstall_backup_/` before anything is deleted. + +> 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`. -If this installer helped you bring your old speakers back to life, please consider showing your support! +**Audio stutters / drops out** -* **⭐ 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. +* 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. + +**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. +* 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..e427dac --- /dev/null +++ b/RaspberryPi-AirPlay-Installer-Scripts/airplay_manager.sh @@ -0,0 +1,117 @@ +#!/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 +} + +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" " Service: $(service_status_line)" + cecho "blue" " AirPlay name: $(current_name)" + 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 " 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 + ;; + 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 63f9420..8ea80d7 --- a/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh +++ b/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh @@ -576,7 +576,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 ) diff --git a/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh b/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh new file mode 100755 index 0000000..73f2ae7 --- /dev/null +++ b/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh @@ -0,0 +1,358 @@ +#!/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" +CONFIG_FILE="/etc/shairport-sync.conf" +SERVICE_NAME="shairport-sync" + +# --- 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 +} + +# --- 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 +} + +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 +} + +# --- 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" " Current name: $(current_name)" + cecho "yellow" " Current device: $(current_output_device)" + cecho "yellow" " Current mixer: $(current_mixer)" + echo + 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 " 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 " 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 ;; + 5) action_test_audio ;; + 6) action_view_config ;; + 7) action_service_status ;; + 8) restart_service ;; + 9) sudo nano "$CONFIG_FILE" && restart_service ;; + 0|q|Q|"") cecho "blue" "Bye!"; return 0 ;; + *) cecho "red" "Invalid choice." ;; + esac + done +} + +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..789788f --- /dev/null +++ b/RaspberryPi-AirPlay-Installer-Scripts/uninstall_airplay.sh @@ -0,0 +1,170 @@ +#!/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)" +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 + +cecho "blue" "Disabling services..." +sudo systemctl disable shairport-sync 2>/dev/null || true +sudo systemctl disable nqptp 2>/dev/null || true + +# --- 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 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)\.service'; then + cecho "yellow" "⚠ Some systemd unit files are still registered" + 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 From b0b2ec85b1ba203713d5f34b91131ceac70a7896 Mon Sep 17 00:00:00 2001 From: ermannonicoletti Date: Mon, 18 May 2026 20:58:28 +0200 Subject: [PATCH 03/12] Add optional Spotify Connect support via raspotify: installer, configuration, and management --- README.md | 15 +- .../airplay_manager.sh | 19 +- .../install_airplay_v3.sh | 123 ++++++++++ .../modify_airplay.sh | 224 ++++++++++++++++-- .../uninstall_airplay.sh | 23 +- 5 files changed, 377 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 143278f..a2f334a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # RaspberryPi-AirPlay-Installer 📻 -Turn any Raspberry Pi (Zero 2 W, 3, 4, 5) into a modern, high-quality **AirPlay 2** receiver 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) with a set of robust, interactive scripts. +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!** @@ -12,6 +12,7 @@ Turn any Raspberry Pi (Zero 2 W, 3, 4, 5) into a modern, high-quality **AirPlay * **🤖 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. @@ -95,6 +96,16 @@ The interactive menu provides: 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. + --- ## 🧹 Uninstalling @@ -110,6 +121,7 @@ Removes: * 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 A backup of the current config is saved under `/tmp/airplay_uninstall_backup_/` before anything is deleted. @@ -163,6 +175,7 @@ sudo systemctl restart shairport-sync ## 🙏 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). --- diff --git a/RaspberryPi-AirPlay-Installer-Scripts/airplay_manager.sh b/RaspberryPi-AirPlay-Installer-Scripts/airplay_manager.sh index e427dac..5d1654e 100755 --- a/RaspberryPi-AirPlay-Installer-Scripts/airplay_manager.sh +++ b/RaspberryPi-AirPlay-Installer-Scripts/airplay_manager.sh @@ -64,6 +64,20 @@ current_name() { | 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" "╔═════════════════════════════════════════════════════╗" @@ -72,8 +86,9 @@ while true; do echo if is_installed; then cecho "green" " ✓ Shairport-Sync installed" - cecho "blue" " Service: $(service_status_line)" - cecho "blue" " AirPlay name: $(current_name)" + 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 diff --git a/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh b/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh index 8ea80d7..0e68cad 100755 --- a/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh +++ b/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh @@ -5,6 +5,7 @@ # # 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 @@ -35,6 +36,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=$? @@ -46,6 +53,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 @@ -474,6 +482,104 @@ 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 + + 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 + + sudo systemctl enable raspotify 2>&1 | tee -a "$LOG_FILE" || true + sudo systemctl restart raspotify 2>&1 | tee -a "$LOG_FILE" || true + sleep 3 + + if check_service "raspotify"; then + cecho "green" "✓ Spotify Connect (raspotify) is running as '$spotify_name'" + else + cecho "yellow" "⚠ raspotify service is not active — check 'journalctl -u raspotify'" + fi + echo +} + # --- Main Installation --- main() { # Initialize log file immediately @@ -514,6 +620,7 @@ main() { select_audio_device get_airplay_name configure_wifi + configure_spotify # --- Confirmation --- echo @@ -526,6 +633,11 @@ main() { cecho "yellow" " 🔊 Audio Output: $audio_device_plug" cecho "yellow" " 🎚️ Volume Control: ${mixer_control:-None (fixed volume)}" 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)" @@ -875,6 +987,9 @@ EOF fi echo + # --- Spotify Connect (optional) --- + install_spotify_connect + # --- Wi-Fi Power Management Instructions --- if [ "$disable_wifi_pm" = true ]; then cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @@ -915,6 +1030,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 @@ -971,6 +1091,9 @@ EOF cecho "yellow" " 📱 Device Name: $airplay_name" cecho "yellow" " 🔊 Audio Output: $audio_device_plug" cecho "yellow" " 🎚️ Volume: ${mixer_control:-Fixed (no hardware control)}" + 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 index 73f2ae7..00985a5 100755 --- a/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh +++ b/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh @@ -13,6 +13,8 @@ IFS=$'\n\t' SCRIPT_VERSION="1.0" CONFIG_FILE="/etc/shairport-sync.conf" SERVICE_NAME="shairport-sync" +RASPOTIFY_CONF="/etc/raspotify/conf" +SPOTIFY_ZEROCONF_PORT="5354" # --- Helpers --- cecho() { @@ -88,6 +90,53 @@ current_mixer() { | 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 +} + +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 @@ -312,6 +361,118 @@ action_service_status() { sudo systemctl status nqptp --no-pager -l | head -10 || 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" + + 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 @@ -322,35 +483,52 @@ main() { cecho "magenta" " AirPlay 2 — Modify Existing Installation v$SCRIPT_VERSION" cecho "magenta" "═══════════════════════════════════════════════════════" echo - cecho "yellow" " Current name: $(current_name)" - cecho "yellow" " Current device: $(current_output_device)" - cecho "yellow" " Current mixer: $(current_mixer)" + 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 " 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 + 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 " 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 " 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 " 0) Exit" + 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 ;; - 5) action_test_audio ;; - 6) action_view_config ;; - 7) action_service_status ;; - 8) restart_service ;; - 9) sudo nano "$CONFIG_FILE" && restart_service ;; + 1) action_change_name ;; + 2) action_change_audio_device ;; + 3) action_change_mixer ;; + 4) action_change_volume_limits ;; + 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 ;; 0|q|Q|"") cecho "blue" "Bye!"; return 0 ;; - *) cecho "red" "Invalid choice." ;; + *) cecho "red" "Invalid choice." ;; esac done } diff --git a/RaspberryPi-AirPlay-Installer-Scripts/uninstall_airplay.sh b/RaspberryPi-AirPlay-Installer-Scripts/uninstall_airplay.sh index 789788f..304cc0f 100755 --- a/RaspberryPi-AirPlay-Installer-Scripts/uninstall_airplay.sh +++ b/RaspberryPi-AirPlay-Installer-Scripts/uninstall_airplay.sh @@ -50,6 +50,9 @@ 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." @@ -72,10 +75,24 @@ echo 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 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..." @@ -139,10 +156,14 @@ 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)\.service'; then +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 From 0d599718add07fab23f9e84911e1517afd6798f9 Mon Sep 17 00:00:00 2001 From: Ermanno Nicoletti <80400985+ermanno00@users.noreply.github.com> Date: Wed, 20 May 2026 17:08:54 +0200 Subject: [PATCH 04/12] Add alternative repository clone option in README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a2f334a..f6ab645 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ After flashing **Raspberry Pi OS** (Lite is fine) and connecting via SSH, you ha ```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 ``` From 34db9ed4e800ce0da1a863b7c648c3d38909fe65 Mon Sep 17 00:00:00 2001 From: ermannonicoletti Date: Sat, 20 Jun 2026 20:57:03 +0200 Subject: [PATCH 05/12] Add rebuild/repair option to fix crashes after system updates --- .../airplay_manager.sh | 9 +++++++++ .../modify_airplay.sh | 14 ++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/RaspberryPi-AirPlay-Installer-Scripts/airplay_manager.sh b/RaspberryPi-AirPlay-Installer-Scripts/airplay_manager.sh index 5d1654e..56b3f2c 100755 --- a/RaspberryPi-AirPlay-Installer-Scripts/airplay_manager.sh +++ b/RaspberryPi-AirPlay-Installer-Scripts/airplay_manager.sh @@ -97,6 +97,7 @@ while true; do 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 @@ -126,6 +127,14 @@ while true; do 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 diff --git a/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh b/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh index 00985a5..946c8d6 100755 --- a/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh +++ b/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh @@ -11,6 +11,7 @@ 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" @@ -361,6 +362,17 @@ action_service_status() { 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 @@ -502,6 +514,7 @@ main() { 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" @@ -527,6 +540,7 @@ main() { 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 From ed049de63d05a38fefeb34d00ba737abfea4def0 Mon Sep 17 00:00:00 2001 From: ermannonicoletti Date: Sat, 20 Jun 2026 20:57:51 +0200 Subject: [PATCH 06/12] Add repair script to rebuild NQPTP and Shairport-Sync after system updates --- .../repair_airplay.sh | 302 ++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100755 RaspberryPi-AirPlay-Installer-Scripts/repair_airplay.sh diff --git a/RaspberryPi-AirPlay-Installer-Scripts/repair_airplay.sh b/RaspberryPi-AirPlay-Installer-Scripts/repair_airplay.sh new file mode 100755 index 0000000..f0083bb --- /dev/null +++ b/RaspberryPi-AirPlay-Installer-Scripts/repair_airplay.sh @@ -0,0 +1,302 @@ +#!/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" +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 + if ! git clone 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 + if ! git clone 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 "$@" From faa1fe6bc4b6858e9e48249ce296aac18da2de7c Mon Sep 17 00:00:00 2001 From: ermannonicoletti Date: Sat, 20 Jun 2026 21:01:13 +0200 Subject: [PATCH 07/12] Improve Spotify Connect setup: ensure custom name fallback, reload units correctly, and verify configuration --- .../install_airplay_v3.sh | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh b/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh index 0e68cad..224dffb 100755 --- a/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh +++ b/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh @@ -553,6 +553,14 @@ install_spotify_connect() { 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 @@ -568,12 +576,28 @@ LIBRESPOT_ZEROCONF_PORT="$SPOTIFY_ZEROCONF_PORT" # <<< airplay-installer <<< EOF + # The raspotify unit was just installed by apt and auto-started with the + # default name. Reload units and do a clean stop→start (not a plain restart + # against the apt-spawned transitional state) so librespot re-reads the conf + # and advertises our name from a fresh process. + sudo systemctl daemon-reload 2>&1 | tee -a "$LOG_FILE" || true sudo systemctl enable raspotify 2>&1 | tee -a "$LOG_FILE" || true - sudo systemctl restart 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 From beb242fabee893619c405133217d5418714effa1 Mon Sep 17 00:00:00 2001 From: ermannonicoletti Date: Sat, 20 Jun 2026 21:27:27 +0200 Subject: [PATCH 08/12] Pin stable versions of Shairport-Sync and NQPTP; fix raspotify auto-start to prevent default name advertisement. --- .../install_airplay_v3.sh | 38 ++++++++++++++++--- .../modify_airplay.sh | 8 ++++ .../repair_airplay.sh | 14 ++++++- 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh b/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh index 224dffb..a769384 100755 --- a/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh +++ b/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh @@ -26,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="" @@ -538,10 +549,21 @@ install_spotify_connect() { sudo apt-get update -qq 2>&1 | tee -a "$LOG_FILE" || true fi + # Mask raspotify BEFORE installing it. The deb's post-install hook + # auto-starts the service, and at that point /etc/raspotify/conf still has + # no LIBRESPOT_NAME, so librespot would briefly advertise the package + # default "raspotify ()" over zeroconf. The Spotify app caches the + # first name it discovers, so that brief window is enough to make the wrong + # name stick. Masking blocks the auto-start; we unmask and start it below, + # only after the correct name is written, so the very first advertisement + # already carries our name. + sudo systemctl mask raspotify 2>/dev/null || true + 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..." + sudo systemctl unmask raspotify 2>/dev/null || true install_spotify=false return 0 fi @@ -576,10 +598,10 @@ LIBRESPOT_ZEROCONF_PORT="$SPOTIFY_ZEROCONF_PORT" # <<< airplay-installer <<< EOF - # The raspotify unit was just installed by apt and auto-started with the - # default name. Reload units and do a clean stop→start (not a plain restart - # against the apt-spawned transitional state) so librespot re-reads the conf - # and advertises our name from a fresh process. + # raspotify was masked before install, so it has never run yet. Unmask it + # now that the correct name is in the conf, then enable and start it: the + # very first zeroconf advertisement already carries our name. + sudo systemctl unmask raspotify 2>&1 | tee -a "$LOG_FILE" || 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 @@ -756,7 +778,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" @@ -816,7 +840,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" diff --git a/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh b/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh index 946c8d6..53df5b4 100755 --- a/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh +++ b/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh @@ -411,20 +411,28 @@ action_install_spotify() { sudo apt-get update -qq || true fi + # Mask raspotify before install so the deb's post-install auto-start cannot + # advertise the default "raspotify ()" name over zeroconf — the + # Spotify app caches the first name it sees. We unmask after writing ours. + sudo systemctl mask raspotify 2>/dev/null || true + cecho "blue" "Installing raspotify..." if ! sudo apt-get install -y raspotify; then cecho "red" "❌ Failed to install raspotify." + sudo systemctl unmask raspotify 2>/dev/null || true return fi if [ ! -f "$RASPOTIFY_CONF" ]; then cecho "red" "❌ $RASPOTIFY_CONF not found after install." + sudo systemctl unmask raspotify 2>/dev/null || true return fi write_spotify_managed_block "$spotify_name" "$cur_dev" cecho "green" "✓ raspotify configured: '$spotify_name' on $cur_dev" + sudo systemctl unmask raspotify 2>/dev/null || true sudo systemctl enable raspotify >/dev/null 2>&1 || true restart_spotify } diff --git a/RaspberryPi-AirPlay-Installer-Scripts/repair_airplay.sh b/RaspberryPi-AirPlay-Installer-Scripts/repair_airplay.sh index f0083bb..378b78e 100755 --- a/RaspberryPi-AirPlay-Installer-Scripts/repair_airplay.sh +++ b/RaspberryPi-AirPlay-Installer-Scripts/repair_airplay.sh @@ -28,6 +28,12 @@ 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" @@ -121,7 +127,9 @@ rebuild_nqptp() { 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 (check your internet connection)" exit 1 fi @@ -164,7 +172,9 @@ rebuild_shairport() { 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 (check your internet connection)" exit 1 fi From 954ba90e8222542970ab60f0e5a7f960091ebd59 Mon Sep 17 00:00:00 2001 From: ermannonicoletti Date: Sat, 20 Jun 2026 21:58:19 +0200 Subject: [PATCH 09/12] Fix raspotify setup: remove masking before install, ensure proper restart for custom name advertisement --- .../install_airplay_v3.sh | 21 ++++++------------- .../modify_airplay.sh | 8 +------ 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh b/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh index a769384..3d2ba65 100755 --- a/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh +++ b/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh @@ -549,21 +549,10 @@ install_spotify_connect() { sudo apt-get update -qq 2>&1 | tee -a "$LOG_FILE" || true fi - # Mask raspotify BEFORE installing it. The deb's post-install hook - # auto-starts the service, and at that point /etc/raspotify/conf still has - # no LIBRESPOT_NAME, so librespot would briefly advertise the package - # default "raspotify ()" over zeroconf. The Spotify app caches the - # first name it discovers, so that brief window is enough to make the wrong - # name stick. Masking blocks the auto-start; we unmask and start it below, - # only after the correct name is written, so the very first advertisement - # already carries our name. - sudo systemctl mask raspotify 2>/dev/null || true - 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..." - sudo systemctl unmask raspotify 2>/dev/null || true install_spotify=false return 0 fi @@ -598,10 +587,12 @@ LIBRESPOT_ZEROCONF_PORT="$SPOTIFY_ZEROCONF_PORT" # <<< airplay-installer <<< EOF - # raspotify was masked before install, so it has never run yet. Unmask it - # now that the correct name is in the conf, then enable and start it: the - # very first zeroconf advertisement already carries our name. - sudo systemctl unmask raspotify 2>&1 | tee -a "$LOG_FILE" || true + # The raspotify unit was just installed by apt and auto-started with the + # default name. Reload units and do a clean stop→start (not a plain restart + # against the apt-spawned transitional state) so librespot re-reads the conf + # and advertises our name from a fresh process. (Defensive unmask in case a + # previous installer run left it masked.) + sudo systemctl unmask raspotify 2>/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 diff --git a/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh b/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh index 53df5b4..74a1a00 100755 --- a/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh +++ b/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh @@ -411,27 +411,21 @@ action_install_spotify() { sudo apt-get update -qq || true fi - # Mask raspotify before install so the deb's post-install auto-start cannot - # advertise the default "raspotify ()" name over zeroconf — the - # Spotify app caches the first name it sees. We unmask after writing ours. - sudo systemctl mask raspotify 2>/dev/null || true - cecho "blue" "Installing raspotify..." if ! sudo apt-get install -y raspotify; then cecho "red" "❌ Failed to install raspotify." - sudo systemctl unmask raspotify 2>/dev/null || true return fi if [ ! -f "$RASPOTIFY_CONF" ]; then cecho "red" "❌ $RASPOTIFY_CONF not found after install." - sudo systemctl unmask raspotify 2>/dev/null || true 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 From 7f23908276ae6a60df600914e15290674424601f Mon Sep 17 00:00:00 2001 From: ermannonicoletti Date: Sat, 20 Jun 2026 22:11:19 +0200 Subject: [PATCH 10/12] Add airplay-volume service to manage max volume at boot; ensure proper cleanup in uninstall and update systemd overrides for consistent device naming --- .../install_airplay_v3.sh | 41 +++++++++++++++++++ .../modify_airplay.sh | 9 ++++ .../uninstall_airplay.sh | 4 ++ 3 files changed, 54 insertions(+) diff --git a/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh b/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh index 3d2ba65..9b966b4 100755 --- a/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh +++ b/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh @@ -585,6 +585,18 @@ 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 <&1 | tee -a "$LOG_FILE" || true + if sudo systemctl enable airplay-volume.service 2>&1 | tee -a "$LOG_FILE"; then + cecho "green" "✓ Max volume will be re-applied on every boot" + else + cecho "yellow" "⚠ Could not enable boot-time volume service" + fi fi echo diff --git a/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh b/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh index 74a1a00..c68f3b4 100755 --- a/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh +++ b/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh @@ -121,6 +121,15 @@ 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() { diff --git a/RaspberryPi-AirPlay-Installer-Scripts/uninstall_airplay.sh b/RaspberryPi-AirPlay-Installer-Scripts/uninstall_airplay.sh index 304cc0f..875afaf 100755 --- a/RaspberryPi-AirPlay-Installer-Scripts/uninstall_airplay.sh +++ b/RaspberryPi-AirPlay-Installer-Scripts/uninstall_airplay.sh @@ -78,6 +78,8 @@ 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 @@ -102,6 +104,8 @@ 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 From 75516f40017bbe106d17958f82b6a329e77a2e3b Mon Sep 17 00:00:00 2001 From: ermannonicoletti Date: Sat, 20 Jun 2026 22:26:45 +0200 Subject: [PATCH 11/12] Enhance max volume management: adjust all playback controls to 100%, add boot-time service for persistent volume settings, and integrate option in modify script. --- .../install_airplay_v3.sh | 27 ++++++-- .../modify_airplay.sh | 66 +++++++++++++++++++ 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh b/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh index 9b966b4..7084f21 100755 --- a/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh +++ b/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh @@ -975,14 +975,34 @@ FALLBACK_EOF cecho "yellow" "⚠ Could not set mixer volume (may not be supported)" fi + # Bring EVERY playback control on the card up to 100%, not just the one + # shairport-sync uses. Many USB DACs expose several controls (PCM, + # Digital, Speaker...) and if any sits at ~50% the Pi's desktop volume + # indicator and the actual output stay below maximum. + 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 + # Persist max volume across reboots. # `alsactl store` (above) saves the state to asound.state and is normally # restored at boot by alsa-restore.service, but on several Pi OS images the # mixer comes up around 50% regardless (the saved state isn't restored for # the selected card, or another component lowers it). We install a tiny - # boot-time oneshot that forces the mixer to 100% on every boot, ordered - # before shairport-sync so playback always starts at full hardware volume. + # boot-time oneshot that forces every control to 100% on every boot, + # ordered before shairport-sync so playback starts at full hardware volume. 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_number set \"$ctl\" 100% unmute"$'\n' + done < <(amixer -c "$card_number" scontrols 2>/dev/null | grep -oP "Simple mixer control '\K[^']+" || true) + # Fallback to the chosen control if enumeration produced nothing. + [ -z "$exec_lines" ] && exec_lines="ExecStart=-$amixer_bin -c $card_number set \"$mixer_control\" 100% unmute"$'\n' + sudo tee /lib/systemd/system/airplay-volume.service > /dev/null < /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 + + 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 +} + +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" "Setting all controls on card $card_number to 100% and enabling boot service..." + if install_max_volume_service "$card_number"; then + cecho "green" "✓ Hardware volume set to maximum (now and on every boot)" + cecho "blue" " Controls on card $card_number:" + amixer -c "$card_number" sget "$(current_mixer)" 2>/dev/null | grep -E '\[[0-9]+%\]' | head -4 || true + fi +} + 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" @@ -520,6 +584,7 @@ main() { 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) Set hardware volume to MAX now + on every boot" echo " 5) Test audio output" echo " 6) View configuration" echo " 7) Show service status" @@ -542,6 +607,7 @@ main() { 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 ;; From 44c21fce905a81e1e0df7f44c7660f5301e1e3cb Mon Sep 17 00:00:00 2001 From: ermannonicoletti Date: Sat, 20 Jun 2026 22:43:57 +0200 Subject: [PATCH 12/12] Switch to software volume control with independent AirPlay/Spotify streams. Pin DAC hardware volume to 100%, considering PipeWire/ALSA setups, and ensure persistent max volume across reboots. --- .../install_airplay_v3.sh | 166 ++++++++++-------- .../modify_airplay.sh | 50 ++++-- 2 files changed, 131 insertions(+), 85 deletions(-) diff --git a/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh b/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh index 7084f21..93e8381 100755 --- a/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh +++ b/RaspberryPi-AirPlay-Installer-Scripts/install_airplay_v3.sh @@ -173,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" "═══════════════════════════════════════" @@ -680,7 +748,7 @@ 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)" @@ -936,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 @@ -965,64 +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 - - # Bring EVERY playback control on the card up to 100%, not just the one - # shairport-sync uses. Many USB DACs expose several controls (PCM, - # Digital, Speaker...) and if any sits at ~50% the Pi's desktop volume - # indicator and the actual output stay below maximum. - 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 - - # Persist max volume across reboots. - # `alsactl store` (above) saves the state to asound.state and is normally - # restored at boot by alsa-restore.service, but on several Pi OS images the - # mixer comes up around 50% regardless (the saved state isn't restored for - # the selected card, or another component lowers it). We install a tiny - # boot-time oneshot that forces every control to 100% on every boot, - # ordered before shairport-sync so playback starts at full hardware volume. - 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_number set \"$ctl\" 100% unmute"$'\n' - done < <(amixer -c "$card_number" scontrols 2>/dev/null | grep -oP "Simple mixer control '\K[^']+" || true) - # Fallback to the chosen control if enumeration produced nothing. - [ -z "$exec_lines" ] && exec_lines="ExecStart=-$amixer_bin -c $card_number set \"$mixer_control\" 100% unmute"$'\n' - - sudo tee /lib/systemd/system/airplay-volume.service > /dev/null <&1 | tee -a "$LOG_FILE" || true - if sudo systemctl enable airplay-volume.service 2>&1 | tee -a "$LOG_FILE"; then - cecho "green" "✓ Max volume will be re-applied on every boot" - else - cecho "yellow" "⚠ Could not enable boot-time volume service" - 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 --- @@ -1191,7 +1207,7 @@ 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 diff --git a/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh b/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh index 8b7e4e4..4cf4a35 100755 --- a/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh +++ b/RaspberryPi-AirPlay-Installer-Scripts/modify_airplay.sh @@ -302,10 +302,12 @@ action_change_mixer() { fi } -install_max_volume_service() { - # Args: card_number. Sets every playback control to 100% now and installs a - # boot-time oneshot that re-applies it on every reboot (alsactl restore is - # unreliable on some Pi OS images, so the desktop volume can come up ~50%). +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; } @@ -316,6 +318,17 @@ install_max_volume_service() { 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 @@ -346,6 +359,15 @@ EOF 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) @@ -358,12 +380,20 @@ action_max_volume() { cecho "red" "Could not parse card number from: $cur_out" return fi - cecho "blue" "Setting all controls on card $card_number to 100% and enabling boot service..." - if install_max_volume_service "$card_number"; then - cecho "green" "✓ Hardware volume set to maximum (now and on every boot)" - cecho "blue" " Controls on card $card_number:" - amixer -c "$card_number" sget "$(current_mixer)" 2>/dev/null | grep -E '\[[0-9]+%\]' | head -4 || true + + 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() { @@ -584,7 +614,7 @@ main() { 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) Set hardware volume to MAX now + on every boot" + 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"