diff --git a/README.md b/README.md index 2cc02b7..b24c58b 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,8 @@ device_type: computer # Spotify device type (icon) audio_device: default # ALSA audio device to use for playback mixer_device: '' # ALSA mixer device for volume synchronization mixer_control_name: Master # ALSA mixer control name for volume synchronization +audio_buffer_time: 500000 # Audio buffer time in microseconds, ALSA only +audio_period_count: 4 # Number of periods to request, ALSA only bitrate: 160 # Playback bitrate (96, 160, 320) volume_steps: 100 # Volume steps count initial_volume: 100 # Initial volume in steps (not applied to the mixer device) diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 6631bad..844a1f2 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -133,8 +133,10 @@ func (app *App) newAppPlayer(ctx context.Context, creds any) (_ *AppPlayer, err if appPlayer.player, err = player.NewPlayer( appPlayer.sess.Spclient(), appPlayer.sess.AudioKey(), !app.cfg.NormalisationDisabled, app.cfg.NormalisationPregain, - appPlayer.countryCode, app.cfg.AudioBackend, app.cfg.AudioDevice, app.cfg.MixerDevice, app.cfg.MixerControlName, - app.cfg.VolumeSteps, app.cfg.ExternalVolume, appPlayer.volumeUpdate, + appPlayer.countryCode, + app.cfg.AudioBackend, app.cfg.AudioDevice, app.cfg.MixerDevice, app.cfg.MixerControlName, + app.cfg.AudioBufferTime, app.cfg.AudioPeriodCount, + app.cfg.ExternalVolume, appPlayer.volumeUpdate, ); err != nil { return nil, fmt.Errorf("failed initializing player: %w", err) } @@ -348,6 +350,8 @@ type Config struct { AudioDevice string `koanf:"audio_device"` MixerDevice string `koanf:"mixer_device"` MixerControlName string `koanf:"mixer_control_name"` + AudioBufferTime int `koanf:"audio_buffer_time"` + AudioPeriodCount int `koanf:"audio_period_count"` Bitrate int `koanf:"bitrate"` VolumeSteps uint32 `koanf:"volume_steps"` InitialVolume uint32 `koanf:"initial_volume"` diff --git a/config_schema.json b/config_schema.json index 3756376..3b9f6f5 100644 --- a/config_schema.json +++ b/config_schema.json @@ -84,6 +84,16 @@ "description": "Which control should be used, leave empty for default. Only useful in combination with 'mixer_device'", "default": "Master" }, + "audio_buffer_time": { + "type": "integer", + "description": "The audio buffer time in microseconds. Available for ALSA backend only, leave empty for default", + "default": 0 + }, + "audio_period_count": { + "type": "integer", + "description": "The number of audio periods to request. Available for ALSA backend only, leave empty for default", + "default": 0 + }, "server": { "type": "object", "properties": { diff --git a/output/driver-alsa.go b/output/driver-alsa.go index a6f5bd2..7961f91 100644 --- a/output/driver-alsa.go +++ b/output/driver-alsa.go @@ -20,8 +20,8 @@ import ( ) const ( - BufferTimeMicro = 500_000 - NumPeriods = 4 // number of periods requested + defaultBufferTimeMicro = 500_000 + defaultNumPeriods = 4 ) type alsaOutput struct { @@ -32,9 +32,11 @@ type alsaOutput struct { lock sync.Mutex - pcmHandle *C.snd_pcm_t // nil when pcmHandle is closed - periodSize int - bufferSize int + pcmHandle *C.snd_pcm_t // nil when pcmHandle is closed + bufferTime int + periodCount int + periodSize int + bufferSize int externalVolume bool @@ -68,6 +70,18 @@ func newAlsaOutput(opts *NewOutputOptions) (*alsaOutput, error) { volumeUpdate: opts.VolumeUpdate, } + if opts.BufferTimeMicro == 0 { + out.bufferTime = defaultBufferTimeMicro + } else { + out.bufferTime = opts.BufferTimeMicro + } + + if opts.PeriodCount == 0 { + out.periodCount = defaultNumPeriods + } else { + out.periodCount = opts.PeriodCount + } + if err := out.setupMixer(); err != nil { if uintptr(unsafe.Pointer(out.mixerHandle)) != 0 { C.snd_mixer_close(out.mixerHandle) @@ -120,7 +134,7 @@ func (out *alsaOutput) setupPcm() error { return out.alsaError("snd_pcm_hw_params_set_rate_near", err) } - bufferTime := C.uint(BufferTimeMicro) + bufferTime := C.uint(out.bufferTime) if err := C.snd_pcm_hw_params_set_buffer_time_near(out.pcmHandle, hwparams, &bufferTime, nil); err < 0 { return out.alsaError("snd_pcm_hw_params_set_buffer_time_near", err) } @@ -132,7 +146,7 @@ func (out *alsaOutput) setupPcm() error { if err := C.snd_pcm_hw_params_get_buffer_size(hwparams, &bufferSize); err < 0 { return out.alsaError("snd_pcm_hw_params_get_buffer_size", err) } - var periodSize C.snd_pcm_uframes_t = C.snd_pcm_uframes_t(bufferSize) / NumPeriods + var periodSize C.snd_pcm_uframes_t = C.snd_pcm_uframes_t(bufferSize) / C.snd_pcm_uframes_t(out.periodCount) if err := C.snd_pcm_hw_params_set_period_size_near(out.pcmHandle, hwparams, &periodSize, nil); err < 0 { return out.alsaError("snd_pcm_hw_params_set_period_size_near", err) } diff --git a/output/output.go b/output/output.go index e8da553..dddf17c 100644 --- a/output/output.go +++ b/output/output.go @@ -67,6 +67,16 @@ type NewOutputOptions struct { // This only works in combination with Mixer Control string + // BufferTimeMicro is the buffer time in microseconds. + // + // This is only supported on the alsa backend. + BufferTimeMicro int + + // PeriodCount is the number of periods to request. + // + // This is only supported on the alsa backend. + PeriodCount int + // InitialVolume specifies the initial output volume. // // This is only supported on the alsa backend. The PulseAudio backend uses diff --git a/player/player.go b/player/player.go index 4f63524..4e6c0db 100644 --- a/player/player.go +++ b/player/player.go @@ -65,7 +65,15 @@ type playerCmdDataSet struct { drop bool } -func NewPlayer(sp *spclient.Spclient, audioKey *audio.KeyProvider, normalisationEnabled bool, normalisationPregain float32, countryCode *string, backend, device, mixer string, control string, volumeSteps uint32, externalVolume bool, volumeUpdate chan float32) (*Player, error) { +// FIXME: we should probably use a struct here +func NewPlayer( + sp *spclient.Spclient, audioKey *audio.KeyProvider, + normalisationEnabled bool, normalisationPregain float32, + countryCode *string, + backend, device, mixer, control string, + bufferTime, periodCount int, + externalVolume bool, volumeUpdate chan float32, +) (*Player, error) { p := &Player{ sp: sp, audioKey: audioKey, @@ -74,16 +82,18 @@ func NewPlayer(sp *spclient.Spclient, audioKey *audio.KeyProvider, normalisation countryCode: countryCode, newOutput: func(reader librespot.Float32Reader, volume float32) (output.Output, error) { return output.NewOutput(&output.NewOutputOptions{ - Backend: backend, - Reader: reader, - SampleRate: SampleRate, - ChannelCount: Channels, - Device: device, - Mixer: mixer, - Control: control, - InitialVolume: volume, - ExternalVolume: externalVolume, - VolumeUpdate: volumeUpdate, + Backend: backend, + Reader: reader, + SampleRate: SampleRate, + ChannelCount: Channels, + Device: device, + Mixer: mixer, + Control: control, + InitialVolume: volume, + BufferTimeMicro: bufferTime, + PeriodCount: periodCount, + ExternalVolume: externalVolume, + VolumeUpdate: volumeUpdate, }) },