163 lines
3.8 KiB
Go
163 lines
3.8 KiB
Go
//go:build beep
|
|
|
|
package beepaudio
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/faiface/beep"
|
|
"github.com/faiface/beep/effects"
|
|
"github.com/faiface/beep/speaker"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
)
|
|
|
|
type BeepTrack struct {
|
|
MediaFile model.MediaFile
|
|
Ctrl *beep.Ctrl
|
|
Volume *effects.Volume
|
|
ActiveStream beep.StreamSeekCloser
|
|
TempfileToCleanup string
|
|
SampleRate beep.SampleRate
|
|
PlaybackDone chan bool
|
|
}
|
|
|
|
func NewTrack(playbackDoneChannel chan bool, mf model.MediaFile) (*BeepTrack, error) {
|
|
t := BeepTrack{}
|
|
|
|
contentType := mf.ContentType()
|
|
log.Debug("loading track", "trackname", mf.Path, "mediatype", contentType)
|
|
|
|
var streamer beep.StreamSeekCloser
|
|
var format beep.Format
|
|
var err error
|
|
var tmpfileToCleanup = ""
|
|
|
|
switch contentType {
|
|
case "audio/mpeg":
|
|
streamer, format, err = DecodeMp3(mf.Path)
|
|
case "audio/x-wav":
|
|
streamer, format, err = DecodeWAV(mf.Path)
|
|
case "audio/mp4":
|
|
streamer, format, tmpfileToCleanup, err = DecodeFLAC(mf.Path)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported content type: %s", contentType)
|
|
}
|
|
|
|
if err != nil {
|
|
log.Error(err)
|
|
return nil, err
|
|
}
|
|
|
|
// save running stream for closing when switching tracks
|
|
t.ActiveStream = streamer
|
|
t.TempfileToCleanup = tmpfileToCleanup
|
|
|
|
log.Debug("Setting up audio device")
|
|
t.Ctrl = &beep.Ctrl{Streamer: streamer, Paused: true}
|
|
t.Volume = &effects.Volume{Streamer: t.Ctrl, Base: 2}
|
|
t.SampleRate = format.SampleRate
|
|
t.PlaybackDone = playbackDoneChannel
|
|
t.MediaFile = mf
|
|
|
|
err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))
|
|
if err != nil {
|
|
log.Error(err)
|
|
}
|
|
log.Debug("speaker.Init() finished")
|
|
|
|
go func() {
|
|
speaker.Play(beep.Seq(t.Volume, beep.Callback(func() {
|
|
log.Info("Hitting end-of-stream, signalling on channel")
|
|
t.PlaybackDone <- true
|
|
log.Debug("Signalling finished")
|
|
})))
|
|
log.Debug("dropping out of speaker.Play()")
|
|
}()
|
|
return &t, nil
|
|
}
|
|
|
|
func (t *BeepTrack) String() string {
|
|
return fmt.Sprintf("Name: %s", t.MediaFile.Path)
|
|
}
|
|
|
|
func (t *BeepTrack) SetVolume(value float64) {
|
|
speaker.Lock()
|
|
t.Volume.Volume += value
|
|
speaker.Unlock()
|
|
}
|
|
|
|
func (t *BeepTrack) Unpause() {
|
|
speaker.Lock()
|
|
if t.Ctrl.Paused {
|
|
t.Ctrl.Paused = false
|
|
} else {
|
|
log.Debug("tried to unpause while not paused")
|
|
}
|
|
speaker.Unlock()
|
|
}
|
|
|
|
func (t *BeepTrack) Pause() {
|
|
speaker.Lock()
|
|
if t.Ctrl.Paused {
|
|
log.Debug("tried to pause while already paused")
|
|
} else {
|
|
t.Ctrl.Paused = true
|
|
}
|
|
speaker.Unlock()
|
|
}
|
|
|
|
func (t *BeepTrack) Close() {
|
|
if t.ActiveStream != nil {
|
|
log.Debug("closing activ stream")
|
|
t.ActiveStream.Close()
|
|
t.ActiveStream = nil
|
|
}
|
|
|
|
speaker.Close()
|
|
|
|
if t.TempfileToCleanup != "" {
|
|
log.Debug("Removing tempfile", "tmpfilename", t.TempfileToCleanup)
|
|
err := os.Remove(t.TempfileToCleanup)
|
|
if err != nil {
|
|
log.Error("error cleaning up tempfile: ", t.TempfileToCleanup)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Position returns the playback position in seconds
|
|
func (t *BeepTrack) Position() int {
|
|
if t.Ctrl.Streamer == nil {
|
|
log.Debug("streamer is not setup (nil), could not get position")
|
|
return 0
|
|
}
|
|
|
|
streamer, ok := t.Ctrl.Streamer.(beep.StreamSeeker)
|
|
if ok {
|
|
position := t.SampleRate.D(streamer.Position())
|
|
posSecs := position.Round(time.Second).Seconds()
|
|
return int(posSecs)
|
|
} else {
|
|
log.Debug("streamer is no beep.StreamSeeker, could not get position")
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// offset = pd.PlaybackQueue.Offset
|
|
func (t *BeepTrack) SetPosition(offset int) error {
|
|
streamer, ok := t.Ctrl.Streamer.(beep.StreamSeeker)
|
|
if ok {
|
|
sampleRatePerSecond := t.SampleRate.N(time.Second)
|
|
nextPosition := sampleRatePerSecond * offset
|
|
log.Debug("SetPosition", "samplerate", sampleRatePerSecond, "nextPosition", nextPosition)
|
|
return streamer.Seek(nextPosition)
|
|
}
|
|
return fmt.Errorf("streamer is not seekable")
|
|
}
|
|
|
|
func (t *BeepTrack) IsPlaying() bool {
|
|
return t.Ctrl != nil && !t.Ctrl.Paused
|
|
}
|