- Python 79.1%
- Shell 20.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
uv tool install as the sendspin-uca222 system user failed reading pyproject.toml from the checkout when that checkout lived under another user's home directory (e.g. /home/wurst/...) with typical restrictive home-dir permissions: error: Failed to read from the distribution cache cause: failed to read from file `.../pyproject.toml`: Permission denied Rather than loosening that user's home directory, copy the source tree to a world-readable /opt/sendspin-uca222-bridge first (dedicated user case only) and install from there. uninstall-systemd.sh now offers to remove that staged copy too. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
| scripts/systemd | ||
| src/sendspin_uca222 | ||
| tests | ||
| .gitignore | ||
| LICENSE | ||
| NOTICE | ||
| pyproject.toml | ||
| README.md | ||
sendspin-uca222-bridge
Bridges a USB line-in capture device — e.g. a Behringer UCA222 connected to a Raspberry Pi — into Music Assistant as a Sendspin source client, so audio from an analog source (AV receiver, turntable, ...) can be streamed to any Sendspin player on your network (e.g. multi-room speakers), synchronized like any other Music Assistant source.
This project adapts bluealsa2sendspin
(Apache-2.0), swapping its Bluetooth/BlueALSA-specific capture (bluealsa.py) for
plain arecord-based ALSA capture (alsa_capture.py); the connection-agnostic
core (bridge.py, cli.py, config.py) is carried over largely unchanged. See
NOTICE for the full attribution.
Requirements
- Raspberry Pi (or any Linux box) with a USB audio capture interface — developed against the Behringer UCA222 (16-bit, up to 48kHz stereo, class-compliant, no driver needed).
alsa-utils(arecord) installed.- Python 3.12+ and
uv. - A Music Assistant server running 2.10 or newer (first release with Sendspin source-role support).
Wiring
Connect your AV receiver's Rec-Out / Pre-Out / Tape-Out (RCA/Cinch) to the UCA222's line-in (white/red RCA). Plug the UCA222 into a USB port on the Pi.
Verify it's recognized:
lsusb
arecord -l
arecord -D plughw:<card>,<device> --dump-hw-params # confirm supported rate/format
By default this project looks for a capture card whose name contains
"USB Audio CODEC" (the UCA222's default ALSA card name). If your arecord -l
output shows a different name, pass --device-match (see below) with a distinctive
substring from it.
Install
curl -LsSf https://astral.sh/uv/install.sh | sh # if uv isn't installed yet
git clone <this-repo> sendspin-uca222-bridge # or copy the directory to the Pi
cd sendspin-uca222-bridge
uv tool install .
Pair with Music Assistant
Find your Sendspin server's WebSocket URL in Music Assistant under
Settings → Sendspin (typically ws://<ma-host>:8927/sendspin), then:
sendspin-uca222 pair --server-url ws://192.168.178.50:8927/sendspin
This prints a client ID and an 8-digit PIN. In Music Assistant, under Settings → Sendspin Source, adopt the newly discovered source and enter the PIN.
Run
sendspin-uca222 run --server-url ws://192.168.178.50:8927/sendspin
Useful flags (all optional, shown with their UCA222-friendly defaults):
| Flag | Default | Purpose |
|---|---|---|
--device-match |
USB Audio CODEC |
Substring matched against arecord -l card names |
--sample-rate |
48000 |
Capture sample rate in Hz |
--channels |
2 |
Number of channels |
--bit-depth |
16 |
16, 24, or 32 |
--client-name |
uca222 |
Name shown in Music Assistant |
Once running, route the new source to any Sendspin player/group from the Music Assistant UI and play something on the AV receiver.
Run as a systemd service
sudo ./scripts/systemd/install-systemd.sh
Walks through installing, pairing (if not already done), and enabling a
sendspin-uca222-bridge systemd service that starts on boot. Logs via
journalctl -u sendspin-uca222-bridge -f. Uninstall with
sudo ./scripts/systemd/uninstall-systemd.sh.
Latency
Measuring this bridge's own contribution
Run with --log-level DEBUG (or set it in the systemd config) to get a summary
line every 5 seconds:
sendspin-uca222 run --server-url ws://192.168.178.50:8927/sendspin --log-level DEBUG
Capture pipeline (last 5s, 250 chunks, target read interval 20ms): read interval avg=20.1ms max=23.4ms | feed() avg=0.8ms max=4.2ms
- read interval: time between chunks arriving from
arecord. Should hover around the target shown in the log line (_READ_CHUNK_MSinbridge.py); a higher average or spiky max means audio is arriving late from ALSA itself (Pi under load, USB hiccup, buffer/period sizing) -- latency added before this process ever sees the audio. - feed() duration: how long handing a chunk to the Sendspin client takes (encode + queue for send). Should be a small fraction of the read interval; if not, this process is falling behind.
Neither number includes network transit, Music Assistant, or the player's own jitter/sync buffer (see below) -- in practice this pipeline typically adds well under 100ms end-to-end, so a delay of a second or more points elsewhere.
Two client-side knobs affect end-to-end delay:
_READ_CHUNK_MSinbridge.py(default 20ms): how much PCM this bridge accumulates before handing it off toSourceCapture.feed(). Lower is less latency but slightly more per-chunk overhead; there's little reason to go below the codec's own natural granularity._BUFFER_TIME_US/_PERIOD_TIME_USinalsa_capture.py(default 50ms / 10ms): the ALSA hardware buffer/period size passed toarecord. Lower reduces capture-side latency but risks overruns (dropped audio) if the Pi is under load; raise these first if you see gaps oroverrun!!!in the logs.
Beyond that, remaining delay is largely the Sendspin/Music Assistant playback buffer on the player side — multi-room sync needs a shared margin to absorb network jitter across all speakers in a group, so it can't be tuned away here without risking dropouts or desync. That's configured in Music Assistant, not in this project.
Known limitations
- Signal presence (
line_sense) currently reflects whetherarecordis running, not whether the AVR is actually outputting non-silent audio. A future version could add RMS-level detection over the captured PCM chunks to drive Music Assistant's autostart-on-signal behavior more precisely — today, start/stop is driven by Music Assistant's ownserver/command, which still works fine, it just won't auto-activate purely from AVR power-on/off. - Card discovery re-polls
arecord -levery 5s (seealsa_capture.py) rather than reacting to a hotplug event; fine for a USB interface that's normally always plugged in. - If Music Assistant issues
server/command startimmediately after connecting (autostart fromline_sense), the client's clock may not have synchronized with the server yet —SourceCapture.start()requires it.bridge.pywaits up to_TIME_SYNC_TIMEOUT_S(10s, converges in practice within ~1s on a LAN) before giving up on that particular start request.
Development
uv sync --group dev --group test
uv run pytest
uv run ruff check .
uv run mypy src