Skip to content

Tags: Sendspin/sendspin-python-cli

Tags

7.5.0

Toggle 7.5.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Reduce audible startup sync correction (#255)

## Summary

This reduces audible pitch shift / warble during Sendspin client
playback startup and other stream transitions.

The main issue appears to be that the client can start playback with a
consistent initial sync offset, then correct that offset quite
aggressively using sample insert/drop correction. On my Linux endpoint
this was especially noticeable at the beginning of tracks. Some
discussion of this occurred at #107, however it was indeterminate and
difficult to pin down the cause.

With analysis and help from GPT5.5, with my full understanding and
detailed review, this PR makes three related changes:

- Fixes the drop-frame correction path so it discards one input frame
and outputs the following frame, instead of repeating the previous
output frame while consuming two input frames (bug fix)
- Reduces the maximum correction rate from +/-4% to +/-0.2% (much more
reliably below audible threshold)
- Adds a short startup grace period before sync correction begins, so
DAC/time-sync estimates can settle before the client starts inserting or
dropping samples (most impactful improvement; basically correction was
correcting issues that were not present simply due to lack of data)

## Analysis

I was hearing pitch shift and warble on a fully up-to-date sendspin
endpoint, most noticeably at playback start and during track changes.

Looking through `sendspin/audio.py`, the most suspicious path was the
sync correction logic. The previous drop correction branch did this:

1. Read one frame.
2. Read another frame.
3. Output the previous frame again.

That effectively produced a duplicate-then-skip pattern, which is more
audible than a simple one-frame drop.

Separately, the correction loop allowed up to +/-4% playback speed
correction over a 2 second target window. On real playback that is
enough to sound like pitch movement, especially right after startup when
the first sync estimate is still settling.

This was changed to a maximum +/-0.2% correction over an 8 second
window, which is more conservative, but still within reasonable sync
delay expectations (counting to 8 will help provide confidence; if we
believe users would reasonably be OK with out-of-sync clients converging
within 8 seconds, then this is a reasonable default). Importantly, this
is well below the threshold of audible pitch shift or warble while still
providing a means to converge.

In any case, if the sync is too far out, a reanchor will be triggered. 

Additionally, a 750ms sync correction delay was added; this does not
delay audible playback, it only suppresses sample insert/drop correction
briefly after playback enters the PLAYING state. That gives the DAC
timing and clock-sync estimates a short window to settle before the
client starts making speed adjustments based on them. In practice this
avoids reacting to the first unstable startup measurements while still
allowing scheduled playback to begin on time.

## Real Endpoint Comparison

I tested this on my actual Sendspin Linux endpoint using the same daemon
config, audio device, and negotiated format:

- Device: HiFiBerry DAC+
- Format: `flac:48000:24:2`
- Server: Music Assistant
- Output latency reported by PortAudio: ~42.7 ms

The original version repeatedly started streams around `-42 ms` sync
error, then corrected aggressively.

Original observed debug stats:

- Underflows: `0`
- Reanchors: `0`
- Warnings/errors: `0`
- Speed range: `98.29%` to `100.11%`
- Max inserted frames per 1s log window: `821`
- Max dropped frames per 1s log window: `55`

Patched observed debug stats:

- Underflows: `0`
- Reanchors: `0`
- Warnings/errors: `0`
- Speed range: `99.78%` to `100.04%`
- Max inserted frames per 1s log window: `106`
- Max dropped frames per 1s log window: `21`

The startup offset is still visible, but correction is much less
aggressive and avoids the previous drop-frame artifact.

## Tests

Added focused regression tests for:

- Drop correction discarding one frame without repeating the previous
output frame.
- Startup correction grace period suppressing immediate insert/drop
correction.

Local verification:

```bash
uv run --extra test ruff check sendspin/audio.py tests/test_audio.py
uv run --extra test mypy sendspin
uv run --extra test pytest
```

Results:

```text
All checks passed
Success: no issues found in 30 source files
91 passed
```

Testing for a day in real world scenarios has also been very successful.

7.4.0

Toggle 7.4.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Add `visualizer@v1` with beats, peaks, pitch, and frequency (#259)

Upgrades the TUI visualizer from the old `_draft_r1` draft to the newer
`visualizer@v1` role, rendering everything the server streams
time-aligned to the playhead: a beats strip with estimated BPM and
downbeats, a peaks strip of energy onsets, and pitch and
dominant-frequency (`f_peak`) cursors over the spectrum.

Only data that is sent by the server is displayed. For example, Music
Assistant 2.9 does not support pitch.

7.3.1

Toggle 7.3.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Fix `sendspin serve` crash on Python 3.12 (#252)

`sendspin serve` raised `TypeError: type 'Synchronized' is not
subscriptable` at import time on Python 3.12 because
`multiprocessing.sharedctypes.Synchronized[int]` only gained
`__class_getitem__` in 3.13.

7.3.0

Toggle 7.3.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Bump `aiosendspin` to 5.2 (#247)

Includes many syncing fixes for the `sendspin serve` subcommand

7.2.0

Toggle 7.2.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Fix ALSA device not closing (#233) (#241)

This PR fixes the issue of ALSA staying open after stream end (#233), by
calling the existing `AudioPlayer::_close_stream` from
`AudioStreamHandler::_on_stream_end`.

Tested on Linux, with this change the ALSA stream immediatecly closes on
pause, and resumes on play. Haven't tested any othere audio backends.

---------

Co-authored-by: Claude <[email protected]>

7.1.0

Toggle 7.1.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Fix audio underflow on stream start by buffering before playback (#238)

Includes #232 by @tobsch + addresses comments

Closes #232

---------

Co-authored-by: Tobias Schlottke <[email protected]>
Co-authored-by: Claude <[email protected]>

7.0.0

Toggle 7.0.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Fix ALSA device matching for hw:CARD=...,DEV=... format (#224)

## Summary
- Catch `ValueError` (in addition to `PortAudioError`) from
`sounddevice.check_output_settings` — PortAudio raises `ValueError` for
`hw:CARD=...,DEV=...` names it doesn't recognize
- Validate the device exists in the ALSA device list before accepting it
with safe defaults
- Add focused tests for `_try_alsa_device`

Simpler alternative to #218 — 6 lines changed in production code vs ~80.

Fixes #208

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>

6.0.0

Toggle 6.0.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Refactor CLI to use subcommands for utility functions (#215)

## Summary
Refactored the CLI to introduce dedicated subcommands (`audio-devices`,
`servers`, `clients`) for utility functions, replacing the previous
flag-based approach (`--list-audio-devices`, `--list-servers`,
`--list-clients`). The old flags are deprecated but remain functional
with warnings for backward compatibility.

## Key Changes

- **New subcommand structure**: Added three new top-level subcommands
with `list` subcommands:
  - `sendspin audio-devices list` (replaces `--list-audio-devices`)
  - `sendspin servers list` (replaces `--list-servers`)
  - `sendspin clients list` (replaces `--list-clients`)

- **Parser updates**: 
- Added `audio-devices`, `servers`, and `clients` to `EXPLICIT_APPS`
frozenset
- Created dedicated argument parsers for each new subcommand with
appropriate help text
  - Added routing logic in `main()` to handle the new subcommands

- **Backward compatibility**:
- Deprecated flags still work but now print a warning message directing
users to the new subcommands
- Old flags remain in the player argument parser with updated help text
indicating deprecation

- **Documentation updates**:
  - Updated help text throughout to reference new subcommand syntax
  - Updated README.md examples to use new subcommand format
  - Updated AGENTS.md to reflect new device enumeration approach
  - Updated systemd installation script to use new subcommand syntax
- Added example output showing both old and new syntax in
`list_audio_devices()`

## Implementation Details

- New subcommands are handled before the main player/daemon/serve logic
in `main()`
- Each new subcommand validates that a `list` subcommand was provided;
otherwise shows help
- Deprecation warnings are printed to stdout when old flags are used
- All three new subcommands follow the same pattern for consistency and
extensibility

https://claude.ai/code/session_018Mj2pQj7Lqu5YTGnB97E94

---------

Co-authored-by: Claude <[email protected]>

5.9.0

Toggle 5.9.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Add real-time audio spectrum visualizer to the TUI (#192)

## Summary
Adds a real-time frequency spectrum visualizer to the TUI, rendered
below the info panels.
While the final `visualizer` role isn't part of the Sendspin Spec yet.
This uses the first WIP version of the role (with role id
`visualizer@_draft_r1`) from [this
PR](Sendspin/spec#28), available in Music
Assistant 2.8.

The spectrum and loudness data is computed on the server, and then sent
through the Sendspin protocol.

Toggle it by pressing the `v` key.

## Screenshot

<img width="2424" height="1046" alt="image"
src="https://github.com/user-attachments/assets/fa8ca719-1046-4e14-b57e-20d70b2025a9"
/>

---------

Co-authored-by: Claude Sonnet 4.6 <[email protected]>

5.8.0

Toggle 5.8.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Support ALSA plugin devices (dmix) for dual mono setups (#193)

## Summary

- Refactor audio device handling to new audio_devices.py
- Allow `--audio-device` to accept raw ALSA device names (e.g.,
`dmixer`, `olohuone`) in addition to numeric indices and name prefixes
- When a string doesn't match any PortAudio-enumerated device, it's
validated and passed directly to sounddevice, letting PortAudio's ALSA
backend open named plugin devices like dmix
- This enables multiple daemon instances to share a single sound card
via dmix for multi-room dual mono setups

## Root cause

PortAudio only enumerates hardware ALSA devices, not plugin devices
(dmix, plug, etc.). When one daemon opens a hardware device via
PortAudio, it acquires exclusive access, preventing the second daemon
from even enumerating devices — `sounddevice.query_devices()` returns an
empty list. ALSA dmix is designed for device sharing, but PortAudio
bypasses it by opening the raw hardware directly.

## Changes

- **`sendspin/audio.py`**: Extended `AudioDevice` with optional
`alsa_device_name` field and `device_id` property; updated type
signatures to accept `int | str | None` for device parameters
- **`sendspin/cli.py`**: Added `_try_alsa_device()` fallback in device
resolution; updated help text and `--list-audio-devices` output with
ALSA hint on Linux
- **`sendspin/tui/app.py`** and **`sendspin/daemon/daemon.py`**: Use
`device_id` instead of `index` for format detection
- **`README.md`**: Documented ALSA device name usage with dual mono
example

## Usage

```bash
# Room 1: left channel via dmix
sendspin daemon --name "Living Room" --audio-device olohuone

# Room 2: right channel via dmix  
sendspin daemon --name "Kitchen" --audio-device keittio
```

## Test plan

- [ ] Verify `--list-audio-devices` still works and shows ALSA hint on
Linux
- [ ] Verify numeric device index selection still works (`--audio-device
0`)
- [ ] Verify name prefix selection still works (`--audio-device
"MacBook"`)
- [ ] Verify raw ALSA device name works on a Linux system with dmix
configured
- [ ] Verify two daemons can run simultaneously with different dmix
devices
- [ ] Verify invalid ALSA device name gives a clear error message

Fixes #58

https://claude.ai/code/session_01G86ZS2aLyMqx3rDwQnsaUS

---------

Co-authored-by: Claude <[email protected]>