228 lines
5.7 KiB
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")
|
|
}
|