223 lines
4.8 KiB
Go
223 lines
4.8 KiB
Go
package playback
|
|
|
|
import (
|
|
"io"
|
|
"time"
|
|
|
|
"git.dayanhub.com/sagi/subsonic-tui/internal/client"
|
|
"github.com/delucks/go-subsonic"
|
|
"github.com/gopxl/beep/v2"
|
|
"github.com/gopxl/beep/v2/mp3"
|
|
"github.com/gopxl/beep/v2/speaker"
|
|
)
|
|
|
|
type PlaybackState int
|
|
|
|
const (
|
|
PlaybackStateStopped = iota
|
|
PlaybackStatePaused
|
|
PlaybackStatePlaying
|
|
)
|
|
|
|
type Controller struct {
|
|
client *client.Client
|
|
stream beep.StreamSeekCloser
|
|
song *subsonic.Child
|
|
songElapsedFunc func(song *subsonic.Child, elapsed time.Duration)
|
|
initialFormat *beep.Format
|
|
currentFormat *beep.Format
|
|
position float64
|
|
closeChan chan bool
|
|
songEndedChan chan bool
|
|
songEndedFunc func(song *subsonic.Child)
|
|
ctrl *beep.Ctrl
|
|
desktopPlayback DesktopPlayback
|
|
queue *queue
|
|
playbackState PlaybackState
|
|
}
|
|
|
|
func NewController(client *client.Client) *Controller {
|
|
controller := &Controller{
|
|
client: client,
|
|
closeChan: make(chan bool),
|
|
songEndedChan: make(chan bool),
|
|
playbackState: PlaybackStateStopped,
|
|
queue: newQueue(),
|
|
}
|
|
controller.desktopPlayback = desktopPlayer(controller)
|
|
controller.desktopPlayback.Start()
|
|
go controller.playbackTicker()
|
|
return controller
|
|
}
|
|
|
|
func (c *Controller) State() PlaybackState {
|
|
return c.playbackState
|
|
}
|
|
|
|
func (c *Controller) Play(song *subsonic.Child) {
|
|
if song == nil {
|
|
return
|
|
}
|
|
r, err := c.client.Stream(song.ID)
|
|
if err != nil {
|
|
//TODO: Log error
|
|
c.Stop()
|
|
return
|
|
}
|
|
c.Stream(r, song)
|
|
}
|
|
func (c *Controller) Next() {
|
|
song := c.queue.Next()
|
|
if song != nil {
|
|
c.Play(song)
|
|
}
|
|
}
|
|
|
|
func (c *Controller) AddToQueue(songs []*subsonic.Child) {
|
|
shouldPlay := c.queue.Add(songs...)
|
|
if shouldPlay {
|
|
c.Play(c.queue.GetCurrentSong())
|
|
}
|
|
c.desktopPlayback.OnPlaylistChanged()
|
|
}
|
|
|
|
func (c *Controller) GetQueuePosition() int {
|
|
return c.queue.GetPosition()
|
|
}
|
|
|
|
func (c *Controller) GetCurrentSong() *subsonic.Child {
|
|
return c.queue.GetCurrentSong()
|
|
}
|
|
|
|
func (c *Controller) SetQueue(songs []*subsonic.Child) {
|
|
c.queue.Clear()
|
|
c.Stop()
|
|
c.queue.Set(songs)
|
|
c.Play(c.queue.GetCurrentSong())
|
|
c.desktopPlayback.OnPlaylistChanged()
|
|
}
|
|
|
|
func (c *Controller) SetQueuePosition(position int) {
|
|
s := c.queue.SetPosition(position)
|
|
c.Play(s)
|
|
}
|
|
|
|
func (c *Controller) Prev() {
|
|
song := c.queue.Prev()
|
|
if song != nil {
|
|
c.Play(song)
|
|
}
|
|
}
|
|
func (c *Controller) GetQueue() []*subsonic.Child {
|
|
return c.queue.Get()
|
|
}
|
|
|
|
func (c *Controller) ClearQueue() {
|
|
c.Stop()
|
|
c.queue.Clear()
|
|
c.desktopPlayback.OnPlayPause()
|
|
c.desktopPlayback.OnSongChanged()
|
|
c.desktopPlayback.OnPlaylistChanged()
|
|
}
|
|
|
|
func (c *Controller) SetSongElapsedFunc(f func(sing *subsonic.Child, elapsed time.Duration)) {
|
|
c.songElapsedFunc = f
|
|
}
|
|
|
|
func (c *Controller) SetSongEndedFunc(f func(song *subsonic.Child)) {
|
|
c.songEndedFunc = f
|
|
}
|
|
|
|
func (c *Controller) Close() error {
|
|
c.Stop()
|
|
c.closeChan <- true
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) TogglePlayPause() {
|
|
if c.playbackState != PlaybackStateStopped {
|
|
c.ctrl.Paused = !c.ctrl.Paused
|
|
if c.ctrl.Paused {
|
|
c.playbackState = PlaybackStatePaused
|
|
} else {
|
|
c.playbackState = PlaybackStatePlaying
|
|
}
|
|
}
|
|
c.desktopPlayback.OnPlayPause()
|
|
}
|
|
|
|
func (c *Controller) Stop() {
|
|
if c.ctrl == nil {
|
|
return
|
|
}
|
|
speaker.Clear()
|
|
c.ctrl.Paused = true
|
|
c.playbackState = PlaybackStateStopped
|
|
c.ctrl = nil
|
|
c.stream = nil
|
|
c.songElapsedFunc(c.song, time.Duration(0))
|
|
c.song = nil
|
|
c.position = 0
|
|
c.desktopPlayback.OnPlayPause()
|
|
c.desktopPlayback.OnPlaylistChanged()
|
|
}
|
|
|
|
func (c *Controller) playbackTicker() {
|
|
for {
|
|
select {
|
|
case <-c.closeChan:
|
|
return
|
|
case <-c.songEndedChan:
|
|
c.Stop()
|
|
c.Next()
|
|
default:
|
|
if c.playbackState == PlaybackStatePlaying && c.song != nil {
|
|
if c.stream != nil {
|
|
pos := c.stream.Position()
|
|
elapsed := c.currentFormat.SampleRate.D(pos).Round(time.Second)
|
|
c.position = elapsed.Seconds()
|
|
c.songElapsedFunc(c.song, elapsed)
|
|
c.desktopPlayback.OnPositionChanged(int(c.position))
|
|
}
|
|
}
|
|
}
|
|
time.Sleep(time.Second)
|
|
}
|
|
}
|
|
|
|
func (c *Controller) Stream(reader io.Reader, song *subsonic.Child) {
|
|
c.Stop()
|
|
// Ensure artwork cache...
|
|
_, _ = c.client.GetCoverArt(song.CoverArt)
|
|
|
|
readerCloser := io.NopCloser(reader)
|
|
decodedMp3, format, err := mp3.Decode(readerCloser)
|
|
decodedMp3.Position()
|
|
if err != nil {
|
|
panic("mp3.NewDecoder failed: " + err.Error())
|
|
}
|
|
|
|
if c.initialFormat == nil {
|
|
c.initialFormat = &format
|
|
}
|
|
|
|
c.currentFormat = &format
|
|
|
|
var stream beep.Streamer = decodedMp3
|
|
if err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)); err != nil {
|
|
stream = beep.Resample(3, format.SampleRate, c.initialFormat.SampleRate, decodedMp3)
|
|
}
|
|
|
|
c.stream = decodedMp3
|
|
c.song = song
|
|
|
|
ctrl := &beep.Ctrl{Streamer: stream}
|
|
c.ctrl = ctrl
|
|
c.playbackState = PlaybackStatePlaying
|
|
speaker.Play(beep.Seq(ctrl, beep.Callback(func() {
|
|
c.songEndedFunc(song)
|
|
c.songEndedChan <- true
|
|
})))
|
|
c.desktopPlayback.OnSongChanged()
|
|
c.desktopPlayback.OnPlayPause()
|
|
}
|