Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ audio_output_pipe: '' # Path to a named pipe for audio output
audio_output_pipe_format: s16le # Audio output pipe format (s16le, s32le, f32le)
bitrate: 160 # Playback bitrate (96, 160, 320)
crossfade_duration: 0 # Crossfade duration between tracks in milliseconds (0 to disable)
skip_debounce_ms: 600 # Coalesce rapid next/prev presses; the selected track loads once presses stop (0 to disable)
volume_steps: 100 # Volume steps count
initial_volume: 100 # Initial volume in steps (not applied to the mixer device)
ignore_last_volume: false # Whether to ignore the last saved volume and always use initial_volume
Expand Down
10 changes: 10 additions & 0 deletions api-spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,16 @@ components:
allOf:
- $ref: '#/components/schemas/track'
nullable: true
pending_track_uri:
description: >-
The track selected by a not-yet-settled skip: rapid next/prev
presses move the selection immediately but defer loading it until
the presses stop (skip_debounce_ms), so while browsing this holds
the target track URI and the track object still describes the last
loaded one. Absent when no skip is pending.
type: string
nullable: true
x-omitempty: true
root:
x-go-name: ApiRoot
description: API reachability and playback readiness
Expand Down
5 changes: 5 additions & 0 deletions cmd/daemon/cli_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"path/filepath"
"strconv"
"strings"
"time"

"github.com/devgianlu/go-librespot/daemon"
"github.com/gofrs/flock"
Expand Down Expand Up @@ -53,6 +54,7 @@ type cliConfig struct {
NormalisationUseAlbumGain bool `koanf:"normalisation_use_album_gain"`
NormalisationPregain float32 `koanf:"normalisation_pregain"`
CrossfadeDuration int `koanf:"crossfade_duration"`
SkipDebounceMs int `koanf:"skip_debounce_ms"`
ExternalVolume bool `koanf:"external_volume"`
ZeroconfEnabled bool `koanf:"zeroconf_enabled"`
ZeroconfPort int `koanf:"zeroconf_port"`
Expand Down Expand Up @@ -119,6 +121,7 @@ func (c *cliConfig) toDaemonConfig() *daemon.Config {
NormalisationUseAlbumGain: c.NormalisationUseAlbumGain,
NormalisationPregain: c.NormalisationPregain,
CrossfadeDuration: c.CrossfadeDuration,
SkipDebounce: time.Duration(c.SkipDebounceMs) * time.Millisecond,
ExternalVolume: c.ExternalVolume,
DisableAutoplay: c.DisableAutoplay,

Expand Down Expand Up @@ -199,6 +202,8 @@ func loadCLIConfig(cfg *cliConfig) error {
"volume_steps": 100,
"initial_volume": 100,

"skip_debounce_ms": 600,

"credentials.type": "zeroconf",

"cache.enabled": false,
Expand Down
9 changes: 9 additions & 0 deletions cmd/daemon/cli_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ import (
"github.com/stretchr/testify/require"
)

func TestSkipDebounceMapping(t *testing.T) {
var c cliConfig
c.SkipDebounceMs = 400
require.Equal(t, int64(400_000_000), int64(c.toDaemonConfig().SkipDebounce))

c.SkipDebounceMs = 0
require.Zero(t, c.toDaemonConfig().SkipDebounce)
}

func TestParseSize(t *testing.T) {
cases := []struct {
in string
Expand Down
6 changes: 6 additions & 0 deletions config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,12 @@
"default": 0,
"minimum": 0
},
"skip_debounce_ms": {
"type": "integer",
"description": "How long to wait after a burst of next/prev commands before loading the selected track, in milliseconds, so rapid skipping costs one audio-key request instead of one per press. The first skip of a burst always loads immediately, so this only affects how long the daemon waits to see whether another skip follows. 0 disables debouncing",
"default": 600,
"minimum": 0
},
"external_volume": {
"type": "boolean",
"description": "Whether volume is controlled externally, if true the app will not pre-multiply samples",
Expand Down
3 changes: 3 additions & 0 deletions daemon/api_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions daemon/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,9 @@ func (app *App) newAppPlayer(ctx context.Context, creds any) (_ *AppPlayer, err
appPlayer.prefetchTimer = time.NewTimer(math.MaxInt64)
appPlayer.prefetchTimer.Stop()

appPlayer.settleTimer = time.NewTimer(math.MaxInt64)
appPlayer.settleTimer.Stop()

if appPlayer.sess, err = session.NewSessionFromOptions(ctx, &session.Options{
Log: app.log,
DeviceType: app.deviceType,
Expand Down
8 changes: 8 additions & 0 deletions daemon/config.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package daemon

import "time"

// Config carries the runtime configuration for a daemon instance.
type Config struct {
DeviceId string
Expand Down Expand Up @@ -28,6 +30,12 @@ type Config struct {
ExternalVolume bool
DisableAutoplay bool

// SkipDebounce is how long to wait after a burst of next/prev commands
// before actually loading the track the pointer landed on, so mashing the
// skip button costs one audio-key request instead of one per press. Zero
// disables debouncing (every skip loads immediately).
SkipDebounce time.Duration

ZeroconfEnabled bool
ZeroconfPort int
ZeroconfBackend string
Expand Down
Loading
Loading