subsonic-tui/internal/playback/controller.go
Sagi Dayan 26e526f07e
initial commit
Signed-off-by: Sagi Dayan <sagidayan@gmail.com>
2024-02-20 19:10:14 +02:00

150 lines
3 KiB
Go

package playback
import (
"io"
"sync"
"time"
"github.com/delucks/go-subsonic"
"github.com/gopxl/beep"
"github.com/gopxl/beep/mp3"
"github.com/gopxl/beep/speaker"
)
type Controller struct {
stream beep.StreamSeekCloser
song *subsonic.Child
songElapsedFunc func(elapsed time.Duration)
format *beep.Format
closeChan chan bool
songEndedFunc func(song *subsonic.Child)
isPlaying bool
ctrl *beep.Ctrl
tickerRunning bool
controllerLock *sync.Mutex
}
func NewController() *Controller {
controller := &Controller{
closeChan: make(chan bool),
controllerLock: &sync.Mutex{},
}
return controller
}
func (c *Controller) Playing() bool {
return c.isPlaying
}
func (c *Controller) SetSongElapsedFunc(f func(elapsed time.Duration)) {
c.songElapsedFunc = f
}
func (c *Controller) SetSongEndedFunc(f func(song *subsonic.Child)) {
c.songEndedFunc = f
}
func (c *Controller) Close() error {
if c.tickerRunning {
c.closeChan <- true
}
if c.stream != nil {
c.Stop()
speaker.Close()
}
return nil
}
func (c *Controller) TogglePlayPause() {
if c.ctrl == nil {
if c.stream != nil {
c.ctrl.Streamer = c.stream
c.ctrl.Paused = false
return
}
return
}
c.ctrl.Paused = !c.ctrl.Paused
}
func (c *Controller) Stop() {
if c.ctrl == nil {
return
}
go func() {
c.controllerLock.Lock()
speaker.Clear()
c.ctrl.Paused = true
c.isPlaying = false
c.ctrl = nil
c.stream = nil
c.song = nil
c.songElapsedFunc(time.Duration(0))
c.controllerLock.Unlock()
}()
}
func (c *Controller) playbackTicker(format beep.Format) {
if c.tickerRunning {
return
}
c.tickerRunning = true
for {
c.controllerLock.Lock()
select {
case <-c.closeChan:
c.tickerRunning = false
c.controllerLock.Unlock()
return
default:
if c.isPlaying && c.stream != nil {
pos := c.stream.Position()
songDuration := time.Duration(c.song.Duration) * time.Second
elapsed := format.SampleRate.D(pos).Round(time.Second)
if elapsed >= songDuration {
elapsed = songDuration
c.songElapsedFunc(elapsed)
c.tickerRunning = false
c.controllerLock.Unlock()
c.songEndedFunc(c.song)
return
}
c.songElapsedFunc(elapsed)
}
}
c.controllerLock.Unlock()
time.Sleep(time.Second)
}
}
func (c *Controller) Stream(reader io.Reader, song *subsonic.Child) {
c.Stop()
readerCloser := io.NopCloser(reader)
decodedMp3, format, err := mp3.Decode(readerCloser)
decodedMp3.Position()
if err != nil {
panic("mp3.NewDecoder failed: " + err.Error())
}
if c.format == nil {
c.format = &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.format.SampleRate, decodedMp3)
}
c.stream = decodedMp3
c.song = song
ctrl := &beep.Ctrl{Streamer: stream}
speaker.Play(beep.Seq(ctrl))
c.ctrl = ctrl
c.isPlaying = true
if !c.tickerRunning {
go c.playbackTicker(format)
}
}