68 lines
1.9 KiB
Go
68 lines
1.9 KiB
Go
// Package player provides the audio engine for MP3 playback with
|
|
// a 10-band parametric EQ, volume control, and sample capture for visualization.
|
|
package player
|
|
|
|
import (
|
|
"sync/atomic"
|
|
|
|
"github.com/gopxl/beep/v2"
|
|
)
|
|
|
|
// tap is a streamer wrapper that copies samples into a ring buffer
|
|
// for real-time FFT visualization. It sits in the audio pipeline
|
|
// before the volume control so the visualizer sees pre-volume amplitude.
|
|
//
|
|
// The write position is updated atomically, allowing the audio thread
|
|
// (sole writer) and the UI thread (infrequent reader at 50ms intervals)
|
|
// to operate without mutex contention. Minor sample tearing at the
|
|
// read boundary is invisible in FFT-based spectrum visualization.
|
|
type tap struct {
|
|
s beep.Streamer
|
|
buf []float64
|
|
pos atomic.Int64
|
|
size int
|
|
}
|
|
|
|
// newTap wraps a streamer with a ring buffer of the given size.
|
|
func newTap(s beep.Streamer, bufSize int) *tap {
|
|
return &tap{
|
|
s: s,
|
|
buf: make([]float64, bufSize),
|
|
size: bufSize,
|
|
}
|
|
}
|
|
|
|
// Stream passes audio through while capturing a mono mix into the ring buffer.
|
|
func (t *tap) Stream(samples [][2]float64) (int, bool) {
|
|
n, ok := t.s.Stream(samples)
|
|
p := int(t.pos.Load())
|
|
for i := range n {
|
|
t.buf[p] = (samples[i][0] + samples[i][1]) / 2
|
|
p = (p + 1) % t.size
|
|
}
|
|
t.pos.Store(int64(p))
|
|
return n, ok
|
|
}
|
|
|
|
// Err returns the underlying streamer's error.
|
|
func (t *tap) Err() error {
|
|
return t.s.Err()
|
|
}
|
|
|
|
// SamplesInto copies the last len(dst) samples into dst, avoiding allocation.
|
|
// Uses two copy() calls for the ring buffer wraparound instead of per-element
|
|
// modulo, which is significantly faster for large buffers (e.g. FFT size 2048).
|
|
func (t *tap) SamplesInto(dst []float64) int {
|
|
n := min(len(dst), t.size)
|
|
p := int(t.pos.Load())
|
|
start := (p - n + t.size) % t.size
|
|
if start+n <= t.size {
|
|
copy(dst, t.buf[start:start+n])
|
|
} else {
|
|
first := t.size - start
|
|
copy(dst[:first], t.buf[start:])
|
|
copy(dst[first:], t.buf[:n-first])
|
|
}
|
|
return n
|
|
}
|