Sendspin CLI is a synchronized audio player client for the Sendspin Protocol. It connects to Sendspin servers via WebSocket, receives time-synchronized audio streams, and plays them back with precise timing to enable multi-room synchronized audio.
Note: If uncertain about how something in Sendspin is supposed to work, fetch and refer to the protocol specification for authoritative implementation details.
cli.py (main entry)
│
┌───────────────┼───────────────┐
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ tui/ │ │ daemon/ │
└──────────┘ └──────────┘
│ │
┌───────────┼───────────┐ │
│ │ │ │
▼ ▼ ▼ ▼
┌────────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────┐
│ keyboard │ │ ui │ │ app │ │ daemon │
│ │ │ │ │ │ │ │
│ - Keys │ │ - TUI │ │ - State │ │ - Headless │
│ - Commands │ │ - Panel │ │ - Tasks │ │ - Signals │
└────────────┘ └─────────┘ └─────────┘ └─────────────┘
│ │ │ │
└───────────┼───────────┴───────────────────┘
│
┌───────────┼───────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌───────────────┐ ┌──────────────┐
│ audio.py │ │ discovery.py │ │ aiosendspin │
│ │ │ │ │ (external) │
│ - Playback │ │ - mDNS │ │ │
│ - Time sync │ │ - Servers │ │ - WebSocket │
└─────────────┘ └───────────────┘ └──────────────┘
Project documentation, installation instructions, usage guide (including command line arguments), configuration options.
Main entry point and orchestrator. Responsibilities:
- Argument parsing: Commands (
serve,daemon), flags (--url,--name,--id,--audio-device,--static-delay-ms) - Command routing: Routes to appropriate mode (TUI app, daemon, or server)
- Device enumeration: Lists audio devices and servers via
--list-audio-devicesand--list-servers - Backward compatibility: Handles deprecated
--headlessflag with warning
Interactive terminal UI mode with keyboard control.
TUI mode application logic. Responsibilities:
- Service discovery: mDNS discovery of Sendspin servers
- Connection management:
ConnectionManagerhandles reconnection with exponential backoff - State management:
AppStatedataclass mirrors server state (playback, metadata, volume) - Audio stream handling:
AudioStreamHandlerbridges between client andAudioPlayer - Event callbacks: Routes server messages to appropriate handlers (metadata, group updates, commands)
- UI coordination: Manages
SendspinUIand keyboard input loop
Keyboard input handling for interactive control. Responsibilities:
- Key capture: Uses
readcharlibrary for single-keypress detection - Command parsing:
CommandHandlerclass parses and executes commands - Media commands: play, pause, stop, next, previous, shuffle, repeat modes, switch group
- Volume control: Group volume (
vol+/vol-/mute) and player volume (pvol+/pvol-/pmute) - Delay adjustment: Real-time static delay adjustment via
delaycommand - Keyboard shortcuts: Arrow keys (prev/next/volume), space (toggle), m (mute), g (switch group), q (quit)
Rich-based terminal UI for visual feedback. Responsibilities:
- Now Playing panel: Track title, artist, album with playback shortcuts
- Volume panel: Group and player volume with mute indicators
- Progress bar: Real-time track progress with interpolation during playback
- Status line: Connection status and quit shortcut
- Shortcut highlighting: Visual feedback when shortcuts are pressed (0.15s highlight)
- State management:
UIStatedataclass for all display state
Headless operation mode for background services.
Daemon mode for headless operation (via sendspin daemon or deprecated --headless). Responsibilities:
- Headless operation: Runs without UI or keyboard input
- Service discovery: Same mDNS discovery as TUI mode
- Connection management: Reuses
ConnectionManagerfromtui/app.py - Audio playback: Same synchronized audio playback
- Event logging: Prints events to stdout instead of UI
- Signal handling: SIGINT/SIGTERM for graceful shutdown
Time-synchronized audio playback engine. Responsibilities:
- Audio output: Uses
sounddevice(PortAudio) for low-latency playback - Time synchronization: DAC-to-loop time calibration for precise playback timing
- Buffer management: Queue-based buffering with gap/overlap handling
- Sync correction: Sample drop/insert for drift correction (Kalman-filtered)
- Playback state machine: INITIALIZING → WAITING_FOR_START → PLAYING → REANCHORING
- Volume control: Software volume with power curve for natural control
Package entry point, exports main from cli.py.
- aiosendspin: Async Sendspin protocol client (WebSocket, time sync, messages)
- sounddevice: PortAudio wrapper for audio I/O
- rich: Terminal UI rendering
- readchar: Cross-platform keyboard input
- zeroconf: mDNS service discovery
- numpy: Audio sample processing
uv run sendspin # TUI mode with auto-discovery
uv run sendspin --url ws://host:port/sendspin # TUI mode with direct connection
uv run sendspin daemon # Daemon mode (headless) with auto-discovery
uv run sendspin daemon --url ws://host:port/sendspin # Daemon mode with direct connectionuv run ruff check --fix . # Lint and auto-fix
uv run ruff format . # Format
uv run mypy sendspin # Type check- Add key handler in
tui/keyboard.pykeyboard_loop()function - Add UI highlight call:
ui.highlight_shortcut("shortcut_name") - Execute command via
handler.execute("command") - Update shortcut display in
tui/ui.py_build_now_playing_panel()or relevant panel
- Check if
MediaCommandenum in aiosendspin supports it - Add command alias in
CommandHandler.execute()intui/keyboard.py - Command is sent via
client.send_group_command(MediaCommand.XXX)