150 lines
3 KiB
Go
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)
|
||
|
}
|
||
|
}
|