aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2022-07-21 21:48:59 +0200
committerFlorian Fischer <florian.fischer@muhq.space>2022-07-21 21:48:59 +0200
commit8171af55daad6ce9a5cdacf95527801e83f25c08 (patch)
tree0e879a159bea07624687586df78f5a99d344396f
parent8b4335c18752ce96e452dc232e57695c2fdf6a6d (diff)
downloadWRMS-8171af55daad6ce9a5cdacf95527801e83f25c08.tar.gz
WRMS-8171af55daad6ce9a5cdacf95527801e83f25c08.zip
WIP spotify playback commit
* display spotify search results in search overlay * synchronously play songs added from search
-rw-r--r--dummyBackend.go8
-rw-r--r--player.go4
-rw-r--r--spotify.go119
-rw-r--r--wrms.go5
4 files changed, 123 insertions, 13 deletions
diff --git a/dummyBackend.go b/dummyBackend.go
index f2eae57..d4461db 100644
--- a/dummyBackend.go
+++ b/dummyBackend.go
@@ -7,7 +7,7 @@ import (
type DummyBackend struct{}
-func (dummy *DummyBackend) Play(song Song) {
+func (dummy *DummyBackend) Play(song *Song) {
}
func (dummy *DummyBackend) Pause() {
@@ -19,11 +19,9 @@ func (dummy *DummyBackend) Search(keyword string) []Song {
return []Song{s}
}
-func NewDummySong(title string, artist string) Song {
- s := Song{title, artist, "dummy", "", 0, 0, map[string]struct{}{}, map[string]struct{}{}}
+func NewDummySong(title, artist string) Song {
h := sha1.New()
h.Write([]byte(title))
h.Write([]byte(artist))
- s.Uri = base64.URLEncoding.EncodeToString(h.Sum(nil))
- return s
+ return NewSong(title, artist, "dummy", base64.URLEncoding.EncodeToString(h.Sum(nil)))
}
diff --git a/player.go b/player.go
index d46311f..6e377a7 100644
--- a/player.go
+++ b/player.go
@@ -5,7 +5,7 @@ import (
)
type Backend interface {
- Play(song Song)
+ Play(song *Song)
Pause()
Search(keyword string) []Song
}
@@ -29,7 +29,7 @@ func NewPlayer() Player {
return Player{available_backends}
}
-func (player *Player) Play(song Song) {
+func (player *Player) Play(song *Song) {
player.Backends[song.Source].Play(song)
}
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
+ }
+}
diff --git a/wrms.go b/wrms.go
index f7a7395..ee21c27 100644
--- a/wrms.go
+++ b/wrms.go
@@ -21,6 +21,10 @@ type Song struct {
Downvotes map[string]struct{} `json:"-"`
}
+func NewSong(title, artist, source, uri string) Song {
+ return Song{title, artist, source, uri, 0, 0, map[string]struct{}{}, map[string]struct{}{}}
+}
+
func NewSongFromJson(data []byte) (Song, error) {
var s Song
err := json.Unmarshal(data, &s)
@@ -104,6 +108,7 @@ func (wrms *Wrms) PlayPause() {
wrms.Next()
}
ev = Event{"play", []Song{*wrms.CurrentSong}}
+ wrms.Player.Play(wrms.CurrentSong)
} else {
ev = Event{"pause", []Song{}}
}