- Add GenreFilterButton to Favorites > Albums toolbar
- Filter albums by genre name (multi-select supported)
- Use genreFilterVersion for reactivity tracking
Note: Artists tab doesn't have genre filter since artist
objects don't contain genre info directly.
- Allow multiple genre selection (checkbox behavior)
- Add getSelectedGenreNames() for multiple name retrieval
- Merge results from multiple API calls for featured albums
- Clear button now closes the popup
- Button shows "N genres" when multiple selected
- Add getSelectedGenreName() to genre filter store
- Add filterAlbumsByGenre() helper in HomeView
- Apply genre filter to Recently Played and Favorite Albums
- Fetch 3x more items when filtering to ensure enough results
- Reduce card height from aspect-ratio to fixed 36px
- Use neutral bg-tertiary background instead of colored gradients
- Horizontal layout with name and check circle
- Selected state uses accent-primary
- Better spacing to prevent overlap
Use onMount instead of $effect to load genres once.
Add initialized flag to prevent onFilterChange from firing
during initial subscription setup, which was causing
loadHome -> re-render -> loadGenres -> notify -> loadHome loop.
Remove genre_id from base schema and let migration add it.
This ensures existing databases don't fail when trying to create
index on non-existent column during initial schema execution.
- Add GenreFilterButton to home header
- Pass genre_id to fetchFeaturedAlbums API calls
- Reload home content when genre filter changes
- Featured sections now filter by selected genre
- Add reco_backfill_genres command to populate genre_id for existing events
- Add reco_needs_genre_backfill command to check if backfill is needed
- Fetches album details from Qobuz API and updates events in batches
- Includes rate limiting to avoid API throttling
- Add genre_id column to reco_events table
- Add migration to add column to existing databases
- Update RecoEventInput and RecoEventRecord structs
- Update insert_event and get_events_since to handle genre_id
- Add get_album_ids_without_genre() for backfill
- Add update_genre_for_album() for backfill
- Add optional genre_id parameter to client method
- Add optional genre_id parameter to Tauri command
- Qobuz API filters featured albums by genre when provided
- Add playPlaylistById to set queue and play first track
- Add queuePlaylistNextById to add playlist tracks next
- Add queuePlaylistLaterById to add playlist tracks later
- Add copyPlaylistToLibraryById using subscribe_playlist
- Add sharePlaylistQobuzLinkById to copy link to clipboard
- Connect all handlers to SearchView component
- Repositioned toggle to appear right after Streaming Quality dropdown
- Updated description to clarify it overrides quality setting when device
doesn't support the selected sample rate
Frontend changes for quality limiting feature:
- Add limitQualityToDevice state and handler
- Add AudioSettings interface fields for limit_quality_to_device and device_max_sample_rate
- Update handleBackendDeviceChange to send max_sample_rate when device changes
- Update handleOutputDeviceChange to find max_sample_rate from backend devices
- Add Settings UI toggle for "Limit Quality to Device"
This completes the quality limiting feature that prevents requesting tracks
that exceed the device's sample rate capabilities, ensuring bit-perfect playback.
- Add limit_quality_to_device setting (enabled by default)
- Add device_max_sample_rate field to store selected device's max rate
- Implement limit_quality_for_device() to restrict quality based on device
- Apply quality limiting in play_track and prefetch_track commands
- Add set_audio_device_max_sample_rate Tauri command
This ensures bit-perfect playback by not requesting tracks that
exceed the device's sample rate capabilities. When enabled:
- Devices <= 48kHz: limited to Lossless (44.1kHz)
- Devices <= 96kHz: limited to HiRes (up to 96kHz)
- Devices > 96kHz: allow UltraHiRes (up to 192kHz)
Add new audio setting to limit streaming quality based on device
capabilities. When enabled (default: true), QBZ will avoid requesting
tracks that exceed the device's max supported sample rate.
This ensures bit-perfect playback by preventing the need to resample
high-res tracks on devices that don't support them.
- Add limit_quality_to_device field to AudioSettings
- Add database migration for new column
- Add Tauri command for frontend integration
cc
- 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
- Add S24_3LE (S243LE) to format priority list as first choice
- Implement S24_3LE write handler with 3-byte packed conversion
- Add S24LE write handler (was missing explicit implementation)
- Reorder format priority: S24_3LE > S32LE > S24LE > S16LE > FloatLE
This fixes bit-perfect playback on USB DACs that only support
the S24_3LE format (SMSL, Topping, Fosi with TAS1020B chip).
Closes#30
cc
- Reduce FFT to 30fps with 16 bins (mirrored to 32 visual bars)
- Use 1024 FFT size for faster processing
- Extract dominant colors from album artwork
- Center artwork with track info and quality badge
- Bars render from edges inward with color gradient
- Increase artwork to 108px (20% larger)
- Reorder info: Artist (bold) > Album (italic) > Track
- Add album prop to VisualizerPanel
- Adjust text styling hierarchy
- Position track info at bottom-right with margin
- Reduce artwork to 90x90px
- Text aligned right, to the left of artwork
- Compact layout that doesn't obstruct visualization
- Add 'visualizer' to FocusTab type
- Add Visualizer tab with Activity icon in ImmersiveHeader
- Update VisualizerPanel with mirror mode (bars up and down from center)
- Integrate VisualizerPanel in ImmersivePlayer focus view
- Three-color gradient: teal -> purple -> pink
- Track info overlay with artwork, title, artist
- Instanced bar rendering with vertex/fragment shaders
- Receives binary frequency data via Tauri events
- R32F texture for efficient frequency data upload
- Cyan-to-magenta gradient coloring
- Auto-enables/disables backend when mounted/unmounted
- 64 frequency bars with logarithmic mapping from backend
- Add VisualizerTap to Player for sample capture
- Create wrap_source helper in audio thread
- Wrap all audio sources with TappedSource at 4 append points
- Player now captures samples for visualization when enabled
- Zero overhead when disabled (fast atomic check)
Audio samples flow: Source -> TappedSource -> RingBuffer -> FFT Thread -> Frontend
- Doubled horizontal range to ±120px
- Doubled vertical range to ±35px
- Added power curve (0.7) for hypersensitivity near center
- Small mouse movements now produce dramatic response
- Vinyl stays perfectly centered, only rotates
- Removed X and Y parallax from vinyl position
- Removed parallax uniform from shader
- Only the cover art (sleeve) responds to mouse movement
- Set rotation speed to 33.333 RPM (0.0582 rad/frame at 60fps)
- Remove Y-axis parallax from vinyl position (stays centered)
- Remove Y parallax from shader for perfectly concentric rotation
- Add mipmaps and trilinear filtering for anti-aliasing
- Enable anisotropic filtering when available
- Use PNG instead of SVG for better WebGL compatibility
- Extreme horizontal parallax: cover 200px, vinyl 120px
- Reduced vertical parallax for subtle depth
- Shader parallax: 0.15 horizontal, 0.05 vertical
- Disc appears to slide out of sleeve with minimal mouse movement
- Add padding around vinyl (scale UV by 1.15x)
- Discard pixels outside bounds to avoid edge artifacts
- Much more aggressive parallax: cover 80/50px, vinyl 50/35px
- Shader parallax offset increased to 0.08
- Vinyl size reduced to 85% for more breathing room
- Updated qbz-logo.svg and qbz-logo.png
- Added qbz-vinyl-v3.svg for VinylPanel
- Regenerated all Tauri icons from new logo
- Updated Android icons and favicon
- Use WebGL2 canvas for vinyl rendering instead of CSS
- Load qbz-vinyl-v3.svg as texture
- Shader-based rotation when playing
- Parallax effect via uniforms (no DOM updates)
- Album cover slides left to reveal vinyl
- Much lower CPU usage than CSS animation
- Double parallax sensitivity (30/20px cover, 18/12px disc)
- Cover and disc now vertically centered together
- Cover slides 15% left when playing to reveal disc
- Simplified transform calculations
- Correct z-ordering: cover in front (z-10), disc behind
- Cover slides left when playing to reveal disc
- Add mouse parallax effect for depth
- More visible grooves with alternating brightness
- Neutral gray colors instead of amber vintage
- Use system typography instead of Oswald
- Radial gradient on disc for realistic shading
- Add VinylPanel.svelte with spinning vinyl disc animation
- Album cover slides left when playing to reveal disc
- CSS-based grooves (40 rings) and amber center label
- Vintage typography with "33 1/3 RPM" badge
- Add 'vinyl' as new Focus tab option in ImmersiveHeader
- Fix MPRIS/PipeWire volume feedback loop by checking if volume
actually changed before calling setVolume
- Increase animation speed to 1.8x for more dynamic movement
- Add per-channel histogram normalization to expand color range
- Boost saturation to 2.5x and brightness to 0.85
- Add 12% red channel boost for warmer tones
- Target color range 30-240 to lift shadows and preserve highlights
Problem: brightness(0.5) made whites gray while blacks stayed black,
causing dark areas to dominate even in mostly-white artwork.
Solution:
- Increased brightness from 0.5 to 0.7
- Added shadow lift: maps 0-255 to 25-255
- Black now becomes dark gray (~10% brightness floor)
- Whites remain visible and balanced
Result: 'Panda/cow' effect - both light and dark areas visible.
New effects added:
- Layer rotation: each layer rotates slowly (1-2° range)
- Scale breathing: zoom levels pulse in/out per layer
- Pulsing vignette: edge darkening varies over time
- Increased drift amplitudes: far ±0.20, mid ±0.14, near ±0.08
- Stronger brightness breathing: ±12% (was ±8%)
All effects are decorrelated (different speeds/phases) for organic feel.
Default intensity now 1.0 for maximum visibility.
Replace expensive properties with GPU-accelerated ones:
- transform: scaleX() instead of width (no layout recalc)
- Solid color instead of gradient
- Removed box-shadow blur (very expensive)
- Added will-change: transform hint
This runs entirely on GPU compositor thread.
Revert to glowing underline approach with QBZ brand blue (#5281ff):
- Text stays bright white (no background-clip: text)
- Underline sweeps from left to right during line duration
- Blue glow effect with box-shadow
- Much better readability than gradient text
Replace underline with metallic shimmer that sweeps across text:
- Mostly white (40% each side) with purple highlight band in center
- Gradient moves from right to left during line duration
- Uses ease-in-out for natural feel
- GPU-accelerated, single element, no JS polling
The background-clip: text technique made white appear gray.
New approach uses a pseudo-element underline that:
- Sweeps from left to right with the line duration
- Has purple glow effect (#a78bfa)
- Keeps text bright white and readable
- No impact on text color/brightness
Replace JS-polled karaoke with pure CSS animation:
- Calculate line duration from timing data (timeMs)
- Use CSS animation with background-position sweep
- Animation runs once per line change, no polling needed
- Uses purple (#c4b5fd) to white gradient for visibility
Data flow: Pass full LyricsLine objects (with timeMs) through
component chain instead of stripping timing data.
Problem: Lyrics split view consumed more CPU than coverflow due to:
- Polling every 80-200ms for karaoke progress updates
- Inline styles on ALL ~50+ lines recalculated on every render
- Transitions tracked on all lines (4 properties each)
Solution:
1. lyricsStore: Add setProgressTrackingEnabled() to skip progress
updates in immersive mode (only track line index changes)
2. LyricsLines: Use CSS classes for opacity instead of inline styles
in immersive mode (distance-1, distance-2, distance-3, distance-far)
3. LyricsLines: Add CSS containment (contain: layout style) to isolate
paint/layout recalculations
4. LyricsLines: Reduce transitions on non-active lines (2 vs 4 props)
5. ImmersivePlayer: Disable progress tracking on mount, re-enable on exit
Result: Immersive lyrics now updates only on line changes (~10-20/min)
instead of continuous progress updates (~300-750/min)
- Book collage: 3 album covers with left/center/right positioning
- Rotated side covers (-12deg/+12deg) with hover animation
- Custom tooltip: opens bottom-right with high z-index (1000)
- Info trigger: 24px with dark background for visibility
- Fade-in animation for tooltip appearance
Queue/History:
- Removed History as separate tab from header
- Added History as sub-tab within QueuePanel
- Sub-tabs: Queue | History with counts
- Removed history-focus from focus mode tabs
- Removed keyboard shortcut 'H' and '5' for history
Background transition:
- Added 400ms crossfade when artwork changes
- Works for both WebGL and CSS fallback renderers
- Fades out, loads new texture, fades back in
- Removed book collage (playlist.images are same image at different sizes)
- Using medium quality image (images[1]) for playlist cards
- Fixed z-index on Qobuz/QBZ badges (z-index: 10, above overlay)
- Increased badge icons by 2pt (Qobuz: 22px, QBZ: 19px)
- Replaced native title tooltip with Tooltip component for radio info
- Shows 3 covers like album spines when available
- Center cover fully visible, left/right at -15% offset
- Falls back to medium-quality single image if < 3 images
- Uses images[1] (medium) instead of images[0] (largest)
Working state:
- Atmospheric background (no recognizable artwork)
- Multi-sample blur in shader
- Gradient tint (cool top, warm bottom)
- Oswald font for lyrics (28px/30px)
- Lyrics panel 700px wide
- Text shadow for contrast
- Karaoke effect disabled in immersive
Ready for Heavy Immersion shader upgrade.
- Font size: 28px inactive, 30px active
- Disabled background-clip karaoke effect in immersive mode
- Active line now pure white (#ffffff)
- Past lines dimmed to 35% opacity
Shader:
- Fixed luminance formula that was killing blue (30% blend instead of 100%)
Lyrics:
- Added Oswald font for immersive lyrics
- Increased font size by 2pt (26px inactive, 28px active)
- Active lines now pure white with higher weight for clarity
Implement proper atmospheric background pipeline:
- Random NON-CENTERED crop (150-300% zoom) to break composition
- Extreme blur at small size (destroys all semantic detail)
- Scale up with smoothing for additional blur
- Color adjustments (saturation 1.4x, brightness 0.5x)
- Vignette overlay
The background is now an unrecognizable color field derived from
artwork colors. Acceptance test: "Can I identify the album from
the background alone?" must be NO.
Applies to both WebGL and CSS fallback renderers.
- texture-loader.ts: Multi-pass blur (4×15px) at 256x256 for CSS-equivalent quality
- ambient.frag: Simplified to UV drift, zoom breathing, and color breathing only
- Blur computed ONCE at load time, shader only does motion effects
- No per-frame blur, image processing, or texture uploads
- Source canvas now 256x256 (was 64x64)
- Light pre-blur (8px), GPU shader does main blur
- Increased shader blur radius and samples (16 samples)
- Should produce much smoother results
- Multi-sample blur: 12 samples in circular pattern
- Noise-based UV distortion to break up pixelation
- Random sample offsets for organic blur effect
- All blur computation is GPU-side (no CPU cost)
- Canvas size back to 64x64 (like original)
- Blur radius 20px applied 3 times (20 + 20 + 10 = ~50px total)
- Expanded canvas for blur overflow, then crop to final
- Brightness 0.55, saturation 1.3 (matching original)
Blur fix:
- Use three separate canvases (source, blur, final)
- Apply blur filter when drawing BETWEEN canvases (not to self)
- Canvas size reduced to 16x16 for extreme blur when scaled
- Double blur pass for extra smoothness
CPU fix:
- Reduce animation from 30fps to 15fps
- Slow ambient motion doesn't need high frame rate
- Reduce canvas to 32x32 (was 128) for more natural scaling blur
- Apply triple-pass blur technique:
1. Scale down (interpolation blur)
2. Apply blur filter and redraw
3. Third pass for extra smoothness
- Adjust brightness to 0.45 (darker) and saturation to 1.3
- Lower JPEG quality to 0.7 (blur hides artifacts anyway)
- ImmersiveBackground now checks WebGL2 availability on mount
- Dynamically imports ImmersiveAmbientCanvas when WebGL2 available
- Falls back to CSS blur when WebGL2 unavailable
- New props: enableAmbient (default true), ambientIntensity
Automatic renderer selection:
1. Check BUILD_IMMERSIVE_ENABLED (compile-time)
2. Check isRuntimeEnabled() (user preference)
3. Check isWebGL2Available() (browser capability)
4. If all pass: use WebGL2 with ambient motion
5. Otherwise: use CSS fallback (original behavior)
- Add utils/texture-loader.ts with:
- Async pre-blur generation using canvas 2D
- Blur cache (max 20 entries)
- Request cancellation for rapid track changes
- Abort signal support throughout
- Update ImmersiveAmbientCanvas.svelte:
- Use texture-loader for blurred artwork
- Handle cancellation gracefully
- Report texture metrics
- Clean up pending loads on destroy
Pre-blur is computed once per track change (CPU), then uploaded
to GPU texture. No per-frame blur computation.
- Add utils/webgl-utils.ts with context creation, shader compilation,
texture loading, and cleanup utilities
- Add shaders/fullscreen.vert for fullscreen quad rendering
- Add shaders/static.frag for basic texture display
- Add shaders/index.ts for Vite ?raw shader imports
- Add ImmersiveAmbientCanvas.svelte component with:
- WebGL2 context management
- Context loss/restore handling
- Automatic resize handling
- Performance metrics reporting
- Update app.d.ts with shader file type declarations
This is a static renderer only - no animation yet.
Phase 2 will connect to real artwork with pre-blur.
- Create src/lib/immersive/ module structure
- Add types.ts with ImmersiveState, ImmersiveConfig interfaces
- Add config.ts with build-time and runtime feature flags
- Add ImmersiveRenderer.ts with lifecycle management and stores
- Add index.ts as public API boundary
- Add VITE_IMMERSIVE_ENABLED to vite.config.js
- Add app.d.ts for TypeScript env declarations
Build without immersive: QBZ_IMMERSIVE=false pnpm build
No visual changes yet - this is capability detection and guards only.
- Create HistoryPanel component for viewing playback history
- Add 'history' tab to split mode tabs
- Add 'history-focus' tab to focus mode tabs
- Sync history when immersive mode opens (fix coverflow prev tracks)
- Keyboard shortcuts: H for history, 5 for history-focus
- Rename old CoverflowPanel to StaticPanel (single artwork view)
- Create new CoverflowPanel with actual coverflow effect:
- Shows 5 album covers (2 prev, current, 2 next)
- 3D perspective rotation on side covers
- Click on side covers to navigate queue
- Smooth animation on track change
- Add 'static' tab to focus mode (4 tabs total)
- Update keyboard shortcuts: 1-4 for focus tabs