Fix ALSA device filtering and quality badge hardware sample rate

Problem 1: ALSA backend showed only 1 device or listed plugins/virtual devices
that don't work for playback (null, lavrate, samplerate, etc.).

Solution: Filter aplay -L output to only include actual playback devices:
- Hardware devices (hw:, plughw:, sysdefault:CARD=, front:CARD=)
- System defaults (default, pipewire, pulse)
- Digital outputs (hdmi:, iec958:, surround)

Problem 2: Quality badge showed file sample rate (e.g., 44.1kHz) instead of
actual hardware sample rate (e.g., 192kHz) when DAC Passthrough was active.

Solution: ExpandedPlayer now polls hardware_sample_rate every second when
DAC Passthrough is enabled and displays the real hardware rate instead of
the file metadata rate.

Changes:
- alsa_backend.rs: Add is_valid_device() filter for playback devices
- ExpandedPlayer.svelte: Poll get_hardware_audio_status every 1s
- ExpandedPlayer.svelte: Use displaySamplingRate (hardware rate when DAC on)

This ensures the quality badge accurately reflects bit-perfect playback status.
This commit is contained in:
vicrodh committed 2026-01-17 18:08:18 -06:00
1 parent c0a5736c0f
commit 01022f2eea
2 files changed
+70 -7

No files matched your search

+21 -6
View File
@@ -44,11 +44,11 @@ impl AlsaBackend {
Ok(Self { host })
}
/// Enumerate ALSA devices using aplay -L for complete device list
/// Enumerate ALSA devices using aplay -L, filtering only playback devices
fn enumerate_alsa_devices(&self) -> BackendResult<Vec<AudioDevice>> {
let mut devices = Vec::new();
// Use aplay -L to get all ALSA device names (including virtual devices)
// Use aplay -L to get all ALSA device names
let output = Command::new("aplay")
.arg("-L")
.output()
@@ -62,12 +62,27 @@ impl AlsaBackend {
let mut current_device: Option<String> = None;
let mut current_description: Option<String> = None;
// Helper to check if device is valid for playback
let is_valid_device = |id: &str| -> bool {
// Only include actual hardware devices and useful virtual devices
id == "default"
|| id == "pipewire"
|| id == "pulse"
|| id.starts_with("hw:")
|| id.starts_with("plughw:")
|| id.starts_with("sysdefault:CARD=")
|| id.starts_with("default:CARD=")
|| id.starts_with("front:CARD=")
|| id.starts_with("surround")
|| id.starts_with("hdmi:")
|| id.starts_with("iec958:")
};
for line in stdout.lines() {
if line.is_empty() {
// Empty line marks end of device entry
if let (Some(id), Some(desc)) = (current_device.take(), current_description.take()) {
// Skip null device
if id != "null" {
if is_valid_device(&id) {
let is_default = id == "default";
devices.push(AudioDevice {
id: id.clone(),
@@ -90,7 +105,7 @@ impl AlsaBackend {
// Don't forget last device
if let (Some(id), Some(desc)) = (current_device, current_description) {
if id != "null" {
if is_valid_device(&id) {
let is_default = id == "default";
devices.push(AudioDevice {
id: id.clone(),
@@ -102,7 +117,7 @@ impl AlsaBackend {
}
}
log::info!("[ALSA Backend] Enumerated {} devices via aplay -L", devices.len());
log::info!("[ALSA Backend] Enumerated {} devices via aplay -L (filtered)", devices.len());
for (idx, dev) in devices.iter().enumerate() {
log::info!(" [{}] {} - {}", idx, dev.name, dev.description.as_deref().unwrap_or(""));
}
+49 -1
View File
@@ -1,9 +1,20 @@
<script lang="ts">
import { invoke } from '@tauri-apps/api/core';
import { X, Shuffle, SkipBack, Play, Pause, SkipForward, Repeat, Heart, List, Maximize2, MoreHorizontal, Cast } from 'lucide-svelte';
import QualityBadge from './QualityBadge.svelte';
import LyricsLines from './lyrics/LyricsLines.svelte';
import { startActiveLineUpdates } from '$lib/stores/lyricsStore';
interface HardwareAudioStatus {
hardware_sample_rate: number | null;
hardware_format: string | null;
is_active: boolean;
}
interface AudioSettings {
dac_passthrough: boolean;
}
interface LyricsLine {
text: string;
}
@@ -89,10 +100,47 @@
let volumeRef: HTMLDivElement | null = $state(null);
let isDraggingProgress = $state(false);
let isDraggingVolume = $state(false);
let hardwareSampleRate = $state<number | null>(null);
let dacPassthrough = $state(false);
const progress = $derived((currentTime / duration) * 100 || 0);
const hasLyrics = $derived(lyricsLines.length > 0);
// Use hardware sample rate when DAC passthrough is active, otherwise use track sample rate
const displaySamplingRate = $derived(
dacPassthrough && hardwareSampleRate
? hardwareSampleRate / 1000 // Convert Hz to kHz
: samplingRate
);
// Poll hardware sample rate when DAC passthrough is active
$effect(() => {
if (!isOpen) return;
async function loadStatus() {
try {
const [settings, hwStatus] = await Promise.all([
invoke<AudioSettings>('get_audio_settings'),
invoke<HardwareAudioStatus>('get_hardware_audio_status').catch(() => null)
]);
dacPassthrough = settings.dac_passthrough;
hardwareSampleRate = hwStatus?.hardware_sample_rate ?? null;
} catch (err) {
console.error('Failed to load audio status:', err);
}
}
loadStatus();
const pollInterval = setInterval(() => {
if (dacPassthrough) {
loadStatus();
}
}, 1000); // Poll every second when DAC passthrough is active
return () => clearInterval(pollInterval);
});
// Ensure lyrics updates run when ExpandedPlayer is open with synced lyrics
$effect(() => {
if (isOpen && isPlaying && lyricsSynced) {
@@ -200,7 +248,7 @@
<h2 class="artist">{artist}</h2>
<h3 class="album">{album}</h3>
<div class="quality-info">
<QualityBadge {quality} {bitDepth} {samplingRate} />
<QualityBadge {quality} {bitDepth} samplingRate={displaySamplingRate} />
</div>
</div>