Skip to content

Commit

Permalink
feat: redo output driver and audio sources for gapless playback
Browse files Browse the repository at this point in the history
  • Loading branch information
devgianlu committed May 26, 2024
1 parent 822355e commit 02c394f
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 92 deletions.
4 changes: 3 additions & 1 deletion cmd/daemon/controls.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ func (p *AppPlayer) prefetchNext() {
return
}

p.player.SetSecondaryStream(p.secondaryStream.Source)

log.WithField("uri", nextId.Uri()).
Infof("prefetched %s %s (duration: %dms)", nextId.Type(),
strconv.QuoteToGraphic(p.secondaryStream.Media.Name()), p.secondaryStream.Media.Duration())
Expand Down Expand Up @@ -219,7 +221,7 @@ func (p *AppPlayer) loadCurrentTrack(paused bool) error {
}
}

if err := p.player.SetStream(p.primaryStream.Source, paused); err != nil {
if err := p.player.SetPrimaryStream(p.primaryStream.Source, paused); err != nil {
return fmt.Errorf("failed setting stream for %s: %w", spotId, err)
}

Expand Down
10 changes: 10 additions & 0 deletions output.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
package go_librespot

import "errors"

var ErrDrainReader = errors.New("please drain output")

type Float32Reader interface {
// Read reads 32bit little endian floats from the stream
// until EOF or ErrDrainReader is returned.
Read([]float32) (n int, err error)

// Drained must be called when Read returns ErrDrainReader and
// the reader the has finished draining the output.
Drained()
}

type AudioSource interface {
Expand Down
46 changes: 14 additions & 32 deletions output/driver_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ type output struct {
sampleRate int
device string
reader librespot.Float32Reader
done chan error

cond *sync.Cond

Expand All @@ -36,38 +35,29 @@ type output struct {
volume float32
paused bool
closed bool
eof bool
released bool

err chan error
}

func newOutput(reader librespot.Float32Reader, sampleRate int, channels int, device string, initiallyPaused bool, initialVolume float32) (*output, error) {
func newOutput(reader librespot.Float32Reader, sampleRate int, channels int, device string, initialVolume float32) (*output, error) {
out := &output{
reader: reader,
channels: channels,
sampleRate: sampleRate,
device: device,
volume: initialVolume,
err: make(chan error, 1),
cond: sync.NewCond(&sync.Mutex{}),
done: make(chan error, 1),
}

if err := out.openAndSetup(); err != nil {
return nil, err
}

if initiallyPaused {
_ = out.Pause()
}

go func() {
err := out.loop()
out.err <- out.loop()
_ = out.Close()

if err != nil {
out.done <- err
} else {
out.done <- nil
}
}()

return out, nil
Expand Down Expand Up @@ -139,15 +129,18 @@ func (out *output) loop() error {

for {
n, err := out.reader.Read(floats)
if errors.Is(err, io.EOF) {
out.eof = true

if errors.Is(err, io.EOF) || errors.Is(err, librespot.ErrDrainReader) {
// drain pcm ignoring errors
out.cond.L.Lock()
C.snd_pcm_drain(out.handle)
out.cond.L.Unlock()

return nil
if errors.Is(err, io.EOF) {
return nil
}

out.reader.Drained()
continue
} else if err != nil {
return fmt.Errorf("failed reading source: %w", err)
}
Expand Down Expand Up @@ -291,22 +284,11 @@ func (out *output) SetVolume(vol float32) {
out.volume = vol
}

func (out *output) WaitDone() <-chan error {
out.cond.L.Lock()
defer out.cond.L.Unlock()

if out.closed {
return nil
}

return out.done
}

func (out *output) IsEOF() bool {
func (out *output) Error() <-chan error {
out.cond.L.Lock()
defer out.cond.L.Unlock()

return out.eof
return out.err
}

func (out *output) Close() error {
Expand Down
16 changes: 4 additions & 12 deletions output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,12 @@ type NewOutputOptions struct {
// This feature is support only for the unix driver.
Device string

// InitiallyPaused specifies whether the output device should be paused from the start.
InitiallyPaused bool

// InitialVolume specifies the initial output volume.
InitialVolume float32
}

func NewOutput(options *NewOutputOptions) (*Output, error) {
out, err := newOutput(options.Reader, options.SampleRate, options.ChannelCount, options.Device, options.InitiallyPaused, options.InitialVolume)
out, err := newOutput(options.Reader, options.SampleRate, options.ChannelCount, options.Device, options.InitialVolume)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -75,14 +72,9 @@ func (c *Output) SetVolume(vol float32) {
c.output.SetVolume(vol)
}

// WaitDone waits for the playback loop to exit.
func (c *Output) WaitDone() <-chan error {
return c.output.WaitDone()
}

// IsEOF returns whether the reader reached EOF.
func (c *Output) IsEOF() bool {
return c.output.IsEOF()
// Error returns the error that stopped the device (if any).
func (c *Output) Error() <-chan error {
return c.output.Error()
}

// Close closes the output.
Expand Down
116 changes: 69 additions & 47 deletions player/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"go-librespot/proto/spotify/metadata"
"go-librespot/spclient"
"go-librespot/vorbis"
"io"
"time"
)

Expand All @@ -27,7 +26,7 @@ type Player struct {
sp *spclient.Spclient
audioKey *audio.KeyProvider

newOutput func(source librespot.Float32Reader, paused bool, volume float32) (*output.Output, error)
newOutput func(source librespot.Float32Reader, volume float32) (*output.Output, error)

cmd chan playerCmd
ev chan Event
Expand Down Expand Up @@ -58,8 +57,9 @@ type playerCmd struct {
}

type playerCmdDataSet struct {
source librespot.AudioSource
paused bool
source librespot.AudioSource
primary bool
paused bool
}

func NewPlayer(sp *spclient.Spclient, audioKey *audio.KeyProvider, normalisationEnabled bool, normalisationPregain float32, countryCode *string, device string, volumeSteps uint32, externalVolume bool) (*Player, error) {
Expand All @@ -69,14 +69,13 @@ func NewPlayer(sp *spclient.Spclient, audioKey *audio.KeyProvider, normalisation
normalisationEnabled: normalisationEnabled,
normalisationPregain: normalisationPregain,
countryCode: countryCode,
newOutput: func(reader librespot.Float32Reader, paused bool, volume float32) (*output.Output, error) {
newOutput: func(reader librespot.Float32Reader, volume float32) (*output.Output, error) {
return output.NewOutput(&output.NewOutputOptions{
Reader: reader,
SampleRate: SampleRate,
ChannelCount: Channels,
Device: device,
InitiallyPaused: paused,
InitialVolume: volume,
Reader: reader,
SampleRate: SampleRate,
ChannelCount: Channels,
Device: device,
InitialVolume: volume,
})
},
externalVolume: externalVolume,
Expand All @@ -91,43 +90,57 @@ func NewPlayer(sp *spclient.Spclient, audioKey *audio.KeyProvider, normalisation
}

func (p *Player) manageLoop() {
var volume float32
// currently available output device
var out *output.Output
var source librespot.AudioSource
var done <-chan error
outErr := make(<-chan error)

// initial volume is 1
volume = 1
volume := float32(1)

// init main source
source := NewSwitchingAudioSource()

loop:
for {
select {
case cmd := <-p.cmd:
switch cmd.typ {
case playerCmdSet:
if out != nil {
_ = out.Close()
data := cmd.data.(playerCmdDataSet)
if !data.primary {
source.SetSecondary(data.source)
cmd.resp <- nil
break
}

data := cmd.data.(playerCmdDataSet)
source = data.source
// create a new output device if needed
if out == nil {
var err error
out, err = p.newOutput(source, volume)
if err != nil {
cmd.resp <- err
break
}

outErr = out.Error()
log.Debugf("created new output device")
}

var err error
out, err = p.newOutput(source, data.paused, volume)
if err != nil {
source = nil
cmd.resp <- err
// set source
source.SetPrimary(data.source)
if data.paused {
_ = out.Pause()
} else {
done = out.WaitDone()
p.startedPlaying = time.Now()
_ = out.Resume()
}

cmd.resp <- nil
p.startedPlaying = time.Now()
cmd.resp <- nil

if data.paused {
p.ev <- Event{Type: EventTypePaused}
} else {
p.ev <- Event{Type: EventTypePlaying}
}
if data.paused {
p.ev <- Event{Type: EventTypePaused}
} else {
p.ev <- Event{Type: EventTypePlaying}
}
case playerCmdPlay:
if out != nil {
Expand All @@ -150,15 +163,17 @@ loop:
case playerCmdStop:
if out != nil {
_ = out.Close()
out = nil
outErr = make(<-chan error)
}

cmd.resp <- struct{}{}
p.ev <- Event{Type: EventTypeStopped}
case playerCmdSeek:
if source != nil && out != nil {
if out != nil {
if err := source.SetPositionMs(cmd.data.(int64)); err != nil {
cmd.resp <- err
} else if err := out.Drop(); err != nil {
} else if err = out.Drop(); err != nil {
cmd.resp <- err
} else {
cmd.resp <- nil
Expand All @@ -167,7 +182,7 @@ loop:
cmd.resp <- nil
}
case playerCmdPosition:
if source != nil && out != nil {
if out != nil {
delay, err := out.DelayMs()
if err != nil {
log.WithError(err).Warnf("failed getting output device delay")
Expand All @@ -190,27 +205,28 @@ loop:
default:
panic("unknown player command")
}
case err := <-done:
case err := <-outErr:
if err != nil {
log.WithError(err).Errorf("playback failed")
}
log.WithError(err).Errorf("output device failed")

done = nil
if err != nil || out.IsEOF() {
p.ev <- Event{Type: EventTypeNotPlaying}
_ = out.Close()
out = nil
outErr = make(<-chan error)
}

p.ev <- Event{Type: EventTypeStopped}
case <-source.Done():
p.ev <- Event{Type: EventTypeNotPlaying}
}
}

close(p.cmd)

// teardown
if s, ok := source.(io.Closer); ok && s != nil {
_ = s.Close()
}
_ = source.Close()

if out != nil {
_ = out.Close()
out = nil
}
}

Expand Down Expand Up @@ -270,16 +286,22 @@ func (p *Player) PositionMs() int64 {
return pos.(int64)
}

func (p *Player) SetStream(source librespot.AudioSource, paused bool) error {
func (p *Player) SetPrimaryStream(source librespot.AudioSource, paused bool) error {
resp := make(chan any)
p.cmd <- playerCmd{typ: playerCmdSet, data: playerCmdDataSet{source, paused}, resp: resp}
p.cmd <- playerCmd{typ: playerCmdSet, data: playerCmdDataSet{source: source, primary: true, paused: paused}, resp: resp}
if err := <-resp; err != nil {
return err.(error)
}

return nil
}

func (p *Player) SetSecondaryStream(source librespot.AudioSource) {
resp := make(chan any)
p.cmd <- playerCmd{typ: playerCmdSet, data: playerCmdDataSet{source: source, primary: false}, resp: resp}
<-resp
}

const DisableCheckMediaRestricted = true

func (p *Player) NewStream(spotId librespot.SpotifyId, bitrate int, mediaPosition int64) (*Stream, error) {
Expand Down
Loading

0 comments on commit 02c394f

Please sign in to comment.