Move the per-generation progress + ended-detection task (~205 LOC)
out of audio/commands.rs into audio/progress_task.rs. The task is now
a sibling submodule that both audio_play (commands.rs) and
audio_play_radio (radio_commands.rs) import as
`super::progress_task::spawn_progress_task`, replacing the previous
`super::commands::spawn_progress_task` cross-import that radio was
using as a workaround.
audio/commands.rs: 1185 → 977 LOC. Cleanup: dropped now-unused
AtomicU32 and AudioCurrent imports from commands.rs.
What's left in commands.rs is now just audio_play (~760 LOC) and
audio_chain_preload (~195 LOC) — the playback orchestrator proper.
Splitting those further is a bigger structural job than file moves.
Pull audio_preload (background fetch + analysis seed for the next
track in the queue) out of audio/commands.rs into
audio/preload_commands.rs (~67 LOC). It's a self-contained
fetch-and-cache flow — distinct from audio_chain_preload (which
constructs the gapless source chain) and audio_play (which starts
playback) so it makes sense to live alongside them rather than inside
the same file.
audio/commands.rs: 1241 → 1185 LOC. lib.rs invoke_handler updated.
Pull audio_pause / audio_resume / audio_stop / audio_seek (~210 LOC)
out of audio/commands.rs into audio/transport_commands.rs. They mutate
state.current on an already-running sink and coordinate radio
warm/cold resume — distinct concern from playback startup
(audio_play / audio_chain_preload / audio_preload).
audio/commands.rs: 1447 → 1240 LOC. Imports trimmed: TryLockError,
radio_download_task, RADIO_BUF_CAPACITY are no longer pulled in here;
they followed the transport block to its new home. lib.rs
invoke_handler updated.
The remaining commands.rs is now focused on the playback orchestration
proper (track + chain preload + initial preload + the shared
spawn_progress_task helper).
Pull audio_play_radio (~165 LOC) out of audio/commands.rs into
audio/radio_commands.rs. Live-radio playback differs from main track
playback: no gapless chain, no seek, no replay-gain, no preload —
collecting it on its own makes both modules easier to reason about.
The shared spawn_progress_task helper (still used by audio_play /
audio_chain_preload / audio_resume in commands.rs and now by radio)
moves from private to `pub(super)` so radio_commands can call it.
A follow-up could lift it into helpers.rs proper.
audio/commands.rs: 1614 → 1447 LOC. lib.rs invoke_handler updated.
Cleanup: dropped now-unused imports (`super::sources::*`,
`RadioLiveState`, `RadioSharedFlags`) from commands.rs.
Pull the six configuration setters out of audio/commands.rs into
audio/mix_commands.rs (~170 LOC):
- audio_set_volume (with replay-gain-aware ramp)
- audio_update_replay_gain (resolves cache-backed loudness, computes
gain, ramps sink, emits NormalizationStatePayload)
- audio_set_eq (10-band gains + pre-gain)
- audio_set_crossfade
- audio_set_gapless
- audio_set_normalization
These are pure AudioEngine state mutations + (for normalization) an
ipc emit. They don't drive playback; collecting them in one place
makes commands.rs more focused on the playback orchestrators.
lib.rs invoke_handler updated. audio/commands.rs: 1784 → 1614 LOC.
Pull autoeq_entries + autoeq_fetch_profile out of audio/commands.rs
into audio/autoeq_commands.rs (~52 LOC). They proxy autoeq.app +
GitHub raw content via Rust to bypass WebView CORS — pure HTTP-fetch
flow with no playback state coupling, so they don't need to live next
to the playback orchestrator.
lib.rs invoke_handler updated to register them under
`audio::autoeq_commands::*`. audio/commands.rs: 1830 → 1784 LOC.
Hotspot J first slice: split the device-listing + device-selection
commands out of audio/commands.rs (1912 LOC monolith) into a new
audio/device_commands.rs (~95 LOC). Moved:
- audio_canonicalize_selected_device
- audio_list_devices_for_engine (kept pub for cli/exchange.rs callers)
- audio_list_devices
- audio_default_output_device_name
- audio_set_device
audio/mod.rs re-exports audio_default_output_device_name +
audio_list_devices_for_engine from the new submodule. The four
#[tauri::command] device handlers are registered in lib.rs run() under
their new path `audio::device_commands::*`.
audio/commands.rs goes from 1912 → 1830 LOC and drops its
`use super::dev_io::*` since playback/radio/EQ don't touch device
enumeration. Behaviour-preserving — same Tauri commands, same dev_io
helpers, same selected_device + stream_handle mutations.
* chore: upgrade dependencies and migrate playback to rodio 0.22
Bump npm and Rust crates; adapt symphonia decoding, ringbuf 0.5, lofty tags,
and discord-rich-presence usage. Use native rodio Player/MixerDeviceSink and
cpal device descriptions; drop the unused cpal patch. Align Vite 8 build
targets and chunking; remove redundant dynamic imports and fix hot-cache debug
logging imports.
* perf(build): lazy-load routes and restore default chunk warnings
Lazy-load all routed pages with React.lazy to shrink the main bundle; wrap root
Routes in Suspense for lazy Login. Drop chunkSizeWarningLimit override so Vite
uses the default 500 kB threshold.
* fix(windows): tray double-click without spurious menu; clean unused import
Disable tray menu on left mouse-up on Windows so a double-click to hide the
main window does not immediately reopen the context menu (tray-icon default
menu_on_left_click). Gate std::fs in app_api/core behind cfg(linux) for
/proc-only code so Windows builds stay warning-free.
* fix(sidebar): preserve new-releases read state under storage cap
When merging seen album ids, keep the current newest sample first so the
500-id localStorage limit does not truncate freshly marked reads and bring
back the unread badge.
* fix(audio): hot-cache replay, analysis no-op skips, playback source UI
Retain stream_completed_cache across audio_stop so end-of-queue replay can
use RAM promote or disk hot file instead of re-ranging HTTP.
Add cpu_seed_redundant_for_track gate before file/bytes seeds and local-file
spawn; emit analysis:waveform-updated only on Upserted. Ranged/legacy promote
checks generation after await before filling the slot.
Frontend: promote on same-track and cold resume; set currentPlaybackSource on
resume, queue undo restore, and gapless track switch so cache/stream icons stay
accurate. Import tauri::Manager for try_state in audio_play.
* fix(ts): narrow activeServerId for hot-cache promote calls
promoteCompletedStreamToHotCache expects a string; bind non-null server ids
in repeat-one, playTrack prev/same-track, and cold resume paths so tauri
production build (tsc) succeeds.
* fix(player): handle same-track hot-cache promote promise chain
Add .catch for promoteCompletedStreamToHotCache → runPlayTrackBody so sync
throws and unexpected rejections do not surface as unhandled in DevTools;
reset defer-hot-cache prefetch and isPlaying on failure.
* chore(nix): sync npmDepsHash with package-lock.json
* chore(release): finalize 1.46.0 CHANGELOG with PR #463 links
Document the release with full GitHub PR #463 on every subsection so
entries stay attributable if sections are reordered. Fix ContextMenu
lines where dynamic imports were accidentally merged onto one line.
* docs(contributors): credit cucadmuh for #463
* feat(linux): optional native GDK for Nix gdk-session
Introduce PSYSONIC_ALLOW_NATIVE_GDK so main skips the default GDK_BACKEND=x11
pin when the Nix gdk-session wrapper sets the flag. Remove GDK_BACKEND from
the npm tauri:dev script so it does not override nix develop defaults.
* fix(ui): portal server switch menu above sidebar
Main column stacks below the sidebar (layout z-index), so an in-tree dropdown
could never win over the left nav. Render the menu via createPortal to
document.body with fixed coordinates, matching the library scope picker.
* feat(perf): add mainstage probe controls and cut WebKit repaint load
Add a dedicated performance probe surface for mainstage/home toggles and wire Linux CPU diagnostics to isolate expensive UI paths. Tune waveform drawing and Home artwork clipping/windowing so visible content loads immediately while reducing WebKit compositor pressure during playback.
* fix(perf): stop hero rotation when section is off-screen
Gate hero auto-rotation and backdrop crossfade by real viewport visibility using the actual scrolling ancestor. This prevents periodic 10-second CPU spikes from hidden hero updates while preserving normal behavior when the hero is visible.
* fix(perf): isolate player progress updates from mainstage diagnostics
Add probe toggles for PlayerBar waveform and live progress UI updates to confirm playback progress churn as the main CPU driver.
Restore Home artwork quality defaults and keep visual-degradation modes opt-in via debug flags only.
* fix(hero): resume background and autoplay after viewport return
Re-check hero visibility on focus/visibility changes and add a short recovery poll while off-screen so missed scroll/RAF events cannot leave hero animation paused.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(perf): decouple playback progress from mainstage compositing pressure
Throttle audio progress delivery and route live seekbar timing through a lightweight progress channel to cut focus-time WebKit CPU spikes.
Add focused diagnostics in Performance Probe and restore hero/waveform behavior so visuals remain stable while profiling.
* fix(debug): open performance probe with Ctrl+Shift+D
Replace logo-triggered opening with a keyboard shortcut and keep logo purely decorative to avoid accidental probe activation.
* docs(changelog): document experiment/performance probe and playback work
Add an [Unreleased] section for the performance probe, throttled audio
progress IPC, snapshot-based live UI updates, WaveformSeek scheduling
over the same canvas bar renderer, Hero/Home rail fixes, and Linux/Nix
GDK dev ergonomics.
* perf(linux): add WebKit probe, throttle progress IPC, snapshot playback UI
Ship Performance Probe (Ctrl+Shift+D), Rust-throttled audio:progress, a
playback progress snapshot channel with coarse Zustand timeline commits,
Linux /proc CPU readout for the probe, Hero and Home rail artwork fixes,
Tracks SongRail windowing parity, MPRIS cleanup, gated perf counters, and
WaveformSeek paused-seek correctness. Documented in CHANGELOG for PR #452.
* docs(changelog): fold perf work into 1.45.0 and refresh date
Drop the separate 1.45.1 heading; keep PR #452 notes under 1.45.0 Added and
set the section date to 2026-05-04. Restore the safety preface before the
versioned sections.
* docs(changelog): order 1.45.0 Added entries by PR number
Sort the 1.45.0 release notes so subsections follow ascending PR id (390
through 452), with PR #452 last.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(audio): frame-align gapless-off track-separation silence
The 500 ms silence prepended between tracks when gapless playback is
disabled and the previous track ended naturally was built with
`Zero<f32>::new(ch, sr).take_duration(500ms)`. Rodio's `TakeDuration`
computes its sample count via integer-nanosecond division
(`1_000_000_000 / (sr * ch)`), which truncates: at 44.1 kHz / 2 ch
this emits 44103 samples = 22051.5 frames, half a frame short.
That half-frame leak shifts the next source's L/R parity in the
device frame stream. Multiple users have reported the next track
playing only on the right channel — exactly when gapless is OFF and
the previous track ended naturally (manual skip and album-first-play
bypass the silence prepend, which matches the reproducer report).
Replace with `SamplesBuffer::new(ch, sr, vec![0; frames * ch])`
where `frames = sr / 2`. Frame-aligned by construction, same
audible effect.
* docs(changelog): add #439 mono-channel fix entry
* chore(credits): add #439 to Psychotoxical contributions
* docs(changelog): strip @ from non-contributor mention in #435 entry
Plain-text 'zunoz on Discord' instead of '@zunoz' so GitHub does not
attribute the requester as a contributor on subsequent merges.
* fix(audio): promote WASAPI render thread to MMCSS Pro Audio on Windows
Wraps the outermost audio source in a `PriorityBoostSource` that calls
`AvSetMmThreadCharacteristicsW("Pro Audio")` on its first sample. The
cpal output-stream callback runs `Source::next` on the WASAPI render
thread, which is otherwise normal-priority and gets preempted under
WebView2 / DWM / GPU pressure — producing the audible click/stutter
reported in issue #334. No-op on Linux/macOS (PipeWire/rtkit and
CoreAudio promote their audio threads externally).
* fix(build): repair Windows compile after audio split + lib decompose
Two pre-existing build breakers on Windows that surfaced after the
`use super::*;` cleanup (9cc74a7) and the lib-decompose refactor
(cfeec22):
- `audio/device_watcher.rs` lost the `output_enumeration_includes_pinned`
import. Add it cfg-gated to `not(target_os = "linux")` since it's only
called inside the non-Linux pinned-device fallback path.
- `lib_commands/sync/tray.rs::is_tiling_wm()` is `#[cfg(target_os = "linux")]`,
but its Tauri command wrapper `is_tiling_wm_cmd()` is unconditional.
Add a `#[cfg(not(target_os = "linux"))]` stub returning `false`.
No behavior change on Linux. Restores `cargo build` on Windows + macOS.
* perf(ui): pause cosmetic animations when window loses OS focus
Companion to the WASAPI MMCSS fix in this branch — issue #334
reported audible audio stutter on low-end laptops, partly traced
to WebView2 still compositing CSS animations + waveform canvas at
full rate while the user has alt-tabbed into another app.
Existing `data-app-hidden` / `data-psy-native-hidden` flags pause
animations only when the window is fully hidden (minimized or
behind a tray). When the window is visible-but-unfocused — the
common "music in the background while I work" case — every infinite
keyframe + 60fps rAF loop keeps running.
Add a parallel `data-app-blurred` flag driven by `window.focus`/
`window.blur`, plus matching `__psyBlurred` on the global window
object for imperative checks. The CSS rule that pauses every
`animation-play-state` on hidden gets a third selector for blurred.
WaveformSeek's two 60fps rAF loops (animated styles + settings demo)
extend their existing `document.hidden || __psyHidden` short-circuit
to also include the new blurred flag, falling back to the same
400ms polling path that already exists for hidden.
Verified visually on the dashboard: alt-tab → `<html
data-app-blurred="true">`, mesh blobs in the fullscreen player +
marquee + waveform 60fps loop all pause. Click back into the
window → blob/marquee/waveform resume immediately.
Cross-platform: focus/blur events fire reliably on all targets, so
the optimization applies to Windows, Linux (incl. WebKitGTK with
software compositing where the savings are largest), and macOS.
* chore: remove stale app-icon.png from repo root
* perf(ui): add 'Reduce animations' toggle that caps animated seekbar styles to 30 fps
Off by default. When on, animated seekbar styles (pulsewave, particletrail,
liquidfill, retrotape) skip every other rAF and advance the animation timer by
a doubled delta, so wave speed is unchanged while draws are halved. Aimed at
low-end Windows GPUs where the 60 fps shadow-blur loop drives ~40 % WebView2
GPU usage even when focused.
Toggle lives directly under the seekbar style picker in Settings → Appearance.
i18n in all 8 supported languages.
Move audio engine functionality from a single large file into focused Rust modules while preserving behavior and command surface. This keeps boundaries clearer for future extraction and the upcoming lib.rs split.