diff --git a/Cargo.toml b/Cargo.toml index f334a9736..15bd056f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ default = ["realtime-dbus"] # Promotes audio callback threads to real-time or high-priority scheduling for lower latency # Requires: (Linux/BSD) `rtprio` granted in `limits.conf` (e.g. `@audio - rtprio 95`) # Platform: Linux, DragonFly BSD, FreeBSD, NetBSD, Windows, Android -realtime = ["dep:audio_thread_priority"] +realtime = ["dep:audio_thread_priority", "dep:alsa-sys"] # D-Bus/rtkit support on top of `realtime` for RT scheduling on Linux/BSD desktop systems # Requires: D-Bus development libraries (libdbus-1-dev or equivalent) @@ -115,6 +115,7 @@ jack = { version = "0.13", optional = true } [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies] alsa = "0.11" +alsa-sys = { version = "0.4", optional = true } libc = "0.2" audio_thread_priority = { version = "0.35", optional = true, default-features = false } jack = { version = "0.13", optional = true } diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index d53217b57..1b4c0bdf6 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -3,6 +3,8 @@ //! Default backend on Linux and BSD systems. extern crate alsa; +#[cfg(feature = "realtime")] +extern crate alsa_sys; extern crate libc; use std::{ @@ -393,7 +395,10 @@ impl Device { if buffer_size == 0 { return Err(Error::with_message( ErrorKind::DeviceNotAvailable, - "initialization resulted in a null buffer", + format!( + "PCM '{}': initialization resulted in a null buffer", + self.pcm_id + ), )); } @@ -402,7 +407,7 @@ impl Device { if handle.count() == 0 { return Err(Error::with_message( ErrorKind::DeviceNotAvailable, - "poll descriptor count for stream was 0", + format!("PCM '{}': poll descriptor count is 0", self.pcm_id), )); } @@ -428,6 +433,7 @@ impl Device { let stream_inner = StreamInner { dropping: AtomicBool::new(false), handle, + pcm_id: self.pcm_id.clone(), sample_format, sample_rate: conf.sample_rate, frame_size, @@ -619,7 +625,10 @@ impl Device { Err(err) if err.kind() == ErrorKind::InvalidInput => { return Err(Error::with_message( ErrorKind::UnsupportedOperation, - "device does not support the requested direction", + format!( + "PCM '{}' does not support the requested direction", + self.pcm_id + ), )); } Err(err) => return Err(err), @@ -635,7 +644,7 @@ impl Device { .unwrap_or_else(|| f.with_max_sample_rate())), None => Err(Error::with_message( ErrorKind::UnsupportedConfig, - "no supported configuration for this device", + format!("PCM '{}': no supported configuration", self.pcm_id), )), } } @@ -728,6 +737,9 @@ struct StreamInner { // The ALSA handle. handle: alsa::pcm::PCM, + // ALSA PCM identifier used to open this stream. + pcm_id: String, + // Format of the samples. sample_format: SampleFormat, @@ -988,6 +1000,52 @@ fn output_stream_worker( fn boost_current_thread_priority( stream: &StreamInner, ) -> Result { + use alsa_sys::*; + // SAFETY: `alsa::pcm::PCM` is `pub struct PCM(*mut snd_pcm_t, Cell)`. The crate + // does not expose a public `as_ptr()`, but we can cast and read from it. + // TODO: replace with `stream.handle.as_ptr()` once alsa-rs exposes it publicly. + let raw = unsafe { + (&stream.handle as *const alsa::pcm::PCM) + .cast::<*mut snd_pcm_t>() + .read() + }; + let pcm_type = unsafe { snd_pcm_type(raw) }; + + // Only promote to RT for kernel-backed and pure-computation plugins. Others can exhaust + // RLIMIT_RTTIME when they block or coordinate with non-RT servers and trigger SIGXCPU + // on an RT thread. IOPLUG and EXTPLUG are excluded: no reliable way to distinguish + // RT-safe drivers (e.g. pipewire-alsa) from server-backed ones (e.g. pcm_pulse). + if !matches!( + pcm_type, + SND_PCM_TYPE_HW + | SND_PCM_TYPE_HOOKS + | SND_PCM_TYPE_NULL + | SND_PCM_TYPE_COPY + | SND_PCM_TYPE_LINEAR + | SND_PCM_TYPE_ALAW + | SND_PCM_TYPE_MULAW + | SND_PCM_TYPE_ADPCM + | SND_PCM_TYPE_RATE + | SND_PCM_TYPE_ROUTE + | SND_PCM_TYPE_PLUG + | SND_PCM_TYPE_LINEAR_FLOAT + | SND_PCM_TYPE_IEC958 + | SND_PCM_TYPE_SOFTVOL + ) { + let type_name = unsafe { + std::ffi::CStr::from_ptr(snd_pcm_type_name(pcm_type)) + .to_str() + .unwrap_or("unknown") + }; + return Err(Error::with_message( + ErrorKind::RealtimeDenied, + format!( + "PCM '{}' ({type_name}) cannot be promoted to real-time priority", + stream.pcm_id, + ), + )); + } + let period_frames = u32::try_from(stream.period_size).unwrap_or(0); audio_thread_priority::promote_current_thread_to_real_time(period_frames, stream.sample_rate) .map_err(Error::from) @@ -1073,7 +1131,7 @@ fn poll_for_period( if revents.intersects(alsa::poll::Flags::HUP | alsa::poll::Flags::NVAL) { return Err(Error::with_message( ErrorKind::DeviceNotAvailable, - "device disconnected", + format!("PCM '{}' disconnected", stream.pcm_id), )); } // POLLERR signals an xrun or suspend; avail_delay() below returns EPIPE/ESTRPIPE accordingly. @@ -1364,7 +1422,7 @@ impl StreamTrait for Stream { if !hw_params.can_pause() { return Err(Error::with_message( ErrorKind::UnsupportedOperation, - "hardware does not support pausing this stream", + format!("PCM '{}' does not support pausing", self.inner.pcm_id), )); } if self.inner.handle.state() != alsa::pcm::State::Paused {