mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
fix(audio): frame-align gapless-off track-separation silence (#439)
* 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.
This commit is contained in:
committed by
GitHub
parent
6019a253cd
commit
dcec30166a
@@ -614,6 +614,14 @@ pub async fn audio_play(
|
||||
|
||||
// Gapless OFF: prepend a short silence so tracks are clearly separated.
|
||||
// Only when this is an auto-advance (near end), not on manual skip.
|
||||
//
|
||||
// Use a frame-aligned `SamplesBuffer` rather than `Zero + take_duration` —
|
||||
// the latter computes its sample count via integer-nanosecond division
|
||||
// (1_000_000_000 / (sr * ch)), which at common rates leaks an odd number
|
||||
// of samples (e.g. 44103 at 44.1 kHz / 2 ch / 500 ms = 22051.5 frames).
|
||||
// The half-frame leak shifts the next source's L/R parity in the device
|
||||
// stream and can manifest as a dead channel for the rest of the track
|
||||
// (reported by users for natural-end-without-gapless transitions only).
|
||||
if !gapless {
|
||||
let cur_pos = {
|
||||
let cur = state.current.lock().unwrap();
|
||||
@@ -625,10 +633,12 @@ pub async fn audio_play(
|
||||
};
|
||||
let is_auto_advance = cur_dur > 3.0 && cur_pos >= cur_dur - 3.0;
|
||||
if is_auto_advance {
|
||||
let silence = rodio::source::Zero::<f32>::new(
|
||||
source.channels(),
|
||||
source.sample_rate(),
|
||||
).take_duration(Duration::from_millis(500));
|
||||
let ch = source.channels();
|
||||
let sr = source.sample_rate();
|
||||
// 500 ms in whole frames, then expand to interleaved samples.
|
||||
let frames = (sr / 2) as usize;
|
||||
let total_samples = frames.saturating_mul(ch as usize);
|
||||
let silence = rodio::buffer::SamplesBuffer::new(ch, sr, vec![0f32; total_samples]);
|
||||
sink.append(silence);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user