This document captures the full end-to-end process for building a standalone headless CD music player using:
- Raspberry Pi Zero 2 W
- USB laptop CD/DVD drive
- Bluetooth speaker
- No monitor
- No keyboard during normal usage
- Optional future GPIO buttons for control
It documents the actual setup flow discussed and debugged, including:
- Raspberry Pi OS setup
- package installation
- Bluetooth pairing and auto-reconnect
- Audio output validation
- CD drive detection
- autoplay on disc insert
- service and script files
- troubleshooting steps
- the shift from batch ripping to direct playback from the audio CD
The final preferred design is:
- Raspberry Pi Zero 2 W boots headlessly.
- Pi reconnects automatically to the Bluetooth speaker.
- User inserts an audio CD and pushes the tray in.
- Linux detects the optical drive event.
udevtriggers asystemdservice.- The service runs an autoplay script.
- The script waits for the disc to become readable.
- The script starts direct CD playback using VLC.
- Audio plays through the Bluetooth speaker.
- Raspberry Pi Zero 2 W
- microSD card
- 5V power supply for Pi
- micro-USB OTG adapter
- Powered USB hub
- USB laptop-style CD/DVD drive
- Bluetooth speaker
- Wi-Fi network for setup
- Another computer for SSH access
The Pi Zero 2 W has only one USB OTG data port and limited USB power budget. A slim optical drive can draw unstable current during spin-up and reads.
Using a powered USB hub helps avoid:
- drive disconnects
- random reboots
- tray issues
- unreliable disc reads
flowchart TD
A[Power on Raspberry Pi Zero 2 W] --> B[Boot Raspberry Pi OS Lite]
B --> C[Start Bluetooth reconnect service]
C --> D[Reconnect Bluetooth speaker]
D --> E[Wait for CD tray insert]
E --> F[Audio CD inserted and tray pushed in]
F --> G[udev detects /dev/sr0 change event]
G --> H[systemd starts cdplayer.service]
H --> I[autoplay_cd.sh runs]
I --> J[Check /dev/sr0 exists]
J --> K[Retry until audio CD is readable]
K --> L[Set Bluetooth sink as default]
L --> M[Launch VLC direct CD playback]
M --> N[Music plays on Bluetooth speaker]
Use Raspberry Pi OS Lite on the Pi Zero 2 W.
While flashing, enable:
- SSH
- Wi-Fi credentials
- hostname
- locale/timezone
- username and password
Example hostname:
cdplayerFrom another machine on the same network:
ping cdplayer.local
ssh pizero@cdplayer.localIf .local does not resolve, find the Pi IP from your router’s DHCP/connected devices page.
sudo apt update
sudo apt full-upgrade -yInstall all required packages for:
- Bluetooth management
- Bluetooth audio
- VLC playback
- CD reading
- tray control
- future GPIO control
sudo apt install -y \
bluez bluetooth \
pipewire pipewire-pulse pipewire-audio pipewire-alsa pulseaudio-utils \
wireplumber \
vlc cdparanoia eject \
python3 python3-pip python3-gpiozerobluezbluetooth
Used for:
- pairing devices
- trusting devices
- reconnecting speaker
- controlling Bluetooth from terminal using
bluetoothctl
pipewirepipewire-pulsepipewire-audiopipewire-alsapulseaudio-utilswireplumber
Used for:
- Bluetooth audio sink support
- audio routing
pactlcontrol- default output sink selection
- headless user audio session
vlc
Used for:
-
headless audio playback with
cvlc -
direct audio CD playback
-
cdparanoia
Used for:
-
verifying the inserted disc is a readable audio CD
-
eject
Used for:
- software tray eject / close / toggle
python3-gpiozero
Used later for optional physical buttons:
- play/pause
- next
- previous
- eject
sudo rebootReconnect via SSH after reboot.
Connect the CD drive through:
- Pi Zero 2 W USB data port
- OTG adapter
- powered USB hub
- USB optical drive
Check for optical drive detection:
lsusb
dmesg | tail -n 80
ls /dev/sr*Expected:
/dev/sr0This confirms Linux can see the optical drive as a block CD device.
If /dev/sr0 does not exist, nothing else in the setup will work.
Put the Bluetooth speaker in pairing mode.
Run:
bluetoothctlInside bluetoothctl:
power on
agent on
default-agent
scan on
Wait until the speaker appears.
Then run:
pair XX:XX:XX:XX:XX:XX
trust XX:XX:XX:XX:XX:XX
connect XX:XX:XX:XX:XX:XX
scan off
exit
Replace the MAC address with your real speaker address.
At one point the following happened:
Failed to set power on: org.bluez.Error.Failed
SetDiscoveryFilter failed: org.bluez.Error.NotReady
Failed to start discovery: org.bluez.Error.NotReady
This indicated Bluetooth controller readiness problems.
sudo systemctl status bluetooth --no-pager
sudo systemctl restart bluetooth
rfkill list bluetooth
sudo rfkill unblock bluetooth
bluetoothctl list
bluetoothctl showAfter pairing:
pactl list sinks short
pactl get-default-sink
bluetoothctl devices ConnectedExpected sink format:
bluez_output.XX_XX_XX_XX_XX_XX.1
Set speaker as default output:
pactl set-default-sink bluez_output.XX_XX_XX_XX_XX_XX.1Use a test sound:
paplay /usr/share/sounds/alsa/Front_Center.wavIf the test file is unavailable, use any existing WAV file.
Because the setup is headless and audio playback must work without desktop login, enable linger for the user:
sudo loginctl enable-linger pizeroThen start user audio services:
systemctl --user daemon-reload
systemctl --user enable --now pipewire pipewire-pulse wireplumbersystemctl --user status pipewire --no-pager
systemctl --user status pipewire-pulse --no-pager
systemctl --user status wireplumber --no-pagerCreate:
nano /home/pizero/bt-reconnect.shContents:
#!/bin/bash
MAC="XX:XX:XX:XX:XX:XX"
sleep 8
bluetoothctl <<EOF
power on
connect $MAC
EOF
sleep 3
SINK=$(pactl list sinks short | awk '/bluez_output/ {print $2; exit}')
if [ -n "$SINK" ]; then
pactl set-default-sink "$SINK"
fiMake executable:
chmod +x /home/pizero/bt-reconnect.shIt ensures that after reboot the Pi:
- powers Bluetooth on
- reconnects to the previously paired speaker
- resets the Bluetooth speaker as the default audio sink
Create:
sudo nano /etc/systemd/system/bt-reconnect.serviceContents:
[Unit]
Description=Reconnect Bluetooth Speaker
After=bluetooth.target network.target
[Service]
User=pizero
Type=oneshot
ExecStart=/home/pizero/bt-reconnect.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.targetEnable it:
sudo systemctl daemon-reload
sudo systemctl enable bt-reconnect.serviceStart it manually for testing:
sudo systemctl start bt-reconnect.serviceCheck logs/status:
sudo systemctl status bt-reconnect.service
sudo journalctl -u bt-reconnect.service -n 50 --no-pagerBecause the service is Type=oneshot, it runs once and exits. That is normal.
Adding:
RemainAfterExit=yesmakes it show as active/exited after success.
The first working version used cdparanoia -B to batch rip all tracks, then start VLC.
This worked technically, but had a major user experience issue:
- no music starts until ripping of all tracks finishes
This caused long silence before playback.
The CD insertion logs showed:
- CD detection worked
- ripping worked
- playback failed because VLC was being launched as root
Example error seen:
VLC is not supposed to be run as root. Sorry.
The original script was being run directly from udev, and udev runs commands as root.
VLC refused to run as root, and root does not use the normal user’s Bluetooth audio session.
This led to two fixes:
- launch long work via
systemdinstead of directly insideudev - run VLC as user
pizerousingrunuser
Create or update:
sudo nano /etc/udev/rules.d/99-cd-autoplay.rulesContents:
SUBSYSTEM=="block", KERNEL=="sr0", ACTION=="change", TAG+="systemd", ENV{SYSTEMD_WANTS}="cdplayer.service"
Reload rules:
sudo udevadm control --reload-rules
sudo udevadm triggerudev is good for detecting hardware events.
systemd is better for running:
- longer scripts
- logging
- retries
- user-aware commands
So udev only triggers systemd, and systemd runs the CD playback logic.
Create:
sudo nano /etc/systemd/system/cdplayer.serviceContents:
[Unit]
Description=CD Autoplay Service
After=bluetooth.target
[Service]
Type=oneshot
ExecStart=/home/pizero/cdplayer/autoplay_cd.shReload:
sudo systemctl daemon-reloadThis was chosen over batch ripping because:
- it starts music much faster
- it feels more like a real CD player
- it removes the long ripping delay
- it simplifies file management
Even with direct playback, cdparanoia -Q is still useful to verify:
- the disc is present
- the disc is actually a readable audio CD
- the drive is ready before playback starts
Create directory structure if missing:
mkdir -p /home/pizero/cdplayer/logsCreate:
nano /home/pizero/cdplayer/autoplay_cd.shContents:
#!/bin/bash
USER_NAME="pizero"
USER_HOME="/home/$USER_NAME"
USER_ID=$(id -u "$USER_NAME")
WORKDIR="$USER_HOME/cdplayer"
LOG="$WORKDIR/logs/cdplayer.log"
LOCKFILE="/tmp/cdplayer.lock"
mkdir -p "$WORKDIR/logs"
if [ -f "$LOCKFILE" ]; then
echo "$(date) : already running, exiting" >> "$LOG"
exit 0
fi
touch "$LOCKFILE"
trap 'rm -f "$LOCKFILE"' EXIT
echo "----------------------------------------" >> "$LOG"
echo "$(date) : CD event received" >> "$LOG"
if [ ! -e /dev/sr0 ]; then
echo "$(date) : ERROR - /dev/sr0 not found" >> "$LOG"
exit 1
fi
echo "$(date) : Drive found" >> "$LOG"
# Wait until the disc is actually readable
DISC_READY=0
for i in {1..8}; do
echo "$(date) : Checking disc readiness (attempt $i/8)" >> "$LOG"
cdparanoia -Q -d /dev/sr0 >> "$LOG" 2>&1
STATUS=$?
if [ $STATUS -eq 0 ]; then
DISC_READY=1
break
fi
sleep 3
done
if [ $DISC_READY -ne 1 ]; then
echo "$(date) : ERROR - disc is not readable as audio CD after retries" >> "$LOG"
exit 1
fi
echo "$(date) : Audio CD detected" >> "$LOG"
# Stop previous playback
pkill -u "$USER_NAME" -f "vlc.*cdda" >/dev/null 2>&1
sleep 1
# Ensure Bluetooth sink is still available
SINK=$(runuser -u "$USER_NAME" -- env \
XDG_RUNTIME_DIR="/run/user/$USER_ID" \
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" \
PULSE_SERVER="unix:/run/user/$USER_ID/pulse/native" \
pactl list sinks short | awk '/bluez_output/ {print $2; exit}')
if [ -n "$SINK" ]; then
runuser -u "$USER_NAME" -- env \
XDG_RUNTIME_DIR="/run/user/$USER_ID" \
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" \
PULSE_SERVER="unix:/run/user/$USER_ID/pulse/native" \
pactl set-default-sink "$SINK"
echo "$(date) : Default sink set to $SINK" >> "$LOG"
else
echo "$(date) : WARNING - No Bluetooth sink found, using current default output" >> "$LOG"
fi
echo "$(date) : Starting direct CD playback" >> "$LOG"
runuser -u "$USER_NAME" -- env \
XDG_RUNTIME_DIR="/run/user/$USER_ID" \
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" \
PULSE_SERVER="unix:/run/user/$USER_ID/pulse/native" \
cvlc cdda:///dev/sr0 >> "$LOG" 2>&1 &
sleep 2
if pgrep -u "$USER_NAME" -f "vlc.*cdda" >/dev/null 2>&1; then
echo "$(date) : Playback launched successfully" >> "$LOG"
else
echo "$(date) : ERROR - Playback process did not start" >> "$LOG"
fi
exit 0Make it executable:
chmod +x /home/pizero/cdplayer/autoplay_cd.shThis script does all of the following in order:
- prevents duplicate launches with a lock file
- confirms
/dev/sr0exists - retries until the audio CD is actually readable
- stops any previous direct-CD playback
- re-applies the Bluetooth speaker sink
- launches VLC as user
pizero - logs success or failure
Before relying on insert-trigger automation, manually test direct playback:
runuser -u pizero -- env \
XDG_RUNTIME_DIR="/run/user/$(id -u pizero)" \
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$(id -u pizero)/bus" \
PULSE_SERVER="unix:/run/user/$(id -u pizero)/pulse/native" \
cvlc cdda:///dev/sr0If this works, the direct playback audio path is correct.
With an audio CD inserted:
/home/pizero/cdplayer/autoplay_cd.shCheck:
tail -n 100 /home/pizero/cdplayer/logs/cdplayer.log
ps aux | grep vlcExpected successful log flow:
CD event received
Drive found
Checking disc readiness...
Audio CD detected
Default sink set to ...
Starting direct CD playback
Playback launched successfully
After setting everything up:
- Open tray
- Insert audio CD
- Push tray in
- Wait a few seconds
Then inspect:
sudo journalctl -u cdplayer.service -n 50 --no-pager
tail -n 100 /home/pizero/cdplayer/logs/cdplayer.log
ps aux | grep vlcping cdplayer.local
ssh pizero@cdplayer.locallsusb
dmesg | tail -n 80
ls /dev/sr*sudo systemctl status bluetooth --no-pager
sudo systemctl restart bluetooth
rfkill list bluetooth
sudo rfkill unblock bluetooth
bluetoothctl list
bluetoothctl showbluetoothctlThen:
power on
agent on
default-agent
scan on
pair XX:XX:XX:XX:XX:XX
trust XX:XX:XX:XX:XX:XX
connect XX:XX:XX:XX:XX:XX
scan off
exit
pactl list sinks short
pactl get-default-sink
bluetoothctl devices Connectedpaplay /usr/share/sounds/alsa/Front_Center.wavsystemctl --user status pipewire --no-pager
systemctl --user status pipewire-pulse --no-pager
systemctl --user status wireplumber --no-pagersudo systemctl start bt-reconnect.service
sudo systemctl status bt-reconnect.service
sudo journalctl -u bt-reconnect.service -n 50 --no-pagerrunuser -u pizero -- env \
XDG_RUNTIME_DIR="/run/user/$(id -u pizero)" \
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$(id -u pizero)/bus" \
PULSE_SERVER="unix:/run/user/$(id -u pizero)/pulse/native" \
cvlc cdda:///dev/sr0/home/pizero/cdplayer/autoplay_cd.shsudo journalctl -u cdplayer.service -n 50 --no-pager
tail -n 100 /home/pizero/cdplayer/logs/cdplayer.log
ps aux | grep vlcOpen tray:
eject /dev/sr0Close tray:
eject -t /dev/sr0Toggle tray:
eject -T /dev/sr0Stop current VLC playback:
pkill -u pizero -f "vlc.*cdda"power onfailedscan onfailed- controller not ready
- restart Bluetooth service
- check
rfkill - unblock Bluetooth
- verify controller with
bluetoothctl list/show
pactl list sink startpactl list sinks shortpipewire-plusepipewire-pulseVLC is not supposed to be run as root
udev launched script as root, so VLC was run as root.
- trigger
systemdfromudev - run
cvlcaspizerousingrunuser - use user audio session environment variables
No sound until all tracks had ripped.
Move to direct CD playback:
cvlc cdda:///dev/sr0Logs initially showed:
Unable to read table of contents headerdisc is not readable as audio CD
The tray event fired before the disc was fully ready.
Add retry loop using:
cdparanoia -Q -d /dev/sr0until the disc becomes readable.
- Pi boots.
bt-reconnect.serviceruns.- Bluetooth speaker reconnects.
- Speaker sink becomes default output.
- Tray is pushed in.
/dev/sr0emits a block change event.udevtellssystemdto startcdplayer.service.cdplayer.serviceruns/home/pizero/cdplayer/autoplay_cd.sh.- Script waits until the audio CD becomes readable.
- Script sets the Bluetooth sink if present.
- Script launches
cvlc cdda:///dev/sr0as userpizero. - Music starts playing.
Not implemented yet, but discussed as future improvements:
- GPIO buttons for:
- play/pause
- next track
- previous track
- eject
- status LED
- transparent enclosure-mounted controls
- software eject button
- custom acrylic / transparent frame build
Possible GPIO mapping later:
- GPIO17 → play/pause
- GPIO27 → next
- GPIO22 → previous
You should end up with these key files:
/home/pizero/bt-reconnect.sh
/home/pizero/cdplayer/autoplay_cd.sh
/etc/systemd/system/bt-reconnect.service
/etc/systemd/system/cdplayer.service
/etc/udev/rules.d/99-cd-autoplay.rules
For this project, the best current setup is:
- Pi Zero 2 W
- powered USB hub
- USB laptop CD drive
- Bluetooth speaker
- direct audio CD playback using VLC
udev→systemdtrigger flow- Bluetooth auto-reconnect on boot
This gives the best balance of:
- fast startup
- simpler user experience
- fewer delays than ripping
- clean headless operation