The most reliable, high-fidelity raspberry pi internet radio build you can assemble today avoids the bloat of pre-packaged media center images and the dependency hell of VLC bindings. The direct answer for a dedicated, headless kitchen or workshop streamer is a Raspberry Pi Zero 2 W paired with a HiFiBerry DAC+ Standard, controlled via physical GPIO buttons and an mpv IPC socket backend. This combination draws under 2W at idle, boots in seconds, and delivers 24-bit/192kHz audio that completely bypasses the Pi’s notoriously noisy onboard PWM audio.
The Hardware Decision Matrix: Picking Your Pi and DAC
Before buying parts, you need to match the compute module to your physical interface requirements. Here is the decision path to select your board:
| If your use case is... | Choose this Board | Choose this DAC |
|---|---|---|
| Headless, button-controlled, low power (wall-wart powered) | Pi Zero 2 W | HiFiBerry DAC+ Standard |
| Local FLAC library + 7-inch touchscreen + Spotify Connect | Pi 4 Model B (4GB) | HiFiBerry DAC2 HD |
| Multi-room sync server running heavy DSP/EQ plugins | Pi 5 (8GB) | HiFiBerry DSP Add-on |
Bill of Materials and GPIO Pin Mapping
This build uses five tactile buttons for transport and volume control. We rely on the Pi's internal pull-up resistors, meaning you only need to wire one side of each button to GPIO and the other to Ground. No external 10kΩ resistors are required.
| Component | Exact Variant / Spec | Approx. Cost (2026) |
|---|---|---|
| Compute Board | Raspberry Pi Zero 2 W (with headers soldered) | $15.00 |
| Audio DAC | HiFiBerry DAC+ Standard (I2S, RCA out) | $34.90 |
| MicroSD | 32GB SanDisk Extreme (Class 10, A1) | $8.50 |
| Controls | 5x 12mm Tactile Push Buttons (normally open) | $3.00 |
| Power | 5V 2.5A USB-C/Micro-USB Power Supply | $10.00 |
GPIO Pin Mapping Table
| Function | BCM GPIO Pin | Physical Pin (40-pin header) | Wiring |
|---|---|---|---|
| Volume Up | 17 | 11 | Switch between Pin 11 and GND (Pin 9) |
| Volume Down | 27 | 13 | Switch between Pin 13 and GND (Pin 14) |
| Previous Track/Stream | 22 | 15 | Switch between Pin 15 and GND (Pin 20) |
| Play / Pause | 5 | 29 | Switch between Pin 29 and GND (Pin 25) |
| Next Track/Stream | 6 | 31 | Switch between Pin 31 and GND (Pin 30) |
I2S Wiring and Boot Configuration
The HiFiBerry DAC+ communicates over the I2S bus, not USB. This requires telling the Pi's bootloader to load the correct device tree overlay. Note: In Pi OS Bookworm and later, the config file moved to /boot/firmware/config.txt.
- Stack the HiFiBerry DAC+ onto the Pi Zero 2 W's 40-pin header. Ensure the pins align perfectly; the DAC covers the entire footprint of the Zero.
- Flash Raspberry Pi OS Lite (64-bit, Bookworm) to your MicroSD card using Raspberry Pi Imager. Enable SSH and configure your WiFi in the Imager's advanced settings.
- Boot the Pi, SSH in, and open the boot configuration file:
sudo nano /boot/firmware/config.txt - Locate the line
dtparam=audio=onand comment it out by adding a#at the start. This disables the onboard PWM audio, which conflicts with I2S. - Add the HiFiBerry overlay at the bottom of the file:
dtoverlay=hifiberry-dac - Save, exit, and reboot. Verify the DAC is recognized by running
aplay -l. You should seesnd_rpi_hifiberry_daclisted as card 0. - Install the audio backend and Python GPIO library:
sudo apt update sudo apt install mpv python3-gpiozero -y
Headless Python Control Script (IPC Backend)
We avoid python-vlc because compiling VLC dependencies on ARM64 often results in broken GUI bindings. Instead, we use mpv as a headless daemon and control it via a Unix domain socket using JSON IPC. This is bulletproof and consumes minimal RAM.
Create a playlist file at /home/pi/radio.m3u containing your stream URLs (one per line). Then, create the control script:
#!/usr/bin/env python3
import subprocess
import socket
import json
import time
import os
from gpiozero import Button
from signal import pause
# --- PIN DEFINITIONS ---
BTN_VOL_UP = Button(17, pull_up=True, bounce_time=0.05)
BTN_VOL_DOWN = Button(27, pull_up=True, bounce_time=0.05)
BTN_PREV = Button(22, pull_up=True, bounce_time=0.05)
BTN_PLAY_PAUSE = Button(5, pull_up=True, bounce_time=0.05)
BTN_NEXT = Button(6, pull_up=True, bounce_time=0.05)
SOCKET_PATH = "/tmp/mpvsocket"
PLAYLIST_PATH = "/home/pi/radio.m3u"
def start_mpv():
"""Launches mpv in headless mode with an IPC socket."""
if os.path.exists(SOCKET_PATH):
os.remove(SOCKET_PATH)
cmd = [
"mpv", "--no-video", "--no-terminal",
"--playlist", PLAYLIST_PATH,
f"--input-ipc-server={SOCKET_PATH}",
"--loop-playlist=inf", "--volume=70"
]
return subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def send_mpv_command(command):
"""Sends a JSON IPC command to the mpv socket with error handling."""
try:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(1.0)
sock.connect(SOCKET_PATH)
payload = json.dumps({"command": command}) + "\n"
sock.send(payload.encode('utf-8'))
sock.close()
except ConnectionRefusedError as e:
print(f"[ERROR] MPV socket not ready: {e}")
except FileNotFoundError as e:
print(f"[ERROR] Socket file missing: {e}")
except socket.timeout:
print("[ERROR] MPV socket timed out.")
except Exception as e:
print(f"[ERROR] IPC failed: {e}")
# --- BUTTON CALLBACKS ---
def vol_up(): send_mpv_command(["add", "volume", 5])
def vol_down(): send_mpv_command(["add", "volume", -5])
def prev_stream(): send_mpv_command(["playlist-prev"])
def next_stream(): send_mpv_command(["playlist-next"])
def toggle_pause(): send_mpv_command(["cycle", "pause"])
BTN_VOL_UP.when_pressed = vol_up
BTN_VOL_DOWN.when_pressed = vol_down
BTN_PREV.when_pressed = prev_stream
BTN_NEXT.when_pressed = next_stream
BTN_PLAY_PAUSE.when_pressed = toggle_pause
if __name__ == "__main__":
print("Starting mpv backend...")
mpv_process = start_mpv()
time.sleep(2) # Allow mpv to initialize the socket
print("Radio ready. Listening for GPIO presses.")
try:
pause()
except KeyboardInterrupt:
print("\nShutting down...")
mpv_process.terminate()
if os.path.exists(SOCKET_PATH):
os.remove(SOCKET_PATH)
Debugging: Socket Refusals and I2S Silence
When building embedded audio, the most common failure point is the IPC handshake or the ALSA routing. If your script runs but buttons do nothing, or if you get no audio output, follow this diagnostic path.
The Exact Error: ConnectionRefusedError: [Errno 111] Connection refused
If your console spams this exact error string when you press a button, the Python script is trying to talk to /tmp/mpvsocket before mpv has finished booting, or mpv crashed on launch.
Ranked Causes and Fixes:
- mpv crashed due to missing audio device: If the I2S overlay failed to load,
mpvwill exit immediately because it cannot find an ALSA output. Checkaplay -l. If the HiFiBerry isn't listed, yourconfig.txtedit failed or you are using an incompatible OS version. - Race condition on boot: The
time.sleep(2)in the script might not be enough on a heavily loaded Pi Zero 2 W. Increase it totime.sleep(4)or implement awhile not os.path.exists(SOCKET_PATH)loop. - Stale socket file: If the script was killed ungracefully (e.g., power loss), a dead socket file might remain in
/tmp/. Runrm /tmp/mpvsocketand restart the script.
- Boot Config: Verify
dtparam=audio=onis commented out anddtoverlay=hifiberry-dacis active in/boot/firmware/config.txt. - Backend Installation: Ensure
mpvis actually installed and in your PATH by runningmpv --version. - Socket Existence: Run
ls -l /tmp/mpvsocket. If it doesn't exist while the script is running,mpvis failing to launch.
Scaling the Build: Simplify or Extend
This architecture is modular. Depending on your enclosure constraints and budget, you can scale the hardware up or down without rewriting the core IPC logic.
How to Simplify (The $20 Streamer)
If you don't need physical buttons and just want a dedicated streamer that auto-plays a single station on boot, drop the DAC and the buttons. Use the Pi's onboard audio (PWM) via a 3.5mm jack. Remove the gpiozero code, strip the dtoverlay from config.txt, and simply add mpv --no-video http://your-stream-url to your rc.local or systemd user service. Total hardware cost drops to just the Pi Zero 2 W and a power supply.
How to Extend (The Audiophile Upgrade)
If you want to drive high-impedance headphones or add a display, swap the DAC+ Standard for the HiFiBerry DAC2 HD ($59), which uses the newer PCM5122 DAC chip for a vastly improved signal-to-noise ratio (114dB vs 106dB). To add metadata display, integrate an SSD1306 128x64 I2C OLED screen. You can query the mpv socket for the media-title property using the same JSON IPC method shown in the Python script, parsing the response to render the current station name on the OLED via the luma.oled library.
For detailed I2S bus timing and hardware specifications, refer to the official Raspberry Pi Device Tree documentation and the mpv JSON IPC manual.






