🎉 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,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")
}