Skip to content

Commit ea02188

Browse files
authored
Merge branch 'devgianlu:master' into metadata-pipe
2 parents d44c298 + 5c8f359 commit ea02188

7 files changed

Lines changed: 166 additions & 8 deletions

File tree

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ on the [GitHub Container Registry](https://github.com/devgianlu/go-librespot/pkg
2626

2727
An example Docker Compose configuration for PulseAudio is available [here](/docker-compose.pulse.yml).
2828

29+
### Using Brew
30+
31+
You can also install go-librespot [using Brew](https://formulae.brew.sh/formula/go-librespot)
32+
on macOS and Linux (thanks @kriive):
33+
34+
```shell
35+
brew install go-librespot
36+
```
37+
2938
### Building from source
3039

3140
To build from source the following prerequisites are necessary:
@@ -51,7 +60,8 @@ Details about cross-compiling go-librespot are described [here](/CROSS_COMPILE.m
5160

5261
## Configuration
5362

54-
The default directory for configuration files is `~/.config/go-librespot`. On macOS devices, this is `~/Library/Application Support/go-librespot`. You can change this directory with the
63+
The default directory for configuration files is `~/.config/go-librespot`. On macOS devices, this is
64+
`~/Library/Application Support/go-librespot`. You can change this directory with the
5565
`-config_dir` flag. The configuration directory contains:
5666

5767
- `config.yml`: The main configuration (does not exist by default)
@@ -79,7 +89,9 @@ credentials:
7989
If `persist_credentials` is `true`, after connecting to the device for the first time credentials will be stored locally
8090
and you can switch to interactive mode without having to authenticate manually.
8191

82-
If `zeroconf_interfaces_to_advertise` is provided, you can limit interfaces that will be advertised. For example, if you have Docker installed on your host, you may want to disable advertising to its bridge interface, or you may want to disable interfaces that will not be reachable.
92+
If `zeroconf_interfaces_to_advertise` is provided, you can limit interfaces that will be advertised. For example, if you
93+
have Docker installed on your host, you may want to disable advertising to its bridge interface, or you may want to
94+
disable interfaces that will not be reachable.
8395

8496
### Interactive mode
8597

@@ -155,6 +167,7 @@ audio_output_pipe_format: s16le # Audio output pipe format (s16le, s32le, f32le)
155167
bitrate: 160 # Playback bitrate (96, 160, 320)
156168
volume_steps: 100 # Volume steps count
157169
initial_volume: 100 # Initial volume in steps (not applied to the mixer device)
170+
ignore_last_volume: false # Whether to ignore the last saved volume and always use initial_volume
158171
external_volume: false # Whether volume is controlled externally
159172
disable_autoplay: false # Whether autoplay of more songs should be disabled
160173
```

cmd/daemon/controls.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,12 @@ func (p *AppPlayer) updateVolume(newVal uint32) {
769769
p.app.log.Debugf("update volume requested to %d/%d", newVal, player.MaxStateVolume)
770770
p.player.SetVolume(newVal)
771771

772+
// Save the volume to the state
773+
p.app.state.LastVolume = &newVal
774+
if err := p.app.state.Write(); err != nil {
775+
p.app.log.WithError(err).Error("failed writing state after volume change")
776+
}
777+
772778
// If there is a value in the channel buffer, remove it.
773779
select {
774780
case <-p.volumeUpdate:

cmd/daemon/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ type Config struct {
396396
Bitrate int `koanf:"bitrate"`
397397
VolumeSteps uint32 `koanf:"volume_steps"`
398398
InitialVolume uint32 `koanf:"initial_volume"`
399+
IgnoreLastVolume bool `koanf:"ignore_last_volume"`
399400
NormalisationDisabled bool `koanf:"normalisation_disabled"`
400401
NormalisationUseAlbumGain bool `koanf:"normalisation_use_album_gain"`
401402
NormalisationPregain float32 `koanf:"normalisation_pregain"`

cmd/daemon/player.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,11 @@ func (p *AppPlayer) handleDealerMessage(ctx context.Context, msg dealer.Message)
8686
if !p.app.cfg.ExternalVolume && len(p.app.cfg.MixerDevice) == 0 {
8787
// update initial volume
8888
p.initialVolumeOnce.Do(func() {
89-
p.updateVolume(p.app.cfg.InitialVolume * player.MaxStateVolume / p.app.cfg.VolumeSteps)
89+
if lastVolume := p.app.state.LastVolume; !p.app.cfg.IgnoreLastVolume && lastVolume != nil {
90+
p.updateVolume(*lastVolume)
91+
} else {
92+
p.updateVolume(p.app.cfg.InitialVolume * player.MaxStateVolume / p.app.cfg.VolumeSteps)
93+
}
9094
})
9195
}
9296
} else if strings.HasPrefix(msg.Uri, "hm://connect-state/v1/connect/volume") {

config_schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,11 @@
232232
"min": 0,
233233
"default": 100
234234
},
235+
"ignore_last_volume": {
236+
"type": "boolean",
237+
"description": "Whether the last saved volume should be ignored",
238+
"default": false
239+
},
235240
"normalisation_disabled": {
236241
"type": "boolean",
237242
"description": "Whether track/album normalisation should be disabled",

output/driver-alsa.go

Lines changed: 133 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type alsaOutput struct {
3838
periodCount int
3939
periodSize int
4040
bufferSize int
41+
pcmFormat C.snd_pcm_format_t
4142

4243
externalVolume bool
4344

@@ -100,6 +101,15 @@ func (out *alsaOutput) alsaError(name string, err C.int) error {
100101
return fmt.Errorf("ALSA error at %s: %s", name, C.GoString(C.snd_strerror(err)))
101102
}
102103

104+
func (out *alsaOutput) isFormatSupported(hwparams *C.snd_pcm_hw_params_t, format C.snd_pcm_format_t) bool {
105+
errCode := C.snd_pcm_hw_params_test_format(out.pcmHandle, hwparams, format)
106+
if errCode < 0 {
107+
return false
108+
}
109+
110+
return true
111+
}
112+
103113
func (out *alsaOutput) setupPcm() error {
104114
cdevice := C.CString(out.device)
105115
defer C.free(unsafe.Pointer(cdevice))
@@ -119,8 +129,24 @@ func (out *alsaOutput) setupPcm() error {
119129
return out.alsaError("snd_pcm_hw_params_set_access", err)
120130
}
121131

122-
if err := C.snd_pcm_hw_params_set_format(out.pcmHandle, hwparams, C.SND_PCM_FORMAT_FLOAT_LE); err < 0 {
123-
return out.alsaError("snd_pcm_hw_params_set_format", err)
132+
var formatFound bool
133+
for _, format := range []C.snd_pcm_format_t{
134+
C.SND_PCM_FORMAT_FLOAT_LE, // 32-bit floating point, little-endian
135+
C.SND_PCM_FORMAT_S32_LE, // 32-bit signed integer, little-endian
136+
C.SND_PCM_FORMAT_S16_LE, // 16-bit signed integer, little-endian
137+
} {
138+
if !out.isFormatSupported(hwparams, format) {
139+
continue
140+
}
141+
142+
if err := C.snd_pcm_hw_params_set_format(out.pcmHandle, hwparams, format); err < 0 {
143+
return out.alsaError("snd_pcm_hw_params_set_format", err)
144+
}
145+
formatFound = true
146+
}
147+
148+
if !formatFound {
149+
return fmt.Errorf("could not find a supported PCM format")
124150
}
125151

126152
if err := C.snd_pcm_hw_params_set_channels(out.pcmHandle, hwparams, C.unsigned(out.channels)); err < 0 {
@@ -173,6 +199,13 @@ func (out *alsaOutput) setupPcm() error {
173199
out.bufferSize = int(frames)
174200
}
175201

202+
var pcmFormat C.snd_pcm_format_t
203+
if err := C.snd_pcm_hw_params_get_format(hwparams, &pcmFormat); err < 0 {
204+
return out.alsaError("snd_pcm_hw_params_get_format", err)
205+
} else {
206+
out.pcmFormat = pcmFormat
207+
}
208+
176209
var swparams *C.snd_pcm_sw_params_t
177210
C.snd_pcm_sw_params_malloc(&swparams)
178211
defer C.free(unsafe.Pointer(swparams))
@@ -202,6 +235,45 @@ func (out *alsaOutput) setupPcm() error {
202235
return nil
203236
}
204237

238+
func pcmFormatName(format C.snd_pcm_format_t) string {
239+
switch format {
240+
case C.SND_PCM_FORMAT_S8:
241+
return "S8"
242+
case C.SND_PCM_FORMAT_U8:
243+
return "U8"
244+
case C.SND_PCM_FORMAT_S16_LE:
245+
return "S16_LE"
246+
case C.SND_PCM_FORMAT_S16_BE:
247+
return "S16_BE"
248+
case C.SND_PCM_FORMAT_U16_LE:
249+
return "U16_LE"
250+
case C.SND_PCM_FORMAT_U16_BE:
251+
return "U16_BE"
252+
case C.SND_PCM_FORMAT_S24_LE:
253+
return "S24_LE"
254+
case C.SND_PCM_FORMAT_S24_BE:
255+
return "S24_BE"
256+
case C.SND_PCM_FORMAT_U24_LE:
257+
return "U24_LE"
258+
case C.SND_PCM_FORMAT_U24_BE:
259+
return "U24_BE"
260+
case C.SND_PCM_FORMAT_S32_LE:
261+
return "S32_LE"
262+
case C.SND_PCM_FORMAT_S32_BE:
263+
return "S32_BE"
264+
case C.SND_PCM_FORMAT_U32_LE:
265+
return "U32_LE"
266+
case C.SND_PCM_FORMAT_U32_BE:
267+
return "U32_BE"
268+
case C.SND_PCM_FORMAT_FLOAT_LE:
269+
return "FLOAT_LE"
270+
case C.SND_PCM_FORMAT_FLOAT_BE:
271+
return "FLOAT_BE"
272+
default:
273+
return "unknown"
274+
}
275+
}
276+
205277
func (out *alsaOutput) logParams(params *C.snd_pcm_hw_params_t) error {
206278
var dir C.int
207279

@@ -235,12 +307,51 @@ func (out *alsaOutput) logParams(params *C.snd_pcm_hw_params_t) error {
235307
return out.alsaError("snd_pcm_hw_params_get_periods", err)
236308
}
237309

238-
out.log.Debugf("alsa driver configured, rate = %d bps, period time = %d us, period size = %d frames, buffer time = %d us, buffer size = %d frames, periods per buffer = %d frames",
239-
rate, periodTime, frames, bufferTime, bufferSize, periods)
310+
var format C.snd_pcm_format_t
311+
if err := C.snd_pcm_hw_params_get_format(params, &format); err < 0 {
312+
return out.alsaError("snd_pcm_hw_params_get_format", err)
313+
}
314+
315+
out.log.Debugf("alsa driver configured, rate = %d bps, period time = %d us, period size = %d frames, buffer time = %d us, buffer size = %d frames, periods per buffer = %d frames, PCM format = %s",
316+
rate, periodTime, frames, bufferTime, bufferSize, periods, pcmFormatName(format))
240317

241318
return nil
242319
}
243320

321+
func floatLeToS16Le(floats []float32) []C.int16_t {
322+
shorts := make([]C.int16_t, len(floats))
323+
for i, f := range floats {
324+
// Clip the float to the range [-1.0, 1.0]
325+
if f < -1.0 {
326+
f = -1.0
327+
}
328+
if f > 1.0 {
329+
f = 1.0
330+
}
331+
332+
// Convert to int16 (short) in the range [-32768, 32767]
333+
shorts[i] = C.int16_t(f * 32767)
334+
}
335+
return shorts
336+
}
337+
338+
func floatLeToS32Le(floats []float32) []C.int32_t {
339+
ints := make([]C.int32_t, len(floats))
340+
for i, f := range floats {
341+
// Clip the float to the range [-1.0, 1.0]
342+
if f < -1.0 {
343+
f = -1.0
344+
}
345+
if f > 1.0 {
346+
f = 1.0
347+
}
348+
349+
// Convert to int32 in the range [-2147483648, 2147483647]
350+
ints[i] = C.int32_t(f * 2147483647)
351+
}
352+
return ints
353+
}
354+
244355
func (out *alsaOutput) outputLoop(pcmHandle *C.snd_pcm_t) {
245356
floats := make([]float32, out.channels*out.periodSize)
246357

@@ -296,7 +407,24 @@ func (out *alsaOutput) outputLoop(pcmHandle *C.snd_pcm_t) {
296407
// be very fast. It might be delayed a bit however if the sleep above
297408
// didn't sleep long enough to wait for room in the buffer.
298409
if n > 0 {
299-
if nn := C.snd_pcm_writei(pcmHandle, unsafe.Pointer(&floats[0]), C.snd_pcm_uframes_t(n/out.channels)); nn < 0 {
410+
var data unsafe.Pointer
411+
switch out.pcmFormat {
412+
case C.SND_PCM_FORMAT_FLOAT_LE:
413+
data = unsafe.Pointer(&floats[0])
414+
case C.SND_PCM_FORMAT_S32_LE:
415+
ints := floatLeToS32Le(floats)
416+
data = unsafe.Pointer(&ints[0])
417+
case C.SND_PCM_FORMAT_S16_LE:
418+
shorts := floatLeToS16Le(floats)
419+
data = unsafe.Pointer(&shorts[0])
420+
default:
421+
out.err <- fmt.Errorf("unsupported PCM format: %s", pcmFormatName(out.pcmFormat))
422+
out.closed = true
423+
out.lock.Unlock()
424+
return
425+
}
426+
427+
if nn := C.snd_pcm_writei(pcmHandle, data, C.snd_pcm_uframes_t(n/out.channels)); nn < 0 {
300428
// Got an error, so must recover (even for an underrun).
301429
errCode := C.snd_pcm_recover(pcmHandle, C.int(nn), 1)
302430
if errCode < 0 {

state.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type AppState struct {
2020
Username string `json:"username"`
2121
Data []byte `json:"data"`
2222
} `json:"credentials"`
23+
LastVolume *uint32 `json:"last_volume"`
2324
}
2425

2526
func (s *AppState) SetLogger(log Logger) {

0 commit comments

Comments
 (0)