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

107 lines
2.8 KiB
Go

package player
import (
"sync"
"sync/atomic"
"github.com/gopxl/beep/v2"
)
// gaplessStreamer is a beep.Streamer that sequences tracks with zero-gap
// transitions. It sits at the bottom of the audio pipeline and manages
// track sources while the EQ/volume/tap/ctrl chain above it lives forever.
//
// It always returns (len(samples), true) — it never stops the speaker.
// When no audio is available, it fills silence.
type gaplessStreamer struct {
mu sync.Mutex
current beep.Streamer // active track (decoded + resampled)
next beep.Streamer // preloaded next track
drained atomic.Bool // true when current exhausts with no next
onSwap func() // called (in goroutine) on gapless transition
}
// Stream reads samples from the current track. On exhaustion, it seamlessly
// fills remaining samples from the next track (like beep.Seq). If no next
// track is available, it sets drained=true and fills silence.
func (g *gaplessStreamer) Stream(samples [][2]float64) (int, bool) {
g.mu.Lock()
cur := g.current
g.mu.Unlock()
if cur == nil {
// No active track — fill silence
clear(samples)
return len(samples), true
}
n, ok := cur.Stream(samples)
if !ok || n < len(samples) {
// Current track exhausted — try to seamlessly continue with next
g.mu.Lock()
next := g.next
g.next = nil
g.current = next
swapFn := g.onSwap
g.mu.Unlock()
if next != nil {
// Fill remaining buffer from next track — zero gap
if n < len(samples) {
filled, _ := next.Stream(samples[n:])
n += filled
}
// Notify about the transition (non-blocking)
if swapFn != nil {
go swapFn()
}
g.drained.Store(false)
} else {
// No next track — we've drained
g.drained.Store(true)
}
}
// Fill any remaining with silence
for i := n; i < len(samples); i++ {
samples[i] = [2]float64{}
}
return len(samples), true
}
// Err always returns nil — errors are handled per-track at decode time.
func (g *gaplessStreamer) Err() error { return nil }
// SetNext preloads the next track's resampled streamer for gapless transition.
func (g *gaplessStreamer) SetNext(s beep.Streamer) {
g.mu.Lock()
defer g.mu.Unlock()
g.next = s
}
// Replace interrupts the current track and starts a new one immediately.
// Used for manual skip/prev/select operations.
func (g *gaplessStreamer) Replace(s beep.Streamer) {
g.mu.Lock()
defer g.mu.Unlock()
g.current = s
g.next = nil
g.drained.Store(false)
}
// Clear removes both current and next tracks. The streamer will output
// silence until Replace or SetNext is called.
func (g *gaplessStreamer) Clear() {
g.mu.Lock()
defer g.mu.Unlock()
g.current = nil
g.next = nil
g.drained.Store(false)
}
// Drained reports whether the current track ended with no next track queued.
func (g *gaplessStreamer) Drained() bool {
return g.drained.Load()
}