feat(audio): implement controlled hw→plughw fallback for ALSA Direct
- Add AlsaDirectError enum for structured error classification - Add BitPerfectMode enum to track runtime bit-perfect status - Implement automatic fallback: hw fails with format error → try plughw - Abort on non-recoverable errors (busy, permissions, invalid params) - Respect ALSA plugin selection from UI settings - Log runtime mode for debugging This allows SMSL-class DACs that fail on hw: to work via plughw: while maintaining bit-perfect guarantees (format conversion only, no resampling). cc
This commit is contained in:
1 parent
4670b1548a
commit
da5ec2abcb
3 files changed
+170
-12
No files matched your search
@@ -411,31 +411,101 @@ impl AlsaBackend {
|
|||||||
|
|
||||||
/// Try to create direct ALSA stream for hw: devices (bypasses CPAL)
|
/// Try to create direct ALSA stream for hw: devices (bypasses CPAL)
|
||||||
/// Returns None if device is not a hw: device (should use CPAL instead)
|
/// Returns None if device is not a hw: device (should use CPAL instead)
|
||||||
|
///
|
||||||
|
/// Implements controlled fallback:
|
||||||
|
/// 1. Try direct hw access first
|
||||||
|
/// 2. If format unsupported, try plughw (format conversion only, no resampling)
|
||||||
|
/// 3. Abort on other errors (busy, permissions, etc.)
|
||||||
pub fn try_create_direct_stream(
|
pub fn try_create_direct_stream(
|
||||||
&self,
|
&self,
|
||||||
config: &BackendConfig,
|
config: &BackendConfig,
|
||||||
) -> Option<Result<super::AlsaDirectStream, String>> {
|
) -> Option<Result<(super::AlsaDirectStream, super::backend::BitPerfectMode), String>> {
|
||||||
let device_id = config.device_id.as_ref()?;
|
let device_id = config.device_id.as_ref()?;
|
||||||
|
|
||||||
// Only use direct ALSA for hw: and plughw: devices
|
// Only use direct ALSA for hw:/plughw:/front: devices
|
||||||
if !super::AlsaDirectStream::is_hw_device(device_id) {
|
if !super::AlsaDirectStream::is_hw_device(device_id) {
|
||||||
log::info!("[ALSA Backend] Device '{}' is not hw:/plughw:, using CPAL", device_id);
|
log::info!("[ALSA Backend] Device '{}' is not hw:/plughw:/front:, using CPAL", device_id);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine the base device path for hw/plughw attempts
|
||||||
|
// front:CARD=X,DEV=Y -> extract card name for hw attempts
|
||||||
|
let (hw_device, plughw_device) = if device_id.starts_with("front:CARD=") {
|
||||||
|
// front:CARD=AMP,DEV=0 -> need to find corresponding hw:X,0
|
||||||
|
// For now, try the front: device directly as it's already hardware-direct
|
||||||
|
(device_id.to_string(), format!("plug:{}", device_id))
|
||||||
|
} else if device_id.starts_with("hw:") {
|
||||||
|
(device_id.to_string(), device_id.replace("hw:", "plughw:"))
|
||||||
|
} else if device_id.starts_with("plughw:") {
|
||||||
|
// Already plughw, try it directly
|
||||||
|
(device_id.replace("plughw:", "hw:"), device_id.to_string())
|
||||||
|
} else {
|
||||||
|
(device_id.to_string(), format!("plug:{}", device_id))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Respect ALSA plugin selection from settings
|
||||||
|
let try_hw_first = match config.alsa_plugin {
|
||||||
|
Some(AlsaPlugin::Hw) => true,
|
||||||
|
Some(AlsaPlugin::PlugHw) => false, // Skip hw, go directly to plughw
|
||||||
|
Some(AlsaPlugin::Pcm) => {
|
||||||
|
log::info!("[ALSA Backend] PCM mode selected, not using direct ALSA");
|
||||||
|
return None; // Use CPAL instead
|
||||||
|
}
|
||||||
|
None => true, // Default: try hw first
|
||||||
|
};
|
||||||
|
|
||||||
|
if try_hw_first {
|
||||||
|
log::info!(
|
||||||
|
"[ALSA Backend] Attempting DIRECT hw stream: {} ({}Hz, {}ch)",
|
||||||
|
hw_device,
|
||||||
|
config.sample_rate,
|
||||||
|
config.channels
|
||||||
|
);
|
||||||
|
|
||||||
|
match super::AlsaDirectStream::new(&hw_device, config.sample_rate, config.channels) {
|
||||||
|
Ok(stream) => {
|
||||||
|
log::info!("[ALSA Backend] ✓ Direct hw stream created successfully");
|
||||||
|
return Some(Ok((stream, super::backend::BitPerfectMode::DirectHardware)));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let error = super::backend::AlsaDirectError::from_alsa_error(&e);
|
||||||
|
log::warn!("[ALSA Backend] hw attempt failed: {}", error);
|
||||||
|
|
||||||
|
if !error.allows_plughw_fallback() {
|
||||||
|
// Non-recoverable error (busy, permissions, etc.)
|
||||||
|
log::error!("[ALSA Backend] Cannot fallback - error type: {:?}", error);
|
||||||
|
return Some(Err(format!(
|
||||||
|
"ALSA Direct failed: {}. Device may be in use or inaccessible.",
|
||||||
|
error
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("[ALSA Backend] Format unsupported on hw, trying plughw fallback...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try plughw fallback (format conversion only)
|
||||||
log::info!(
|
log::info!(
|
||||||
"[ALSA Backend] Creating DIRECT ALSA stream for hw: device: {} ({}Hz, {}ch)",
|
"[ALSA Backend] Attempting plughw stream: {} ({}Hz, {}ch)",
|
||||||
device_id,
|
plughw_device,
|
||||||
config.sample_rate,
|
config.sample_rate,
|
||||||
config.channels
|
config.channels
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create direct ALSA stream (bypasses rodio/CPAL)
|
match super::AlsaDirectStream::new(&plughw_device, config.sample_rate, config.channels) {
|
||||||
Some(super::AlsaDirectStream::new(
|
Ok(stream) => {
|
||||||
device_id,
|
log::info!("[ALSA Backend] ✓ plughw stream created (bit-perfect with format conversion)");
|
||||||
config.sample_rate,
|
Some(Ok((stream, super::backend::BitPerfectMode::PluginFallback)))
|
||||||
config.channels,
|
}
|
||||||
))
|
Err(e) => {
|
||||||
|
log::error!("[ALSA Backend] plughw fallback also failed: {}", e);
|
||||||
|
Some(Err(format!(
|
||||||
|
"Bit-perfect playback could not be established. hw failed, plughw failed: {}",
|
||||||
|
e
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,6 +117,91 @@ pub struct BackendConfig {
|
|||||||
/// Result type for backend operations
|
/// Result type for backend operations
|
||||||
pub type BackendResult<T> = Result<T, String>;
|
pub type BackendResult<T> = Result<T, String>;
|
||||||
|
|
||||||
|
/// ALSA Direct stream error classification
|
||||||
|
/// Used to determine if fallback to plughw is appropriate
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum AlsaDirectError {
|
||||||
|
/// PCM format not supported by hardware (can fallback to plughw)
|
||||||
|
UnsupportedFormat(String),
|
||||||
|
/// Device is busy/in use by another application
|
||||||
|
DeviceBusy(String),
|
||||||
|
/// Permission denied to access device
|
||||||
|
PermissionDenied(String),
|
||||||
|
/// Invalid parameters (channels, sample rate)
|
||||||
|
InvalidParams(String),
|
||||||
|
/// Device not found
|
||||||
|
DeviceNotFound(String),
|
||||||
|
/// Generic/unknown error
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for AlsaDirectError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
AlsaDirectError::UnsupportedFormat(msg) => write!(f, "Unsupported format: {}", msg),
|
||||||
|
AlsaDirectError::DeviceBusy(msg) => write!(f, "Device busy: {}", msg),
|
||||||
|
AlsaDirectError::PermissionDenied(msg) => write!(f, "Permission denied: {}", msg),
|
||||||
|
AlsaDirectError::InvalidParams(msg) => write!(f, "Invalid parameters: {}", msg),
|
||||||
|
AlsaDirectError::DeviceNotFound(msg) => write!(f, "Device not found: {}", msg),
|
||||||
|
AlsaDirectError::Other(msg) => write!(f, "{}", msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlsaDirectError {
|
||||||
|
/// Check if this error allows fallback to plughw
|
||||||
|
pub fn allows_plughw_fallback(&self) -> bool {
|
||||||
|
matches!(self, AlsaDirectError::UnsupportedFormat(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create from raw ALSA error message
|
||||||
|
pub fn from_alsa_error(msg: &str) -> Self {
|
||||||
|
let msg_lower = msg.to_lowercase();
|
||||||
|
|
||||||
|
if msg_lower.contains("no supported audio format")
|
||||||
|
|| msg_lower.contains("format")
|
||||||
|
|| msg_lower.contains("s24_3le")
|
||||||
|
|| msg_lower.contains("s24le")
|
||||||
|
|| msg_lower.contains("sample format")
|
||||||
|
{
|
||||||
|
AlsaDirectError::UnsupportedFormat(msg.to_string())
|
||||||
|
} else if msg_lower.contains("busy")
|
||||||
|
|| msg_lower.contains("resource temporarily unavailable")
|
||||||
|
|| msg_lower.contains("device or resource busy")
|
||||||
|
{
|
||||||
|
AlsaDirectError::DeviceBusy(msg.to_string())
|
||||||
|
} else if msg_lower.contains("permission")
|
||||||
|
|| msg_lower.contains("access denied")
|
||||||
|
|| msg_lower.contains("operation not permitted")
|
||||||
|
{
|
||||||
|
AlsaDirectError::PermissionDenied(msg.to_string())
|
||||||
|
} else if msg_lower.contains("not found")
|
||||||
|
|| msg_lower.contains("no such")
|
||||||
|
|| msg_lower.contains("doesn't exist")
|
||||||
|
{
|
||||||
|
AlsaDirectError::DeviceNotFound(msg.to_string())
|
||||||
|
} else if msg_lower.contains("invalid")
|
||||||
|
|| msg_lower.contains("channels")
|
||||||
|
|| msg_lower.contains("rate")
|
||||||
|
{
|
||||||
|
AlsaDirectError::InvalidParams(msg.to_string())
|
||||||
|
} else {
|
||||||
|
AlsaDirectError::Other(msg.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runtime mode for bit-perfect status tracking
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum BitPerfectMode {
|
||||||
|
/// Direct hardware access (hw:), guaranteed bit-perfect
|
||||||
|
DirectHardware,
|
||||||
|
/// Plugin hardware fallback (plughw:), bit-perfect with format conversion only
|
||||||
|
PluginFallback,
|
||||||
|
/// Not using bit-perfect path (pcm, pipewire, pulse)
|
||||||
|
Disabled,
|
||||||
|
}
|
||||||
|
|
||||||
/// Audio backend trait
|
/// Audio backend trait
|
||||||
///
|
///
|
||||||
/// All audio backends must implement this trait to provide
|
/// All audio backends must implement this trait to provide
|
||||||
|
|||||||
@@ -451,7 +451,10 @@ fn try_init_stream_with_backend(
|
|||||||
// Downcast backend to AlsaBackend to access try_create_direct_stream
|
// Downcast backend to AlsaBackend to access try_create_direct_stream
|
||||||
if let Some(alsa_backend) = backend.as_any().downcast_ref::<crate::audio::alsa_backend::AlsaBackend>() {
|
if let Some(alsa_backend) = backend.as_any().downcast_ref::<crate::audio::alsa_backend::AlsaBackend>() {
|
||||||
if let Some(result) = alsa_backend.try_create_direct_stream(&config) {
|
if let Some(result) = alsa_backend.try_create_direct_stream(&config) {
|
||||||
return Some(result.map(|stream| StreamType::AlsaDirect(Arc::new(stream))));
|
return Some(result.map(|(stream, mode)| {
|
||||||
|
log::info!("ALSA Direct stream created with mode: {:?}", mode);
|
||||||
|
StreamType::AlsaDirect(Arc::new(stream))
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user