Path C: legacy CPAL output-sink enumeration was inlined directly in
commands/playback.rs (cfg-gated Linux + non-Linux variants), with
no equivalent in any domain crate. Extract a domain function
`qbz_audio::output_sinks::list_output_sinks` that returns the same
{name, description, volume, is_default} shape (mirrored as the new
public `OutputSinkInfo` struct) for both Linux and non-Linux
platforms, preserving the original logging cadence and the rule
that name == CPAL device name (so saved IDs reopen correctly).
Add v2_get_pipewire_sinks in commands_v2/audio.rs that calls into
qbz_audio directly — no `crate::commands::*` delegation. Updates the
two frontend call sites (AudioOutputBadges.loadStatus and
SettingsView.loadAudioDevices). Legacy command in
commands/playback.rs is preserved untouched; the audio-protected
files (pipewire_backend.rs, audio_settings.rs, player init_device)
are not modified.
cc
Path B: legacy command read state.player.state.current_device() and
state.player.state.is_playing() — both methods already exist on the
qbz-player Player.state in the V2 architecture. No domain extraction
needed; the v2 command reads them via CoreBridge.player() like the
existing v2_get_hardware_audio_status does.
Adds v2_get_audio_output_status returning V2AudioOutputStatus
(serialized to the same {device_name, is_playing} shape so the
AudioOutputBadges frontend type is unchanged). Updates both
AudioOutputBadges call sites (initial loadStatus + the 1s polling
interval). The legacy command in commands/playback.rs is preserved
untouched.
cc
AirPlay was scaffolded in both crates/qbz-cast/src/airplay and
src-tauri/src/cast/airplay but never worked at runtime: the RAOP
sender required an Apple MFi-licensed stack with no free/open
alternative, and the backend state was already disabled via the
commented `.manage(airplay_state)` line in src-tauri/src/lib.rs.
The CastPicker UI already hid the AirPlay tab pending streaming
support that was never going to ship.
Delete:
- crates/qbz-cast/src/airplay/ (entire directory)
- src-tauri/src/cast/airplay/ (entire directory)
- 10x v2_airplay_* commands from commands_v2/library.rs + their
tauri::generate_handler entries
- AirPlayError from qbz-cast errors
- 'airplay' from CastProtocol + every case arm in castStore.ts,
CastPicker.svelte, AudioOutputBadges.svelte
- Corresponding i18n keys if present
Zero behavior change for Chromecast / DLNA / QConnect / MPRIS. If
AirPlay ever becomes feasible we'll revisit from scratch with a
real streaming backend.
cc
AudioOutputBadges (DAC/HW/EXC/CAST) used to update only on mouseover
or during the 1 Hz polling loop — which itself was gated to
dac_passthrough or ALSA, so PipeWire users without passthrough never
saw the active device reflect a new stream.
The comment in the file noted that an earlier $effect watching
is_playing caused an infinite loop because loadStatus() writes
outputStatus, which re-triggered the effect. That's fixed here by
subscribing to the player store with manual edge detection: we only
call loadStatus() when is_playing transitions false→true or when
track_id changes. Neither signal is written by loadStatus(), so
there is no loop.
cc
Mono font only kept in Settings > Dev > Logs (LogsModal),
command blocks, and keyboard shortcut keys. All other UI
elements (durations, metadata, badges) now use --font-sans.
Replace transition: all across 63 files with explicit property lists
(color, background-color, border-color, opacity). Special cases:
- NowPlayingBar control-btn: includes transform for :active scale
- CoverflowPanel: transform + opacity for 3D carousel
- ImmersiveHeader: max-width + opacity for expand animation
The device name was only read on mount and on hover, so it stayed
as "No device" until the user hovered the badges area. Now the
1-second poll also reads get_audio_output_status (cheap in-memory
read) alongside hardware status, keeping the device name current.
Fall back to settings.output_device when the player hasn't reported
a device yet (before first playback). Previously showed "No device"
even though a device was configured in audio settings. Also change
the final fallback from "No device" to "System Default" since no
configured device means the system default is being used.
The $effect watching outputStatus.is_playing would call loadStatus(),
which updates outputStatus, triggering the effect again. This caused
~30 calls per second when hovering over the audio badges.
Removed the problematic $effect - mouseenter handler and 1s polling
for hardware status are sufficient.
DAC badge tooltip now says "Active" instead of "Bit-perfect" since
PipeWire passthrough is best-effort, not guaranteed bit-perfect like
ALSA Direct hw:X,Y. Added a brief note in Settings when DAC Passthrough
is enabled clarifying the distinction. Also i18n'd the hardcoded
dacPassthroughTooltipOverride string in SettingsView.
Moved all hardcoded English/Spanish tooltip text to i18n keys
under audioBadges section in all 4 locale files.
Removed dead dacTooltip derived property (unused in template).
- Clear both L1 memory and L2 disk caches when quality setting changes
This ensures cached tracks at wrong quality are removed
- Fix device name not showing in popup on first hover
Now refreshes status on each hover instead of only when null
- Add quality downgrade indicator when stream quality is lower than
track's original quality (shows yellow arrow with tooltip)
- Store original track quality values for comparison with actual stream
- Add cast-aware badge that shows protocol name (DLNA/CAST/AIR)
- Show protocol-specific SVG icons in tooltip (dlna.svg, cast_audio.svg)
- Hide DAC passthrough and exclusive mode badges when casting
- Stop hardware polling when casting to avoid misleading info
- Add purple theme styling for cast badge consistent with cast UI
cpvsc
Add --alpha-* CSS variables (4-95%) to app.css that adapt to theme:
- Dark themes: white-based rgba
- Light themes: black-based rgba (auto-inverts for proper contrast)
Mass-replaced 100+ instances of rgba(255,255,255,...) across 23 files
with var(--alpha-*) for consistent theming support.
This fixes borders, overlays, and UI elements being invisible or
having poor contrast in light themes.
Adds scrolling animation when device names overflow tooltip width, borrowed from AlbumCard component pattern. Also fixes accessibility warning by adding role and aria-label to badge container.
This reverts the event-driven approach (9242b95) and restores lightweight
polling, but ONLY for hardwareStatus.
Problems with event-driven approach:
1. False yellow warnings: hardwareStatus had stale data from previous song
- Hardware: 88200 Hz (correct)
- Cached hardwareStatus: 44100 Hz (from previous song)
- Badge logic: 88.2 !== 44.1 → WARNING (false positive)
2. Lost device name: outputStatus never updated after mount
- loadStatus() only called once on mount
- outputStatus.device_name became stale
- Badge tooltip showed "Default" instead of "hw:4,0"
Solution - Lightweight polling (best of both worlds):
- Call loadStatus() once on mount (loads everything)
- Poll ONLY hardwareStatus every 1s (no device enumeration)
- get_hardware_audio_status just reads /proc/asound (very cheap)
- No CPAL device enumeration (no console spam)
- No get_pipewire_sinks calls (no ALSA errors)
Benefits:
- Badge always shows current hardware status (accurate resampling detection)
- Device name preserved (loaded once, doesn't change)
- Zero console spam (no device enumeration)
- Very lightweight (just read /proc/asound file)
- ~1000x cheaper than full loadStatus() polling
This gives us accurate bit-perfect validation without the overhead.
Changed hardware status polling from continuous (every 1s) to event-driven
(only when sample rate changes).
Rationale:
- Hardware sample rate ONLY changes when playing new song with different rate
- Continuous polling was overkill - 3600 unnecessary calls per hour
- If system is properly configured, status doesn't change mid-song
- Polling was originally added for "instant feedback" but wastes resources
Changes:
- Removed setInterval polling (lines 162-171)
- Added $effect that watches samplingRate prop changes
- Only calls get_hardware_audio_status when:
* samplingRate changes (new song or format change)
* Using DAC Passthrough or ALSA Direct (bit-perfect modes)
- Tracks previousSamplingRate to detect actual changes
Benefits:
- Zero backend calls during playback (unless rate changes)
- Still validates bit-perfect when it matters (new sample rate)
- Massive reduction in /proc/asound reads
- Better battery life, lower CPU usage
- Same UX - badge updates when song changes
Testing plan:
- Play songs with same sample rate → zero backend calls
- Play songs with different rates → update on each rate change
- Verify badge still shows correct status
- If issues found, can revert to 1s polling
The (() => { if (outputStatus?.is_playing) loadStatus(); }) was
causing massive console spam ("spam masivo") that continued even after
removing mouse from badge.
Root causes:
1. was cherry-picked from feature/ui-fixes for badge auto-update
2. But now we have 1s polling that already keeps data fresh
3. outputStatus object was being mutated/recreated frequently
4. This triggered repeatedly, calling loadStatus()
5. Each loadStatus() enumerated 12 CPAL devices + PipeWire sinks
6. CPAL enumeration causes ALSA errors: "Cannot get card index for 0"
Previous commit (0c3ca16) removed loadStatus() from onmouseenter, but
spam continued because kept triggering during playback.
Solution:
- Removed completely (lines 150-156)
- loadStatus() now only called once on mount
- Polling interval (1s) only calls get_hardware_audio_status
- get_hardware_audio_status just reads /proc/asound (no device enumeration)
Result:
- Console spam eliminated completely
- Badge still updates every 1s via polling
- Zero CPAL/ALSA enumeration during playback
- Massive reduction in CPU/battery usage
Root cause:
- onmouseenter was calling loadStatus() which enumerates ALL devices
- loadStatus() calls: get_audio_output_devices (12 devices), get_pipewire_sinks, get_hardware_audio_status
- Each hover triggered 3 expensive backend calls + CPAL/ALSA enumeration
- User reported "spam masivo en la consola" when hovering badge
Solution:
- Removed loadStatus() call from onmouseenter (line 180)
- Only set isHovering = true to show tooltip
- Polling interval (1s) already keeps hardware status fresh
- Tooltip now shows cached data instead of triggering new enumeration
Impact:
- Hover now instant with zero backend calls
- Console spam eliminated
- Battery/CPU usage reduced significantly
- Tooltip still shows current data via 1s polling
Add effect that reloads audio settings and status when playback begins, ensuring DAC/EXC badges show correct state without requiring hover interaction.
1. Change hardware volume to visible help text (like Offline settings)
- More critical info should be always visible
- Clearer explanation of experimental nature
2. Add blue HW badge for ALSA Direct mode
- Replaces DAC badge (mutually exclusive)
- Blue color simulates audio equipment LED
- Shows 'Hardware Direct: Bit-perfect ALSA' in tooltip
- Displays device name and sample rate
- Only visible when: ALSA Direct + hw plugin
3. Gapless logic already correct
- Only restricts gapless with DAC Passthrough (PipeWire)
- ALSA Direct can do gapless (same sample rate)
- Note: Different sample rates need investigation
Add effect that reloads audio settings and status when playback begins, ensuring DAC/EXC badges show correct state without requiring hover interaction.
User requested faster badge color changes. Previously badge only updated
on mount or hover, causing 2+ second delays.
Solution: Poll hardware status every 1 second when DAC Passthrough is
enabled. This provides instant visual feedback without affecting playback:
- Only polls when dac_passthrough is true (conditional)
- Only reads hardware status (no playback impact)
- Cleanup on component unmount
- Fast enough for instant feedback, light enough for performance
Addresses user feedback: badge updates now appear instantly instead of
requiring hover or waiting multiple seconds.
PROBLEM:
When user selected a specific audio device (e.g., Cambridge Audio DAC),
audio would always play through system default device (laptop speakers)
instead of the selected device.
ROOT CAUSE:
All calls to reinit_audio_device() were passing device: null instead of
the actual selected device name. When device: null is passed, the Player
falls back to system default regardless of user selection.
Affected functions:
- handleOutputDeviceChange() - Primary bug, selected device ignored
- handleExclusiveModeChange() - Reset to default when toggling
- handleDacPassthroughChange() - Reset to default when toggling
- handleGaplessPlaybackChange() - Reset to default when auto-disabling DAC
- handleCrossfadeChange() - Reset to default when auto-disabling DAC
FIX:
Created getCurrentDeviceSinkName() helper that:
- Returns the internal sink name for selected device
- Returns null only for "System Default" selection
- Used consistently across all audio setting changes
SECONDARY CHANGES:
Improved DAC Passthrough warning messages in AudioOutputBadges:
- "ERROR: PipeWire is resampling" instead of generic warning
- Clearer tooltip guidance for fixing resampling issues
- Better distinction between active/warning/disabled states
This ensures device selection persists across all audio setting changes
and provides clearer feedback when bit-perfect playback is compromised.
Tooltip improvements:
- Consolidate duplicate tooltips into single unified tooltip
- Show device info + DAC status + sample rate in one place
- Remove native 'title' attributes that were causing conflicts
- Add visual indicators (colored dots) for setting states
- Display resampling warning with actual rates when detected
- Better structured tooltip with sections
Mutual exclusion enforcement:
- Validate settings on load in loadAudioSettings()
- Auto-disable Gapless and Crossfade if DAC Passthrough is enabled
- Prevents invalid state from persisted settings
This fixes the issue where users could have incompatible settings
active simultaneously after app restart.
AudioOutputBadges was importing currentTrack from playerStore, which doesn't
export a reactive store. This caused the app to crash with a 500 error.
Solution: Pass samplingRate as a prop from NowPlayingBar instead of trying
to import track data directly. This matches the existing pattern used by
other components like QualityBadge.
Backend changes:
- Add audio_diagnostics module to read actual hardware sample rate from /proc/asound
- New command get_hardware_audio_status() parses hw_params for real-time monitoring
- Add glob dependency for file pattern matching
Frontend (AudioOutputBadges):
- Implement 3-state DAC badge system:
- Green: DAC passthrough working correctly (rates match)
- Yellow: DAC passthrough ON but resampling detected (warning)
- Gray: DAC passthrough OFF (intentionally disabled)
- Dynamic tooltips showing actual hardware sample rate
- Compare file sample rate vs hardware sample rate
- Increase tooltip z-index to 9999 to prevent occlusion issues
- Add pointer-events: none to tooltip to prevent interaction issues
Frontend (SettingsView):
- Replace hardcoded "192 kHz" with dynamic hardware sample rate
- Load and display actual hardware audio status
- Show format details (e.g., S32_LE) when available
- Display "No active playback" when no audio is playing
- Add muted styling for inactive state
Frontend (QualityBadge):
- Add native tooltip showing tier and format
- Add cursor: help to indicate interactivity
i18n:
- Add "noActivePlayback" translations (EN/ES)
This allows users to verify bit-perfect playback is working and detect
resampling issues that would compromise audio quality.
Remove isExternal requirement for DAC Passthrough badge activation.
The badge now lights up when DAC Passthrough setting is enabled,
regardless of whether the device is detected as external or internal.
This fixes the issue where the badge would remain off even when
DAC passthrough was working (confirmed by 192kHz reaching hardware).
Changed quality-indicator to column flex layout with badges row below
the QualityBadge. Both badges now expand to fill the width equally,
matching the QualityBadge width.
Session restore:
- Add pendingSessionRestore state to playerStore
- When restored track is played, load from Qobuz first
- Seek to saved position after track loads
Audio badges:
- Always show DAC/EXC badges (very muted when off)
- Adjust margins and sizing for better alignment
- Validate device has supported output configs before using
- Fall back to default if saved device is invalid
- Reduces polling frequency to 10 seconds
- Add AudioOutputBadges component showing DAC/Exclusive mode status
- Badges are reactive to actual player state, not just settings
- Add device tooltip on hover showing current output device
- Create audioDeviceNames helper for ALSA name to friendly name mapping
- Use pretty names in Settings dropdown while storing raw names
- Add get_audio_output_status backend command
- Store current device name in player SharedState