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
Install the library:
go get github.com/Sendspin/sendspin-gopackage 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 {}
}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 {}
}See the examples/ directory for more complete examples:
- basic-player/ - Simple player with status monitoring
- basic-server/ - Simple server with test tone
- custom-source/ - Custom audio source implementation
- 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
- 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
- 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
You'll need pkg-config, Opus libraries, and optionally ffmpeg for HLS streaming:
# macOS
brew install pkg-config opus opusfile ffmpeg
# Ubuntu/Debian
sudo apt-get install pkg-config libopus-dev libopusfile-dev ffmpeg
# Fedora
sudo dnf install pkg-config opus-devel opusfile-devel ffmpegWindows (MSYS2):
Install MSYS2 from https://www.msys2.org/, then in a MSYS2 MinGW 64-bit shell:
pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-pkg-config \
mingw-w64-x86_64-opus mingw-w64-x86_64-opusfileAll subsequent go build, go test, and make commands must be run from a shell with the MSYS2 MinGW 64-bit toolchain on PATH:
export PATH="/c/msys64/mingw64/bin:$PATH"Note: ffmpeg is only required for HLS/m3u8 stream support. Local files and direct HTTP MP3 streams work without it.
Build both server and player:
makeOr build individually:
make server # Builds sendspin-server
make player # Builds sendspin-playerOn 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).
Start a server with the interactive TUI (default, plays 440Hz test tone):
./sendspin-serverStream a local audio file:
./sendspin-server --audio /path/to/music.mp3
./sendspin-server --audio /path/to/album.flacStream from HTTP/HTTPS:
./sendspin-server --audio http://example.com/stream.mp3Stream HLS/m3u8 (live radio):
./sendspin-server --audio "https://stream.radiofrance.fr/fip/fip.m3u8?id=radiofrance"Run without TUI (streaming logs to stdout):
./sendspin-server --no-tui--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
- Local file path:
--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
The server TUI shows:
- Server name and port
- Uptime
- Currently playing audio
- Connected clients with codec and state
- Press
qorCtrl+Cto quit
Start a player (auto-discovers servers via mDNS):
./sendspin-player --name "Living Room"Connect to a specific server manually:
./sendspin-player --server ws://192.168.1.100:8927 --name "Kitchen"--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 persistedclient_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
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 — 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):
--config <path>flag$SENDSPIN_PLAYER_CONFIG~/.config/sendspin/player.yaml(macOS:~/Library/Application Support/sendspin/player.yaml; Windows:%AppData%\sendspin\player.yaml)/etc/sendspin/player.yaml(daemon/system-wide)
Value precedence, for every flag:
- CLI flag if passed
- Env var
SENDSPIN_PLAYER_<UPPER_SNAKE>(e.g.SENDSPIN_PLAYER_BUFFER_MS=200) - Config file key
- Built-in default
Example player.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 defaultOn 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:
$ 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:
./sendspin-player --audio-device "USB Audio Device"Or in player.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."
The player sends a stable client_id so controllers like Music Assistant recognize it as the same player across restarts. Resolution order:
--client-idflag (when set, also persisted to the config file asclient_id)client_idkey in the loadedplayer.yaml- MAC address of the primary network interface (
xx:xx:xx:xx:xx:xx) - Freshly generated UUID (written to
player.yamlasclient_idand 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:
./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.
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
mto mute/unmute - Press
qorCtrl+Cto quit
Sendspin Go is built with a library-first architecture, providing three layers of APIs:
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
Lower-level building blocks for custom implementations:
pkg/audio: Format types, sample conversions, Bufferpkg/audio/decode: PCM, Opus, FLAC, MP3 decoderspkg/audio/encode: PCM, Opus encoderspkg/audio/resample: Sample rate conversionpkg/audio/output: Audio playback via malgo (miniaudio); 16/24/32-bit nativepkg/protocol: WebSocket client, message typespkg/sync: Clock synchronization with drift compensationpkg/discovery: mDNS service discovery
Thin wrappers around the library APIs:
cmd/sendspin-server: Full-featured server with TUIcmd/sendspin-player: Full-featured player with TUI (main.go at root)
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:
- Audio source (file decoder or test tone generator)
- Per-client codec negotiation (Opus or PCM)
- Timestamp generation using monotonic clock
- WebSocket binary message streaming
The player uses a sophisticated scheduling system to ensure perfectly synchronized playback across multiple rooms.
Processing flow:
- WebSocket client receives timestamped audio chunks
- Clock sync system converts server timestamps to local time
- Priority queue scheduler with startup buffering (200ms)
- Persistent audio player with streaming I/O pipe
- Software volume control and mixing
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
Terminal 1 - Start the server:
./sendspin-server --audio ~/Music/favorite-album.mp3Terminal 2 - Living room player:
./sendspin-player --name "Living Room"Terminal 3 - Kitchen player:
./sendspin-player --name "Kitchen"Both players will discover the server via mDNS and start playing in perfect sync.
Run tests:
make testClean binaries:
make cleanInstall to GOPATH/bin:
make installThe Sendspin protocol conformance suite 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:
make conformanceThis 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 and Python 3.12+.
The published conformance report for the main branch is at https://sendspin.github.io/conformance/.
Found a bug or have a feature request? Please check existing issues or create a new one:
v1.2.0 — drop the oto backend and unify on malgo for true 24-bit output (see #3 and #26)
v1.1.0 — server-initiated client discovery, Kalman clock filter, code-path audit
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@v1exist; 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)
- 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: see issues #27–#34
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
Implements the Sendspin Protocol 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)