cpal icon indicating copy to clipboard operation
cpal copied to clipboard

Enumerating ALSA devices leaves device open, preventing opening other devices

Open nyanpasu64 opened this issue 3 years ago • 1 comments

As a result of https://github.com/RustAudio/cpal/pull/506, when iterating through host.{devices,input_devices,output_devices}(), each Device holds its corresponding alsa::PCM/snd_pcm_ts open until dropped. This causes problems in practice:

  • ALSA often won't let you snd_pcm_open more than one hw device at once (or two if one is USB audio), returning error EBUSY. Plug devices are unaffected, and sysdefault (no clue, never used it, is it dmix?) is somewhere in between.
    • As a result, if you collect input devices into a Vec/etc. instead of iterating them one by one (dropping before fetching the next item), all but 1-2 hw device is missing entirely. Holding a GPU device open prevents you from opening motherboard audio.
  • When you call host.input_devices(), every returned device is opened for both input and output. If you keep exactly one input device in-scope ("front:CARD=Generic,DEV=0" is motherboard audio) and call host.output_devices(), that device disappears from the list, since trying to open it for output returns EBUSY and causes cpal to skip over them. Additionally, some but not all devices disappear from the list (other motherboard channels vanish, GPU and USB audio can still be opened).
    • Should cpal special-case EBUSY and set an "already opened" flag (or raise an error when you actually create a stream) instead of making the device disappear?

I created a debug branch which modifies examples/feedback.rs to print the results of (iterating over all devices, collecting into a Vec, and collecting a second time), before and after opening "front:CARD=Generic,DEV=0" as input: https://github.com/RustAudio/cpal/compare/master...nyanpasu64:test-device-open

  • Results at https://gist.github.com/nyanpasu64/308a912f6b81c00e72140ed375765f54

Alternatively, https://github.com/RustAudio/cpal/compare/master...nyanpasu64:log-device-open adds noisy verbose printing to cpal::host::alsa::DeviceHandles::try_open() (which is called both by host.devices() and by host.input_devices() calling supported_input_configs -> supported_configs -> get_mut). Unless I made a mistake scanning over the results, all failed opens which initially succeeded failed due to EBUSY.

  • Results at https://gist.github.com/nyanpasu64/b1798d72d78a6f0705ba7c0241f1d01e

Suggestions

One option is to revert #506 which holds open alsa::PCM when the user is enumerating devices and keeps devices around. This is probably the most natural solution to users, since the impression I get is that Audacity (portaudio) and possibly other ALSA apps don't hold more than 1 device open at a time, and they just open the device once to probe and once to stream, and I personally haven't had issues with that behavior so far.

Another option is to document that you cannot hold more than 1 device's metadata around at a time, and hope users will get the message (I think users won't, and they'll keep .collect()ing audio devices and writing wrong programs, and it's basically impossible to statically prohibit incorrect programs).

  • .collect() will result in the library malfunctioning.
    • You could change Host::output_devices() to a streaming iterator so the compiler won't let you collect into a Vec or hold onto one device when fetching the next. I don't like this though, since streaming iterators are nonstandard (a third-party crate) and can't be used in for loops.
    • This won't fix the user calling Host::output_devices() twice, and the two iterators interfering with each other.
  • Even if users don't collect(), you need to fix using the same device name as an input and output.
    • You'd probably have to change Host::output_devices() to only open output handles, and same with Host::input_devices() (they can't merely filter Host::devices() anymore). I'm not sure if this will break existing code, or if it's incompatible with other backends where if the same name appears in input_devices() and output_devices(), both offer both input and output. I don't think it's a likely issue, but who knows.
    • Alternatively, you could hold around handles somewhere (unsure), and reuse them if the same name is requested twice. This way, if you call Host::input_devices() and pick "front:CARD=Generic,DEV=0", it's opened in both input and output, and if you call Host::output_devices(), it has access to the same alsa::PCM objects. Right now, Device and DeviceHandles is Send + Sync and already uses mutexes, so I suppose you can throw Arc around DeviceHandles.

System info

Kernel: 5.16.0-zen1-1-zen Motherboard: Gigabyte B550M DS3H Audio devices:

  • 0b:00.4 Audio device: Advanced Micro Devices, Inc. [AMD] Starship/Matisse HD Audio Controller (primary)
  • 09:00.1 Audio device: NVIDIA Corporation GK208 HDMI/DP Audio Controller (rev a1) (not in use)
  • Bus 003 Device 006: ID 1852:7022 GYROCOM C&C Co., LTD Fiio E10 (output only, misreports as a duplex device)
  • pipewire-git 7c6649b5e57ffeea179cb896102bc051f89b7d4f (a few days old, the exact version shouldn't matter)

nyanpasu64 avatar Jan 17 '22 14:01 nyanpasu64

I just ran into this same issue with ASIO.

I thought I could call .collect::<Vec<_>>(), and it made it look like I only had a single device (the one that appears first in the list when iterating one device at a time).

I think at a minimum the documentation should note this gotcha.

j-n-f avatar Jan 19 '25 08:01 j-n-f