Enumerating ALSA devices leaves device open, preventing opening other devices
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_openmore than one hw device at once (or two if one is USB audio), returning errorEBUSY. 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 callhost.output_devices(), that device disappears from the list, since trying to open it for output returnsEBUSYand 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
EBUSYand set an "already opened" flag (or raise an error when you actually create a stream) instead of making the device disappear?
- Should cpal special-case
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.
- You could change
- 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 withHost::input_devices()(they can't merely filterHost::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 ininput_devices()andoutput_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 callHost::output_devices(), it has access to the samealsa::PCMobjects. Right now,DeviceandDeviceHandlesisSend + Syncand already uses mutexes, so I suppose you can throwArcaroundDeviceHandles.
- You'd probably have to change
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)
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.