//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 }