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 { 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() }