Multi-room audio system with smart home integration
One binary. AirPlay + Spotify + Subsonic + MQTT + KNX. Snapcast-based audio delivery.
SnapDog turns a Linux box (or a Mac) into a synchronized multi-room audio system with deep smart home integration. It embeds a Snapcast compatible server completly reimplemented in pure Rust (see snapcast-rs), runs AirPlay and Spotify Connect receivers per zone, streams from subsonic-compatible media servers like Navidrome, plays internet radio — and bridges everything tightly to MQTT and KNX.
| 🔊 Snapcast | Synchronized playback, embedded server snapcast-rs or external snapcast process |
| 🎵 AirPlay 1 + 2 | Per-zone receivers, stream from iPhone/Mac |
| 🎧 Spotify Connect | Per-zone receivers via librespot |
| 📻 Internet Radio | Station list with live ICY metadata (artist/title parsing, dynamic cover art) |
| 📚 Subsonic/Navidrome | Personal music library with playlist navigation and seek |
| 💾 Track Cache | Disk-backed LRU cache for Subsonic tracks — instant seek, replay, and look-ahead prefetch |
| ⚡ Source Conflict | Configurable priority: last_wins or receiver_wins (AirPlay/Spotify vs local) |
| 🎨 Cover Art | Content-addressed caching, ICY StreamUrl fallback, unified per-zone endpoint |
| 🎛️ Multiband Parametric EQ | Per-zone and per-client, genre presets, real-time via custom protocol |
| 🔊 Speaker Correction | Per-client Spinorama profiles (1000+ speakers from (https://spinorama.org)) |
| 🔀 Audio Fade | Smooth transitions: zone switch (client-side) and source switch (server-side) |
| 🏠 MQTT | Bidirectional smart home integration, Home Assistant auto-discovery |
| 🏢 KNX | Building automation — client mode (tunnel/router) or device mode (ETS-programmable, 35 group objects per zone, 11 group objects per client, presence detection mode) |
| 🌐 REST API | ~90 endpoints, full zone/client/media control |
| 📡 WebSocket | Real-time state push notifications |
| 🖥️ WebUI | Responsive SPA, drag-and-drop, tabbed EQ overlay, i18n (5 languages) |
docker run -d --name snapdog \
-v snapdog-data:/var/lib/snapdog \
-v ./snapdog.toml:/etc/snapdog/snapdog.toml:ro \
-p 5555:5555 -p 1704:1704 -p 3671:3671/udp \
ghcr.io/metaneutrons/snapdog:latestDocker Compose (Production)
services:
snapdog:
image: ghcr.io/metaneutrons/snapdog:latest
restart: unless-stopped
volumes:
- snapdog-data:/var/lib/snapdog
- ./snapdog.toml:/etc/snapdog/snapdog.toml:ro
ports:
- "5555:5555" # WebUI + REST API
- "1704:1704" # Snapcast streaming
- "3671:3671/udp" # KNX/IP device
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:5555/health"]
interval: 30s
timeout: 5s
retries: 3
snapdog-client:
image: ghcr.io/metaneutrons/snapdog-client:latest
restart: unless-stopped
devices:
- /dev/snd
command: ["--server", "snapdog"]
volumes:
snapdog-data: # Persists KNX programming, state, EQ config# Start as KNX/IP device — ETS programs zones, clients, group addresses
snapdog --knx-device --knx-address 1.1.100
# Programming mode can be enabled via CLI flag, About dialog in WebUI, or REST API
snapdog --knx-device --knx-address 1.1.100 --knx-prog-mode
# Dual-stack IPv4+IPv6
snapdog --knx-device --knx-address 1.1.100 --bind ::The .knxprod file for ETS import is available from Releases.
Download from Releases, then:
snapdog --config snapdog.tomlOn Windows, SnapDog can run as a native service:
sc create SnapDog binPath= "\"C:\Program Files\SnapDog\snapdog.exe\" --service --config \"C:\ProgramData\snapdog\snapdog.toml\""
sc start SnapDogecho "deb [trusted=yes] https://metaneutrons.github.io/snapdog/debian stable main" \
| sudo tee /etc/apt/sources.list.d/snapdog.list
sudo apt update
sudo apt install snapdog snapdog-clientbrew tap metaneutrons/tap
brew install snapdog
brew install snapdog-clientDownload SnapDog-Server-*.dmg from Releases. The menu bar app embeds the server binary, manages start/stop, and includes Sparkle auto-update.
cargo build --release
./target/release/snapdog --config snapdog.tomlAccess:
| WebUI | http://localhost:5555 |
| API | http://localhost:5555/api/v1/zones |
| Health | http://localhost:5555/health |
| WebSocket | ws://localhost:5555/ws |
Single file: snapdog.example.toml
# Server display name — used for mDNS, MQTT, browser title
# name = "SnapDog"
[http]
port = 5555
bind = "0.0.0.0" # :: for dual-stack IPv4+IPv6
base_url = "http://192.168.1.10:5555" # Required for MQTT cover art URLs; REST API uses relative URLs
# tls_cert = "/etc/snapdog/tls/fullchain.pem" # Enables HTTPS
# tls_key = "/etc/snapdog/tls/privkey.pem"
[audio]
sample_rate = 48000
bit_depth = 16 # FLAC: max 24; f32lz4: always 32 (setting ignored)
channels = 2
source_conflict = "last_wins" # last_wins | receiver_wins
zone_switch_fade_ms = 300 # Client zone switch fade (0 to disable, max 1000)
source_switch_fade_ms = 300 # Source change fade within a zone (0 to disable, max 1000)
[snapcast]
streaming_port = 1704
unknown_clients = "accept" # accept | ignore | reject
default_zone = "Living Room" # Zone for unknown clients (accept only)
[mdns]
# enabled = true # Advertises _snapdog._tcp via OS daemon
# advertise_snapcast = false # Additionally advertise _snapcast._tcp
[airplay]
# mode = "airplay2" # airplay1 | airplay2
# password = "1234"
[mqtt]
broker = "192.168.1.10:1883"
# client_id = "snapdog" # Must be unique per broker
# username = "user"
# password = "pass"
base_topic = "snapdog/"
[subsonic]
url = "https://music.example.com"
username = "user"
password = "pass"
# format = "raw" # raw | flac | mp3 | opus
# [subsonic.cache]
# path = "~/.cache/snapdog/tracks" # Cache directory
# max_size_mb = 2048 # LRU eviction
[knx]
# role = "client" # Connect to a KNX/IP gateway
url = "udp://192.168.1.50:3671"
# role = "device" # Run as ETS-programmable KNX/IP device
# individual_address = "1.1.100"
[[zone]]
# name = "SnapDog"
[[client]]
name = "Kitchen Speaker"
mac = "02:42:ac:11:00:10"
zone = "Living Room"
[[radio]]
name = "Deutschlandfunk"
url = "https://st01.sslstream.dlf.de/dlf/01/high/aac/stream.aac"Snapcast sink paths, stream names, and AirPlay names are auto-generated from zone/client definitions. KNX addresses are explicit in client mode (fits into existing installations). In device mode, ETS assigns group addresses via the .knxprod product database.
Home Assistant Integration
SnapDog publishes MQTT Discovery messages automatically. Zones appear as media_player entities in Home Assistant with zero configuration — just point both at the same MQTT broker.
Supported features: play, pause, stop, next/previous, volume, mute, shuffle, repeat (off/one/all), seek, cover art, track metadata, and availability.
API Authentication
If api_keys is set in [http], all /api/v1/* and /ws endpoints require authentication:
- REST:
Authorization: Bearer <key>header - WebSocket:
ws://host:port/ws?token=<key>query parameter; query tokens are accepted only on/ws - Health endpoints and the WebUI are always accessible
Subsonic Server Notes
When using Navidrome, ensure transcoding is configured for the format specified in format. Without transcoding, files in non-streamable containers (ALAC/AAC in MP4) will be downloaded fully before playback, causing significant latency.
SnapDog advertises _snapdog._tcp via the OS DNS-SD daemon (Avahi on Linux, mDNSResponder on macOS). Clients discover the server automatically on the local network.
Advertised service:
| Field | Value |
|---|---|
| Service type | _snapdog._tcp |
| Port | REST API port (default 5555) |
TXT api_version |
1 |
TXT server_id |
Persistent UUID v4 — identifies the same server across interfaces/restarts |
TXT snapcast_port |
Binary audio streaming port (default 1704) |
TXT auth |
true if API keys are configured |
TXT tls |
true if HTTPS is enabled (only present when active) |
TXT docker |
true if running in a container (only present when detected) |
TXT base_url |
Canonical server URL (see below) |
base_url resolution: The base_url TXT record is included when the server is not reachable on all interfaces. Clients should prefer base_url over the mDNS hostname when present:
base_urlexplicitly configured → use configured value (reverse proxy, custom domain)[http].bindset to a specific IP (not::or0.0.0.0) → auto-derived ashttp://{bind}:{port}- Otherwise →
base_urlomitted; clients use the mDNS hostname + port directly
Optionally, _snapcast._tcp can be advertised on the streaming port for standard Snapcast client compatibility:
[mdns]
advertise_snapcast = trueSnapDog builds on a family of Rust crates:
| Crate | Description |
|---|---|
| snapcast-server | Embeddable Snapcast server with per-stream codecs, custom protocol, encryption |
| shairplay-rust | AirPlay 1 + 2 receiver library (RAOP/AirTunes) |
| knx-rs | KNX protocol stack — core types, KNXnet/IP, device stack, TP-UART, .knxprod generator |
| snapdog-common | Shared types and constants between server and client (EQ, protocol IDs, volume curve) |
A specialized Snapcast client that understands SnapDog's custom protocol extensions:
-
F32+LZ4 codec — lossless 32-bit float audio with LZ4 compression (not supported by stock snapclients)
-
Per-client parametric EQ — receives EQ curves via custom protocol, applies biquad filters before output
-
Speaker correction — second EQ stage for Spinorama-based speaker profiles
-
Audio fade — smooth fade-out/fade-in on zone switch (triggered by server)
-
Hardware volume — native ALSA mixer control with perceptual (quadratic) curve
-
MIDI CC volume — send volume as MIDI Control Change (e.g., for professional mixing consoles)
# Send volume on MIDI channel 1, CC7 (default) to a USB MIDI interface snapdog-client --mixer midi:hw:1:0 # Send volume on MIDI channel 3, CC11 (expression) to a named port snapdog-client --mixer midi:"Scarlett 18i8":2:11
-
Encryption — PSK-based chunk encryption matching the embedded server
Available as binary and Docker image (ghcr.io/metaneutrons/snapdog-client).
┌─────────────────────────────────────────────────────┐
│ SnapDog │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ ZonePlayer│ │ ZonePlayer│ │ ZonePlayer│ ... │
│ │ (tokio) │ │ (tokio) │ │ (tokio) │ │
│ └────┬──────┘ └────┬──────┘ └────┬──────┘ │
│ │ │ │ │
│ ┌────┴──────────────┴──────────────┴─────┐ │
│ │ Embedded Snapcast Server │ │
│ │ (per-zone streams + encoders) │ │
│ └───────────────────┬────────────────────┘ │
│ │ │
│ ┌─────────┐ ┌──────┴────┐ ┌──────────┐ │
│ │ AirPlay │ │ REST API │ │ MQTT │ │
│ │receivers│ │ + WebUI │ │ bridge │ │
│ └─────────┘ └───────────┘ └──────────┘ │
│ ┌──────────┐ │
│ │ KNX │ │
│ │ bridge │ │
│ └──────────┘ │
└─────────────────────────────────────────────────────┘
│
┌────┴──────┐
│snapclients│
│(per room) │
└───────────┘
- ZonePlayer — per-zone tokio task, owns audio pipeline (decode → resample → encode → Snapcast)
- Dual Snapcast backend — embedded server (default) or external process via JSON-RPC
- Volume via Snapcast — never PCM amplitude scaling, full dynamic range preserved
- MAC-based client matching — clients auto-assigned to zones from config
| Feature | Default | Description |
|---|---|---|
snapcast-embedded |
✅ | In-process Snapcast server (snapcast-server) |
snapcast-process |
— | External snapserver binary + JSON-RPC |
ap2 |
— | AirPlay 2 (encrypted transport, HAP pairing) |
spotify |
✅ | Spotify Connect receiver (librespot) |
See Architecture Decision Records for design rationale.
make setup # Git hooks
docker compose -f docker-compose.dev.yml up -d # Dev infrastructure
cargo run -- --config snapdog.dev.toml # Run
cargo xtask ci # Run all CI checks locally
cargo test # Test
cargo clippy -- -D warnings # LintDev Infrastructure (Docker Compose)
| Service | Purpose |
|---|---|
| 3× snapclient | Simulated rooms (Living Room, Kitchen, Bedroom) |
| mqtt | Mosquitto MQTT broker |
| knxd | KNX gateway simulator |
| knx-monitor | Visual KNX bus debugging |
| navidrome | Subsonic-compatible music server |
If SnapDog is useful to you, consider buying me a coffee ☕