🎉 live server seems to be working now
This commit is contained in:
179
third_party/sendspin-go/examples/README.md
vendored
Normal file
179
third_party/sendspin-go/examples/README.md
vendored
Normal 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.
|
||||
74
third_party/sendspin-go/examples/basic-player/README.md
vendored
Normal file
74
third_party/sendspin-go/examples/basic-player/README.md
vendored
Normal 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
|
||||
88
third_party/sendspin-go/examples/basic-player/main.go
vendored
Normal file
88
third_party/sendspin-go/examples/basic-player/main.go
vendored
Normal 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...")
|
||||
}
|
||||
101
third_party/sendspin-go/examples/basic-server/README.md
vendored
Normal file
101
third_party/sendspin-go/examples/basic-server/README.md
vendored
Normal 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
|
||||
77
third_party/sendspin-go/examples/basic-server/main.go
vendored
Normal file
77
third_party/sendspin-go/examples/basic-server/main.go
vendored
Normal 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")
|
||||
}
|
||||
158
third_party/sendspin-go/examples/custom-source/README.md
vendored
Normal file
158
third_party/sendspin-go/examples/custom-source/README.md
vendored
Normal 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
|
||||
227
third_party/sendspin-go/examples/custom-source/main.go
vendored
Normal file
227
third_party/sendspin-go/examples/custom-source/main.go
vendored
Normal 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")
|
||||
}
|
||||
Reference in New Issue
Block a user