116 lines
3.3 KiB
Go
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
|
|
}
|