Files
-rpi-sendspin/third_party/sendspin-go/examples/custom-source/main.go

228 lines
5.7 KiB
Go

// 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")
}