Skip to content

ALSA backend hardcodes SND_PCM_FORMAT_FLOAT_LE with no fallback; failure is silently swallowed (readyChan closes on success or failure) #291

Description

@ceffo

Summary

On Linux, the ALSA backend hardcodes the PCM format request to SND_PCM_FORMAT_FLOAT_LE with no fallback. On a device that doesn't support float formats, snd_pcm_hw_params_set_format fails — but that failure happens inside a background goroutine and is never surfaced through NewContext's synchronous error return. readyChan closes regardless of success or failure (defer close(c.ready)), so every caller waiting on <-readyChan proceeds as if setup succeeded. Playback then silently does nothing: the PCM handle is opened but hw_params is never actually committed, so it never leaves ALSA's OPEN state.

Root cause (traced in driver_unix.go, v3.4.0)

alsaPcmHwParams (called from the background goroutine spawned in newContext):

if err := C.snd_pcm_hw_params_set_format(c.handle, params, C.SND_PCM_FORMAT_FLOAT_LE); err < 0 {
    return alsaError("snd_pcm_hw_params_set_format", err)
}

This is unconditional — there's no attempt to check what formats the device actually supports (which snd_pcm_hw_params_any + HW_REFINE already tells you at that point) and no fallback to another format (e.g. S32_LE, S16_LE).

When this fails, the error is stored in c.err and the goroutine returns early — but:

go func() {
    defer close(c.ready)
    ...
    if err := c.alsaPcmHwParams(...); err != nil {
        c.err.TryStore(err)
        return   // <-- c.ready still closes via the defer above
    }
    ...
}()
return c, c.ready, nil   // <-- this synchronous error is always nil for async failures

newContext's synchronous error return value only covers errors before the goroutine is spawned. Everything inside the goroutine — including snd_pcm_hw_params_set_format failing — is invisible to a caller doing the normal:

ctx, ready, err := oto.NewContext(opts)
if err != nil { ... }   // never true for this failure
<-ready                  // returns immediately, failure or not
player := ctx.NewPlayer(...)
player.Play()            // silently does nothing

The only way to discover the failure afterward is to call ctx.Err() — but nothing in the public API or docs indicates callers must poll that after <-ready, and libraries built on oto (e.g. gopxl/beep's speaker package) don't.

Reproduction

Hardware: FiiO K7 USB DAC (hw:5,0 on Linux/ALSA). Its supported format mask, per snd_pcm_hw_params_any:

FORMAT: S16_LE S32_LE DSD_U32_BE

No float format at all. Confirmed independently of oto:

$ aplay -D hw:5,0 -f FLOAT_LE -r 48000 -c 2 test.wav
Warning: format is changed to S16_LE

aplay's own convenience layer detects the mismatch and substitutes a supported format automatically. oto's snd_pcm_hw_params_set_format call has no equivalent fallback — it just fails.

Confirmed via strace -f -v -e trace=ioctl against a real oto-based app (cliamp, using gopxl/beep/v2) targeting this device: the trace shows two SNDRV_PCM_IOCTL_HW_REFINE calls (the initial hw_params_any probe, then narrowing ACCESS to RW_INTERLEAVED) and then nothing else, ever — no further ioctls, no SNDRV_PCM_IOCTL_HW_PARAMS commit. /proc/asound/cardN/pcmXpX/subX/status stays at state: OPEN / hw_params: no setup indefinitely. The app reports "playing" the whole time since it has no way to know otherwise.

Suggested fix

Either (or both):

  1. Try a small ordered list of candidate formats (e.g. FLOAT_LE, S32_LE, S16_LE) in alsaPcmHwParams, falling through to the next on -EINVAL, similar to how aplay/most ALSA apps handle this.
  2. Don't close c.ready until the outcome (success or failure) can be distinguished by the caller — e.g. document that ctx.Err() must be checked after <-readyChan returns, or better, make NewContext return the async setup error synchronously by waiting for the goroutine's first setup phase before returning (only the long-running read/write loop needs to stay async).

Environment

  • oto v3.4.0 (via gopxl/beep/v2 v2.1.1, as used by bjarneo/cliamp v1.63.2)
  • Arch Linux, kernel with snd-usb-audio, ALSA via alsa-lib
  • Device: FiiO K7 USB DAC, hw:5,0

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions