mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
fix(audio): defer chained-track volume to gapless transition (#277)
`audio_chain_preload` runs ~30 s before the current track ends to queue the next source for sample-accurate gapless playback. It was also calling `Sink::set_volume(effective_volume)` with the *next* track's volume*ReplayGain — but `Sink::set_volume` affects the whole Sink, including the still-playing current source. Result: loudness of the currently-playing track audibly shifted exactly 30 s before its end, toward the next track's level. Move `set_volume` to the gapless transition block in `spawn_progress_task`, where `cur.replay_gain_linear` / `cur.base_volume` are already swapped. The boundary itself is still sample-accurate; only the volume update is deferred until the chained source is actually live. Side effects audited: - hi-res rate-mismatch fallback returns before the sink block — unaffected - manual skip during chain pending: audio_play creates a fresh Sink — unaffected - audio_set_volume / audio_update_replay_gain before transition operate on the current track's gain — still correct - after transition, cur.replay_gain_linear holds the chained track's gain before the new set_volume runs, so user volume changes remain correct - crossfade is mutually exclusive with gapless — separate code path Closes #275 Co-authored-by: Psychotoxical <dev@psysonic.app>
This commit is contained in:
committed by
GitHub
parent
b97b6c545b
commit
56d44046e3
+17
-3
@@ -3064,7 +3064,12 @@ pub async fn audio_chain_preload(
|
||||
|
||||
let raw_bytes = Arc::new(data);
|
||||
|
||||
let (gain_linear, effective_volume) = compute_gain(replay_gain_db, replay_gain_peak, pre_gain_db, fallback_db, volume);
|
||||
// Only `gain_linear` is needed — `effective_volume` is intentionally NOT
|
||||
// applied to the Sink here. `audio_chain_preload` runs ~30 s before the
|
||||
// current track ends, and `Sink::set_volume` affects the WHOLE Sink (incl.
|
||||
// the still-playing current source). Volume for the chained track is
|
||||
// applied at the gapless transition in `spawn_progress_task`, not here.
|
||||
let (gain_linear, _effective_volume) = compute_gain(replay_gain_db, replay_gain_peak, pre_gain_db, fallback_db, volume);
|
||||
|
||||
let done_next = Arc::new(AtomicBool::new(false));
|
||||
// Use a dedicated counter for the chained source — it will be swapped into
|
||||
@@ -3116,11 +3121,11 @@ pub async fn audio_chain_preload(
|
||||
}
|
||||
|
||||
// Append to the existing Sink. The audio hardware stream never stalls.
|
||||
// Note: `set_volume` is deliberately NOT called here (see comment above).
|
||||
{
|
||||
let cur = state.current.lock().unwrap();
|
||||
match &cur.sink {
|
||||
Some(sink) => {
|
||||
sink.set_volume(effective_volume);
|
||||
sink.append(source);
|
||||
}
|
||||
None => return Ok(()), // playback stopped — bail
|
||||
@@ -3208,7 +3213,12 @@ fn spawn_progress_task(
|
||||
// a one-time value copy would go stale immediately.
|
||||
samples_played = info.sample_counter;
|
||||
|
||||
// Update tracking state.
|
||||
// Update tracking state and apply the chained track's
|
||||
// effective volume. Deferred from `audio_chain_preload`
|
||||
// (which runs ~30 s before the current track ends) to
|
||||
// avoid changing loudness of the still-playing current
|
||||
// track. `Sink::set_volume` affects the whole Sink, so it
|
||||
// must only be called at the boundary, not at preload.
|
||||
{
|
||||
let mut cur = current_arc.lock().unwrap();
|
||||
cur.replay_gain_linear = info.replay_gain_linear;
|
||||
@@ -3216,6 +3226,10 @@ fn spawn_progress_task(
|
||||
cur.duration_secs = info.duration_secs;
|
||||
cur.seek_offset = 0.0;
|
||||
cur.play_started = Some(Instant::now());
|
||||
if let Some(sink) = &cur.sink {
|
||||
let effective = (cur.base_volume * cur.replay_gain_linear * MASTER_HEADROOM).clamp(0.0, 1.0);
|
||||
sink.set_volume(effective);
|
||||
}
|
||||
}
|
||||
|
||||
// Record the gapless switch timestamp for ghost-command guard.
|
||||
|
||||
Reference in New Issue
Block a user