speakerctrl is the central control daemon for
SpeakerOS.
It turns a Raspberry Pi into a single-purpose, appliance-style network speaker
by orchestrating audio sources, networking state, and system policy.
speakerctrl does not implement audio protocols itself — instead, it
coordinates well-tested Linux services and enforces clear, predictable behaviour.
This project was vibe coded as an exercise in learning agentic coding tools. It works well and I've been using it, but I'm not vouching for the code quality.
SpeakerOS and speakerctrl are designed as appliances, not general-purpose computers. Reliability comes first — the system should boot predictably, recover from errors, and keep working months later without intervention. There are no hidden modes or magic behaviours. The system adapts to the user, not the other way around. Rather than reimplementing complex subsystems, speakerctrl builds on PipeWire, WirePlumber, BlueZ, and standard Linux service management.
- Allows multiple active audio sources simultaneously
- Bluetooth (A2DP sink)
- Spotify Connect
- AirPlay 1
- Monitors system state via D-Bus and PipeWire
- Ensures audio services remain discoverable and available
- Manages Wi-Fi connectivity and AP fallback mode
- Serves a local web UI for setup and status
- Automatic fallback to setup mode if Wi-Fi connection fails for 3 minutes after boot
- Implement Bluetooth, AirPlay, or Spotify protocols
- Perform audio mixing or DSP
- Replace PipeWire or WirePlumber
- Raspberry Pi 4 Model B
- Raspberry Pi OS Lite (64-bit), Bookworm
- systemd, PipeWire, WirePlumber
speakerctrl/
├── crates/
│ ├── speakerctrl-core/ # State machine and policy logic
│ └── speakerctrl-daemon/ # Main daemon (includes embedded web UI)
├── packaging/ # systemd units, Debian packaging
├── docs/ # Architecture and development notes
└── README.md
All persistent data lives under /persist/speakerctrl/.
Human-editable configuration is stored in TOML (config.toml); machine-managed
state is stored in JSON (state.json). See docs/ for details on the
configuration schema, persistence layout, and D-Bus interface.
Example config.toml:
version = 1
device_name = "SpeakerOS"
log_level = "info"
log_to_file = true
log_max_size_bytes = 5242880 # 5MBv0.4.2 — Fix AP fallback not triggering when WiFi network unavailable.
Recent improvements include:
- AP fallback now triggers correctly when the configured WiFi network is unavailable
- Recovery now reconciles Bluetooth and PipeWire sources consistently
- Factory reset clears in-memory Wi-Fi credentials and re-enters setup mode
- Wi-Fi scans prefer fresh results to avoid stale lists
cargo build --release # Build for host
./build_package.sh # Create .deb for host architecture
./build_package.sh aarch64 # Cross-compile for Raspberry PiCross-compilation requires aarch64-linux-gnu-gcc and the Rust target:
rustup target add aarch64-unknown-linux-gnu
sudo apt install gcc-aarch64-linux-gnuFor local testing without a mounted /persist, set:
SPEAKERCTRL_PERSIST_ROOT=/tmp/speakerctrl-persist
Hardware validation guidance lives in docs/TEST_PLAN.md.
Licensed under the Apache License, Version 2.0.
See the LICENSE file for details.