Tags: Sendspin/sendspin-python-cli
Tags
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.
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.
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]>
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]>
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]>
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]>
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]>
PreviousNext