diff --git a/rust/include/qobuz_backend.h b/rust/include/qobuz_backend.h index 01b602b..83d66a0 100644 --- a/rust/include/qobuz_backend.h +++ b/rust/include/qobuz_backend.h @@ -74,6 +74,7 @@ int qobuz_backend_take_track_finished(QobuzBackendOpaque *backend); // ReplayGain / Gapless 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); // Playlist management diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 1ab104a..e1044b5 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -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); } +#[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] pub unsafe extern "C" fn qobuz_backend_prefetch_track( ptr: *mut Backend, diff --git a/rust/src/player/mod.rs b/rust/src/player/mod.rs index b28f69c..872f1b5 100644 --- a/rust/src/player/mod.rs +++ b/rust/src/player/mod.rs @@ -51,6 +51,8 @@ pub struct PlayerStatus { pub seek_target_secs: Arc, /// Linear gain factor to apply (1.0 = unity). Updated each time a new track starts. pub replaygain_gain: Arc>, + /// When false the audio output is torn down after each track, producing a gap. + pub gapless: Arc, } impl PlayerStatus { @@ -65,6 +67,7 @@ impl PlayerStatus { seek_requested: Arc::new(AtomicBool::new(false)), seek_target_secs: Arc::new(AtomicU64::new(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, status: PlayerStatu pending_info = Some(next_info); } 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.track_finished.store(true, Ordering::SeqCst); } diff --git a/src/backend/qobuzbackend.cpp b/src/backend/qobuzbackend.cpp index 2ca0eea..dd5b826 100644 --- a/src/backend/qobuzbackend.cpp +++ b/src/backend/qobuzbackend.cpp @@ -86,6 +86,11 @@ void QobuzBackend::setReplayGain(bool 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) { qobuz_backend_prefetch_track(m_backend, trackId, formatId); diff --git a/src/backend/qobuzbackend.hpp b/src/backend/qobuzbackend.hpp index d76da97..4ac0a29 100644 --- a/src/backend/qobuzbackend.hpp +++ b/src/backend/qobuzbackend.hpp @@ -38,6 +38,7 @@ public: // --- playback options --- void setReplayGain(bool enabled); + void setGapless(bool enabled); void prefetchTrack(qint64 trackId, int formatId = 6); // --- playlist management --- diff --git a/src/dialog/settings.cpp b/src/dialog/settings.cpp index a3fef51..2b990a7 100644 --- a/src/dialog/settings.cpp +++ b/src/dialog/settings.cpp @@ -41,6 +41,10 @@ SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent) m_replayGain->setChecked(AppSettings::instance().replayGainEnabled()); 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); // --- Last.fm group --- @@ -96,6 +100,7 @@ void SettingsDialog::applyChanges() { AppSettings::instance().setPreferredFormat(m_formatBox->currentData().toInt()); AppSettings::instance().setReplayGainEnabled(m_replayGain->isChecked()); + AppSettings::instance().setGaplessEnabled(m_gapless->isChecked()); AppSettings::instance().setLastFmEnabled(m_lastFmEnabled->isChecked()); AppSettings::instance().setLastFmApiKey(m_lastFmApiKey->text().trimmed()); AppSettings::instance().setLastFmApiSecret(m_lastFmApiSecret->text().trimmed()); diff --git a/src/dialog/settings.hpp b/src/dialog/settings.hpp index 534b4cb..10ff78b 100644 --- a/src/dialog/settings.hpp +++ b/src/dialog/settings.hpp @@ -19,6 +19,7 @@ private: // Playback QComboBox *m_formatBox = nullptr; QCheckBox *m_replayGain = nullptr; + QCheckBox *m_gapless = nullptr; // Last.fm QCheckBox *m_lastFmEnabled = nullptr; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 3174f3d..0838733 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -160,6 +160,7 @@ MainWindow::MainWindow(QobuzBackend *backend, QWidget *parent) // Apply playback options from saved settings m_backend->setReplayGain(AppSettings::instance().replayGainEnabled()); + m_backend->setGapless(AppSettings::instance().gaplessEnabled()); tryRestoreSession(); } @@ -269,8 +270,8 @@ void MainWindow::onTrackChanged(const QJsonObject &track) statusBar()->showMessage( artist.isEmpty() ? title : QStringLiteral("▶ %1 — %2").arg(artist, title)); - // Prefetch next track URL to minimise the gap between tracks - if (m_queue->canGoNext()) { + // Prefetch next track URL when gapless is enabled + if (AppSettings::instance().gaplessEnabled() && m_queue->canGoNext()) { const auto upcoming = m_queue->upcomingTracks(1); if (!upcoming.isEmpty()) { const qint64 nextId = static_cast(upcoming.first()["id"].toDouble()); diff --git a/src/util/settings.hpp b/src/util/settings.hpp index 49828dc..e637ac6 100644 --- a/src/util/settings.hpp +++ b/src/util/settings.hpp @@ -38,6 +38,9 @@ public: bool replayGainEnabled() const { return m_settings.value("playback/replaygain", false).toBool(); } 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 --- bool lastFmEnabled() const { return m_settings.value("lastfm/enabled", false).toBool(); } void setLastFmEnabled(bool v) { m_settings.setValue("lastfm/enabled", v); }