Files
raspi-airplay/install-airplay-to-raspi.sh

1004 lines
39 KiB
Bash

#!/bin/bash
# ===================================================================================
# Shairport-Sync AirPlay 2 ROBUST Installer - ENHANCED VERSION 3.0
#
# Tailored for: Raspberry Pi (Zero 2/3/4/5) with USB DAC
# Version: 3.0 - Production Ready
# Features:
# - Comprehensive error handling with rollback capability
# - Dependency validation before installation
# - Build failure recovery
# - Audio device validation
# - Service health checks
# - Firewall configuration
# - Latest package versions
# ===================================================================================
# curl -X PUT "http://192.168.23.218:3689/api/player/stop"
# curl -X POST "http://192.168.23.218:3689/api/queue/items/add?uris=http://icecast.radiofrance.fr/fip-hifi.aac&clear=true&playback=start"
set -eo pipefail # Exit on error and pipe failures
IFS=$'\n\t' # Safer word splitting
# --- Global Variables ---
SCRIPT_VERSION="3.0"
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
# Audio configuration variables
audio_device=""
audio_device_plug=""
card_number=""
device_number=""
mixer_control=""
selected_device=""
airplay_name=""
disable_wifi_pm=false
# --- Cleanup Handler ---
cleanup() {
local exit_code=$?
if [ $exit_code -ne 0 ] && [ $INSTALLATION_FAILED -eq 1 ]; then
cecho "red" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cecho "red" " Installation Failed - Cleaning Up"
cecho "red" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Stop services if they were started
sudo systemctl stop shairport-sync 2>/dev/null || true
sudo systemctl stop nqptp 2>/dev/null || true
# Restore backups if they exist
if [ -d "$BACKUP_DIR" ]; then
cecho "yellow" "Restoring original configuration..."
[ -f "$BACKUP_DIR/shairport-sync.conf" ] && \
sudo cp "$BACKUP_DIR/shairport-sync.conf" /etc/shairport-sync.conf 2>/dev/null || true
fi
cecho "yellow" "Installation log saved to: $LOG_FILE"
cecho "yellow" "Please check the log for details."
fi
# Cleanup temp build directories
rm -rf /tmp/nqptp /tmp/shairport-sync 2>/dev/null || true
}
trap cleanup EXIT ERR INT TERM
# --- Logging Function ---
log() {
# Create log file if it doesn't exist
touch "$LOG_FILE" 2>/dev/null || true
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
# --- Helper Functions ---
cecho() {
local code="\033["
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" ;;
*) color="${code}0m" ;;
esac
echo -e "${color}$2\033[0m"
}
# Show spinner during long operations
show_spinner() {
local pid=$1
local delay=0.1
local spinstr='|/-\'
while ps -p $pid > /dev/null 2>&1; do
local temp=${spinstr#?}
printf " [%c] " "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep $delay
printf "\b\b\b\b\b\b"
done
printf " \b\b\b\b"
}
# Check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Check if a service is running properly
check_service() {
local service_name=$1
local max_retries=${2:-3}
local retry=0
while [ $retry -lt $max_retries ]; do
if systemctl is-active --quiet "$service_name"; then
cecho "green" "$service_name is running"
return 0
fi
retry=$((retry + 1))
[ $retry -lt $max_retries ] && sleep 2
done
cecho "red" "$service_name failed to start after $max_retries attempts"
cecho "yellow" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cecho "yellow" " Diagnostic Information:"
cecho "yellow" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
echo "Service Status:" | tee -a "$LOG_FILE"
sudo systemctl status "$service_name" --no-pager -l 2>&1 | tee -a "$LOG_FILE"
echo
echo "Recent Logs:" | tee -a "$LOG_FILE"
sudo journalctl -u "$service_name" -n 30 --no-pager 2>&1 | tee -a "$LOG_FILE"
echo
cecho "yellow" "Check the log file for more details: $LOG_FILE"
return 1
}
# Validate package installation
validate_package() {
local package=$1
if dpkg -l "$package" 2>/dev/null | grep -q "^ii"; then
return 0
else
cecho "red" "✗ Package $package failed to install"
return 1
fi
}
# Safe directory change
safe_cd() {
cd "$1" || {
cecho "red" "Failed to change directory to: $1"
exit 1
}
}
# --- Pre-flight Checks ---
pre_flight_checks() {
cecho "blue" "═══════════════════════════════════════"
cecho "blue" " Running Pre-Flight Checks..."
cecho "blue" "═══════════════════════════════════════"
echo
# Check for root user
if [ "$EUID" -eq 0 ]; then
cecho "red" "❌ Error: Don't run this script with sudo or as root."
cecho "yellow" " Just run: bash install_airplay_v3.sh"
exit 1
fi
# Check if user can sudo
if ! sudo -n true 2>/dev/null; then
cecho "yellow" "Checking sudo access..."
if ! sudo true; then
cecho "red" "❌ This script requires sudo access."
exit 1
fi
fi
cecho "green" "✓ Sudo access confirmed"
# Check for internet connection
cecho "yellow" "Checking internet connection..."
local test_hosts=("8.8.8.8" "1.1.1.1" "github.com")
local connection_ok=0
for host in "${test_hosts[@]}"; do
# Use timeout command to prevent hanging
cecho "blue" " Testing $host..."
timeout 8 ping -c 1 -W 5 "$host" >/dev/null 2>&1 || true
local result=$?
if [ $result -eq 0 ]; then
connection_ok=1
log "Internet check: Successfully pinged $host"
cecho "green" " ✓ Connected"
break
elif [ $result -eq 124 ]; then
log "Internet check: Timeout pinging $host"
cecho "yellow" " ✗ Timeout"
else
log "Internet check: Failed to ping $host (exit $result)"
cecho "yellow" " ✗ Failed"
fi
done
if [ $connection_ok -eq 0 ]; then
cecho "red" "❌ No internet connection detected."
cecho "yellow" " Troubleshooting:"
cecho "yellow" " 1. Check Wi-Fi is connected: iwconfig"
cecho "yellow" " 2. Test manually: ping -c 3 8.8.8.8"
cecho "yellow" " 3. Check DNS: ping -c 3 github.com"
echo
cecho "blue" " Your network status:"
ip addr show wlan0 2>/dev/null | grep "inet " || echo " No wlan0 IP address"
echo
read -p "Skip internet check and continue anyway? (y/N): " skip_check || true
if [[ "$skip_check" =~ ^[Yy]$ ]]; then
cecho "yellow" "⚠ Continuing without internet check (may fail later)"
log "User skipped internet check"
else
exit 1
fi
else
cecho "green" "✓ Internet connection OK"
fi
# Check if running on Raspberry Pi
if [ ! -f /proc/device-tree/model ]; then
cecho "yellow" "⚠ Warning: This doesn't appear to be a Raspberry Pi."
read -p "Continue anyway? (y/N): " continue_choice || true
[[ ! "$continue_choice" =~ ^[Yy]$ ]] && exit 1
else
local pi_model
pi_model=$(tr -d '\0' < /proc/device-tree/model)
cecho "green" "✓ Detected: $pi_model"
# Warn on older Pi models
if echo "$pi_model" | grep -qE "Pi Zero W|Pi 1"; then
cecho "yellow" "⚠ Warning: $pi_model may not have enough power for AirPlay 2"
cecho "yellow" " Recommended: Pi Zero 2 or newer"
read -p "Continue anyway? (y/N): " continue_choice || true
[[ ! "$continue_choice" =~ ^[Yy]$ ]] && exit 1
fi
fi
# Check available disk space (need at least 1GB)
local available_space
available_space=$(df / | tail -1 | awk '{print $4}')
if [ "$available_space" -lt 1000000 ]; then
cecho "red" "❌ Not enough disk space. Need at least 1GB free."
cecho "yellow" " Current available: $((available_space / 1024)) MB"
exit 1
fi
cecho "green" "✓ Sufficient disk space available: $((available_space / 1024)) MB"
# Check available memory
local available_mem
available_mem=$(free -m | awk '/^Mem:/{print $7}')
if [ "$available_mem" -lt 100 ]; then
cecho "yellow" "⚠ Warning: Low available memory ($available_mem MB)"
cecho "yellow" " Consider closing other applications."
else
cecho "green" "✓ Available memory: $available_mem MB"
fi
# Check for required base tools
cecho "yellow" "Checking required tools..."
local required_tools=("git" "gcc" "make" "aplay" "amixer")
local missing_tools=()
for tool in "${required_tools[@]}"; do
if ! command_exists "$tool"; then
missing_tools+=("$tool")
fi
done
if [ ${#missing_tools[@]} -gt 0 ]; then
cecho "yellow" "⚠ Missing tools: ${missing_tools[*]}"
cecho "yellow" " These will be installed with dependencies."
else
cecho "green" "✓ All base tools present"
fi
echo
}
# --- Detect and Select Audio Device (FIXED) ---
select_audio_device() {
echo
cecho "yellow" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cecho "yellow" " Step 1: Audio Device Selection"
cecho "yellow" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
cecho "cyan" "⏸ PLEASE RESPOND TO THIS PROMPT ⏸"
echo
# Get list of all audio cards into an array
cecho "blue" "Scanning for audio devices..."
mapfile -t all_cards < <(aplay -l 2>/dev/null | grep '^card')
if [ ${#all_cards[@]} -eq 0 ]; then
cecho "red" "❌ No audio devices detected at all!"
cecho "yellow" " Make sure your USB DAC is connected."
cecho "yellow" " Try running 'lsusb' to check hardware connection."
exit 1
fi
# Always show the menu and force manual selection
cecho "green" "Available audio devices:"
for i in "${!all_cards[@]}"; do
# Display 1-based index for user friendliness
echo " [$((i+1))] ${all_cards[$i]}"
done
echo
local device_choice
local array_index
while true; do
read -p "Enter the number of your device [1-${#all_cards[@]}]: " device_choice || true
# Check if input is a number
if [[ "$device_choice" =~ ^[0-9]+$ ]]; then
# Convert 1-based input to 0-based array index
array_index=$((device_choice - 1))
# Check if index exists in array
if [ "$array_index" -ge 0 ] && [ "$array_index" -lt "${#all_cards[@]}" ]; then
selected_device="${all_cards[$array_index]}"
break
fi
fi
cecho "red" "Invalid selection. Please enter a number between 1 and ${#all_cards[@]}."
done
cecho "green" "✓ You selected: $selected_device"
# Extract card and device numbers
# Regex looks for "card X" and "device Y"
card_number=$(echo "$selected_device" | grep -oP 'card \K\d+' || echo "")
device_number=$(echo "$selected_device" | grep -oP 'device \K\d+' || echo "0")
if [ -z "$card_number" ]; then
cecho "red" "❌ Failed to extract card number from selection."
exit 1
fi
audio_device="hw:$card_number,$device_number"
audio_device_plug="plughw:$card_number,$device_number"
cecho "green" "✓ Audio device set to: $audio_device_plug"
# Validate the audio device actually works
cecho "blue" "Validating audio device..."
if aplay -D "$audio_device_plug" -l >/dev/null 2>&1; then
cecho "green" "✓ Audio device validation passed"
else
cecho "red" "❌ Audio device validation failed"
cecho "yellow" " The device may not support playback."
read -p "Continue anyway? (y/N): " continue_choice || true
[[ ! "$continue_choice" =~ ^[Yy]$ ]] && exit 1
fi
# Detect available mixer controls for this card
cecho "blue" "Detecting volume controls..."
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 hardware mixer controls found."
cecho "yellow" " Software volume will be used."
mixer_control=""
else
cecho "green" "Available mixer controls:"
for mixer in "${mixers[@]}"; do
echo " - $mixer"
done
# Try to find the best mixer control
mixer_control=""
for preferred in "PCM" "Master" "Speaker" "Headphone" "Digital"; do
for mixer in "${mixers[@]}"; do
if [[ "$mixer" == "$preferred" ]]; then
mixer_control="$mixer"
break 2
fi
done
done
# If no preferred mixer found, use the first one
if [ -z "$mixer_control" ]; then
mixer_control="${mixers[0]}"
fi
cecho "green" "✓ Selected volume control: $mixer_control"
fi
echo
}
# --- Get AirPlay Name ---
get_airplay_name() {
echo
cecho "yellow" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cecho "yellow" " Step 2: Name Your AirPlay Device"
cecho "yellow" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
cecho "cyan" "⏸ PLEASE RESPOND TO THIS PROMPT ⏸"
echo
local hostname
hostname=$(hostname)
cecho "blue" "This is the name that will appear on your iPhone/iPad."
cecho "blue" "Examples: Living Room, Bedroom Speaker, Kitchen Audio"
echo
cecho "green" ">>> "
read -p "Enter a name (or press Enter for '$hostname AirPlay'): " airplay_name || true
if [ -z "$airplay_name" ]; then
airplay_name="$hostname AirPlay"
fi
# Sanitize the name (remove special characters that might cause issues)
airplay_name=$(echo "$airplay_name" | sed 's/[^a-zA-Z0-9 _-]//g')
cecho "green" "✓ AirPlay name: '$airplay_name'"
echo
}
# --- Wi-Fi Power Management ---
configure_wifi() {
echo
cecho "yellow" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cecho "yellow" " Step 3: Wi-Fi Optimization"
cecho "yellow" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
cecho "cyan" "⏸ PLEASE RESPOND TO THIS PROMPT ⏸"
echo
# Check if Wi-Fi is actually being used
if ! ip link show wlan0 &>/dev/null; then
cecho "yellow" "⚠ No Wi-Fi interface detected (wlan0 not found)"
cecho "yellow" " Skipping Wi-Fi optimization."
disable_wifi_pm=false
echo
return
fi
cecho "blue" "Wi-Fi power saving can cause audio stuttering and dropouts."
cecho "blue" "Disabling it ensures smooth, uninterrupted playback."
echo
cecho "green" ">>> "
read -p "Disable Wi-Fi power saving? (Y/n): " wifi_choice || true
if [[ -z "$wifi_choice" || "$wifi_choice" =~ ^[Yy]$ ]]; then
disable_wifi_pm=true
cecho "green" "✓ Wi-Fi power management will be disabled"
else
disable_wifi_pm=false
cecho "yellow" "⚠ Keeping default Wi-Fi settings (may cause dropouts)"
fi
echo
}
# --- Main Installation ---
main() {
# Initialize log file immediately
touch "$LOG_FILE" 2>/dev/null || LOG_FILE="/tmp/airplay_install_fallback.log"
clear
cecho "green" "╔═════════════════════════════════════════════════════╗"
cecho "green" "║ ║"
cecho "green" "║ AirPlay 2 Installer for Raspberry Pi + DAC ║"
cecho "green" "║ Version $SCRIPT_VERSION"
cecho "green" "║ ║"
cecho "green" "╚═════════════════════════════════════════════════════╝"
echo
cecho "blue" "This installer will turn your Raspberry Pi into a"
cecho "blue" "high-quality AirPlay 2 receiver. Just follow the prompts!"
echo
cecho "yellow" "Installation log: $LOG_FILE"
echo
log "=== AirPlay 2 Installation Started ==="
log "Script Version: $SCRIPT_VERSION"
log "Date: $(date)"
log "User: $(whoami)"
log "System: $(uname -a)"
echo
# Check if running interactively
if [ -t 0 ]; then
read -p "Press Enter to begin..." || true
else
cecho "yellow" "⚠ Non-interactive mode detected - using defaults"
sleep 2
fi
echo
# Run all setup steps
pre_flight_checks
select_audio_device
get_airplay_name
configure_wifi
# --- Confirmation ---
echo
echo
cecho "magenta" "╔═════════════════════════════════════════════════════╗"
cecho "magenta" "║ INSTALLATION CONFIGURATION ║"
cecho "magenta" "╚═════════════════════════════════════════════════════╝"
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" " 📡 Disable Wi-Fi PM: $disable_wifi_pm"
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)"
echo
echo
cecho "cyan" "⏸ FINAL CONFIRMATION - PRESS ENTER TO CONTINUE ⏸"
echo
if [ -t 0 ]; then
read -p "Press Enter to start installation, or Ctrl+C to cancel..." || true
else
cecho "yellow" "Auto-starting in 5 seconds (non-interactive mode)..."
sleep 5
fi
echo
INSTALLATION_FAILED=1 # Mark that installation has started
# Create backup directory
mkdir -p "$BACKUP_DIR"
[ -f /etc/shairport-sync.conf ] && cp /etc/shairport-sync.conf "$BACKUP_DIR/" 2>/dev/null || true
# --- System Update ---
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cecho "blue" " Updating System Packages..."
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log "Updating package lists..."
if ! sudo apt-get update -qq 2>&1 | tee -a "$LOG_FILE"; then
cecho "red" "❌ Failed to update package lists"
exit 1
fi
cecho "yellow" "Upgrading existing packages (this may take a few minutes)..."
if ! sudo apt-get upgrade -y 2>&1 | tee -a "$LOG_FILE"; then
cecho "yellow" "⚠ Package upgrade had issues, but continuing..."
fi
cecho "green" "✓ System updated"
echo
# --- Install Dependencies ---
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cecho "blue" " Installing Dependencies..."
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log "Installing build dependencies..."
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 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 dependencies"
exit 1
fi
# Validate critical packages
local critical_packages=("build-essential" "git" "libasound2-dev" "avahi-daemon")
for package in "${critical_packages[@]}"; do
if ! validate_package "$package"; then
cecho "red" "❌ Critical package $package is not installed"
exit 1
fi
done
cecho "green" "✓ Dependencies installed"
echo
# Make sure avahi-daemon is running
if ! systemctl is-active --quiet avahi-daemon; then
cecho "yellow" "Starting avahi-daemon..."
sudo systemctl enable avahi-daemon
sudo systemctl start avahi-daemon
fi
# --- Install NQPTP ---
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cecho "blue" " Installing NQPTP (Timing System)..."
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log "Cloning NQPTP repository..."
# Verify /tmp is writable
if [ ! -w /tmp ]; then
cecho "red" "❌ /tmp directory is not writable"
exit 1
fi
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"
cecho "yellow" " Possible causes:"
cecho "yellow" " - No internet connection"
cecho "yellow" " - GitHub is down"
cecho "yellow" " - Firewall blocking access"
exit 1
fi
safe_cd nqptp
log "Building NQPTP..."
if ! autoreconf -fi 2>&1 | tee -a "$LOG_FILE"; then
cecho "red" "❌ NQPTP autoreconf failed"
exit 1
fi
if ! ./configure --with-systemd-startup 2>&1 | tee -a "$LOG_FILE"; then
cecho "red" "❌ NQPTP configure failed"
exit 1
fi
if ! make -j"$(nproc)" 2>&1 | tee -a "$LOG_FILE"; then
cecho "red" "❌ NQPTP compilation failed"
exit 1
fi
if ! sudo make install 2>&1 | tee -a "$LOG_FILE"; then
cecho "red" "❌ NQPTP installation failed"
exit 1
fi
# Verify binary was installed
if ! command_exists nqptp; then
cecho "red" "❌ NQPTP binary not found after installation"
cecho "yellow" " Expected location: /usr/local/bin/nqptp"
exit 1
fi
# Enable and start NQPTP
sudo systemctl enable nqptp 2>&1 | tee -a "$LOG_FILE"
sudo systemctl restart nqptp 2>&1 | tee -a "$LOG_FILE"
sleep 3
if ! check_service "nqptp"; then
cecho "red" "❌ NQPTP service failed to start"
exit 1
fi
echo
# --- Install Shairport-Sync ---
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cecho "blue" " Installing Shairport-Sync..."
cecho "blue" " (This takes 10-20 mins on slower Pis)"
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log "Cloning Shairport-Sync repository..."
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"
cecho "yellow" " Possible causes:"
cecho "yellow" " - No internet connection"
cecho "yellow" " - GitHub is down"
cecho "yellow" " - Firewall blocking access"
exit 1
fi
safe_cd shairport-sync
log "Building 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..."
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, this takes time)..."
log "Starting compilation with $(nproc) cores..."
# Run make in background so we can show spinner
local make_log="${LOG_FILE}.make"
make -j"$(nproc)" > "$make_log" 2>&1 &
local make_pid=$!
show_spinner $make_pid
# Wait for make to complete and check status
if ! wait $make_pid; then
cecho "red" "❌ Shairport-Sync compilation failed"
cecho "yellow" "Last 20 lines of build log:"
tail -20 "$make_log" 2>/dev/null || echo " (log file not available)"
exit 1
fi
cat "$make_log" >> "$LOG_FILE" 2>/dev/null || true
cecho "yellow" "Installing..."
# Note: make install may fail on systemd service install, but that's OK
# We'll create the service file manually later
sudo make install 2>&1 | tee -a "$LOG_FILE" || true
# What matters is that the binary was installed
if ! command_exists shairport-sync; then
cecho "red" "❌ Shairport-Sync binary not found after installation"
cecho "yellow" " Expected location: /usr/local/bin/shairport-sync"
cecho "yellow" " The 'make install' may have failed - check the log"
exit 1
fi
cecho "green" "✓ Shairport-Sync compiled and installed"
log "Note: make install may have shown errors about systemd service - this is normal"
echo
# --- Configure Shairport-Sync ---
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cecho "blue" " Configuring Shairport-Sync..."
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log "Creating configuration file from sample..."
# Copy sample config as base (installed by make install)
if [ -f /etc/shairport-sync.conf.sample ]; then
sudo cp /etc/shairport-sync.conf.sample /etc/shairport-sync.conf
log "Copied sample config to /etc/shairport-sync.conf"
else
cecho "yellow" "⚠ Sample config not found, creating minimal config"
# Fallback to minimal config if sample doesn't exist
sudo tee /etc/shairport-sync.conf > /dev/null <<'FALLBACK_EOF'
// Minimal configuration - sample file was not available
general = {};
alsa = {};
FALLBACK_EOF
fi
# Now edit the config file to set our values using simple, reliable sed commands
log "Configuring AirPlay name: $airplay_name"
# Set the AirPlay device name - uncomment and set it
sudo sed -i "s|^//[[:space:]]*name = .*| name = \"$airplay_name\";|" /etc/shairport-sync.conf
# Set output device - uncomment and set it
log "Configuring audio output: $audio_device_plug"
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
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
# Set volume settings
sudo sed -i "s|^//[[:space:]]*volume_max_db = .*| volume_max_db = 4.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
# Verify config file was created
if [ ! -f /etc/shairport-sync.conf ]; then
cecho "red" "❌ Configuration file was not created"
exit 1
fi
cecho "green" "✓ Configuration file created and customized"
# Set mixer volume to maximum if available
if [ -n "$mixer_control" ]; then
cecho "blue" "Setting mixer volume to 100%..."
if amixer -c "$card_number" set "$mixer_control" 100% unmute > /dev/null 2>&1; then
sudo alsactl store > /dev/null 2>&1 || true
cecho "green" "✓ Mixer volume set to maximum"
else
cecho "yellow" "⚠ Could not set mixer volume (may not be supported)"
fi
fi
echo
# --- Create/Update Systemd Service ---
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cecho "blue" " Setting Up Auto-Start Service..."
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log "Creating shairport-sync user and group..."
# Create user and group for shairport-sync service
if ! getent group shairport-sync >/dev/null 2>&1; then
sudo groupadd -r shairport-sync
log "Created shairport-sync group"
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
log "Created shairport-sync user"
fi
log "Creating systemd service manually (make install sometimes fails at this step)..."
# Create systemd service file manually - this is more reliable than 'make install'
# which often fails on the systemd service installation step on Raspberry Pi
sudo tee /lib/systemd/system/shairport-sync.service > /dev/null <<EOF
[Unit]
Description=Shairport Sync - AirPlay Audio Receiver
After=sound.target network-online.target
[Service]
ExecStart=/usr/local/bin/shairport-sync
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable shairport-sync 2>&1 | tee -a "$LOG_FILE"
sudo systemctl restart shairport-sync 2>&1 | tee -a "$LOG_FILE"
sleep 5
if ! check_service "shairport-sync" 5; then
cecho "red" "❌ Shairport-Sync service failed to start"
cecho "yellow" "Checking service status..."
sudo systemctl status shairport-sync --no-pager -l | tail -20
exit 1
fi
# Verify avahi-daemon is running (required for AirPlay discovery)
cecho "blue" "Checking Avahi daemon (required for device discovery)..."
if ! systemctl is-active --quiet avahi-daemon; then
cecho "yellow" "⚠ Avahi daemon is not running, attempting to start..."
sudo systemctl start avahi-daemon
sleep 2
if systemctl is-active --quiet avahi-daemon; then
cecho "green" "✓ Avahi daemon started"
else
cecho "red" "❌ Avahi daemon failed to start - AirPlay device may not be discoverable"
fi
else
cecho "green" "✓ Avahi daemon is running"
fi
echo
# --- Wi-Fi Power Management Instructions ---
if [ "$disable_wifi_pm" = true ]; then
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cecho "blue" " Wi-Fi Power Management"
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log "User requested Wi-Fi power management disable"
cecho "yellow" "📝 Manual Wi-Fi Power Management Configuration Needed:"
echo
cecho "blue" "After installation completes, disable Wi-Fi power saving to prevent"
cecho "blue" "audio dropouts. You have two options:"
echo
cecho "green" "Option 1: Using raspi-config (Recommended)"
cecho "blue" " 1. Run: sudo raspi-config"
cecho "blue" " 2. Go to: Performance Options → Wireless LAN → Power Management"
cecho "blue" " 3. Select: Disable"
echo
cecho "green" "Option 2: Manual command"
cecho "blue" " Run: sudo iw dev wlan0 set power_save off"
cecho "blue" " (Note: This is temporary, resets on reboot)"
echo
cecho "yellow" "⚠ We're not doing this automatically to avoid disconnecting your SSH session"
log "Wi-Fi power management instructions provided to user"
echo
fi
# --- Configure Firewall (if active) ---
if command_exists ufw && sudo ufw status | grep -q "Status: active"; then
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cecho "blue" " Configuring Firewall..."
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Allow mDNS for AirPlay discovery
sudo ufw allow 5353/udp comment 'mDNS for AirPlay' 2>&1 | tee -a "$LOG_FILE"
# Allow NQPTP ports
sudo ufw allow 319/udp comment 'NQPTP PTP' 2>&1 | tee -a "$LOG_FILE"
sudo ufw allow 320/udp comment 'NQPTP PTP' 2>&1 | tee -a "$LOG_FILE"
# AirPlay ports
sudo ufw allow 7000/tcp comment 'AirPlay' 2>&1 | tee -a "$LOG_FILE"
cecho "green" "✓ Firewall rules added"
echo
fi
# --- Audio Test ---
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
cecho "blue" " Testing Audio Output..."
cecho "blue" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
read -p "Do you want to test audio output? (Y/n): " test_audio || true
if [[ -z "$test_audio" || "$test_audio" =~ ^[Yy]$ ]]; then
cecho "yellow" "Playing test sound in 2 seconds..."
cecho "yellow" "(You should hear a voice saying 'Front Left', 'Front Right')"
sleep 2
if timeout 10 speaker-test -D "$audio_device_plug" -c 2 -t wav -l 1 > /dev/null 2>&1; then
echo
cecho "green" "✓ Audio test completed!"
read -p "Did you hear the test sound? (y/N): " heard_sound || true
if [[ ! "$heard_sound" =~ ^[Yy]$ ]]; then
cecho "yellow" "⚠ If you didn't hear sound, check:"
cecho "yellow" " - Speaker/headphone connections"
cecho "yellow" " - Volume level on your amplifier/speakers"
cecho "yellow" " - USB DAC power"
fi
else
cecho "yellow" "⚠ Audio test couldn't run, but setup is complete."
cecho "yellow" " You can test manually with: speaker-test -D $audio_device_plug -c 2 -t wav"
fi
fi
echo
# --- Cleanup ---
cecho "blue" "Cleaning up temporary files..."
rm -rf /tmp/nqptp /tmp/shairport-sync 2>/dev/null || true
rm -f "${LOG_FILE}.make" 2>/dev/null || true
cecho "green" "✓ Cleanup complete"
echo
INSTALLATION_FAILED=0 # Installation succeeded
# --- Success Message ---
cecho "green" "╔═════════════════════════════════════════════════════╗"
cecho "green" "║ ║"
cecho "green" "║ ✅ INSTALLATION COMPLETE! ✅ ║"
cecho "green" "║ ║"
cecho "green" "╚═════════════════════════════════════════════════════╝"
echo
log "=== Installation completed successfully ==="
cecho "magenta" "🎵 Your AirPlay 2 device is ready!"
echo
cecho "yellow" " 📱 Device Name: $airplay_name"
cecho "yellow" " 🔊 Audio Output: $audio_device_plug"
cecho "yellow" " 🎚️ Volume: ${mixer_control:-Fixed (no hardware control)}"
echo
cecho "blue" "┌─────────────────────────────────────────────────────┐"
cecho "blue" "│ How to use: │"
cecho "blue" "│ 1. Open Music/Spotify/YouTube on your iPhone/iPad │"
cecho "blue" "│ 2. Tap the AirPlay icon (📡) │"
cecho "blue" "│ 3. Select '$airplay_name' │"
cecho "blue" "│ 4. Enjoy high-quality wireless audio! │"
cecho "blue" "└─────────────────────────────────────────────────────┘"
echo
cecho "yellow" "💡 Tips:"
cecho "yellow" " • Device should appear within 30 seconds after reboot"
cecho "yellow" " • Make sure iPhone and Pi are on the same Wi-Fi network"
cecho "yellow" " • For best quality, use lossless audio sources"
if [ "$disable_wifi_pm" = true ]; then
echo
cecho "yellow" "📝 IMPORTANT - After reboot:"
cecho "yellow" " Don't forget to disable Wi-Fi power management using raspi-config!"
cecho "yellow" " This prevents audio dropouts and stuttering."
fi
echo
cecho "blue" "📋 Useful commands:"
cecho "blue" " View live logs: sudo journalctl -u shairport-sync -f"
cecho "blue" " Restart service: sudo systemctl restart shairport-sync"
cecho "blue" " Check status: sudo systemctl status shairport-sync"
cecho "blue" " Edit config: sudo nano /etc/shairport-sync.conf"
cecho "blue" " Installation log: $LOG_FILE"
echo
read -p "Press Enter to reboot now (recommended), or Ctrl+C to reboot later..." || {
echo
cecho "yellow" "Reboot cancelled. Remember to reboot later with: sudo reboot"
exit 0
}
log "User initiated reboot"
cecho "yellow" "Rebooting in 3 seconds..."
sleep 3
sudo reboot
}
# --- Script Entry Point ---
main "$@"