Files
wehub-resource-sync ead81af521
Deploy to GitHub Pages / deploy (push) Failing after 0s
CI / go (push) Has been cancelled
CI / build (darwin, macos-14) (push) Has been cancelled
CI / build (windows, windows-2025) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:13 +08:00

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
}