fix(audio): expand ALSA bit-perfect detection for front:CARD devices
- Add front:CARD=X,DEV=Y device enumeration from aplay -L output - Recognize front: devices as bit-perfect in AlsaDirectStream - Group front:CARD and "bit-perfect" named devices correctly in UI - Add tooltip to device dropdown showing full device name - Add current selection indicator in device dropdown - Extend dropdown idle timeout to 10 seconds for long device names Addresses issue #30 for USB DACs that expose front: but not hw: devices
This commit is contained in:
1 parent
9680bbb9cd
commit
7a76973920
3 files changed
+83
-9
No files matched your search
@@ -256,6 +256,54 @@ impl AlsaBackend {
|
||||
});
|
||||
}
|
||||
|
||||
// Fifth: Add front:CARD=X,DEV=Y devices from aplay -L (issue #30)
|
||||
// Some USB DACs (e.g., SMSL AD18) don't expose hw: devices but have working front: devices
|
||||
// DeaDBeef and Audacious use these for bit-perfect playback
|
||||
for (device_id, desc) in &description_map {
|
||||
// Match front:CARD=X,DEV=Y pattern
|
||||
if device_id.starts_with("front:CARD=") {
|
||||
// Check if we already have this device
|
||||
let already_exists = devices.iter().any(|d| d.name == *device_id);
|
||||
if already_exists {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract card name for friendly description
|
||||
// front:CARD=AMP,DEV=0 -> "AMP"
|
||||
let card_name = device_id
|
||||
.strip_prefix("front:CARD=")
|
||||
.and_then(|s| s.split(',').next())
|
||||
.unwrap_or("Unknown");
|
||||
|
||||
// Use card_map description if available, otherwise use aplay -L description
|
||||
let friendly_desc = card_map
|
||||
.iter()
|
||||
.find(|(_, card_desc)| {
|
||||
// Match by card short name in the description
|
||||
card_desc.to_lowercase().contains(&card_name.to_lowercase())
|
||||
})
|
||||
.map(|(_, card_desc)| card_desc.clone())
|
||||
.unwrap_or_else(|| desc.clone());
|
||||
|
||||
log::info!(
|
||||
"[ALSA Backend] Adding front: device for bit-perfect: {} ({})",
|
||||
device_id,
|
||||
friendly_desc
|
||||
);
|
||||
|
||||
devices.push(AudioDevice {
|
||||
id: device_id.clone(),
|
||||
name: device_id.clone(),
|
||||
description: Some(format!("{} (Direct Hardware - Bit-perfect)", friendly_desc)),
|
||||
is_default: false,
|
||||
max_sample_rate: Some(384000), // Assume high sample rate capability
|
||||
supported_sample_rates: None,
|
||||
device_bus: None,
|
||||
is_hardware: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("[ALSA Backend] Enumerated {} ALSA devices", devices.len());
|
||||
for (idx, dev) in devices.iter().enumerate() {
|
||||
log::info!(
|
||||
|
||||
@@ -285,9 +285,12 @@ impl AlsaDirectStream {
|
||||
Err(format!("No volume control found for {}. DAC may not support hardware mixer.", self.device_id))
|
||||
}
|
||||
|
||||
/// Check if device is hw: device
|
||||
/// Check if device is a bit-perfect hardware device
|
||||
/// Includes: hw:X,Y, plughw:X,Y, and front:CARD=X,DEV=Y
|
||||
pub fn is_hw_device(device_id: &str) -> bool {
|
||||
device_id.starts_with("hw:") || device_id.starts_with("plughw:")
|
||||
device_id.starts_with("hw:")
|
||||
|| device_id.starts_with("plughw:")
|
||||
|| device_id.starts_with("front:CARD=")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +320,7 @@ impl AlsaDirectStream {
|
||||
2
|
||||
}
|
||||
|
||||
/// Check if device is a bit-perfect hardware device (always false on non-Linux)
|
||||
pub fn is_hw_device(_device_id: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
openMenu as openGlobalMenu,
|
||||
closeMenu as closeGlobalMenu,
|
||||
subscribe as subscribeFloatingMenu,
|
||||
getActiveMenuId,
|
||||
MENU_INACTIVITY_TIMEOUT
|
||||
getActiveMenuId
|
||||
} from '$lib/stores/floatingMenuStore';
|
||||
|
||||
// Extended timeout for device dropdown (10 seconds) - devices have long names
|
||||
const DEVICE_DROPDOWN_TIMEOUT = 10000;
|
||||
|
||||
interface DeviceOption {
|
||||
value: string; // Display name
|
||||
id: string; // Device ID (for categorization)
|
||||
@@ -62,9 +64,15 @@
|
||||
defaults.push(device);
|
||||
} else if (device.id === 'default' || device.isDefault) {
|
||||
defaults.push(device);
|
||||
} else if (device.id.startsWith('hw:') || device.id.startsWith('iec958:')) {
|
||||
} else if (
|
||||
device.id.startsWith('hw:') ||
|
||||
device.id.startsWith('iec958:') ||
|
||||
device.id.startsWith('front:CARD=') ||
|
||||
device.value.toLowerCase().includes('bit-perfect')
|
||||
) {
|
||||
// hw: = direct hardware access, iec958: = S/PDIF digital output
|
||||
// Both are bit-perfect capable
|
||||
// front:CARD= = front channel device (bit-perfect for some USB DACs)
|
||||
// Also include any device with "bit-perfect" in its display name
|
||||
bitPerfect.push(device);
|
||||
} else if (device.id.startsWith('plughw:')) {
|
||||
pluginHw.push(device);
|
||||
@@ -258,7 +266,7 @@
|
||||
if (idleTimer) clearTimeout(idleTimer);
|
||||
idleTimer = setTimeout(() => {
|
||||
if (isOpen && !isHovering) closeDropdown();
|
||||
}, MENU_INACTIVITY_TIMEOUT);
|
||||
}, DEVICE_DROPDOWN_TIMEOUT);
|
||||
};
|
||||
|
||||
if (!isHovering) scheduleIdleClose();
|
||||
@@ -283,7 +291,7 @@
|
||||
</script>
|
||||
|
||||
<div class="dropdown" class:wide bind:this={dropdownRef}>
|
||||
<button class="trigger" onclick={() => isOpen ? closeDropdown() : openDropdown()}>
|
||||
<button class="trigger" onclick={() => isOpen ? closeDropdown() : openDropdown()} title={value}>
|
||||
<span class="value-text">{value}</span>
|
||||
<ChevronDown size={16} class="chevron" />
|
||||
</button>
|
||||
@@ -332,6 +340,7 @@
|
||||
onclick={() => handleOptionClick(device)}
|
||||
title={device.id !== device.value ? `${device.value}\n${device.id}` : device.value}
|
||||
>
|
||||
<span class="current-indicator" class:visible={device.value === value}>▸</span>
|
||||
<div class="option-content">
|
||||
<span class="option-text">{device.value}</span>
|
||||
{#if sampleRatesText}
|
||||
@@ -486,10 +495,11 @@
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
padding: 6px 12px;
|
||||
padding-left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
gap: 4px;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
@@ -500,6 +510,18 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.current-indicator {
|
||||
width: 12px;
|
||||
font-size: 10px;
|
||||
color: var(--accent-color, #3b82f6);
|
||||
opacity: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.current-indicator.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.option.has-subtitle {
|
||||
min-height: 44px;
|
||||
padding: 4px 12px;
|
||||
|
||||
Reference in new issue
Block a user