Skip to content

sendspin-cli: add new package - #30577

Open
mguaylam wants to merge 1 commit into
openwrt:masterfrom
mguaylam:sendspin-cli
Open

mguaylam wants to merge 1 commit into
openwrt:masterfrom
mguaylam:sendspin-cli

Conversation

@mguaylam

@mguaylam mguaylam commented Sep 22, 2026

Copy link
Copy Markdown

Maintainer

Michaël Guay-Lambert mguaylam.dev@gmail.com, submitting as the maintainer of this package.

Description

Sendspin is the synchronized multi-room audio protocol used by Music Assistant, and sendspin-cli is its headless player. It plays through ALSA, so a router with a USB DAC becomes a player — the same role snapcast and shairport-sync already fill in this feed.

No patches, no vendored code

Upstream builds its dependencies with CMake FetchContent. Rather than patch that out, each archive is downloaded through its own Download block and handed to CMake with FETCHCONTENT_SOURCE_DIR_<NAME>, with FETCHCONTENT_FULLY_DISCONNECTED=ON so the build never reaches the network. micro-opus, which bundles a copy of Opus, is replaced by a small CMake shim linking the feed's libopus through pkg-config. The result has no patches/ directory and no third-party sources in the tree.

Two behaviours that are easier to read explained than discovered

service_stopped() reloads umdns after the player exits. The player says goodbye with reason shutdown whenever it stops, and a server reconnects only once mDNS announces it again. umdns announces a service when it appears and withdraws it when it disappears, but a restart that swaps a service for an identical one does neither.

The iface hotplug script restarts the player when a network comes up. The umdns in 25.12 does not announce service instances on ifup, and on a network with an mDNS reflector it takes its own reflected probe for a name conflict and stops announcing, so the player is not rediscovered after a reboot. Both are addressed by a pending pull request, openwrt/mdnsd#36, and the script goes away when it ships. It is a workaround and is commented as one.

Why @!BIG_ENDIAN

Software volume, the Opus decoder and micro-flac's sample packing all write samples in host byte order into buffers the ALSA sink opens as _LE. Playback on a big-endian target would be wrong, not merely imperfect, so the package declines to build there rather than shipping noise.

This is not a reading of the code. Each site was built unchanged for MIPS big-endian and run under qemu-mips-static, with the same harness on x86_64 as the control:

Where Upstream report Result on MIPS big-endian
apply_volume(), software volume sendspin-cpp-cli#70 57/64 and 60/64 samples wrong
write_samples(), FLAC sample packing micro-flac#36 wrong on the aligned fast paths only
opus_decode(), Opus output buffer sendspin-cpp#132 decoder writes -45, a little-endian reader reads -11265

Upstream has confirmed the first and has no fix yet. The guard is one line and comes off when they land.

Testing

Built for ramips/mt7621, mediatek/filogic and x86/64 against both 25.12 and snapshot.

Run on a D-Link DIR-3040 (mipsel_24kc, 880 MHz MIPS 1004Kc, no FPU) with a USB DAC:

  • over 49 hours of uninterrupted playback, no XRUN and no underrun
  • 1.45 MiB installed, with libopus, libstdcpp6 and umdns
  • 5 MiB resident, 6.3 % of one thread decoding FLAC 48 kHz/16-bit with software volume
  • USB DAC unplugged and replugged twice during playback; the player recovered both times
  • in-place upgrade verified on the device: the conffile is preserved and the new default lands as .apk-new

Two things that look like findings and are not

  • test-version.sh is not needed. sendspin-cli --version prints sendspin-cli 0.3.0, so the generic version check passes. test.sh is present for a functional check: it starts the player on the null output and waits for it to listen.
  • codeload.github.com rather than @GITHUB. That macro resolves to https://raw.githubusercontent.com, which serves individual files and cannot serve a release tarball.

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 1 new commit.


Generated by Claude Code

Comment thread sound/sendspin-cli/Makefile Outdated
Comment on lines +40 to +42
# Not offered on big-endian: software volume and the Opus decoder write
# samples in host byte order into buffers the ALSA sink opens as _LE.
# Reproduced under qemu-mips, see the README. Drop once fixed upstream.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see the README points at a file this PR does not add, so the reader has nowhere to go. The site list is also one short of the commit message — micro-flac's sample packing is missing.

Suggested change
# Not offered on big-endian: software volume and the Opus decoder write
# samples in host byte order into buffers the ALSA sink opens as _LE.
# Reproduced under qemu-mips, see the README. Drop once fixed upstream.
# Not offered on big-endian: software volume, the Opus decoder and
# micro-flac's sample packing write samples in host byte order into buffers
# the ALSA sink opens as _LE. Drop once fixed upstream.

Generated by Claude Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both right, thank you — the dangling README reference especially, since that file lives in my own feed repository and a reader here would have had nowhere to go.

I went slightly further than the suggestion and kept the evidence while replacing the pointer, so the comment now names where a reader can actually check the claim:

  # Not offered on big-endian: software volume, the Opus decoder and
  # micro-flac's sample packing write samples in host byte order into buffers
  # the ALSA sink opens as _LE. Reproduced under qemu-mips and reported as
  # Sendspin/sendspin-cpp-cli#70, Sendspin/sendspin-cpp#132 and
  # esphome-libs/micro-flac#36. Drop once they are fixed.

The three issues carry the reproductions: each site built unchanged for MIPS big-endian and run under qemu-mips-static, with the same harness on x86_64 as the control.


[ "$1" = sendspin-cli ] || exit 0

state=$(mktemp -d)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the mktemp -d directory is never removed, so each run leaks one. libs/redis/test.sh traps EXIT for this.

Suggested change
state=$(mktemp -d)
state=$(mktemp -d)
trap 'rm -rf "$state"' EXIT

Generated by Claude Code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, and I followed the redis precedent a bit further than the suggestion. Its cleanup also kills the server, which matters here for the same reason: if CI interrupts the script between the launch and the kill, the player is left running as well as the directory left behind.

state=$(mktemp -d)
pid=""
cleanup() { [ -n "$pid" ] && kill "$pid" 2>/dev/null; rm -rf "$state"; }
trap cleanup EXIT

The explicit kill/wait before the final grep stays, so the normal path is unchanged and the trap only covers abnormal exits.

Sendspin is the synchronized multi-room audio protocol used by Music
Assistant, and sendspin-cli is its headless player. It plays through
ALSA, so a router with a USB DAC becomes a player in the same way
snapcast and shairport-sync already allow here.

Upstream builds its dependencies with CMake FetchContent. Rather than
patch that out, each archive is downloaded through its own Download
block and handed to CMake with FETCHCONTENT_SOURCE_DIR_<NAME>, with
FETCHCONTENT_FULLY_DISCONNECTED=ON so the build never reaches the
network. micro-opus, which bundles a copy of Opus, is replaced by a
small CMake shim that links the feed's libopus through pkg-config.
The package therefore carries no patches and no vendored code.

The init script is procd-based and validates its UCI section with
uci_load_validate. The player is built without mDNS and announced by
umdns through procd_add_mdns instead.

Two behaviours are worth explaining rather than leaving to be found:

service_stopped() waits for the player to exit and then reloads umdns.
The player says goodbye with reason 'shutdown' whenever it stops, and a
server reconnects only once mDNS announces it again; umdns announces a
service when it appears and withdraws it when it disappears, but a
restart that swaps a service for an identical one does neither.

The iface hotplug script restarts the player when a network umdns
announces on comes up. The umdns in 25.12 does not announce service
instances when a network comes up, and on a network with an mDNS
reflector it takes its own reflected probe for a name conflict and
stops announcing, so the player is not rediscovered after a reboot.
Both are addressed by a pending pull request, openwrt/mdnsd#36; the
script goes away when it ships.

The package is not offered on big-endian targets. Software volume, the
Opus decoder and micro-flac's sample packing all write samples in host
byte order into buffers the ALSA sink opens as _LE, so playback there
would be wrong rather than merely imperfect. All three are reproduced
under qemu-mips-static and reported upstream; the guard is one line and
comes off when they are fixed.

Tested on ramips/mt7621 (D-Link DIR-3040, mipsel_24kc) with a USB DAC:
over 49 hours of uninterrupted playback, no XRUN or underrun, 1.45 MiB
installed, 5 MiB resident, and 6.3% of one 880 MHz thread decoding
FLAC 48 kHz/16-bit with software volume. The in-place upgrade path and
config preservation were verified on the same device.

Signed-off-by: Michaël Guay-Lambert <mguaylam.dev@gmail.com>

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 1 new commit; no new issues found.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants