RustAudio / cpal

Cross-platform audio I/O library in pure Rust
Apache License 2.0
2.69k stars 352 forks source link

Listing devices cannot connect to server #605

Open Ricky12Awesome opened 3 years ago

Ricky12Awesome commented 3 years ago

CPAL Version: 0.13.4 Distro: Garuda Dragonized Linux

I don't really know what's causing this issue, but here's the code I've tried and the output of it

use cpal::default_host;
use cpal::traits::{DeviceTrait, HostTrait};

fn main() {
  let host = default_host();

  println!("{:?}", cpal::available_hosts());

  let devices = host
    .devices()
    .map(|it| it.map(|it| it.name()))
    .unwrap()
    .collect::<Result<Vec<_>, _>>()
    .unwrap();

  println!("{:?}", devices);
}
[Alsa]
Cannot connect to server socket err = No such file or directory
Cannot connect to server request channel
jack server is not running or cannot be started
JackShmReadWritePtr::~JackShmReadWritePtr - Init not done for -1, skipping unlock
JackShmReadWritePtr::~JackShmReadWritePtr - Init not done for -1, skipping unlock
Cannot connect to server socket err = No such file or directory
Cannot connect to server request channel
jack server is not running or cannot be started
JackShmReadWritePtr::~JackShmReadWritePtr - Init not done for -1, skipping unlock
JackShmReadWritePtr::~JackShmReadWritePtr - Init not done for -1, skipping unlock
ALSA lib pcm_oss.c:397:(_snd_pcm_oss_open) Cannot open device /dev/dsp
ALSA lib pcm_oss.c:397:(_snd_pcm_oss_open) Cannot open device /dev/dsp
ALSA lib pcm_usb_stream.c:503:(_snd_pcm_usb_stream_open) Unknown field hint
ALSA lib pcm_usb_stream.c:503:(_snd_pcm_usb_stream_open) Unknown field hint
ALSA lib pcm_route.c:877:(find_matching_chmap) Found no matching channel map
ALSA lib pcm_route.c:877:(find_matching_chmap) Found no matching channel map
ALSA lib pcm_route.c:877:(find_matching_chmap) Found no matching channel map
ALSA lib pcm_usb_stream.c:503:(_snd_pcm_usb_stream_open) Unknown field hint
ALSA lib pcm_usb_stream.c:503:(_snd_pcm_usb_stream_open) Unknown field hint
ALSA lib pcm_usb_stream.c:503:(_snd_pcm_usb_stream_open) Unknown field hint
ALSA lib pcm_usb_stream.c:503:(_snd_pcm_usb_stream_open) Unknown field hint
ALSA lib pcm_dmix.c:1035:(snd_pcm_dmix_open) unable to open slave
ALSA lib pcm_usb_stream.c:503:(_snd_pcm_usb_stream_open) Unknown field hint
ALSA lib pcm_usb_stream.c:503:(_snd_pcm_usb_stream_open) Unknown field hint
["pipewire", "pulse", "default", "hdmi:CARD=NVidia,DEV=0", "hdmi:CARD=NVidia,DEV=1", "hdmi:CARD=NVidia,DEV=2", "hdmi:CARD=NVidia,DEV=3", "hdmi:CARD=NVidia,DEV=4", "hdmi:CARD=NVidia,DEV=5", "hdmi:CARD=NVidia,DEV=6", "sysdefault:CARD=Generic", "front:CARD=Generic,DEV=0", "surround21:CARD=Generic,DEV=0", "surround40:CARD=Generic,DEV=0", "surround41:CARD=Generic,DEV=0", "surround50:CARD=Generic,DEV=0", "surround51:CARD=Generic,DEV=0", "surround71:CARD=Generic,DEV=0", "iec958:CARD=Generic,DEV=0", "sysdefault:CARD=Microphone", "front:CARD=Microphone,DEV=0", "surround21:CARD=Microphone,DEV=0", "surround40:CARD=Microphone,DEV=0", "surround41:CARD=Microphone,DEV=0", "surround50:CARD=Microphone,DEV=0", "surround51:CARD=Microphone,DEV=0", "surround71:CARD=Microphone,DEV=0", "iec958:CARD=Microphone,DEV=0", "sysdefault:CARD=C920", "front:CARD=C920,DEV=0"]

Process finished with exit code 0
LeCodingWolfie commented 1 year ago

~Yay! Too much text nobody is gonna read!~

TL:DR (Too Long, Didn't Read): cpal is not able to handle/catch these errors, as neitheralsa-rs does, becuase they are written to standard error by ALSA, not Rust; which points to the solution, i.e. to catch stderr. Therefore, I will create an issue/pull request to alsa-rs :D

P.D. If any other sound library, like JACK, has a similar issue, a similar fix can be written.


On the other hand, it's worth noting that cpal ignores whatever of these could be catched by Rust only, in order to only show the available devices, not those who do not work, or error, and yet are known to ALSA (such as JACK audio I/O device, in this case), which is also helpful to know whether an audio device is for input or output, even though it's not an issue.

Walkthrough

I couldn't not write this little story, apologies :sweat_smile:

Unnecessarily, and by curiosity, I tried to list all devices to select instead of the defaults, and was surprised to see many errors, none which lead to panic or an error in Rust (with Result) that could be fixed.

That's how, all day, I tried to catch an error within Rust, in my code, cpal and its dependency alsa-rs, until I realized these were thrown by ALSA, nowhere in Rust where they could be catched or suppressed from standard error.

Without further ado, here's what happens.

Explaination

In Linux, unless specified, cpal defaults to ALSA 1, 2, contrary to what the errors may say [^1]. devices has to be called so we can fetch all devices, as it is not done upon creation; however, the method only creates a new Devices 3 instance, which we have to iterate over to get each device (next) 4, which is why the error is thrown even when using count.

next calls DeviceHandles::open 5, which tries to open a handle for both playback and capture, and returns if at least one was opened successfully 6, which is useful to know if a device is either for I/O, even though it's determined differently 7, and is better than the current approach.

Anyways, on both directions try_open is called with PCM::new from alsa-rs 8, which returns a Result from open 9 catched through the macro acheck! 10, which calls the Rust bindings generated in alsa-sys from ALSA.

At the end, the error roots from acheck!, which catches a error different to the ones printed, and nowhere suppresses the errors that are printed by ALSA to standard error, which means this is not a cpal issue, and must be fixed in alsa-rs.

Solution

Before anything, to temporarily edit alsa-rs code unto a project, rhack 11 can be used to readily download the crate code and set it up on the current project, or otherwise manually with Cargo.toml's [patch] 12 (can set a GitHub repository).

We just have to catch the errors from standard error, which we can either print or not. In fact, a similar issue, which is "how to catch a C library's stderr in Rust" 13, has already been solved, and is used as reference in the following code.

Code `src/main.rs`: ```rust let host = cpal::default_host(); println!("{:?}", cpal::available_hosts()); let devices = host.devices() .map(|devices| devices.map(|d| d.name())).unwrap() .collect::, cpal::DeviceNameError>>().unwrap(); println!("{:?}", devices); ``` [`alsa-rs/src/pcm.rs`:][9] ```rust fn catch(mut f: F) -> Result where F: FnMut() -> Result { use std::os::unix::io::FromRawFd; use std::io::prelude::BufRead; use std::io::BufReader; use std::fs::File; extern "C" { fn pipe(fd: *mut i32) -> i32; fn close(fd: i32) -> i32; fn dup2(old_fd: i32, new_fd: i32) -> i32; } const PIPE_READ: usize = 0; const PIPE_WRITE: usize = 1; const STDERR_FILENO: i32 = 2; // Creates a pipe of standard error (2) which writes on a file descriptor that can be read on; // returns if unsucessful. let mut pipefd = [-1; 2]; if unsafe { pipe(&mut pipefd[0]) } == -1 { eprintln!("Unable to create pipe for stderr"); panic!(); } // Creates a file that reads from the pipe let file = unsafe { File::from_raw_fd(pipefd[PIPE_READ]) }; // Redirects the pipe to the file; returns if unsucessful if unsafe { dup2(pipefd[PIPE_WRITE], STDERR_FILENO) } == -1 { eprintln!("Unable to redirect pipe to file"); panic!(); } // Runs the code that writes to stderr let result = f(); // Prints the first line of the stderr let reader = BufReader::new(file); let line = reader.lines().next().unwrap().unwrap(); if line != "" { println!("stderr: {:?}", line); } // Closes stderr unsafe { close(pipefd[PIPE_WRITE]); }; result } // ... pub fn open(name: &CStr, dir: Direction, nonblock: bool) -> Result { // ... catch(|| { let result = acheck!(snd_pcm_open(&mut r, name.as_ptr(), stream, flags)).map(|_| PCM(r, cell::Cell::new(false))); eprintln!(""); result }) } ``` This code is only able to catch the first line of the error (which is cleaner), even though I've tried to catch all. This can only be executed within Linux, due to the use of glibc/Linux libraries. The code that creates and prints the file from the pipe can be deleted to not print any error.

Err?

If you're curious of how to log all the errors from the code flow, here's the code:

Mo' code [`cpal/src/host/alsa/mod.rs`:][6] ```rust fn open(name: &str) -> Result { if capture_err.is_some() | playback_err.is_some() { println!(">> ALSA playback/capture errors: {:?} {:?} in {name}", playback_err, capture_err); } if let Some(err) = capture_err.and(playback_err) { println!("> An error has occurred on both playback/capture: {} in {name}", err); Err(err) } else { Ok(handles) } } ``` **Optional** [`alsa-rs/src/pcm.rs`:][14] ```rust pub fn new(name: &str, dir: Direction, nonblock: bool) -> Result { let pcm = Self::open(&CString::new(name).unwrap(), dir, nonblock); let error = pcm.as_ref().err(); if error.is_some() { println!(">>> ALSA PCM error: {:?} in {name}", error); }; pcm } ``` [`cpal/src/host/alsa/enumerate.rs`:][5] ```rust match DeviceHandles::open(&name) { Ok(handles) => return Some(Device { name, handles: Mutex::new(handles), }), Err(e) => println!("< ALSA error: {}", e) } ```

and here's how it looks (without the optional logs):

Output ``` [Alsa] stderr: "Cannot connect to server socket err = No such file or directory" stderr: "Cannot connect to server socket err = No such file or directory" >> ALSA playback/capture errors: Some(Error("snd_pcm_open", ENOENT)) Some(Error("snd_pcm_open", ENOENT)) in jack > An error has occurred on both playback/capture: ALSA function 'snd_pcm_open' failed with error 'ENOENT: No such file or directory' in jack stderr: "ALSA lib pcm_oss.c:397:(_snd_pcm_oss_open) Cannot open device /dev/dsp" stderr: "ALSA lib pcm_oss.c:397:(_snd_pcm_oss_open) Cannot open device /dev/dsp" >> ALSA playback/capture errors: Some(Error("snd_pcm_open", ENOENT)) Some(Error("snd_pcm_open", ENOENT)) in oss > An error has occurred on both playback/capture: ALSA function 'snd_pcm_open' failed with error 'ENOENT: No such file or directory' in oss stderr: "ALSA lib pcm_route.c:877:(find_matching_chmap) Found no matching channel map" stderr: "ALSA lib pcm_route.c:877:(find_matching_chmap) Found no matching channel map" >> ALSA playback/capture errors: Some(Error("snd_pcm_open", EINVAL)) Some(Error("snd_pcm_open", EINVAL)) in surround21:CARD=PCH,DEV=0 > An error has occurred on both playback/capture: ALSA function 'snd_pcm_open' failed with error 'EINVAL: Invalid argument' in surround21:CARD=PCH,DEV=0 stderr: "ALSA lib pcm_route.c:877:(find_matching_chmap) Found no matching channel map" stderr: "ALSA lib pcm_route.c:877:(find_matching_chmap) Found no matching channel map" >> ALSA playback/capture errors: Some(Error("snd_pcm_open", EINVAL)) Some(Error("snd_pcm_open", EINVAL)) in surround41:CARD=PCH,DEV=0 > An error has occurred on both playback/capture: ALSA function 'snd_pcm_open' failed with error 'EINVAL: Invalid argument' in surround41:CARD=PCH,DEV=0 stderr: "ALSA lib pcm_route.c:877:(find_matching_chmap) Found no matching channel map" stderr: "ALSA lib pcm_route.c:877:(find_matching_chmap) Found no matching channel map" >> ALSA playback/capture errors: Some(Error("snd_pcm_open", EINVAL)) Some(Error("snd_pcm_open", EINVAL)) in surround50:CARD=PCH,DEV=0 > An error has occurred on both playback/capture: ALSA function 'snd_pcm_open' failed with error 'EINVAL: Invalid argument' in surround50:CARD=PCH,DEV=0 >> ALSA playback/capture errors: Some(Error("snd_pcm_open", EACCES)) Some(Error("snd_pcm_open", EACCES)) in usbstream:CARD=PCH > An error has occurred on both playback/capture: ALSA function 'snd_pcm_open' failed with error 'EACCES: Permission denied' in usbstream:CARD=PCH >> ALSA playback/capture errors: None Some(Error("snd_pcm_open", ENOENT)) in hdmi:CARD=NVidia,DEV=0 >> ALSA playback/capture errors: None Some(Error("snd_pcm_open", ENOENT)) in hdmi:CARD=NVidia,DEV=1 >> ALSA playback/capture errors: None Some(Error("snd_pcm_open", ENOENT)) in hdmi:CARD=NVidia,DEV=2 >> ALSA playback/capture errors: None Some(Error("snd_pcm_open", ENOENT)) in hdmi:CARD=NVidia,DEV=3 >> ALSA playback/capture errors: Some(Error("snd_pcm_open", EACCES)) Some(Error("snd_pcm_open", EACCES)) in usbstream:CARD=NVidia > An error has occurred on both playback/capture: ALSA function 'snd_pcm_open' failed with error 'EACCES: Permission denied' in usbstream:CARD=NVidia ["lavrate", "samplerate", "speexrate", "pipewire", "pulse", "speex", "upmix", "vdownmix", "default", "sysdefault:CARD=PCH", "front:CARD=PCH,DEV=0", "surround40:CARD=PCH,DEV=0", "surro und51:CARD=PCH,DEV=0", "surround71:CARD=PCH,DEV=0", "hdmi:CARD=NVidia,DEV=0", "hdmi:CARD=NVidia,DEV=1", "hdmi:CARD=NVidia,DEV=2", "hdmi:CARD=NVidia,DEV=3"] ```

In fact, the errors are thrown when cpal cannot open a device with neither playback or capture. However, it's also possible to catch the individual errors when either one of those do not work, from what I remember.


I've learnt some new things, documented the whole process, and fixed an issue (or two), didn't hurt, right?

[^1]: As mentioned, this is becuase JACK seems to create an audio device that is detected by ALSA, as shown by output of the errors in Err?.