🎉 live server seems to be working now

This commit is contained in:
2026-05-14 14:29:57 +02:00
commit d72e439fd9
181 changed files with 47406 additions and 0 deletions

View File

@@ -0,0 +1,179 @@
# Sendspin Examples
This directory contains example programs demonstrating how to use the Sendspin library.
## Available Examples
### [basic-player](basic-player/)
A simple audio player that connects to a Sendspin server and plays synchronized audio.
**Key features:**
- Simple configuration with callbacks
- Automatic clock synchronization
- Status monitoring and statistics
- Metadata display
**Run it:**
```bash
cd basic-player
go build
./basic-player -server localhost:8927
```
### [basic-server](basic-server/)
A simple streaming server that broadcasts a test tone to connected players.
**Key features:**
- Test tone generation (440 Hz)
- Multi-client support
- Automatic codec negotiation
- mDNS service advertisement
**Run it:**
```bash
cd basic-server
go build
./basic-server
```
### [custom-source](custom-source/)
Demonstrates how to implement custom `AudioSource` interfaces for generating or processing audio.
**Key features:**
- Multiple tone mixing (chords)
- Frequency sweeps (chirps)
- Custom audio generation
- AudioSource interface implementation
**Run it:**
```bash
cd custom-source
go build
./custom-source -mode chord
```
## Quick Start
### 1. Start a server
```bash
cd examples/basic-server
go build && ./basic-server
```
### 2. Connect a player
In another terminal:
```bash
cd examples/basic-player
go build && ./basic-player
```
You should hear a 440 Hz test tone playing with synchronized timing.
## Building All Examples
From the repository root:
```bash
go build ./examples/basic-player/
go build ./examples/basic-server/
go build ./examples/custom-source/
```
## Example Progression
1. **Start with basic-server** - Understand server setup and test tone streaming
2. **Try basic-player** - Learn player configuration and playback
3. **Explore custom-source** - Implement custom audio sources
## Key Concepts Demonstrated
### Player API (`pkg/sendspin`)
```go
// Create and configure player
config := sendspin.PlayerConfig{
ServerAddr: "localhost:8927",
PlayerName: "Living Room",
Volume: 80,
OnMetadata: func(meta sendspin.Metadata) {
fmt.Printf("Now playing: %s - %s\n", meta.Artist, meta.Title)
},
}
player, _ := sendspin.NewPlayer(config)
player.Connect()
player.Play()
```
### Server API (`pkg/sendspin`)
```go
// Create audio source
source := sendspin.NewTestTone(192000, 2)
// Create and start server
config := sendspin.ServerConfig{
Port: 8927,
Source: source,
}
server, _ := sendspin.NewServer(config)
server.Start()
```
### AudioSource Interface
```go
type AudioSource interface {
Read(samples []int32) (int, error)
SampleRate() int
Channels() int
Metadata() (title, artist, album string)
Close() error
}
```
## Audio Format
All examples use hi-res audio:
- **Sample rate**: 192 kHz (configurable)
- **Bit depth**: 24-bit
- **Channels**: 2 (stereo)
- **Format**: int32 PCM samples
## Network Discovery
Servers advertise via mDNS by default. Players can discover local servers automatically.
## Next Steps
- Check out the full CLI tools: `cmd/sendspin-server/` (and root main.go for player)
- Read the API documentation: `pkg/sendspin/`
- Explore lower-level APIs: `pkg/audio/`, `pkg/protocol/`, `pkg/sync/`
## Troubleshooting
**No audio playing?**
- Check server is running: `netstat -an | grep 8927`
- Check firewall allows port 8927
- Verify audio output device is working
**Connection refused?**
- Ensure server is started before connecting player
- Check server address is correct
- Try explicit IP instead of localhost
**Audio glitches or dropouts?**
- Increase buffer size: `-buffer 1000` (player)
- Check network latency
- Monitor clock sync quality in stats
## License
See the repository's LICENSE file for details.

View File

@@ -0,0 +1,74 @@
# Basic Player Example
This example demonstrates how to create a simple Sendspin player that connects to a server and plays audio.
## What it does
- Connects to a Sendspin server
- Starts audio playback with automatic clock synchronization
- Displays metadata (title, artist, album) when received
- Shows playback status and statistics
- Supports volume control and mute
## Building
```bash
cd examples/basic-player
go build
```
## Running
Connect to a local server:
```bash
./basic-player
```
Connect to a specific server:
```bash
./basic-player -server 192.168.1.100:8927
```
Set custom player name and volume:
```bash
./basic-player -name "Living Room" -volume 80
```
## Command-line options
- `-server` - Server address (default: localhost:8927)
- `-name` - Player name (default: "Basic Player")
- `-volume` - Initial volume 0-100 (default: 80)
## Key features demonstrated
1. **Simple configuration** - Just specify server address and player name
2. **Callbacks** - Handle metadata, state changes, and errors
3. **Automatic sync** - Clock synchronization happens automatically
4. **Status monitoring** - Get real-time playback statistics
## Code highlights
```go
// Create player with configuration
config := sendspin.PlayerConfig{
ServerAddr: "localhost:8927",
PlayerName: "Living Room",
Volume: 80,
OnMetadata: func(meta sendspin.Metadata) {
log.Printf("Now playing: %s - %s", meta.Artist, meta.Title)
},
}
player, _ := sendspin.NewPlayer(config)
player.Connect()
player.Play()
```
## Next steps
- See `examples/custom-source/` for custom audio source implementation
- Check out the player CLI (root main.go) for a full-featured TUI player

View File

@@ -0,0 +1,88 @@
// ABOUTME: Basic Sendspin player example
// ABOUTME: Demonstrates how to connect to a server and play audio
package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/Sendspin/sendspin-go/pkg/sendspin"
)
func main() {
serverAddr := flag.String("server", "localhost:8927", "Sendspin server address")
playerName := flag.String("name", "Basic Player", "Player name")
volume := flag.Int("volume", 80, "Initial volume (0-100)")
flag.Parse()
config := sendspin.PlayerConfig{
ServerAddr: *serverAddr,
PlayerName: *playerName,
Volume: *volume,
BufferMs: 500, // 500ms playback buffer
DeviceInfo: sendspin.DeviceInfo{
ProductName: "Sendspin Example Player",
Manufacturer: "Sendspin",
SoftwareVersion: "1.0.0",
},
OnMetadata: func(meta sendspin.Metadata) {
log.Printf("Now playing: %s - %s (%s)", meta.Artist, meta.Title, meta.Album)
},
OnStateChange: func(state sendspin.PlayerState) {
log.Printf("State changed: %s (volume: %d, muted: %v)", state.State, state.Volume, state.Muted)
},
OnError: func(err error) {
log.Printf("Error: %v", err)
},
}
player, err := sendspin.NewPlayer(config)
if err != nil {
log.Fatalf("Failed to create player: %v", err)
}
defer player.Close()
log.Printf("Connecting to %s...", *serverAddr)
if err := player.Connect(); err != nil {
log.Fatalf("Failed to connect: %v", err)
}
log.Printf("Connected! Starting playback...")
if err := player.Play(); err != nil {
log.Fatalf("Failed to start playback: %v", err)
}
go func() {
for {
status := player.Status()
stats := player.Stats()
if status.Connected {
log.Printf("Status: %s | %s %dHz %dch %dbit | Buffer: %dms | RTT: %dμs",
status.State,
status.Codec,
status.SampleRate,
status.Channels,
status.BitDepth,
stats.BufferDepth,
stats.SyncRTT)
}
// Sleep for 5 seconds
select {
case <-make(chan struct{}):
}
}
}()
// Wait for interrupt signal
fmt.Println("\nPress Ctrl+C to stop playback")
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan
log.Printf("Shutting down...")
}

View File

@@ -0,0 +1,101 @@
# Basic Server Example
This example demonstrates how to create a simple Sendspin streaming server that broadcasts a test tone to connected players.
## What it does
- Creates a Sendspin server that streams a 440Hz test tone
- Listens for WebSocket connections from players
- Broadcasts synchronized audio to all connected clients
- Supports mDNS service advertisement for easy discovery
- Handles multiple clients with different codec preferences
## Building
```bash
cd examples/basic-server
go build
```
## Running
Start server with defaults (192kHz/24-bit stereo):
```bash
./basic-server
```
Start on a different port:
```bash
./basic-server -port 9000
```
Configure sample rate and channels:
```bash
./basic-server -rate 48000 -channels 2
```
Disable mDNS:
```bash
./basic-server -mdns=false
```
## Command-line options
- `-port` - Server port (default: 8927)
- `-name` - Server name (default: "Basic Server")
- `-rate` - Sample rate in Hz (default: 192000)
- `-channels` - Number of channels (default: 2)
- `-mdns` - Enable mDNS advertisement (default: true)
## Key features demonstrated
1. **Simple setup** - Just create a source and start the server
2. **Test tone generation** - Built-in test tone for easy testing
3. **Automatic client handling** - Clients are managed automatically
4. **Multi-codec support** - Automatically negotiates best codec per client
5. **mDNS discovery** - Clients can find the server automatically
## Code highlights
```go
// Create audio source (440Hz test tone)
source := sendspin.NewTestTone(192000, 2)
// Create and start server
config := sendspin.ServerConfig{
Port: 8927,
Name: "My Server",
Source: source,
EnableMDNS: true,
}
server, _ := sendspin.NewServer(config)
server.Start()
```
## Testing
Start the server:
```bash
./basic-server
```
In another terminal, connect a player:
```bash
cd ../..
go run . -server localhost:8927
```
You should hear a 440Hz tone (A4 note) playing.
## Next steps
- See `examples/custom-source/` for implementing custom audio sources (files, streams, etc.)
- Check out the server CLI (`cmd/sendspin-server/`) for a full-featured server with TUI
- Modify the test tone frequency or add multiple frequencies for testing

View File

@@ -0,0 +1,77 @@
// ABOUTME: Basic Sendspin server example
// ABOUTME: Demonstrates how to create a simple streaming server
package main
import (
"flag"
"log"
"os"
"os/signal"
"syscall"
"github.com/Sendspin/sendspin-go/pkg/sendspin"
)
func main() {
port := flag.Int("port", 8927, "Server port")
serverName := flag.String("name", "Basic Server", "Server name")
sampleRate := flag.Int("rate", 192000, "Sample rate (Hz)")
channels := flag.Int("channels", 2, "Number of channels")
enableMDNS := flag.Bool("mdns", true, "Enable mDNS service advertisement")
flag.Parse()
log.Printf("Creating test tone source: %dHz, %d channels", *sampleRate, *channels)
source := sendspin.NewTestTone(*sampleRate, *channels)
config := sendspin.ServerConfig{
Port: *port,
Name: *serverName,
Source: source,
EnableMDNS: *enableMDNS,
Debug: false,
}
server, err := sendspin.NewServer(config)
if err != nil {
log.Fatalf("Failed to create server: %v", err)
}
log.Printf("Starting Sendspin server...")
log.Printf(" Name: %s", *serverName)
log.Printf(" Port: %d", *port)
log.Printf(" Audio: %dHz, %d channels, 24-bit", *sampleRate, *channels)
if *enableMDNS {
log.Printf(" mDNS: enabled")
}
errChan := make(chan error, 1)
go func() {
if err := server.Start(); err != nil {
errChan <- err
}
}()
// Print client info periodically
go func() {
for {
select {
case <-make(chan struct{}):
}
}
}()
log.Printf("\nServer running. Press Ctrl+C to stop")
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
select {
case <-sigChan:
log.Printf("Received interrupt signal, shutting down...")
case err := <-errChan:
log.Printf("Server error: %v", err)
}
server.Stop()
log.Printf("Server stopped")
}

View File

@@ -0,0 +1,158 @@
# Custom Audio Source Example
This example demonstrates how to implement the `AudioSource` interface to create custom audio generators for streaming.
## What it does
This example shows three different custom audio source implementations:
1. **MultiToneSource** - Mixes multiple sine waves (creates chords)
2. **SweepSource** - Generates frequency sweeps (chirps)
3. **Built-in TestTone** - Single frequency tone (for comparison)
Each demonstrates different aspects of the `AudioSource` interface.
## Building
```bash
cd examples/custom-source
go build
```
## Running
Generate an A major chord (440, 554, 659 Hz):
```bash
./custom-source -mode chord
```
Generate a frequency sweep from 220 to 880 Hz:
```bash
./custom-source -mode sweep
```
Generate a single 440 Hz tone:
```bash
./custom-source -mode single
```
## Command-line options
- `-port` - Server port (default: 8927)
- `-mode` - Source mode: chord, sweep, single (default: chord)
- `-rate` - Sample rate in Hz (default: 192000)
- `-channels` - Number of channels (default: 2)
## AudioSource Interface
To create a custom audio source, implement this interface:
```go
type AudioSource interface {
// Read fills the buffer with PCM samples (int32 for 24-bit audio)
Read(samples []int32) (int, error)
// SampleRate returns the sample rate
SampleRate() int
// Channels returns the number of channels
Channels() int
// Metadata returns title, artist, album
Metadata() (title, artist, album string)
// Close closes the audio source
Close() error
}
```
## Implementation details
### MultiToneSource
Generates multiple sine waves and mixes them together:
```go
type MultiToneSource struct {
frequencies []float64
sampleRate int
channels int
sampleIndex uint64
mu sync.Mutex
}
func (s *MultiToneSource) Read(samples []int32) (int, error) {
// For each sample frame:
for i := 0; i < numFrames; i++ {
t := float64(s.sampleIndex + i) / float64(s.sampleRate)
// Mix all frequencies
var mixed float64
for _, freq := range s.frequencies {
mixed += math.Sin(2 * math.Pi * freq * t)
}
mixed /= float64(len(s.frequencies))
// Convert to 24-bit PCM (int32)
pcmValue := int32(mixed * 8388607 * 0.5)
// Write to all channels
for ch := 0; ch < s.channels; ch++ {
samples[i*s.channels + ch] = pcmValue
}
}
return len(samples), nil
}
```
### SweepSource
Generates a time-varying frequency sweep:
```go
func (s *SweepSource) Read(samples []int32) (int, error) {
for i := 0; i < numFrames; i++ {
t := float64(s.sampleIndex + i) / float64(s.sampleRate)
// Calculate current frequency based on progress
progress := math.Mod(t, s.duration) / s.duration
freq := s.startFreq + (s.endFreq - s.startFreq) * progress
// Generate sine wave at current frequency
sample := math.Sin(2 * math.Pi * freq * t)
pcmValue := int32(sample * 8388607 * 0.5)
// Write to all channels
for ch := 0; ch < s.channels; ch++ {
samples[i*s.channels + ch] = pcmValue
}
}
return len(samples), nil
}
```
## Key concepts
1. **Sample format**: Use `int32` for 24-bit audio (range: -8388608 to 8388607)
2. **Interleaving**: For stereo (2 channels), samples are stored as [L, R, L, R, ...]
3. **Thread safety**: Use mutex if internal state is accessed from multiple goroutines
4. **Continuous playback**: Track `sampleIndex` to maintain phase continuity
## Use cases for custom sources
- **File streaming**: Read from MP3, FLAC, WAV files
- **Live input**: Capture from microphone or line-in
- **Synthesizers**: Generate music programmatically
- **Network streams**: Proxy audio from internet radio
- **Audio processing**: Apply effects, mixing, filtering
- **Testing**: Generate test signals for debugging
## Next steps
- Implement a file-based source using `pkg/audio/decode`
- Add audio effects (reverb, echo, filters)
- Create a source that mixes multiple input sources
- Implement a source that reads from an audio device

View File

@@ -0,0 +1,227 @@
// ABOUTME: Custom AudioSource implementation example
// ABOUTME: Demonstrates how to create custom audio sources for Sendspin
package main
import (
"flag"
"log"
"math"
"os"
"os/signal"
"sync"
"syscall"
"github.com/Sendspin/sendspin-go/pkg/sendspin"
)
// MultiToneSource generates multiple sine waves at different frequencies
// This demonstrates how to implement the AudioSource interface
type MultiToneSource struct {
frequencies []float64
sampleRate int
channels int
sampleIndex uint64
mu sync.Mutex
}
// NewMultiTone creates a source that mixes multiple frequencies
func NewMultiTone(sampleRate, channels int, frequencies []float64) *MultiToneSource {
return &MultiToneSource{
frequencies: frequencies,
sampleRate: sampleRate,
channels: channels,
}
}
// Read implements AudioSource.Read
// Generates PCM samples by mixing multiple sine waves
func (s *MultiToneSource) Read(samples []int32) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
numFrames := len(samples) / s.channels
for i := 0; i < numFrames; i++ {
// Calculate time for this sample
t := float64(s.sampleIndex+uint64(i)) / float64(s.sampleRate)
// Mix all frequencies together
var mixedSample float64
for _, freq := range s.frequencies {
mixedSample += math.Sin(2 * math.Pi * freq * t)
}
// Average the mixed signal
if len(s.frequencies) > 0 {
mixedSample /= float64(len(s.frequencies))
}
// Convert to 24-bit PCM (int32)
// Scale to 24-bit range with 50% volume to prevent clipping
const max24bit = 8388607 // 2^23 - 1
pcmValue := int32(mixedSample * max24bit * 0.5)
// Duplicate to all channels
for ch := 0; ch < s.channels; ch++ {
samples[i*s.channels+ch] = pcmValue
}
}
s.sampleIndex += uint64(numFrames)
return len(samples), nil
}
// SampleRate implements AudioSource.SampleRate
func (s *MultiToneSource) SampleRate() int {
return s.sampleRate
}
// Channels implements AudioSource.Channels
func (s *MultiToneSource) Channels() int {
return s.channels
}
// Metadata implements AudioSource.Metadata
func (s *MultiToneSource) Metadata() (title, artist, album string) {
return "Multi-Tone Test Signal", "Sendspin Examples", "Custom Sources"
}
// Close implements AudioSource.Close
func (s *MultiToneSource) Close() error {
return nil
}
// SweepSource generates a frequency sweep (chirp)
// Demonstrates time-varying audio generation
type SweepSource struct {
startFreq float64
endFreq float64
duration float64 // seconds
sampleRate int
channels int
sampleIndex uint64
mu sync.Mutex
}
// NewSweep creates a frequency sweep source
func NewSweep(sampleRate, channels int, startFreq, endFreq, duration float64) *SweepSource {
return &SweepSource{
startFreq: startFreq,
endFreq: endFreq,
duration: duration,
sampleRate: sampleRate,
channels: channels,
}
}
// Read implements AudioSource.Read
func (s *SweepSource) Read(samples []int32) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
numFrames := len(samples) / s.channels
for i := 0; i < numFrames; i++ {
t := float64(s.sampleIndex+uint64(i)) / float64(s.sampleRate)
// Loop the sweep
t = math.Mod(t, s.duration)
// Linear frequency sweep
progress := t / s.duration
freq := s.startFreq + (s.endFreq-s.startFreq)*progress
// Generate sine wave at current frequency
sample := math.Sin(2 * math.Pi * freq * t)
// Convert to 24-bit PCM
const max24bit = 8388607
pcmValue := int32(sample * max24bit * 0.5)
// Duplicate to all channels
for ch := 0; ch < s.channels; ch++ {
samples[i*s.channels+ch] = pcmValue
}
}
s.sampleIndex += uint64(numFrames)
return len(samples), nil
}
func (s *SweepSource) SampleRate() int { return s.sampleRate }
func (s *SweepSource) Channels() int { return s.channels }
func (s *SweepSource) Metadata() (string, string, string) {
return "Frequency Sweep", "Sendspin Examples", "Custom Sources"
}
func (s *SweepSource) Close() error { return nil }
func main() {
port := flag.Int("port", 8927, "Server port")
mode := flag.String("mode", "chord", "Source mode: chord, sweep, single")
sampleRate := flag.Int("rate", 192000, "Sample rate (Hz)")
channels := flag.Int("channels", 2, "Number of channels")
flag.Parse()
var source sendspin.AudioSource
// Create audio source based on mode
switch *mode {
case "chord":
// A major chord: A4, C#5, E5
frequencies := []float64{440.0, 554.37, 659.25}
source = NewMultiTone(*sampleRate, *channels, frequencies)
log.Printf("Creating A major chord: %v Hz", frequencies)
case "sweep":
// Sweep from 220 Hz (A3) to 880 Hz (A5) over 5 seconds
source = NewSweep(*sampleRate, *channels, 220.0, 880.0, 5.0)
log.Printf("Creating frequency sweep: 220 - 880 Hz over 5 seconds")
case "single":
// Single 440 Hz tone (same as basic example)
source = sendspin.NewTestTone(*sampleRate, *channels)
log.Printf("Creating single tone: 440 Hz")
default:
log.Fatalf("Invalid mode: %s (use: chord, sweep, single)", *mode)
}
config := sendspin.ServerConfig{
Port: *port,
Name: "Custom Source Example",
Source: source,
EnableMDNS: true,
Debug: false,
}
server, err := sendspin.NewServer(config)
if err != nil {
log.Fatalf("Failed to create server: %v", err)
}
log.Printf("Starting server on port %d...", *port)
log.Printf("Audio: %dHz, %d channels, 24-bit", *sampleRate, *channels)
errChan := make(chan error, 1)
go func() {
if err := server.Start(); err != nil {
errChan <- err
}
}()
log.Printf("\nServer running. Press Ctrl+C to stop")
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
select {
case <-sigChan:
log.Printf("Shutting down...")
case err := <-errChan:
log.Printf("Server error: %v", err)
}
server.Stop()
log.Printf("Server stopped")
}