Files

645 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Sendspin Go
A complete Sendspin Protocol implementation in Go, featuring both server and player components for synchronized multi-room audio streaming.
**Key Highlights:**
- **Library-first design**: Use as a Go library or standalone CLI tools
- **Hi-res audio support**: Up to 192kHz/24-bit streaming
- **Multi-codec**: Opus, FLAC, MP3, PCM
- **Precise synchronization**: Microsecond-level multi-room sync
- **Easy to use**: Simple high-level APIs for common use cases
- **Flexible**: Low-level component APIs for custom implementations
- ~44mb of memory usage in Windows for sendspin-player
## Using as a Library
Install the library:
```bash
go get github.com/Sendspin/sendspin-go
```
### Quick Start - Player
```go
package main
import (
"log"
"github.com/Sendspin/sendspin-go/pkg/sendspin"
)
func main() {
// Create and configure player
player, err := sendspin.NewPlayer(sendspin.PlayerConfig{
ServerAddr: "localhost:8927",
PlayerName: "Living Room",
Volume: 80,
OnMetadata: func(meta sendspin.Metadata) {
log.Printf("Playing: %s - %s", meta.Artist, meta.Title)
},
})
if err != nil {
log.Fatal(err)
}
// Connect and play
if err := player.Connect(); err != nil {
log.Fatal(err)
}
if err := player.Play(); err != nil {
log.Fatal(err)
}
// Keep running
select {}
}
```
### Quick Start - Server
```go
package main
import (
"log"
"github.com/Sendspin/sendspin-go/pkg/sendspin"
)
func main() {
// Create test tone source (or use NewFileSource)
source := sendspin.NewTestTone(192000, 2)
// Create and start server
server, err := sendspin.NewServer(sendspin.ServerConfig{
Port: 8927,
Name: "My Server",
Source: source,
})
if err != nil {
log.Fatal(err)
}
if err := server.Start(); err != nil {
log.Fatal(err)
}
// Keep running
select {}
}
```
### More Examples
See the [examples/](examples/) directory for more complete examples:
- **[basic-player/](examples/basic-player/)** - Simple player with status monitoring
- **[basic-server/](examples/basic-server/)** - Simple server with test tone
- **[custom-source/](examples/custom-source/)** - Custom audio source implementation
### API Documentation
- **High-level API**: `pkg/sendspin` - Player and Server with simple configuration
- **Audio processing**: `pkg/audio` - Format types, codecs, resampling, output
- **Protocol**: `pkg/protocol` - WebSocket client and message types
- **Clock sync**: `pkg/sync` - Precise timing synchronization
- **Discovery**: `pkg/discovery` - mDNS service discovery
Full API documentation: https://pkg.go.dev/github.com/Sendspin/sendspin-go
## Features
### Server
- Stream audio from multiple sources:
- Local files (MP3, FLAC)
- HTTP/HTTPS streams (direct MP3)
- HLS streams (.m3u8 live radio)
- Test tone generator (440Hz)
- Automatic resampling to 48kHz for Opus compatibility
- Multi-codec support (Opus @ 256kbps, PCM fallback)
- mDNS service advertisement for automatic discovery
- Real-time terminal UI showing connected clients
- WebSocket-based streaming with precise timestamps
### Player
- Automatic server discovery via mDNS
- Multi-codec support (Opus, FLAC, PCM)
- Precise clock synchronization for multi-room audio
- Interactive terminal UI with volume control
- Jitter buffer for smooth playback
## Quickstart on Raspberry Pi
For a 64-bit Raspberry Pi OS (Lite is recommended; Bookworm or newer required):
```bash
curl -fsSL https://raw.githubusercontent.com/Sendspin/sendspin-go/main/scripts/quickstart-pi.sh | sudo bash
```
The script installs runtime dependencies, downloads the latest `sendspin-player-linux-arm64` release tarball, and registers the player as a systemd service. Add flags after `--` to pre-configure the player without editing files afterwards:
```bash
curl -fsSL https://raw.githubusercontent.com/Sendspin/sendspin-go/main/scripts/quickstart-pi.sh \
| sudo bash -s -- --name "Living Room" --device "USB Audio Device"
```
Pin to a specific release with `--version v1.6.2`. Remove the player with `--uninstall` (config in `/etc/sendspin/` is preserved). Supported on Pi 3 / 4 / 5 / Zero 2 W; not supported on 32-bit-only hardware (Pi 1 / Zero v1 / Zero W).
After install:
- View live logs: `journalctl -u sendspin-player -f`
- Discover device names: `sendspin-player --list-audio-devices`
- Edit config: `sudo nano /etc/sendspin/player.yaml`
## Installation
### Prerequisites
You'll need `pkg-config`, the Opus library, and optionally `ffmpeg` for HLS streaming:
```bash
# macOS
brew install pkg-config opus ffmpeg
# Ubuntu/Debian
sudo apt-get install pkg-config libopus-dev ffmpeg
# Fedora
sudo dnf install pkg-config opus-devel ffmpeg
```
**Windows (MSYS2):**
Install MSYS2 from https://www.msys2.org/, then in a **MSYS2 MinGW 64-bit** shell:
```bash
pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-pkg-config \
mingw-w64-x86_64-opus
```
All subsequent `go build`, `go test`, and `make` commands must be run from a shell with the MSYS2 MinGW 64-bit toolchain on PATH:
```bash
export PATH="/c/msys64/mingw64/bin:$PATH"
```
**Notes:**
- `ffmpeg` is only required for HLS/m3u8 stream support. Local files and direct HTTP MP3 streams work without it.
- The Makefile sets `GOFLAGS=-tags=nolibopusfile`, which skips the `opus.Stream` parts of `gopkg.in/hraban/opus.v2` and avoids linking `libopusfile`. If you build with raw `go build` instead of `make`, either install `libopusfile-dev` / `opusfile` as well, or export `GOFLAGS=-tags=nolibopusfile` in your shell.
### Build
Build both server and player:
```bash
make
```
Or build individually:
```bash
make server # Builds sendspin-server
make player # Builds sendspin-player
```
On Windows, both binaries are produced in the repo root as `sendspin-server.exe` and `sendspin-player.exe`. Run them from the same MSYS2 MinGW 64-bit shell (or from cmd/PowerShell once the MSYS2 runtime DLLs are on PATH).
## Usage
### Server
Start a server with the interactive TUI (default, plays 440Hz test tone):
```bash
./sendspin-server
```
Stream a local audio file:
```bash
./sendspin-server --audio /path/to/music.mp3
./sendspin-server --audio /path/to/album.flac
```
Stream from HTTP/HTTPS:
```bash
./sendspin-server --audio http://example.com/stream.mp3
```
Stream HLS/m3u8 (live radio):
```bash
./sendspin-server --audio "https://stream.radiofrance.fr/fip/fip.m3u8?id=radiofrance"
```
Run without TUI (streaming logs to stdout):
```bash
./sendspin-server --no-tui
```
#### Server Options
- `--port` - WebSocket server port (default: 8927)
- `--name` - Server friendly name (default: hostname-sendspin-server)
- `--audio` - Audio source to stream:
- Local file path: `/path/to/music.mp3`, `/path/to/audio.flac`
- HTTP stream: `http://example.com/stream.mp3`
- HLS stream: `https://example.com/live.m3u8`
- If not specified, plays 440Hz test tone
- `--log-file` - Log file path (default: sendspin-server.log)
- `--debug` - Enable debug logging
- `--no-mdns` - Disable mDNS advertisement (clients must connect manually)
- `--no-tui` - Disable TUI, use streaming logs instead
- `--daemon` - Daemon mode: log to stdout only (journalctl-friendly), no TUI, no log file. Recommended under systemd.
- `--config` - Path to `server.yaml` config file. Default search: `$SENDSPIN_SERVER_CONFIG`, `~/.config/sendspin/server.yaml`, `/etc/sendspin/server.yaml`.
#### Server TUI
The server TUI shows:
- Server name and port
- Uptime
- Currently playing audio
- Connected clients with codec and state
- Press `q` or `Ctrl+C` to quit
#### Configuration File (`server.yaml`)
Every CLI flag has a matching key in `server.yaml`. Keys use `snake_case` (`--no-mdns``no_mdns`).
A fully-commented starter file lives at [`dist/config/server.example.yaml`](dist/config/server.example.yaml) — copy it to `~/.config/sendspin/server.yaml` (user install) or `/etc/sendspin/server.yaml` (daemon) and uncomment the keys you want to set.
**Search order** (first existing file wins; missing is not an error):
1. `--config <path>` flag
2. `$SENDSPIN_SERVER_CONFIG`
3. `~/.config/sendspin/server.yaml` (macOS: `~/Library/Application Support/sendspin/server.yaml`; Windows: `%AppData%\sendspin\server.yaml`)
4. `/etc/sendspin/server.yaml` (daemon/system-wide)
**Value precedence**, for every flag: CLI > `SENDSPIN_SERVER_<UPPER_SNAKE>` env > `server.yaml` > built-in default.
#### Running under systemd
`make install-daemon` installs both `sendspin-player` and `sendspin-server` as systemd units. To install only one side:
```bash
sudo make install-server-daemon # server only
sudo make install-player-daemon # player only
```
Then enable and start the server:
```bash
sudo systemctl enable --now sendspin-server
journalctl -u sendspin-server -f
```
Configure via `/etc/sendspin/server.yaml` (preferred) or `SENDSPIN_SERVER_OPTS` in `/etc/default/sendspin-server`.
### Player
Start a player (auto-discovers servers via mDNS):
```bash
./sendspin-player --name "Living Room"
```
Connect to a specific server manually:
```bash
./sendspin-player --server ws://192.168.1.100:8927 --name "Kitchen"
```
#### Player Options
- `--config` - Path to player.yaml config file. Default search: `$SENDSPIN_PLAYER_CONFIG`, `~/.config/sendspin/player.yaml`, `/etc/sendspin/player.yaml`.
- `--server` - Manual server WebSocket address (skips mDNS discovery)
- `--port` - Port for mDNS advertisement (default: 8927)
- `--name` - Player friendly name (default: hostname-sendspin-player)
- `--buffer-ms` - Jitter buffer size in milliseconds (default: 150)
- `--log-file` - Log file path (default: sendspin-player.log)
- `--client-id` - Override the persisted `client_id`. When set, the value is written to the config file and reused on subsequent launches.
- `--audio-device` - Playback device name (see `--list-audio-devices`). Empty = miniaudio default.
- `--list-audio-devices` - Print every playback device miniaudio can see and exit.
- `--debug` - Enable debug logging
#### Configuration File (`player.yaml`)
Every CLI flag has a matching key in `player.yaml`. Keys use `snake_case` (`--buffer-ms``buffer_ms`).
A fully-commented starter file lives at [`dist/config/player.example.yaml`](dist/config/player.example.yaml) — copy it to `~/.config/sendspin/player.yaml` (user install) or `/etc/sendspin/player.yaml` (daemon) and uncomment the keys you want to set.
**Search order** (first existing file wins; missing is not an error):
1. `--config <path>` flag
2. `$SENDSPIN_PLAYER_CONFIG`
3. `~/.config/sendspin/player.yaml` (macOS: `~/Library/Application Support/sendspin/player.yaml`; Windows: `%AppData%\sendspin\player.yaml`)
4. `/etc/sendspin/player.yaml` (daemon/system-wide)
**Value precedence**, for every flag:
1. CLI flag if passed
2. Env var `SENDSPIN_PLAYER_<UPPER_SNAKE>` (e.g. `SENDSPIN_PLAYER_BUFFER_MS=200`)
3. Config file key
4. Built-in default
**Example `player.yaml`:**
```yaml
# Identity
name: "Living Room"
client_id: "aa:bb:cc:dd:ee:ff" # auto-derived from MAC if unset
# Network
server: "" # empty = use mDNS
port: 8927
# Audio
buffer_ms: 150
static_delay_ms: 0
preferred_codec: "" # pcm (default), opus, flac
buffer_capacity: 1048576
# Device identity (shown in Music Assistant)
product_name: ""
manufacturer: ""
# Behavior
no_reconnect: false
daemon: false
no_tui: false
log_file: "sendspin-player.log"
audio_device: "" # see --list-audio-devices; empty = miniaudio default
```
#### Selecting a playback device
On Linux/macOS miniaudio picks its first-choice backend and that backend's default sink, which is usually fine on desktops but can route to the wrong card on a headless Pi (e.g. HDMI instead of a USB DAC or speaker HAT). To see what miniaudio sees and pick a specific device:
```bash
$ sendspin-player --list-audio-devices
Playback devices:
[*] HDA Intel PCH: ALC257 Analog (hw:0,0)
[ ] HDMI 0 (hw:0,3)
[ ] USB Audio Device (hw:1,0)
[*] = current default. Use --audio-device "<name>" or set audio_device: in player.yaml.
```
Then either:
```bash
./sendspin-player --audio-device "USB Audio Device"
```
Or in `player.yaml`:
```yaml
audio_device: "USB Audio Device"
```
The name must match exactly (case-sensitive, including any `(hw:X,Y)` suffix ALSA appends). If the name doesn't match, the player fails to start and lists every available device — silent fallback is deliberately not offered, because "it's not playing" is harder to debug than "it refused to start."
#### Player Identity (`client_id`)
The player sends a stable `client_id` so controllers like Music Assistant recognize it as the same player across restarts. Resolution order:
1. `--client-id` flag (when set, also persisted to the config file as `client_id`)
2. `client_id` key in the loaded `player.yaml`
3. MAC address of the primary network interface (`xx:xx:xx:xx:xx:xx`)
4. Freshly generated UUID (written to `player.yaml` as `client_id` and reused next launch)
Removing `client_id` from `player.yaml` causes the next launch to re-derive, which the server will see as a new player.
Running multiple players on one host:
```bash
./sendspin-player --name "Kitchen" --config ~/.config/sendspin/kitchen.yaml &
./sendspin-player --name "Bedroom" --config ~/.config/sendspin/bedroom.yaml &
```
Each config file holds its own `client_id`, so the two instances register as two distinct players.
#### Player TUI
The player TUI shows:
- Player name
- Server connection status
- Current audio title/artist
- Codec and sample rate
- Buffer depth
- Clock sync statistics (offset, RTT, drift)
- Playback statistics (received, played, dropped)
- Volume control (Up/Down arrows or +/- keys)
- Press `m` to mute/unmute
- Press `q` or `Ctrl+C` to quit
## Architecture
Sendspin Go is built with a **library-first architecture**, providing three layers of APIs:
### 1. High-Level API (`pkg/sendspin`)
Simple Player and Server types for common use cases:
- **Player**: Connect, play, control volume, get stats
- **Server**: Stream from AudioSource, manage clients
- **AudioSource**: Interface for custom audio sources
### 2. Component APIs
Lower-level building blocks for custom implementations:
- **`pkg/audio`**: Format types, sample conversions, Buffer
- **`pkg/audio/decode`**: PCM, Opus, FLAC, MP3 decoders
- **`pkg/audio/encode`**: PCM, Opus encoders
- **`pkg/audio/resample`**: Sample rate conversion
- **`pkg/audio/output`**: Audio playback via malgo (miniaudio); 16/24/32-bit native
- **`pkg/protocol`**: WebSocket client, message types
- **`pkg/sync`**: Clock synchronization with drift compensation
- **`pkg/discovery`**: mDNS service discovery
### 3. CLI Tools
Thin wrappers around the library APIs:
- **`cmd/sendspin-server`**: Full-featured server with TUI
- **`cmd/sendspin-player`**: Full-featured player with TUI (main.go at root)
### Server Pipeline
The server streams audio in 20ms chunks with microsecond timestamps. Audio is buffered 500ms ahead to allow for network jitter and clock synchronization.
**Processing flow:**
1. Audio source (file decoder or test tone generator)
2. Per-client codec negotiation (Opus or PCM)
3. Timestamp generation using monotonic clock
4. WebSocket binary message streaming
### Player Pipeline
The player uses a sophisticated scheduling system to ensure perfectly synchronized playback across multiple rooms.
**Processing flow:**
1. WebSocket client receives timestamped audio chunks
2. Clock sync system converts server timestamps to local time
3. Priority queue scheduler with startup buffering (200ms)
4. Persistent audio player with streaming I/O pipe
5. Software volume control and mixing
### Clock Synchronization
The player uses a simple, robust clock synchronization system:
- Calculates server loop origin on first sync
- Direct time base matching (no drift prediction)
- Continuous RTT measurement for quality monitoring
- Microsecond precision timestamps
- 500ms startup buffer matches server's lead time
## Example: Multi-Room Setup
Terminal 1 - Start the server:
```bash
./sendspin-server --audio ~/Music/favorite-album.mp3
```
Terminal 2 - Living room player:
```bash
./sendspin-player --name "Living Room"
```
Terminal 3 - Kitchen player:
```bash
./sendspin-player --name "Kitchen"
```
Both players will discover the server via mDNS and start playing in perfect sync.
## Development
Run tests:
```bash
make test
```
Clean binaries:
```bash
make clean
```
Install to GOPATH/bin:
```bash
make install
```
### Protocol conformance
The [Sendspin protocol conformance suite](https://github.com/Sendspin/conformance) runs real network scenarios between adapter binaries and compares outputs against canonical hashes. sendspin-go has a first-class adapter and is tested on every PR via the `Conformance` GitHub Actions workflow.
Run the same suite locally:
```bash
make conformance
```
This clones `Sendspin/conformance` into `../conformance` (sibling directory) and the `aiosendspin` reference peer on first run, installs the harness with `uv`, and runs `scripts/run_all.py` with this checkout pinned via the `CONFORMANCE_REPO_SENDSPIN_GO` environment variable. Requires [uv](https://docs.astral.sh/uv/getting-started/installation/) and Python 3.12+.
The published conformance report for the `main` branch is at https://sendspin.github.io/conformance/.
## Contributing
Found a bug or have a feature request? Please check existing issues or create a new one:
**[View Issues](https://github.com/Sendspin/sendspin-go/issues)**
### Recently Shipped
**v1.2.0** — drop the oto backend and unify on malgo for true 24-bit output (see [#3](https://github.com/Sendspin/sendspin-go/issues/3) and [#26](https://github.com/Sendspin/sendspin-go/pull/26))
**v1.1.0** — server-initiated client discovery, Kalman clock filter, code-path audit
### Known Issues & Todo
**Protocol & compatibility:**
- [ ] Validate all message types match latest Sendspin Protocol spec
- [ ] Test with additional Sendspin-compatible servers beyond Music Assistant
- [ ] Document protocol extensions or deviations
- [ ] Explicit protocol-version negotiation (versioned roles like `player@v1` exist; a numeric version handshake does not)
**Audio:**
- [ ] Test sample rate conversion quality (FLAC 96kHz → Opus 48kHz)
- [ ] Real FLAC streaming decoder (currently a stub — see [#34](https://github.com/Sendspin/sendspin-go/issues/34))
- [ ] Gapless playback
- [ ] Volume curve optimization (currently linear)
- [ ] Visualizer role support (FFT spectrum data)
**Stability:**
- [ ] Reconnection handling and automatic retry
- [ ] Graceful degradation on clock sync loss
- [ ] Memory leak testing for long-running sessions
- [ ] Stress testing with many clients and multi-room sync accuracy with 5+ players
**Features:**
- [ ] Album artwork end-to-end (downloader exists; not fully wired to TUI surfaces)
- [ ] Player groups and zones
- [ ] Playlist/queue management
- [ ] Cross-fade between tracks
**Developer experience:**
- [ ] Godoc examples for all public APIs
- [ ] Automated cross-platform test matrix (CI runs Linux only today)
- [ ] Docker containers for easy deployment
- [ ] Benchmarking suite
- [ ] Clean up pre-existing tech debt surfaced by [PR #26](https://github.com/Sendspin/sendspin-go/pull/26): see issues [#27#34](https://github.com/Sendspin/sendspin-go/issues)
### Roadmap
**Released**
- **v1.2.0** — oto backend removed, malgo is the only audio output, true 24-bit pipeline end-to-end
- **v1.1.0** — server-initiated client discovery, Kalman time filter, protocol audit fixes
- **v1.0.0** — initial stable release, Music Assistant compatibility, precise multi-room sync
**Planned**
- **v2.0.0 (Advanced Multi-Room)** — player groups and zones, synchronized playback controls, playlist management
## Protocol
Implements the [Sendspin Protocol](https://github.com/Sendspin/spec) specification.
**Implementation Status:**
- ✅ WebSocket transport
- ✅ Client/Server handshake with versioned role negotiation (`player@v1`, `metadata@v1`)
- ✅ Clock synchronization (NTP-style, two-dimensional Kalman filter on offset + drift)
- ✅ Audio streaming (binary frames, microsecond timestamps)
- ✅ Metadata messages (via `server/state`)
- ✅ Control commands (volume, mute)
- ✅ Multi-codec support (Opus with server-side resampling, 24-bit PCM)
- ✅ True 24-bit audio output via malgo (v1.2.0)
- ✅ Server-initiated client discovery (v1.1.0)
- ⚠️ Album artwork — downloader exists, not fully wired through to all TUI surfaces
- ⚠️ Visualizer role (planned)