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

116 lines
3.3 KiB
Go

//go:build !windows
// Package spotify integrates Spotify playback into cliamp via go-librespot.
package spotify
import (
"io"
"time"
librespot "github.com/devgianlu/go-librespot"
librespotPlayer "github.com/devgianlu/go-librespot/player"
"github.com/gopxl/beep/v2"
)
const (
spotifySampleRate = 44100
spotifyChannels = 2
)
// spotifyStreamer bridges a go-librespot AudioSource to beep.StreamSeekCloser.
// go-librespot outputs interleaved stereo float32 at 44100Hz; this converts
// to Beep's [][2]float64 sample format.
type spotifyStreamer struct {
source librespot.AudioSource
stream *librespotPlayer.Stream
buf []float32
durationMs int64
err error
}
// newSpotifyStreamer wraps a go-librespot Stream as a beep.StreamSeekCloser.
func newSpotifyStreamer(stream *librespotPlayer.Stream) *spotifyStreamer {
var dur int64
if stream.Media != nil {
dur = int64(stream.Media.Duration())
}
return &spotifyStreamer{
source: stream.Source,
stream: stream,
durationMs: dur,
}
}
// Stream reads interleaved float32 from the AudioSource and converts to
// [][2]float64 stereo pairs for Beep's audio pipeline.
func (s *spotifyStreamer) Stream(samples [][2]float64) (n int, ok bool) {
// Each stereo sample pair needs 2 float32 values (L, R).
needed := len(samples) * spotifyChannels
if len(s.buf) < needed {
s.buf = make([]float32, needed)
}
nRead, err := s.source.Read(s.buf[:needed])
if err != nil && err != io.EOF {
s.err = err
return 0, false
}
// Ensure we only process complete stereo pairs (drop any trailing mono sample).
nRead -= nRead % spotifyChannels
// Convert interleaved float32 [L0,R0,L1,R1,...] to [][2]float64 pairs.
pairs := nRead / spotifyChannels
for i := range pairs {
samples[i][0] = float64(s.buf[i*2])
samples[i][1] = float64(s.buf[i*2+1])
}
if pairs == 0 && err == io.EOF {
return 0, false
}
return pairs, true
}
func (s *spotifyStreamer) Err() error { return s.err }
// Len returns the total number of sample pairs (at 44100Hz stereo).
func (s *spotifyStreamer) Len() int {
return int(s.durationMs * spotifySampleRate / 1000)
}
// Position returns the current playback position in sample pairs.
func (s *spotifyStreamer) Position() int {
return int(s.source.PositionMs() * spotifySampleRate / 1000)
}
// Seek moves to sample position p (in sample pairs at 44100Hz).
func (s *spotifyStreamer) Seek(p int) error {
ms := int64(p) * 1000 / spotifySampleRate
return s.source.SetPositionMs(ms)
}
// Close releases the stream resources.
// The underlying AudioSource (vorbis.Decoder or flac.Decoder) has a Close()
// method but the AudioSource interface does not expose it. The chunked HTTP
// reader and decryption pipeline will be released when the object is GC'd.
// This is a known limitation for skipped tracks until go-librespot exposes
// Close() on the AudioSource interface.
func (s *spotifyStreamer) Close() error {
return nil
}
// Format returns the Beep audio format for Spotify streams.
func (s *spotifyStreamer) Format() beep.Format {
return beep.Format{
SampleRate: beep.SampleRate(spotifySampleRate),
NumChannels: spotifyChannels,
Precision: 4, // float32 = 4 bytes
}
}
// Duration returns the track duration.
func (s *spotifyStreamer) Duration() time.Duration {
return time.Duration(s.durationMs) * time.Millisecond
}