Commit Graph

90 Commits

Author SHA1 Message Date
cucadmuh 8a38127bf7 fix(queue): respect rating filter in infinite queue top-up (#357)
Apply mix rating settings to infinite queue candidates and keep fetching random batches when filtered results are below the target size, so top-ups can still reach five tracks when possible.
2026-04-29 10:33:50 +03:00
Frank Stellmacher d2080588b9 fix(player): persist queue panel visibility + drop dead loudness binding (#336)
* feat(player): persist queue panel visibility across restarts

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(audio): drop unused resolved_loudness_gain_db binding

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 17:37:22 +02:00
cucadmuh 87373edb17 fix(loudness): target sync, effective pre-analysis trim, and queue/settings copy (#333)
* fix(loudness): target sync, -14 pre-analysis ref, queue UI, and reseed

- Front: coalesce loudness refresh by target LUFS; replay-gain IPC dedupe keys
  include norm target and effective pre-attenuation so TGT changes apply.
- Rust: placeholder gain before integrated LUFS uses pivot at -14 LUFS; UI gain
  from effective trim; reseed loudness after delete when waveform cache would skip.
- Pre-analysis: store attenuation relative to -14 LUFS; engine and UI use an
  offset for other targets; migrate legacy absolute values on rehydrate.
- Queue/Settings: Loudness/TGT labels vs value buttons; styles; i18n for help.

* fix(i18n): simplify loudness pre-analysis helper copy

Remove reference-target wording from loudness pre-analysis helper text and keep
only the effective adjustment shown for the current LUFS target in all locales.
2026-04-27 02:54:58 +03:00
Maxim Isaev 55a49a9fb0 feat(player): restore queue list scroll on queue undo/redo
Store the main queue panel viewport scrollTop in undo snapshots and
reapply it after undo/redo so Ctrl+Z / Ctrl+Shift+Z restores scroll
position alongside queue state.
2026-04-27 00:33:47 +03:00
Maxim Isaev 39f4d03da5 feat(player): queue undo/redo with hotkeys and playback-aware restore
Snapshot queue, current track, time, and pause before queue mutations.
Ctrl+Z/Cmd+Z restores prior state; Ctrl+Shift+Z/Cmd+Shift+Z reapplies undone
edits when the redo stack is non-empty; new edits clear redo.

Resync the Rust audio engine only when the current song identity changes
from the pre-apply state so reorder/enqueue-style edits keep live playback.

Register the document capture listener from main.tsx after the window label
is set. Mini player forwards undo/redo via mini:undo-queue and mini:redo-queue.
2026-04-27 00:17:10 +03:00
Maxim Isaev 8f603e8de9 feat(waveform): store peak+mean bins and blend at 70/30
Persist waveform v4 as paired curves per bin (peak and mean absolute value),
invalidate cache rows with insufficient data shape, and render the seekbar as
0.7 * mean + 0.3 * peak for a denser, more stable contour.

Thanks to @peri4ko for proposing this waveform improvement.
2026-04-26 21:43:39 +03:00
cucadmuh 218aa00718 fix(analysis): CPU seed queue, single waveform emit, and log URL redaction
Serialize heavy PCM seeding through a dedicated queue with optional priority
for the current track. Emit waveform-updated once per completed seed, fix
Lucky Mix waveform refresh tokens, redact Subsonic URLs in logs, and align
hot-cache prefetch with the queued path.
2026-04-26 20:59:33 +03:00
Psychotoxical 2d222e2691 fix(loudness): break feedback loop that froze Windows UI when LUFS active
When loudness normalization is on, every PARTIAL_LOUDNESS_EMIT_INTERVAL_MS
(900 ms per active stream task) the backend emits analysis:loudness-partial.
The frontend listener wrote cachedLoudnessGainByTrackId and unconditionally
called updateReplayGainForCurrentTrack, which invoked audio_update_replay_gain,
which in turn emitted audio:normalization-state back to the renderer — closing
a loop the UI had to drain on every tick. WebKitGTK absorbed it; WebView2 did
not, and after a few minutes the renderer thread stalled while Rust analysis
tasks kept progressing (visible in logs).

Five gates close the loop, smallest first:

Frontend (src/store/playerStore.ts):
- analysis:loudness-partial listener now short-circuits when the new gainDb is
  within 0.05 dB of the cached value.
- audio_update_replay_gain calls go through invokeAudioUpdateReplayGainDeduped
  (250 ms key-based dedupe), mirroring the audio_set_normalization helper from
  #320.
- refreshLoudnessForTrack now coalesces concurrent calls per (trackId, mode)
  via loudnessRefreshInflight, mirroring waveformRefreshInflight.

Backend (src-tauri/src/audio.rs):
- audio:normalization-state emits go through maybe_emit_normalization_state,
  which keeps the last-emitted payload behind a Mutex and skips when engine
  matches and current_gain_db (±0.05 dB) / target_lufs (±0.02) are unchanged.
  Applied at all three emit sites: audio_play, audio_update_replay_gain,
  audio_set_normalization.
- analysis:loudness-partial emits are gated by partial_loudness_should_emit:
  per-track-identity last-emitted-gain map; an emit is suppressed when the new
  gain is within PARTIAL_LOUDNESS_DELTA_THRESHOLD_DB (0.1 dB) of the previous
  one. Applied to both emit_partial_loudness_from_bytes (legacy path) and the
  inline ranged_download_task progress emit.

Drive-by: split estimated_output_latency_secs into two #[cfg]-gated
definitions so the sample_rate_hz parameter is no longer flagged as unused on
Windows / macOS builds. Function originally added in c6fc3ec.

Net effect: when loudness is steady (the common case), zero IPC traffic on
this path. UI stays responsive on Windows. Tested on Windows + Linux.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:59:32 +03:00
cucadmuh a910162cfe fix(analysis): gate partial LUFS, seed RAM paths, clarify logs, dedupe IPC (#320)
- Emit streaming partial loudness only when the loudness normalization engine is active
- Seed waveform/loudness from in-memory full buffers (preloaded, stream cache, gapless reuse) so offline hot cache does not rely on HTTP backfill
- Add explicit ranged/local/preload logs before full-file Symphonia + EBU analysis
- Coalesce concurrent waveform DB reads and skip duplicate audio_set_normalization within 450ms (e.g. StrictMode double init)
2026-04-26 14:25:26 +02:00
Psychotoxical 4217e8e6a0 fix(player): cross-device resume — push queue position on pause + all exit paths
Server queue position was effectively pinned at 0 because syncQueueToServer
only fired on track-change, seek, and queue edits (with a 5s debounce). A
user listening for minutes without seeking would close the app and the
server still held position=0, so a second device restarted the track.

Three pieces:
- 15s heartbeat from audio:progress flushes the live position while playing.
- pause() flushes immediately so a quick close after pause is captured.
- Tray "Exit" and macOS Cmd+Q used to call app.exit(0) directly, bypassing
  the JS close handler. Both now emit a new app:force-quit event that
  shares the same flush + Orbit-teardown path as window:close-requested,
  and exit_app stops the audio engine on its way out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:15:12 +02:00
Maxim Isaev ea93f7fc4d fix(player): keep seekbar waveform on current track during gapless preload
Byte-preload for the next track called refreshWaveformForTrack(next), which
writes global waveformBins and replaced the playing track’s waveform when
seeking near the end. Drop that call and ignore stale RPC results if
currentTrack changed while the fetch was in flight.
2026-04-26 02:36:38 +03:00
Maxim Isaev 4b60495e38 feat(playback): loudness bind rules, analysis seeding, and normalization UI
Resolve integrated loudness from SQLite only at decode bind; keep pre-trim
until a row exists, then allow provisional gain from live updates. Pass
DB-stable hints from the web app into play and gapless preload.

Default pre-measurement attenuation -4.5 dB with an icon reset in Settings;
drop redundant normalization copy and shorten pre-trim descriptions.

analysis_cache seed_from_bytes returns an outcome and skips redundant
waveform work on cache hits; wire callers and related frontend/backend glue.
2026-04-26 02:29:54 +03:00
Maxim Isaev c6fc3ec844 fix(waveform): stabilize progressive rendering and reduce streaming contention
Improve seekbar fallback behavior by using consistent bar-based rendering and smoother transitions to analyzed waveform data. Reduce hot-path analysis overhead during ranged streaming and add controls to clear cached waveform entries.
2026-04-26 00:49:10 +03:00
Maxim Isaev 53cab7654c feat(player,queue): loudness strip controls and normalization readout fixes
- Add Tauri command to delete loudness_cache rows for a track and helpers in AnalysisCache.
- Queue tech strip: click dB to reseed loudness; LUFS target picker via body portal; metric styling aligned with strip (no link chrome).
- Player store: reseed clears local cache and replays analysis seed; show loudness dB from SQLite cache when live state is still null; allow first numeric normalization-state update through the short duplicate filter.
- audio_update_replay_gain: resolve loudness from the requested gain when playback URL is not pinned yet.
2026-04-25 23:38:27 +03:00
Psychotoxical 453151f1fc Merge branch 'main' into feat/waveform-loudness-cache
# Conflicts:
#	src/store/playerStore.ts
2026-04-25 21:47:48 +02:00
Maxim Isaev 86704473ed fix(audio): stabilize loudness normalization and streaming startup behavior
Improve normalization consistency by separating cache refresh for non-playing tracks, hardening track-id cache lookups, and keeping UI gain state aligned with actual playback. Also reduce startup stalls by preferring non-seekable streaming when format hints are missing and by tuning ALSA/analysis paths to avoid underrun-prone churn.
2026-04-25 22:09:38 +03:00
Psychotoxical 7acf95c0a6 fix(orbit): guest resume catches up to host's live position
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:20:25 +02:00
Psychotoxical 59bae4b545 fix(orbit): bulk plays append instead of replacing the shared queue
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 15:11:08 +02:00
Psychotoxical 3bbf628526 fix(orbit): skip bulk confirm when playTrack only navigates the existing queue
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:58:35 +02:00
Psychotoxical 7ba7d6bf25 chore(orbit): guard bulk queue operations
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:49:13 +02:00
Psychotoxical 861798ea7a Merge branch 'main' into exp/orbit 2026-04-24 00:19:14 +02:00
Maxim Isaev ab5c8e0b48 feat(lucky-mix): instant mix from your preferences
Lucky Mix targets ~50 tracks in one run: pick seeds from your most-played artists/albums and 4+ rated songs, add similar tracks, then fill the mix with random library picks, skipping anything you rated 1–2.
On start it trims the old “upcoming” tail so the queue does not show stale next tracks, starts playback on the first viable seed, routes to Now Playing, and can emit structured steps to the backend debug log.
2026-04-23 18:42:52 +03:00
Psychotoxical 2b97a7b01e exp(orbit): host merge, guest queue view, settings popover, i18n
Guest suggestions now auto-merge into the host's play queue at random
positions inside the upcoming range, so they actually surface alongside
host-picked tracks instead of piling up at the end. Per-item dedupe via
addedBy:addedAt:trackId keys survives reshuffles.

Guests get a read-only mirror of the session in place of their queue
panel — live-card for the host's current track, suggestion list with
submitter attribution, and a footer reminding them the host owns
playback. Track metadata is resolved lazily and cached locally.

New host-only settings popover (gear in the bar) with auto-approve and
auto-shuffle toggles plus a "Shuffle now" action. Settings live in the
OrbitState blob and push immediately to Navidrome; missing fields on
older blobs default to "both on".

15-min shuffle now touches the real playerStore queue via a new
shuffleUpcomingQueue action — previous behaviour only reshuffled the
guest-facing suggestion history. A manual shuffle is available from the
settings popover so the interval can be verified without waiting.

Start-modal reworked into a single step: share-link is visible and
copy-able from the start, auto-copied on Start if the host hasn't
already hit Copy. Link carries a live name-derived slug (parser strips
it, SID is authoritative). LAN/remote-server hint above the facts.
Random session-name suggester pulls from three pattern pools for ~10k
unique combinations.

All user-visible Orbit strings moved to a dedicated orbit i18n
namespace (en + de); other locales fall back to en via i18next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 02:49:42 +02:00
Frank Stellmacher 694567843f feat(player): polish sleep-timer UI — circular ring + in-button countdown (#272)
Replaces the text-pill schedule badge with a circular SVG progress ring
around the play/pause button. Accent→lavender gradient stroke, counter-
clockwise depletion synced to the armed deadline, subtle accent glow.

While a timer is armed, the Play/Pause icon inside the button is
swapped for a two-line stack: a small Moon (sleep timer) or Sunrise
(delayed start) glyph above the countdown (m:ss / h:mm:ss). The icon
is the mode marker so the ring can stay unified with the rest of the
app's accent palette.

Redesigns the schedule modal with a mood-tinted header (Moon for
"Pause after", Sunrise for "Start after"), soft radial accent glow in
the background, slide-up open animation. Circular glass close-button
scoped to this modal, with hover rotation + focus ring. Custom-minutes
field gains an inline "min" suffix. A live preview line at the bottom
shows when the action fires ("Pauses at 23:47" / "Starts at 23:47"),
updating as the user hovers a preset or types. Chips gain a small
hover lift plus accent-coloured shadow.

Also fixes a pre-existing duplicate-tooltip on the play/pause button:
title= attributes removed alongside the data-tooltip (title violates
the project's "never native tooltips" rule, so both showed at once).

Adds scheduledPauseStartMs / scheduledResumeStartMs to the player
store so the progress ring has a total-duration baseline, set and
cleared alongside the existing deadline fields.

New usePlaybackScheduleRemaining hook wraps the store subscription
and 500 ms tick into a single hook used by all three player views
(PlayerBar / FullscreenPlayer / MobilePlayerView).

i18n: delayPreviewPause / delayPreviewStart keys across all 8 locales.

Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 00:13:28 +02:00
cucadmuh 624ce56faf feat(player): sleep timer and delayed start via long-press on play/pause (#270)
Add scheduled pause and resume timers in the player store, cleared on stop
and track changes. Long-press opens a compact preset modal anchored above
the transport row; one-tap presets plus custom minutes. Portaled countdown
badge on the play button; clear the long-press click guard when the modal
closes so the first play/pause click works after scheduling.
2026-04-22 23:32:44 +02:00
Frank Stellmacher 76ed6eca55 feat(replaygain): add Auto mode that picks track vs album gain from queue context (#242)
Auto mode is the new default for ReplayGain. It picks album gain when an
adjacent queue neighbour shares the same albumId (so "Play All" on an
album, or any contiguous album block, gets album gain), otherwise track
gain. Falls back to track gain when album gain is missing.

- authStore: replayGainMode widened to 'track' | 'album' | 'auto', default 'auto'
- playerStore: new resolveReplayGainDb(track, prev, next, enabled, mode) helper
  replaces five inline ternaries (audio_play hot path, gapless preload,
  cold-resume success + fallback, live update_replay_gain)
- Settings: third "Auto" button + contextual hint when active
- i18n: replayGainAuto + replayGainAutoDesc added to all 8 locales

Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 19:12:19 +02:00
Frank Stellmacher fa21379dbb fix(player): prevent streaming seek UI freezes and progress snapbacks (#236)
* fix(player): prevent streaming seek UI freezes and progress snapbacks

Move blocking seek work off the command path with a bounded timeout, switch fullscreen/mobile seek bars to commit-on-release, and stabilize fallback progress state so rapid early seeks on streamed tracks do not freeze the UI or jump progress to zero.

* fix(player): avoid second-seek lockups and progress flicker

Add a short lock timeout for audio seek state access to prevent UI stalls when rapid seeks overlap, and keep waveform progress stable right after seek commit to eliminate brief visual snapbacks.

* fix(player): harden seek recovery and reduce stream log noise

Keep seek fallback stable during delayed backend responses to prevent occasional resets to track start, and remove per-read stream blocking logs so normal playback diagnostics stay readable.

---------

Co-authored-by: Maxim Isaev <im@friclub.ru>
2026-04-21 12:10:26 +02:00
Psychotoxical ccf6e6c886 fix(seekbar): commit seek on mouseup + ignore progress ticks while dragging
Ports the WaveformSeek UX changes from PR #219 (original fix by @cucadmuh)
onto test/seekable-streaming:

- Split previewFraction (during drag) / commitSeek (on mouseup) so audio_seek
  fires once per gesture instead of on every mousemove.
- Subscribe-effect skips external progress updates while isDragging — prevents
  cursor flicker from streaming/recovery ticks fighting the dragged position.
- Clear seekTarget in seek() catch branch so a failed seek doesn't permanently
  block progress updates.

Skips the byte-restart fast-path from #219: would defeat RangedHttpSource's
direct seek by restarting the stream after 450ms.

Co-Authored-By: cucadmuh <cucadmuh@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 17:28:20 +02:00
Frank Stellmacher b45bb95522 Merge pull request #201 from cucadmuh/feat/playback-source-indicator
feat(queue): playback source badge + preload-aware source tracking
2026-04-17 15:56:01 +02:00
Frank Stellmacher 9387ceca83 Merge pull request #198 from kveld9/feat/DRP-enhancement
feat: discord rich presence enhancement
2026-04-17 15:34:18 +02:00
Maxim Isaev c165669db5 feat(queue): show playback source in tech strip with preload tracking
Add source badges for offline, cache, and stream playback in the queue tech line, including localized tooltips across shipped locales.

Track Rust preload-ready events by stream track id and latch the selected source per track, with dev logs to debug preload/source mismatches during next-track handoff.
2026-04-17 16:34:11 +03:00
Psychotoxical c50addaacf fix(discord): remove dead fields, timeChanged invoke loop, and unsupported {paused} placeholder
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 15:33:58 +02:00
Maxim Isaev 689d2dc019 fix(audio): harden stream playback transitions and cache promotion
Add resilient stream-first track playback with reconnect/range recovery, seek fallback handling for non-seekable starts, and promotion of completed streamed bytes into hot cache.

Also add crossfade/gapless backup preload windows to keep automatic transitions smooth on imperfect networks.
2026-04-17 13:49:31 +03:00
kveld9 5576fb0d13 fix DRP timer 2026-04-16 21:41:22 -03:00
kveld9 28844e8456 release 2026-04-16 20:34:55 -03:00
Psychotoxical 9383a6f6ed feat(radio): forward ICY StreamTitle to MPRIS metadata
When a radio stream is active, push the station name to MPRIS on start
and forward ICY StreamTitle updates (radio:metadata events) so the OS
now-playing overlay stays in sync. Skips ad metadata (is_ad flag) and
suppresses position ticks for radio streams (no meaningful duration).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 18:05:14 +02:00
Maxim Isaev c75297fcf6 feat(cli): completions, library/audio commands, server switcher
Embed bash and zsh completion scripts and add a `completions` subcommand.
Extend the player CLI (library, audio device, instant mix) and surface
`music_library` in snapshots. Add a shared active-server switcher in the
header and locales; instant mix can reseed the queue from CLI and UI.
2026-04-15 01:47:46 +03:00
kveld9 6996dacdf7 sortable columns, gender filter, age range filter, new columns. 2026-04-14 16:49:06 -03:00
Psychotoxical 16cb4f5a6f feat(audio): audio output device selection (closes #169)
Adds the ability to choose which audio output device Psysonic plays
through — useful for USB DACs, dedicated soundcards, and multi-device setups.

Rust (audio.rs):
- open_stream_for_device_and_rate(device_name, rate): opens a named
  device by cpal name, falls back to system default if not found
- AudioEngine.selected_device: persists the chosen device across
  stream reopens so hi-res rate switches stay on the right device
- audio_list_devices: Tauri command — returns all output device names
- audio_set_device: Tauri command — switches device immediately,
  drops old sinks, emits audio:device-changed
- start_device_watcher: now handles two cases:
    1. No pinned device + system default changed → reopen on new default
    2. Pinned device disappeared (DAC unplugged) → clear selected_device,
       fall back to system default, emit audio:device-reset

Frontend:
- authStore: audioOutputDevice (string | null), persisted
- playerStore: applies stored device on cold start
- App.tsx: listens to audio:device-reset, clears authStore device
  and restarts playback on the fallback device
- Settings → Audio tab: device dropdown at top (above Hi-Res and EQ),
  uses CustomSelect (portal-based, styled), all 8 locales
- CustomSelect: added disabled prop

Note: exclusive mode (WASAPI exclusive, CoreAudio exclusive) is out
of scope for now.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 18:20:23 +02:00
kveld9 592a479c30 fixed some bugs 2026-04-12 20:43:31 -03:00
kveld9 bef6941a2b feat: add multi-selection and context menu for playlist management 2026-04-12 17:53:52 -03:00
cucadmuh 2dd993257a feat(player): build infinite queue using instant-mix strategy (#163)
Prefer artist-driven candidates (Top + Similar) when extending infinite queue, with random songs as a fallback when mix sources are empty.
2026-04-12 12:34:27 +02:00
cucadmuh ae2e1bcb97 feat(folder-browser): keyboard nav, context menus, now-playing path UX
Adds full keyboard navigation (arrow keys, Enter, Ctrl+Enter for context menu) across Folder Browser columns. Context menus for all row types with keyboard-operable submenus and star-rating control via arrow keys + Enter. Now-playing path is visually emphasized and stays stable; rebuilds on hotkey re-invoke or active-path follow-along. Adaptive column layout for deep trees with right-side visibility priority. New configurable 'Open Folder Browser' keybinding. StarRating animation sync for keyboard-driven changes. All 7 locales updated.
2026-04-12 11:27:26 +02:00
Maxim Isaev b4c3124168 fix(player): minor hot-cache eviction, prefetch budget, and settings live size
Small fix for hot playback cache: eviction keeps current and next only;
prefetch up to five tracks when under cap, always fetch the immediate next;
grace for the previous current until debounce; run evict immediately on
MB or folder changes; re-read cap after download; optional Track.size;
live disk usage on Audio settings.
2026-04-11 03:29:59 +03:00
Psychotoxical 20dabbfd03 feat(lyrics): configurable sources with drag-to-reorder + feat(audio): ReplayGain pre-gain & fallback
Lyrics Sources:
- Replace lyricsServerFirst + enableNeteaselyrics toggles with a new
  Settings section — three sources (Server, LRCLIB, Netease) each
  individually toggled and drag-reorderable via psy-drop DnD
- useLyrics.ts iterates sources in user-defined order, skipping disabled
  ones; embedded SYLT from local files still wins unconditionally
- onRehydrateStorage migration from legacy fields on first load

ReplayGain Pre-Gain:
- New authStore fields: replayGainPreGainDb (0…+6 dB) and
  replayGainFallbackDb (-6…0 dB, untagged / radio)
- audio.rs compute_gain applies pre_gain_db and fallback_db
- Settings sliders under ReplayGain mode selector
- Radio HTML5 volume scaled by fallbackDb factor on playRadio

i18n(ru): incorporate PR #148 translation improvements (kilyabin)
All 7 locales updated for new keys.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 23:57:36 +02:00
Psychotoxical c142e9e983 feat(radio): PLS/M3U playlist resolution + ICY metadata for playlist streams
Resolves PLS and M3U/M3U8 playlist URLs to their first direct stream URL
before playback and ICY metadata fetching. Stations configured with a
.pls or .m3u URL (e.g. SomaFM, schizoid.in) now play correctly and
report track metadata via ICY headers.

- Rust: parse_pls_stream_url / parse_m3u_stream_url helpers
- Rust: resolve_playlist_url (shared) + resolve_stream_url Tauri command
- fetch_icy_metadata: auto-resolves playlist URLs before connecting
- playerStore: playRadio() awaits resolve_stream_url before setting audio src

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 23:18:53 +02:00
Psychotoxical f34bd7c0f1 fix(audio): separate gapless chain from preload gate and reduce black flash
- Gapless now always chains at 30s regardless of preloadMode ('off' no
  longer breaks gapless playback)
- Byte pre-download (audio_preload) skips when Hot Cache is active to
  avoid duplicate downloads
- When both Gapless and Preload are active, bytes are pre-fetched early
  and the chain reuses the cached data (separate bytePreloadingId guard)
- player-album-art-wrap gets background: var(--bg-card) so the brief
  opacity-0 loading state shows a themed colour instead of black

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 14:04:30 +02:00
Psychotoxical c49f6af38b perf: reduce CPU usage and unify next-track buffering settings
- Throttle audio:progress from 100ms to 500ms; WaveformSeek and
  FsSeekbar use imperative DOM updates instead of React re-renders
- Fix all usePlayerStore() calls without selectors across pages and
  components (useShallow / individual selectors throughout)
- Remove filter:blur and transform:scale from Hero, AlbumDetail and
  FullscreenPlayer backgrounds — eliminated expensive software
  compositing layers on WebKitGTK
- Replace translate3d with 2D translate in FS mesh and portrait
  animations; remove will-change:transform from mesh blobs
- Move Hot Cache section from Storage tab to Audio tab; group Preload
  and Hot Cache under a shared 'Next Track Buffering' section with a
  mutual-exclusivity note and auto-disable logic
- Add 'off' toggle to Preload (replaces Off button with toggle switch);
  enabling either method now automatically disables the other

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 13:38:33 +02:00
Psychotoxical 74985fe331 fix(audio): fall back to track gain when album gain is missing
In album gain mode, tracks without albumGain tags received no
ReplayGain adjustment at all. Now falls back to trackGain before
defaulting to 0 dB — fixes inconsistent loudness across albums
where only some tracks have albumGain metadata.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 01:12:06 +02:00
Psychotoxical 5f0fb5dcbd feat(audio): auto-switch to new audio output device at runtime
Adds a background device-watcher that polls the OS default output device
every 3 s via CPAL. When the device changes (Bluetooth headphones connecting,
USB DAC plugging in, etc.) the stream is reopened on the new device, the old
Sink is dropped (it was bound to the now-closed OutputStream), and
audio:device-changed is emitted to the frontend.

Frontend handler (TauriEventBridge): if playing, restarts the current track
from the saved position; if paused, clears the warm-pause flag so the next
resume uses the cold path (audio_play + seek) which creates a new Sink on
the new device.

Fixes #143 (audio through speakers after connecting Bluetooth headphones).
Also covers the intermittently reported single-channel output after long idle,
which can be caused by the OS reconfiguring the audio device in the background.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 00:40:12 +02:00