Skip to content

Commit

Permalink
feat: support setting advanced ALSA configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
devgianlu committed Dec 17, 2024
1 parent 9f8ee67 commit 952606d
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 20 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 6 additions & 2 deletions cmd/daemon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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"`
Expand Down
10 changes: 10 additions & 0 deletions config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
28 changes: 21 additions & 7 deletions output/driver-alsa.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import (
)

const (
BufferTimeMicro = 500_000
NumPeriods = 4 // number of periods requested
defaultBufferTimeMicro = 500_000
defaultNumPeriods = 4
)

type alsaOutput struct {
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down
10 changes: 10 additions & 0 deletions output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 21 additions & 11 deletions player/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
})
},

Expand Down

0 comments on commit 952606d

Please sign in to comment.