Skip to content

Add paired source@v1 support and update aiosendspin to 9.1 - #276

Open
jore731 wants to merge 6 commits into
Sendspin:mainfrom
jore731:feat/source-v1-paired
Open

Add paired source@v1 support and update aiosendspin to 9.1#276
jore731 wants to merge 6 commits into
Sendspin:mainfrom
jore731:feat/source-v1-paired

Conversation

@jore731

@jore731 jore731 commented Aug 18, 2026

Copy link
Copy Markdown

Summary

  • add a sendspin source mode for sine-test and local line-in/microphone capture
  • support raw ALSA PCM capture through arecord, including ALSA-only devices outside PortAudio
  • propagate capture-process failures instead of leaving the connection loop running
  • migrate client, daemon, TUI, and serve integrations to aiosendspin 9.1.0
  • persist client identities and pairing stores so Music Assistant can pair once and reconnect securely

Why

The source implementation was built against the older Sendspin client API and could not complete Music Assistant pairing. aiosendspin 9.1 provides the current source capture and Noise pairing APIs, so this update delegates protocol framing, encoding, trust persistence, and connection handling to the SDK.

Raw ALSA support lets headless Linux sources capture named hardware and plugin PCMs such as hw:CARD=USB,DEV=0 without requiring PortAudio to enumerate them.

Validation

  • paired with Music Assistant using its dynamic PIN setup flow
  • verified the stored PSK is reused on reconnect
  • streamed both a sine tone and MacBook microphone audio through Music Assistant
  • targeted ALSA tests: 14 passed
  • Ruff checks pass for changed Python files
  • prior branch validation: pre-commit run --all-files, 147 tests passed, uv lock --check

rudyberends and others added 4 commits August 17, 2026 20:40
Add a 'sendspin source' command that runs a source client: capture audio
from a local input (line-in/microphone) or a sine test tone, encode it
(PCM, FLAC, or Opus via PyAV), and stream it to a server, which mixes and
distributes it to players. The server controls when the source streams.

- source_utils: PyAV SourceEncoder (exposes raw codec header) + RMS level
- source_stream: SourceStreamer capture loop (sounddevice / sine), server
  start/stop handling, optional line-sense signal reporting
- cli: 'source' subcommand and 'audio-devices inputs' listing
- audio_devices: input-device enumeration
- settings: SourceSettings persistence (settings-source.json)
- README: source mode documentation
Follow aiosendspin dropping supported_formats from source@v1_support
(spec#113). A source announces its capture format per stream in
client_stream/start, so advertising it up front negotiated nothing and the
field being required made a conforming hello fail to parse.

Also spell out in the README that starting a source is a policy the server
application makes: the library no longer starts one on its own, so a source
pointed at a server that never asks for audio stays idle by design.
Copilot AI lite review requested due to automatic review settings August 18, 2026 06:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR upgrades Sendspin CLI to aiosendspin 9.1.0 and migrates client/server integrations to the SDK’s newer pairing/identity and source-capture APIs. It also introduces a new “source” mode that captures local audio (line-in/mic or a sine test tone) and streams it into a Sendspin server.

Changes:

  • Bump aiosendspin to 9.1.0 (incl. new Noise/pairing dependencies and source extra) and update call sites to the new identity/pairing APIs.
  • Add a sendspin source subcommand plus supporting capture/streaming implementation and new source-mode settings persistence.
  • Update tests and runtime integrations (TUI/daemon/serve) to use SDK-managed capture, connection, and pairing flows.

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
uv.lock Updates lockfile for aiosendspin 9.1.0 and new dependency graph/extras.
pyproject.toml Switches dependency to aiosendspin[server,source]~=9.1.0.
README.md Documents the new source mode usage and options.
sendspin/cli.py Adds source subcommand, input-device listing, and wires identity/pairing into TUI/daemon/source flows.
sendspin/settings.py Adds identity + pairing-store persistence helpers and introduces SourceSettings.
sendspin/source_stream.py New source capture/streamer implementation (sine + sounddevice line-in capture).
sendspin/source_utils.py New PCM signal-level helper for line-sense behavior.
sendspin/audio_devices.py Adds input-device enumeration for capture devices.
sendspin/audio_connector.py Adapts player state reporting to the updated SDK API.
sendspin/tui/app.py Updates client creation/listeners for new identity/pairing and SDK payload field semantics.
sendspin/daemon/daemon.py Migrates daemon client creation + server-initiated attach flow to SDK APIs.
sendspin/serve/init.py Migrates serve-mode server construction to SDK identity/pairing APIs.
sendspin/serve/worker.py Migrates multi-worker serve-mode server construction to SDK identity/pairing APIs.
tests/test_source_stream.py Adds coverage for source command handling, framing, and line-sense behavior.
tests/test_source_utils.py Adds coverage for signal/RMS calculation helper.
tests/tui/test_volume_state.py Updates TUI test fixtures to provide identity + pairing store.
tests/daemon/test_daemon.py Updates daemon test fixtures to provide identity + pairing store.
tests/tui/test_role_negotiation.py Removes test coverage for server-hello role negotiation that no longer exists in the SDK.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread sendspin/cli.py
Comment on lines +898 to +900
_, client_name = _resolve_client_info(args.id or settings.client_id, args.name)
identity, pairing_store = await get_client_security(settings)
client_id = identity.peer_id
Comment thread sendspin/settings.py
Comment on lines +35 to +45
def load_identity() -> Identity:
try:
private_bytes = b64url_decode(identity_file.read_text())
return Identity.from_private_bytes(private_bytes)
except FileNotFoundError:
identity = Identity.generate()
directory.mkdir(parents=True, exist_ok=True)
fd = os.open(identity_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, "w", encoding="ascii") as file:
file.write(identity.private_b64u)
return identity
Comment thread sendspin/source_stream.py
Comment on lines +164 to +166
cfg = self._config
loop = asyncio.get_running_loop()
queue: asyncio.Queue[bytes] = asyncio.Queue(maxsize=32)
Comment thread sendspin/source_stream.py
Comment on lines +141 to +144
self._last_signal = signal
connection = self._client._admitted_connection # noqa: SLF001
if connection is not None:
create_task(connection.send_source_signal(signal))
Comment on lines 124 to 130
server = SendspinPlayerServer(
loop=event_loop,
server_id=server_id,
identity=Identity.generate(),
server_name=config.name,
pairing_store=InMemoryServerPairingStore(),
allow_unencrypted=True,
)
Comment thread sendspin/serve/worker.py
Comment on lines 105 to 111
self._server = SendspinPlayerServer(
loop=loop,
server_id=server_id,
identity=Identity.generate(),
server_name=f"Sendspin Worker {self.worker_id}",
pairing_store=InMemoryServerPairingStore(),
allow_unencrypted=True,
total_listeners=self._total_listeners,
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants