From 8171af55daad6ce9a5cdacf95527801e83f25c08 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Thu, 21 Jul 2022 21:48:59 +0200 Subject: WIP spotify playback commit * display spotify search results in search overlay * synchronously play songs added from search --- spotify.go | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 113 insertions(+), 6 deletions(-) (limited to 'spotify.go') diff --git a/spotify.go b/spotify.go index a4d8080..6f11e84 100644 --- a/spotify.go +++ b/spotify.go @@ -9,15 +9,19 @@ package main import ( "errors" + "log" "fmt" "os" + "strings" + "sync" + "unsafe" - // "github.com/librespot-org/librespot-golang/Spotify" + "github.com/librespot-org/librespot-golang/Spotify" "github.com/librespot-org/librespot-golang/librespot" "github.com/librespot-org/librespot-golang/librespot/core" - // "github.com/librespot-org/librespot-golang/librespot/utils" + "github.com/librespot-org/librespot-golang/librespot/utils" "github.com/xlab/portaudio-go/portaudio" - // "github.com/xlab/vorbis-go/decoder" + "github.com/xlab/vorbis-go/decoder" ) const ( @@ -58,7 +62,72 @@ func NewSpotify() (*SpotifyBackend, error) { return &spotify, nil } -func (spotify *SpotifyBackend) Play(song Song) { +func (spotify *SpotifyBackend) Play(song *Song) { + trackID := song.Uri + session := spotify.session + fmt.Println("Loading track for play: ", trackID) + + // Get the track metadata: it holds information about which files and encodings are available + track, err := session.Mercury().GetTrack(utils.Base62ToHex(trackID)) + if err != nil { + fmt.Println("Error loading track: ", err) + return + } + + fmt.Println("Track:", track.GetName()) + + // As a demo, select the OGG 160kbps variant of the track. The "high quality" setting in the official Spotify + // app is the OGG 320kbps variant. + var selectedFile *Spotify.AudioFile + for _, file := range track.GetFile() { + if file.GetFormat() == Spotify.AudioFile_OGG_VORBIS_160 { + selectedFile = file + } + } + + // Synchronously load the track + audioFile, err := session.Player().LoadTrack(selectedFile, track.GetGid()) + + // TODO: channel to be notified of chunks downloaded (or reader?) + + if err != nil { + fmt.Printf("Error while loading track: %s\n", err) + } else { + // We have the track audio, let's play it! Initialize the OGG decoder, and start a PortAudio stream. + // Note that we skip the first 167 bytes as it is a Spotify-specific header. You can decode it by + // using this: https://sourceforge.net/p/despotify/code/HEAD/tree/java/trunk/src/main/java/se/despotify/client/player/SpotifyOggHeader.java + fmt.Println("Setting up OGG decoder...") + dec, err := decoder.New(audioFile, samplesPerChannel) + if err != nil { + log.Fatalln(err) + } + + info := dec.Info() + + go func() { + dec.Decode() + dec.Close() + }() + + fmt.Println("Setting up PortAudio stream...") + fmt.Printf("PortAudio channels: %d / SampleRate: %f\n", info.Channels, info.SampleRate) + + var wg sync.WaitGroup + var stream *portaudio.Stream + callback := paCallback(&wg, int(info.Channels), dec.SamplesOut()) + + if err := portaudio.OpenDefaultStream(&stream, 0, info.Channels, sampleFormat, info.SampleRate, + samplesPerChannel, callback, nil); paError(err) { + log.Fatalln(paErrorText(err)) + } + + fmt.Println("Starting playback...") + if err := portaudio.StartStream(stream); paError(err) { + log.Fatalln(paErrorText(err)) + } + + wg.Wait() + } } func (spotify *SpotifyBackend) Pause() { @@ -84,11 +153,14 @@ func (spotify *SpotifyBackend) Search(keyword string) []Song { fmt.Printf("\nTracks: %d (total %d)\n", len(res.Tracks.Hits), res.Tracks.Total) + results := []Song{} for _, track := range res.Tracks.Hits { - fmt.Printf(" => %v (%s)\n", track, track.Uri) + uriParts := strings.Split(track.Uri, ":") + results = append(results, NewSong(track.Name, track.Artists[0].Name, "spotify", uriParts[len(uriParts) - 1])) + // fmt.Printf(" => %v (%s)\n", track, track.Uri) } - return []Song{} + return results } // PortAudio helpers @@ -99,3 +171,38 @@ func paError(err portaudio.Error) bool { func paErrorText(err portaudio.Error) string { return "PortAudio error: " + portaudio.GetErrorText(err) } + +func paCallback(wg *sync.WaitGroup, channels int, samples <-chan [][]float32) portaudio.StreamCallback { + wg.Add(1) + return func(_ unsafe.Pointer, output unsafe.Pointer, sampleCount uint, + _ *portaudio.StreamCallbackTimeInfo, _ portaudio.StreamCallbackFlags, _ unsafe.Pointer) int32 { + + const ( + statusContinue = int32(portaudio.PaContinue) + statusComplete = int32(portaudio.PaComplete) + ) + + frame, ok := <-samples + if !ok { + wg.Done() + return statusComplete + } + if len(frame) > int(sampleCount) { + frame = frame[:sampleCount] + } + + var idx int + out := (*(*[1 << 32]float32)(unsafe.Pointer(output)))[:int(sampleCount)*channels] + for _, sample := range frame { + if len(sample) > channels { + sample = sample[:channels] + } + for i := range sample { + out[idx] = sample[i] + idx++ + } + } + + return statusContinue + } +} -- cgit v1.2.3