fix: preview crossfade no longer strands the main volume (silent playback)

Clicking a track could play silently: the store showed status=playing with
the position advancing and no error, but the sink was at volume 0. The
open/close crossfade ramped the main player's USER VOLUME as its fade
channel and left it at 0 on an ack timeout (or when a track change
suppressed the close resume). User volume persists across tracks and a new
Play doesn't reset it, so every later click played at 0 while the slider
still read 0.7.

Give the main player a separate fade_gain channel (Cmd::SetFadeGain, sink =
volume * norm * fade_gain), reset to 1 by every Play/Stop. The open/close
fades ramp fade_gain instead of set_volume (their volume arg is gone, JS
callers too), so the user volume is never touched -- store and backend can't
desync -- and a clicked track always resets fade_gain to 1, so it can never
be stranded silent. fade_resume lands exactly at 1. Player::fade_gain()
mirrors it into a report atomic; debug overlay shows main fade + this proved
the fix live (open 1->0, close 0->1, new track 1.0, volume steady at 0.7).

39 app + 12 player + 85 JS tests pass.
This commit is contained in:
KeelrLeek committed 2026-07-23 17:31:06 -05:00
1 parent 28aba3a3b3
commit 86e0d86604
7 files changed
+98 -45

No files matched your search

+6
View File
@@ -79,6 +79,9 @@ pub struct DebugStats {
pub normalization_enabled: bool,
pub target_lufs: f64,
pub loudness: Option<crate::loudness::CurrentTrack>,
/// Main player's live fade-gain (1.0 = full; <1 only during a preview
/// open/close crossfade). Proves a fade never strands the volume.
pub main_fade_gain: Option<f32>,
/// The preview voices' resting volume (mirrors the app volume; 0 = muted).
pub preview_volume: Option<f32>,
/// The active preview clip's loudness-normalization gain in dB.
@@ -173,6 +176,9 @@ pub fn debug_stats(app: tauri::AppHandle, state: State<'_, DiagState>) -> DebugS
preview_norm_db: app
.try_state::<crate::preview::PreviewState>()
.map(|s| crate::preview::active_norm_db(&s)),
main_fade_gain: app
.try_state::<crate::playback::PlayerState>()
.map(|p| p.player.fade_gain()),
}
}