The library scanner's album_root_dir() only knew to skip disc folders
(Disc 1, CD2, etc.) when resolving the album directory. Files stored
in quality subfolders like artist/album/FLAC 24-bit - 96 kHz/track.flac
caused each encoding variant to appear as a separate album card.
Add is_encoding_folder() to detect common audio format directory names
(FLAC, MP3, AAC, WAV, etc.) and make album_root_dir() skip past both
encoding and disc subdirectories, handling chained nesting too.
Fixes the workaround from #116.
Allow users to cancel an in-progress album download. The current track
finishes but no further tracks are started. A cancel button appears in
the progress bar during download, and a cancelled state shows how many
tracks completed. Closes#133.
Switch from CSS grid to CSS multi-column layout so categories pack
tightly top-to-bottom instead of creating large empty rows when
columns have uneven heights.
Add Shift+M toggle shortcut for mini player mode (main and mini window).
Add 1-5 number keys for surface switching (micro/compact/artwork/queue/lyrics)
in mini player context. Add playback shortcuts (Space, Ctrl+Arrow) to mini
player window. Fix X button to close app (or hide to tray) instead of
duplicating the Expand button behavior. Closes#134.
Previous shader was too subtle — drift, rotation, and breathing were
imperceptible with the smooth 8x8 texture. Now with much larger values:
- Drift: 0.05 -> 0.20 max (Lissajous with two frequencies)
- Scale breathing: 2% -> 12% range
- Rotation: 0.86deg -> 5deg range
- Added warm/cool color shifting over time
- Stronger vignette pulsing and brightness breathing
Safe because the 8x8-sourced texture has zero structure to reveal.
CSS filter:blur() may not work reliably in all WebView environments,
leaving artwork structure visible as rectangular artifacts. New
approach: draw the full artwork at 8x8 resolution (64 pixels) where
no structure can physically survive, then scale up to 128x128 with
high-quality bilinear smoothing. One optional blur pass for polish.
The 3-layer parallax shader created visible rectangular artifacts on
dark/uniform artwork. Each layer sampled at different scale/offset,
and the spatial weighting transitions between layers showed as
geometric seams regardless of offset magnitude.
New shader uses a single texture sample with organic Lissajous drift,
gentle rotation, scale breathing, pulsing vignette, and vertical tint.
The pre-blurred texture handles the visual complexity — the shader
just adds subtle life without geometric artifacts.
Two issues caused visible dark rectangles on simple/dark artwork:
1. Texture normalization always mapped to [30-240] regardless of source
brightness. For dark artwork (Metallica), this amplified noise 5x and
turned a dark album into a light gray background. Now uses
brightness-aware blending: dark artwork gets gentle lift (preserves
mood), colorful artwork gets full normalization.
2. Shader layer offsets were too large (up to 20% of texture), causing
visible layer boundaries where each sampling layer showed different
parts of the amplified texture. Reduced offsets by ~60% and tightened
scale ranges for smoother parallax.
CLAMP_TO_EDGE stretched edge pixels when ambient shader UVs exceeded
[0,1] range, creating visible dark rectangles on simple/dark artwork
(e.g. Metallica's black album). MIRRORED_REPEAT seamlessly mirrors
the texture instead. Also removed the hard clamp() in the fragment
shader so the mirrored wrapping actually takes effect.
Group immersive-related settings under a new "Immersive" subsection
at the bottom of Settings > Appearance, in order:
1. Immersive Default View
2. Composition (collapsible)
3. Disable Blur Background
4. Animation Frame Rate (collapsible)
Also fix chevron alignment in collapsible sections to be right-aligned
(justify-content: space-between) and clean up unused CSS classes.
- Replace native <select> with Dropdown component for GSK renderer
- Fix collapsible section layout: title+chevron on one row, description below
- Apply section-title-row wrapper to both Animation Frame Rate and Composition
- Add divider between Animation Frame Rate and Mini Player Default View
- Remove unused .composition-select CSS
- Replace raw checkbox with Toggle component for blur background
- Use composition-subsection class for FPS section (adds top border)
- Use composition-title class for consistent 14px font size
- Move desc to section-note inside expanded content
- Add summary key for inline collapsed text
- Shorten title from "Immersive Animation Frame Rate" to
"Animation Frame Rate"
Replace raw text caret and h4 with the standard section-title +
section-summary + ChevronDown/Up icon pattern used by the composition
section. Remove duplicate description that now shows as summary.
Add gskRenderer and disableBlurBackground fields to CompositionProfile.
Update existing profiles: Xwayland Performance now disables blur.
Add new Max Performance profile (Cairo software renderer + blur off)
for maximum compatibility on stutter-affected systems.
Profile matching and apply logic now includes all new fields.
Part of #127.
Detects GPU (NVIDIA/AMD/Intel), display server (Wayland/X11), desktop
environment, and VM status. Computes optimal configuration based on a
decision matrix and writes it to the graphics settings DB.
Usage: qbz --autoconfig-graphics
Part of #127.
When enabled, replaces the expensive blurred album artwork background
with a solid dark color extracted from the artwork at tiny resolution.
Significantly reduces GPU load and fixes stutter on affected systems.
Setting is stored in localStorage (same as FPS settings) and applies
on next immersive mode entry.
Part of #127.
Add gsk_renderer field to graphics settings DB with UI dropdown
(Auto/GL/NGL/Vulkan/Cairo). Value is read at startup and set as
GSK_RENDERER env var. Env var override takes precedence. Also
included in --reset-graphics and reset now restores hw_accel to true.
Part of #127.
Add is_amd_gpu() and is_intel_gpu() detection functions alongside
existing is_nvidia_gpu(). Update GraphicsStartupStatus to expose
has_amd and has_intel to frontend. Log detected GPUs at startup.
Part of #127.
The hardware_acceleration toggle in settings was dead code — stored in
DB but ignored at startup (main.rs only checked QBZ_HARDWARE_ACCEL env
var). Now the DB value is read as the default, with env var override.
Fixes part of #127.
The ContextType enum and v2_set_playback_context match were missing
fav_q and top_q variants, causing an "Invalid context type" error that
blocked all playback from FavQ and TopQ views. Also wrap
setPlaybackContext calls in try/catch so context errors don't prevent
track playback.
Fixes#140
The "free(): corrupted unsorted chunks" crash was caused by secondary
windows (miniplayer) being closed twice during shutdown:
1. CloseRequested handler closes secondary windows before main closes
2. RunEvent::Exit handler tried to close them again
Calling close() on a window already mid-teardown causes WebKit's
EGL/GPU thread to hit a double-free during TLS cleanup, corrupting
the heap. This was visible even without audio playing.
Fix: use an AtomicBool flag (WINDOWS_CLOSED_BY_USER) set in
CloseRequested, checked in RunEvent::Exit. Secondary windows are
only closed in Exit if CloseRequested didn't already handle them
(e.g., SIGTERM from tauri dev hot reload).
Two fixes for issue #139 (window opens larger than screen):
1. Screen resolution clamping at startup: detect monitor size via
xdpyinfo/xrandr and clamp saved window dimensions to 95% of screen.
Prevents windows from opening larger than the display even if the DB
has valid-but-too-large values.
2. Physical-to-logical pixel conversion in resize handler: the Resized
event gives physical pixels but inner_size() expects logical pixels.
With scale factor > 1.0, the window would grow each restart because
physical values were saved and restored as logical. Now divides by
scale_factor before persisting.
Closes#139
When QBZ adjusts quality for device compatibility (e.g. requesting
Hi-Res instead of Ultra Hi-Res because the DAC doesn't support
176.4kHz), show a blue info tooltip instead of the yellow "Resampled
by system" warning. The new message clarifies that playback is
bit-perfect with no resampling — QBZ simply requested a compatible
quality from the source.
Two distinct tooltip cases:
- Yellow: "Resampled by system" (PipeWire/system is resampling)
- Blue: "Quality adjusted for device compatibility" (QBZ chose lower
quality, hardware plays natively)
The smart quality downgrade was only in the network fetch path, but
tracks served from cache (memory L1, disk L2, offline) bypassed it
entirely. Now all cache paths probe the FLAC header for sample rate
and skip the cache if the hardware doesn't support it, falling through
to the network path which re-requests at Hi-Res quality.
Also applies quality downgrade to prefetch so upcoming tracks are
fetched at compatible quality instead of caching incompatible data.
When ALSA backend detects hardware doesn't support a track's sample rate
(e.g. 176400Hz on a DAC that supports up to 192000Hz but not 176.4kHz),
automatically re-request the track from Qobuz at Hi-Res quality instead
of Ultra Hi-Res. This avoids resampling and maintains bit-perfect playback
by getting a version at a rate the hardware natively supports.
Adds device_supports_sample_rate() and get_device_supported_rates()
public API to qbz-audio for hardware capability queries via /proc/asound.
When transitioning from ALSA Direct to CPAL fallback (e.g. for rate
resampling), PipeWire auto-re-acquires the device after ALSA Direct
releases it. CPAL then can't open the device.
Suspend PipeWire sinks before creating CPAL stream in exclusive mode,
same pattern used in ALSA Direct DeviceBusy handler.
When ALSA Direct opens a PCM device and fails to set the rate, the
USB DAC doesn't release the device fast enough for CPAL to open it
afterwards. This caused "device busy" errors even with a delay.
New approach: read /proc/asound/cardN/stream0 to check supported
rates BEFORE opening the device. If the rate isn't supported, skip
ALSA Direct entirely and fall through to CPAL (which has rate
fallback logic). The device is never touched at the wrong rate.
When ALSA Direct opens a PCM device and fails to configure the rate,
the PCM handle is dropped but the kernel may not release the device
instantly. CPAL then fails with "Device busy" when trying to open it.
Add 100ms sleep after InvalidParams to let the kernel fully release
the device before CPAL attempts to open it. Also fix error messages
to show the effective (fallback) rate instead of the original rate.
When ALSA Direct can't handle a rate (e.g. 176.4kHz) and falls back
to CPAL, CPAL also fails because the hardware doesn't support it.
Port find_best_fallback_rate from PipeWire backend: queries supported
rates via CPAL, picks highest rate in same family (44.1k or 48k)
that's <= requested. 176.4kHz on a 96kHz-max DAC now falls back to
88.2kHz and rodio resamples, instead of failing entirely.
When hardware doesn't natively support a sample rate (e.g. 176.4kHz),
return None from try_create_direct_stream instead of a hard error.
This lets the player fall through to CPAL/rodio which can resample.
Applied to both hw and plughw paths — InvalidParams on rate now
triggers graceful fallback instead of killing playback.
Previous commit removed the miniplayer close from CloseRequested,
but that kept the app alive because Tauri won't exit while windows
remain open. Now closes secondary windows with close() (not
destroy()) in CloseRequested before the main window closes.
Also hides the miniplayer when close-to-tray activates.
The miniplayer window was being destroyed twice during shutdown:
once in CloseRequested and again in RunEvent::Exit. This caused
WebKit's TLS destructors to collide during process teardown,
resulting in SIGABRT in WebKitWebProcess.
Changes:
- Remove miniplayer destroy from CloseRequested (was premature)
- Guard global on_window_event to only handle main window
- In RunEvent::Exit, close all secondary windows (miniplayer, oauth)
with close() instead of destroy() for graceful WebKit cleanup
- Add 100ms delay after closing secondary windows to let WebKit
tear down EGL/GPU resources before process exit
Extends PR #136 (save-time validation) with load-time validation.
If the DB contains corrupt sizes (e.g. 9084748x62267212), get_settings()
now returns safe defaults instead of passing garbage to GDK/Cairo.
Also extracts validation into a shared helper and adds NaN/Inf checks.
When switching from PipeWire to ALSA Direct, PipeWire holds the ALSA
device open causing EBUSY on the first snd_pcm_open. Now detects
DeviceBusy, suspends the PipeWire sink via pactl, waits 200ms for
release, then retries. Applied to both hw and plughw paths.
v2_get_hardware_audio_status was reading sample_rate/bit_depth from the
legacy player state, which is never updated since playback runs through
the V2 player (CoreBridge). Now reads from V2 player first, falls back
to legacy only if CoreBridge isn't initialized yet.
cpal 0.15 returned "ALSA" from HostId::name(), cpal 0.17 returns "Alsa"
(now uses stringify! on the enum variant). Use eq_ignore_ascii_case()
for the comparison so it works regardless of cpal version.