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)
|
||||
/// 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(
|
||||
&self,
|
||||
config: &BackendConfig,
|
||||
) -> Option<Result<super::AlsaDirectStream, String>> {
|
||||
) -> Option<Result<(super::AlsaDirectStream, super::backend::BitPerfectMode), String>> {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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!(
|
||||
"[ALSA Backend] Creating DIRECT ALSA stream for hw: device: {} ({}Hz, {}ch)",
|
||||
device_id,
|
||||
"[ALSA Backend] Attempting plughw stream: {} ({}Hz, {}ch)",
|
||||
plughw_device,
|
||||
config.sample_rate,
|
||||
config.channels
|
||||
);
|
||||
|
||||
// Create direct ALSA stream (bypasses rodio/CPAL)
|
||||
Some(super::AlsaDirectStream::new(
|
||||
device_id,
|
||||
config.sample_rate,
|
||||
config.channels,
|
||||
))
|
||||
match super::AlsaDirectStream::new(&plughw_device, config.sample_rate, config.channels) {
|
||||
Ok(stream) => {
|
||||
log::info!("[ALSA Backend] ✓ plughw stream created (bit-perfect with format conversion)");
|
||||
Some(Ok((stream, super::backend::BitPerfectMode::PluginFallback)))
|
||||
}
|
||||
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
|
||||
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
|
||||
///
|
||||
/// 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
|
||||
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) {
|
||||
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