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)
1. play_track now returns PlayTrackResult with format_id from Qobuz API
- Returns actual format (5=MP3, 6=FLAC16, 7=FLAC24, 27=Hi-Res)
- Returns None for cache hits (format unknown)
2. Frontend updates track.format based on format_id
- QualityBadge can now correctly show "MP3" when format_id=5
- Previously badge showed "CD" even for MP3 because format was empty
3. Device name now set after backend system init
- PipeWire and ALSA backends now set current_device from settings
- Fixes tooltip showing no device when using backend system
When streaming_only is enabled, prefetch is now skipped entirely.
This ensures tracks are always streamed fresh from the network
without being pre-cached in the background.
When streaming_only is disabled, prefetch works normally.
Logs now clearly show:
- [CACHE HIT] when playing from memory/disk/offline cache
- [STREAMING] when streaming from network with cache_after flag
- [DOWNLOAD] when using full download mode
- [CACHED] / [NOT CACHED] after download completes
- [Playback Settings] shows stream_first, streaming_only, buffer values
Add streaming_only setting that skips L1+L2 cache writes while still
respecting offline cache (local-first). When enabled:
- download_and_stream() skips final cache.insert()
- Standard download path skips cache.insert()
- Offline cache continues to work normally
This reduces storage usage for users who prefer pure streaming.
Issue #34 - Display actual stream quality, not track metadata
Backend changes:
- Add sample_rate and bit_depth to PlaybackEvent
- Add stream quality tracking to SharedState
- Extract bit_depth from FLAC/M4A headers in streaming path
- Player sets stream quality when decoding audio
Frontend changes:
- Update PlaybackEvent interface with new fields
- Update currentTrack with actual stream quality from backend
This allows the UI to show the actual quality being played (e.g., 44.1kHz
when streaming a 192kHz track at CD Quality) instead of the track's
maximum advertised quality.
- Log requested vs returned format_id and sample_rate in get_stream_url_with_fallback
- Log quality_str, parsed quality, and format_id in play_track and prefetch_track commands
- Add console.log in getStreamingQuality for frontend debugging
This will help diagnose why 192kHz is played despite lower quality settings.
- Use lazy_static to create a single reusable HTTP client instead of
creating a new client for each stream info request
- Add retry logic (3 attempts with 100ms delay) to handle transient
network/TLS initialization failures
- This fixes 'Failed HEAD request: builder error' toasts that required
pressing play button twice
Quality was hardcoded to UltraHiRes in backend. Now:
- Frontend passes quality from localStorage to play_track command
- Backend parses quality string to Quality enum
- All prefetch and stream URL calls use the user's preferred quality
- Pass track duration from frontend to streaming playback for seekbar
- Add StreamInfo struct with speed measurement during probe
- Implement StreamingConfig::from_speed_mbps() for adaptive buffer sizing
- Add play_streaming_dynamic() method using speed-based buffer config
- Enhanced download logging with progress %, speed (current/avg), and ETA
- Auto-cache streamed tracks for instant replay
The seekbar now shows playback progress during streaming by using
the track's known duration from metadata rather than detecting it
from incomplete audio data.
- Add PlayStreaming variant to AudioCommand enum
- Add play_streaming() method to Player for starting stream-first playback
- Modify play_track command to check stream_first_track setting
- Add get_stream_info() for HEAD request to get content-length
- Add extract_audio_format_from_header() to probe audio format from initial bytes
- Add download_and_stream() for async chunked download with buffer push
- Update BufferedMediaSource with create_reader() for independent read positions
- Add has_min_buffer() method for playback readiness check
- Implement Clone for BufferWriter
Phases 3-4/7: Conditional streaming path with async download integration
cpvsc
COMPILATION ERROR FIXED:
- audio_settings field was private
- Can't access from commands module
SOLUTION:
- Added public method: Player::reload_settings(settings)
- Encapsulation preserved
- Commands use public API
VERIFIED: cargo check passes
PROBLEMA ENCONTRADO:
1. Frontend guardaba backend_type correctamente ✅
2. Player leía settings al inicio (backend_type = None) ✅
3. Usuario cambiaba backend en UI → se guardaba en BD ✅
4. Pero Player en memoria tenía settings VIEJOS ❌
5. reinit_audio_device usaba legacy CPAL sin backend ❌
SÍNTOMAS:
- "Backend system not configured, using legacy CPAL path"
- "Device '...' not found, using default"
- Audio salía por laptop speakers, no por DAC
SOLUCIÓN IMPLEMENTADA:
1. init_device() ahora intenta backend system primero:
- Lee thread_settings.backend_type
- Si configurado, llama try_init_stream_with_backend()
- Fallback a legacy CPAL si falla
2. reinit_audio_device() recarga settings desde BD:
- Lee AudioSettings desde database
- Actualiza Player.audio_settings en memoria
- Luego llama reinit_device()
- Garantiza que Player siempre tiene config fresca
AHORA FUNCIONARÁ:
- Cambiar backend → se guarda → se recarga → se usa
- Cambiar device → se guarda → se recarga → se usa
- PULSE_SINK se setea correctamente
- Audio debe salir por Cambridge DAC
TESTING REQUERIDO:
Ir a Settings → Audio → Backend: PipeWire → Device: DacMagic Plus
Reproducir música → debería salir por DAC
Logs now show:
- Device index
- Device name
- Is default
- Supported configs (channels/sample rates)
This helps identify devices when pretty names aren't clear.
PREVIOUS WRONG APPROACH (commit 212aae5):
- Used pactl to enumerate devices (PipeWire sink names)
- Tried to open those sink names with CPAL
- CPAL couldn't find PipeWire sink names → fell back to default
- Attempted workaround: change system default with set_pipewire_default_sink
- UNACCEPTABLE: Changed system-wide default, affecting all apps
ROOT CAUSE (identified by ChatGPT research):
- CPAL can open specific devices by name (research was correct)
- BUT: Name mismatch between pactl sinks and CPAL device names
- pactl returns: "alsa_output.usb-Cambridge_Audio..." (PipeWire sink)
- CPAL sees: "DacMagic Plus Analog Stereo" (user-friendly name)
CORRECT SOLUTION:
- Use CPAL for BOTH enumeration AND opening devices
- CPAL device names are consistent: enumerate → save → find → open
- No need to change system default
- Device selection only affects QBZ
CHANGES:
1. Rewrote get_pipewire_sinks() on Linux to use CPAL instead of pactl
2. Removed all calls to set_pipewire_default_sink()
3. Pass CPAL device names directly to reinit_audio_device()
4. CPAL can now find and open devices because names match
BENEFITS:
- ✅ Device selection only affects QBZ (not system-wide)
- ✅ No regresions
- ✅ No features lost
- ✅ Works exactly like librespot (Spotify client in Rust)
- ✅ Consistent with rodio/CPAL architecture
TESTING REQUIRED:
1. Select Cambridge DAC in QBZ → audio plays through Cambridge
2. Open Firefox/other app → uses its own device (unaffected)
3. Toggle devices in QBZ → changes correctly
4. Restart QBZ → restores saved device
5. DAC passthrough bit-perfect → works
The previous implementation tried to map ALSA device names (from cpal)
to PipeWire descriptions, but the naming schemes are completely different:
- cpal returns names like "plughw:1,0"
- pactl returns sink names like "alsa_output.usb-Cambridge_Audio_..."
This fix changes the approach:
1. Use PipeWire sinks directly for the device dropdown (already filtered
to real outputs and have friendly descriptions like "DacMagic Plus")
2. Store the PipeWire sink name when user selects a device
3. Set the PipeWire default sink using pactl when device is selected
4. Use rodio's default device (which follows PipeWire's default)
This ensures users see 4-5 meaningful devices with friendly names
instead of 30+ raw ALSA device names.
Backend changes:
- Add set_pipewire_default_sink command (pactl set-default-sink)
- Register new command in lib.rs
Frontend changes:
- Use pipewireSinks directly for device dropdown
- Display sink description (friendly name)
- Store sink name (for pactl)
- Call set_pipewire_default_sink when device changes
- Remove unused AudioDevice interface and getDevicePrettyName import
This commit implements proper sample rate passthrough for high-quality
audio playback, addressing issue #10 where DAC sample rates were locked
at 44.1kHz regardless of file quality.
Core Changes:
- Modified Player to accept and store AudioSettings (exclusive mode, DAC passthrough)
- Updated AudioCommand::Play to include sample_rate and channels metadata
- Created create_output_stream_with_config() to build custom StreamConfig
- Added sample rate tracking to detect changes between tracks
- Modified Play handler to recreate OutputStream when sample rate changes
- Updated play_data() to extract audio metadata before playback
- Modified lib.rs to read AudioSettings on startup and pass to Player
Device Detection Improvements (Issue #4):
- Added display_name and device_type fields to AudioDevice struct
- Implemented get_pipewire_device_descriptions() to extract friendly names
- Updated get_audio_devices() to map technical names to user-friendly names
- Added device type detection (USB, HDMI, PCI, Bluetooth)
Technical Details:
- Audio files are decoded twice: once for metadata, once for playback
- Stream is recreated only when sample rate or channel count changes
- Falls back to default device config if custom config fails
- Supports both PipeWire and PulseAudio device naming via pactl
- Logs sample rate transitions and OutputStream configuration
Files Modified:
- src-tauri/src/player/mod.rs: Core playback engine changes
- src-tauri/src/lib.rs: Initialize Player with AudioSettings
- src-tauri/src/commands/playback.rs: Device enumeration improvements
Tested: Compiles successfully on Linux with Rust 1.92
L1 (Memory): 300MB for ~3-4 Hi-Res tracks, instant access
L2 (Disk): 500MB playback cache at ~/.cache/qbz/playback/
Flow:
- When track evicted from memory, saved to disk cache
- When loading: check memory → disk → download cache → network
- Tracks promoted back to memory when accessed from disk
This reduces re-downloads when navigating back to recently played
tracks in mixed playlists.
- Stop playback immediately when starting a Qobuz track to prevent
previous track from continuing during download
- Show buffering toast while downloading Qobuz audio
- Skip buffering UI for local tracks since they load instantly
- Add is_local field to QueueTrack for prefetch optimization
- Update prefetch to skip local tracks (no network needed)
- Update all queue track builders to include is_local flag
- Changed download quality from HiRes to UltraHiRes to support 24-bit/192kHz
- Updated playback streaming to use UltraHiRes for highest quality
- Added debug logging for artwork path storage and retrieval
- Quality fallback will still work for lower subscription tiers
cp
- Add conditional compilation for get_pipewire_sinks():
- Linux: Uses pactl to get PipeWire/PulseAudio sinks with volume info
- macOS/Windows: Uses cpal to enumerate audio devices (no volume)
- AboutModal: Detect platform and show macOS or Linux label
- Easter egg: Tux penguin is 64px on Mac (asserts dominance)
- 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
- SQLite-based index for tracking cached tracks
- Disk-based storage for audio files (~/.cache/qbz-nix/audio/)
- Download progress events via Tauri events
- LRU eviction with configurable cache size limit
- Integration with play_track for automatic local playback
- Open cache folder command for system file manager
- Update MPRIS position every time get_playback_state is polled (~500ms)
- Update MPRIS position when seeking
- Add Default derive to PlaybackState for error fallback
- System media controls now show accurate playback progress
- Add get_audio_devices command to list available output devices
- Show real audio devices in Settings dropdown
- Persist streaming quality preference in localStorage
- Persist 'prefer highest available' setting
- Add AudioCache module with LRU eviction (500MB default)
- Cache downloaded tracks in memory
- Prefetch next track in queue while current track plays
- Play from cache instantly when track is already cached
- Background prefetch task runs without blocking playback
Backend (Rust):
- Add souvlaki crate for MPRIS support on Linux
- Create media_controls module with MediaControlsManager
- Track metadata (title, artist, album, duration, cover)
- Playback state (playing/paused/stopped)
- Wire pause/resume/stop commands to update MPRIS
- Add set_media_metadata Tauri command
Frontend (Svelte):
- Call set_media_metadata when track starts playing
- Send track info to MPRIS for system integration
Now shows track info in system media controls and
supports media key events (logged for now).
- Add app.css import to +layout.svelte (fixes broken/light UI)
- Add logging for audio debugging
- Add image fallbacks for album and track artwork
- Fix sidebar/main-content layout (flex-shrink, min-width)
- Remove placeholder data from HomeView
- Add LoginView and SearchView components
- Update window title and branding to QBZ