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

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

cd examples/custom-source
go build

Running

Generate an A major chord (440, 554, 659 Hz):

./custom-source -mode chord

Generate a frequency sweep from 220 to 880 Hz:

./custom-source -mode sweep

Generate a single 440 Hz tone:

./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:

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:

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:

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