fix: gapless toggle now actually controls audio output lifecycle

When gapless is off, the AudioOutput is dropped after each track ends
naturally, producing a real gap on the next play. When on, the output
stays alive so tracks transition seamlessly. Also re-adds URL prefetch
gating behind the same toggle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
joren
2026-03-24 11:39:24 +01:00
parent c035ce2dee
commit 1e4c234b5c
9 changed files with 31 additions and 3 deletions

View File

@@ -74,6 +74,7 @@ int qobuz_backend_take_track_finished(QobuzBackendOpaque *backend);
// ReplayGain / Gapless // ReplayGain / Gapless
void qobuz_backend_set_replaygain(QobuzBackendOpaque *backend, bool enabled); void qobuz_backend_set_replaygain(QobuzBackendOpaque *backend, bool enabled);
void qobuz_backend_set_gapless(QobuzBackendOpaque *backend, bool enabled);
void qobuz_backend_prefetch_track(QobuzBackendOpaque *backend, int64_t track_id, int32_t format_id); void qobuz_backend_prefetch_track(QobuzBackendOpaque *backend, int64_t track_id, int32_t format_id);
// Playlist management // Playlist management

View File

@@ -478,6 +478,11 @@ pub unsafe extern "C" fn qobuz_backend_set_replaygain(ptr: *mut Backend, enabled
(*ptr).0.replaygain_enabled.store(enabled, std::sync::atomic::Ordering::Relaxed); (*ptr).0.replaygain_enabled.store(enabled, std::sync::atomic::Ordering::Relaxed);
} }
#[no_mangle]
pub unsafe extern "C" fn qobuz_backend_set_gapless(ptr: *mut Backend, enabled: bool) {
(*ptr).0.player.status.gapless.store(enabled, std::sync::atomic::Ordering::Relaxed);
}
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn qobuz_backend_prefetch_track( pub unsafe extern "C" fn qobuz_backend_prefetch_track(
ptr: *mut Backend, ptr: *mut Backend,

View File

@@ -51,6 +51,8 @@ pub struct PlayerStatus {
pub seek_target_secs: Arc<AtomicU64>, pub seek_target_secs: Arc<AtomicU64>,
/// Linear gain factor to apply (1.0 = unity). Updated each time a new track starts. /// Linear gain factor to apply (1.0 = unity). Updated each time a new track starts.
pub replaygain_gain: Arc<std::sync::Mutex<f32>>, pub replaygain_gain: Arc<std::sync::Mutex<f32>>,
/// When false the audio output is torn down after each track, producing a gap.
pub gapless: Arc<AtomicBool>,
} }
impl PlayerStatus { impl PlayerStatus {
@@ -65,6 +67,7 @@ impl PlayerStatus {
seek_requested: Arc::new(AtomicBool::new(false)), seek_requested: Arc::new(AtomicBool::new(false)),
seek_target_secs: Arc::new(AtomicU64::new(0)), seek_target_secs: Arc::new(AtomicU64::new(0)),
replaygain_gain: Arc::new(std::sync::Mutex::new(1.0)), replaygain_gain: Arc::new(std::sync::Mutex::new(1.0)),
gapless: Arc::new(AtomicBool::new(false)),
} }
} }
@@ -197,7 +200,10 @@ fn player_loop(rx: std::sync::mpsc::Receiver<PlayerCommand>, status: PlayerStatu
pending_info = Some(next_info); pending_info = Some(next_info);
} }
Ok(None) => { Ok(None) => {
// Track finished naturally // Track finished naturally — tear down audio output if gapless is off
if !status.gapless.load(Ordering::Relaxed) {
audio_output = None;
}
*status.state.lock().unwrap() = PlayerState::Idle; *status.state.lock().unwrap() = PlayerState::Idle;
status.track_finished.store(true, Ordering::SeqCst); status.track_finished.store(true, Ordering::SeqCst);
} }

View File

@@ -86,6 +86,11 @@ void QobuzBackend::setReplayGain(bool enabled)
qobuz_backend_set_replaygain(m_backend, enabled); qobuz_backend_set_replaygain(m_backend, enabled);
} }
void QobuzBackend::setGapless(bool enabled)
{
qobuz_backend_set_gapless(m_backend, enabled);
}
void QobuzBackend::prefetchTrack(qint64 trackId, int formatId) void QobuzBackend::prefetchTrack(qint64 trackId, int formatId)
{ {
qobuz_backend_prefetch_track(m_backend, trackId, formatId); qobuz_backend_prefetch_track(m_backend, trackId, formatId);

View File

@@ -38,6 +38,7 @@ public:
// --- playback options --- // --- playback options ---
void setReplayGain(bool enabled); void setReplayGain(bool enabled);
void setGapless(bool enabled);
void prefetchTrack(qint64 trackId, int formatId = 6); void prefetchTrack(qint64 trackId, int formatId = 6);
// --- playlist management --- // --- playlist management ---

View File

@@ -41,6 +41,10 @@ SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent)
m_replayGain->setChecked(AppSettings::instance().replayGainEnabled()); m_replayGain->setChecked(AppSettings::instance().replayGainEnabled());
playLayout->addRow(m_replayGain); playLayout->addRow(m_replayGain);
m_gapless = new QCheckBox(tr("Gapless playback"), playGroup);
m_gapless->setChecked(AppSettings::instance().gaplessEnabled());
playLayout->addRow(m_gapless);
layout->addWidget(playGroup); layout->addWidget(playGroup);
// --- Last.fm group --- // --- Last.fm group ---
@@ -96,6 +100,7 @@ void SettingsDialog::applyChanges()
{ {
AppSettings::instance().setPreferredFormat(m_formatBox->currentData().toInt()); AppSettings::instance().setPreferredFormat(m_formatBox->currentData().toInt());
AppSettings::instance().setReplayGainEnabled(m_replayGain->isChecked()); AppSettings::instance().setReplayGainEnabled(m_replayGain->isChecked());
AppSettings::instance().setGaplessEnabled(m_gapless->isChecked());
AppSettings::instance().setLastFmEnabled(m_lastFmEnabled->isChecked()); AppSettings::instance().setLastFmEnabled(m_lastFmEnabled->isChecked());
AppSettings::instance().setLastFmApiKey(m_lastFmApiKey->text().trimmed()); AppSettings::instance().setLastFmApiKey(m_lastFmApiKey->text().trimmed());
AppSettings::instance().setLastFmApiSecret(m_lastFmApiSecret->text().trimmed()); AppSettings::instance().setLastFmApiSecret(m_lastFmApiSecret->text().trimmed());

View File

@@ -19,6 +19,7 @@ private:
// Playback // Playback
QComboBox *m_formatBox = nullptr; QComboBox *m_formatBox = nullptr;
QCheckBox *m_replayGain = nullptr; QCheckBox *m_replayGain = nullptr;
QCheckBox *m_gapless = nullptr;
// Last.fm // Last.fm
QCheckBox *m_lastFmEnabled = nullptr; QCheckBox *m_lastFmEnabled = nullptr;

View File

@@ -160,6 +160,7 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent)
// Apply playback options from saved settings // Apply playback options from saved settings
m_backend->setReplayGain(AppSettings::instance().replayGainEnabled()); m_backend->setReplayGain(AppSettings::instance().replayGainEnabled());
m_backend->setGapless(AppSettings::instance().gaplessEnabled());
tryRestoreSession(); tryRestoreSession();
} }
@@ -269,8 +270,8 @@ void MainWindow::onTrackChanged(const QJsonObject &track)
statusBar()->showMessage( statusBar()->showMessage(
artist.isEmpty() ? title : QStringLiteral("▶ %1 — %2").arg(artist, title)); artist.isEmpty() ? title : QStringLiteral("▶ %1 — %2").arg(artist, title));
// Prefetch next track URL to minimise the gap between tracks // Prefetch next track URL when gapless is enabled
if (m_queue->canGoNext()) { if (AppSettings::instance().gaplessEnabled() && m_queue->canGoNext()) {
const auto upcoming = m_queue->upcomingTracks(1); const auto upcoming = m_queue->upcomingTracks(1);
if (!upcoming.isEmpty()) { if (!upcoming.isEmpty()) {
const qint64 nextId = static_cast<qint64>(upcoming.first()["id"].toDouble()); const qint64 nextId = static_cast<qint64>(upcoming.first()["id"].toDouble());

View File

@@ -38,6 +38,9 @@ public:
bool replayGainEnabled() const { return m_settings.value("playback/replaygain", false).toBool(); } bool replayGainEnabled() const { return m_settings.value("playback/replaygain", false).toBool(); }
void setReplayGainEnabled(bool v) { m_settings.setValue("playback/replaygain", v); } void setReplayGainEnabled(bool v) { m_settings.setValue("playback/replaygain", v); }
bool gaplessEnabled() const { return m_settings.value("playback/gapless", false).toBool(); }
void setGaplessEnabled(bool v) { m_settings.setValue("playback/gapless", v); }
// --- Last.fm --- // --- Last.fm ---
bool lastFmEnabled() const { return m_settings.value("lastfm/enabled", false).toBool(); } bool lastFmEnabled() const { return m_settings.value("lastfm/enabled", false).toBool(); }
void setLastFmEnabled(bool v) { m_settings.setValue("lastfm/enabled", v); } void setLastFmEnabled(bool v) { m_settings.setValue("lastfm/enabled", v); }