From ccce638359dee78edaa5f51346bd889108580a11 Mon Sep 17 00:00:00 2001 From: Mikael Magnusson Date: Wed, 20 May 2026 15:32:20 +0200 Subject: [PATCH 1/2] Use snd_pcm_pause if available --- src/lib/alsa/HwSetup.cxx | 2 + src/lib/alsa/HwSetup.hxx | 1 + src/output/Filtered.cxx | 3 +- src/output/Interface.hxx | 4 ++ src/output/Thread.cxx | 4 ++ src/output/plugins/AlsaOutputPlugin.cxx | 68 ++++++++++++++++++++++--- 6 files changed, 75 insertions(+), 7 deletions(-) diff --git a/src/lib/alsa/HwSetup.cxx b/src/lib/alsa/HwSetup.cxx index ab3482cd1e..828ff5c257 100644 --- a/src/lib/alsa/HwSetup.cxx +++ b/src/lib/alsa/HwSetup.cxx @@ -281,6 +281,8 @@ SetupHw(snd_pcm_t *pcm, if (err < 0) throw Alsa::MakeError(err, "snd_pcm_hw_params_get_period_size() failed"); + result.can_pause = snd_pcm_hw_params_can_pause(hwparams); + return result; } diff --git a/src/lib/alsa/HwSetup.hxx b/src/lib/alsa/HwSetup.hxx index 897b282932..4ab01fd737 100644 --- a/src/lib/alsa/HwSetup.hxx +++ b/src/lib/alsa/HwSetup.hxx @@ -15,6 +15,7 @@ namespace Alsa { struct HwResult { snd_pcm_format_t format; snd_pcm_uframes_t buffer_size, period_size; + bool can_pause; }; /** diff --git a/src/output/Filtered.cxx b/src/output/Filtered.cxx index 747e12c18c..693783bded 100644 --- a/src/output/Filtered.cxx +++ b/src/output/Filtered.cxx @@ -192,7 +192,8 @@ FilteredAudioOutput::Cancel() noexcept void FilteredAudioOutput::BeginPause() noexcept { - Cancel(); + if (!output->SupportsPauseWithoutCancel()) + Cancel(); } bool diff --git a/src/output/Interface.hxx b/src/output/Interface.hxx index c3aeef2c1c..7fed7a1cc2 100644 --- a/src/output/Interface.hxx +++ b/src/output/Interface.hxx @@ -40,6 +40,10 @@ public: return flags & FLAG_PAUSE; } + virtual bool SupportsPauseWithoutCancel() const noexcept { + return false; + } + bool GetNeedFullyDefinedAudioFormat() const noexcept { return flags & FLAG_NEED_FULLY_DEFINED_AUDIO_FORMAT; } diff --git a/src/output/Thread.cxx b/src/output/Thread.cxx index 01308455a8..9048a814c6 100644 --- a/src/output/Thread.cxx +++ b/src/output/Thread.cxx @@ -519,6 +519,10 @@ AudioOutputControl::Task() noexcept the actual playback */ if (source_state == SourceState::OPEN) source.Cancel(); + { + const ScopeUnlock unlock(mutex); + output->Cancel(); + } InternalPause(lock); } else { InternalClose(false); diff --git a/src/output/plugins/AlsaOutputPlugin.cxx b/src/output/plugins/AlsaOutputPlugin.cxx index 4bb2da3138..b703110783 100644 --- a/src/output/plugins/AlsaOutputPlugin.cxx +++ b/src/output/plugins/AlsaOutputPlugin.cxx @@ -232,6 +232,7 @@ class AlsaOutput final const bool close_on_pause; std::atomic_bool paused; + bool hw_can_pause = false; public: AlsaOutput(EventLoop &loop, const ConfigBlock &block); @@ -398,6 +399,11 @@ class AlsaOutput final /* virtual methods from class MultiSocketMonitor */ Event::Duration PrepareSockets() noexcept override; void DispatchSockets() noexcept override; + + bool SupportsPauseWithoutCancel() const noexcept override { + return !close_on_pause && hw_can_pause; + } + }; static constexpr Domain alsa_output_domain("alsa_output"); @@ -563,6 +569,8 @@ AlsaOutput::Setup(AudioFormat &audio_format, AlsaSetupSw(pcm, hw_result.buffer_size - hw_result.period_size, hw_result.period_size); + hw_can_pause = hw_result.can_pause; + auto alsa_period_size = hw_result.period_size; if (alsa_period_size == 0) /* this works around a SIGFPE bug that occurred when @@ -1095,6 +1103,7 @@ AlsaOutput::CancelInternal() noexcept ring_buffer.Clear(); active = false; + paused = false; waiting = false; UnregisterSockets(); @@ -1139,13 +1148,46 @@ AlsaOutput::Cancel() noexcept bool AlsaOutput::Pause() noexcept { - std::lock_guard lock{mutex}; - interrupted = false; + { + std::lock_guard lock{mutex}; + interrupted = false; - if (close_on_pause) - return false; + if (close_on_pause) + return false; + + if (!hw_can_pause) { + paused = true; + return true; + } + } - // TODO use snd_pcm_pause()? + if (LockIsActive()) { + BlockingCall(GetEventLoop(), [this](){ + /* only pause if actually running; if the PCM is + still in PREPARED state, there is nothing in + the hardware buffer worth preserving */ + if (snd_pcm_state(pcm) == SND_PCM_STATE_RUNNING) { + int err = snd_pcm_pause(pcm, /* enable */ 1); + if (err < 0) { + LogError(alsa_output_domain, + "snd_pcm_pause() failed"); + hw_can_pause = false; + return; + } + } + + UnregisterSockets(); + silence_timer.Cancel(); + + /* set waiting=true so that Activate() inside + Play() will re-schedule the event loop on + resume; without this, Activate() sees + active=true/waiting=false and returns early */ + waiting = true; + }); + } + if (!hw_can_pause) + return Pause(); // try again without hw pause paused = true; return true; @@ -1215,7 +1257,12 @@ AlsaOutput::Play(std::span src) assert(!src.empty()); assert(src.size() % in_frame_size == 0); - paused = false; + const bool was_paused = paused.exchange(false); + + if (was_paused) { + std::lock_guard lock{mutex}; + Activate(); + } const size_t max_frames = LockWaitWriteAvailable(); const size_t max_size = max_frames * in_frame_size; @@ -1242,6 +1289,15 @@ AlsaOutput::PrepareSockets() noexcept } try { + /* if the PCM was paused via snd_pcm_pause() (i.e. Pause() + was called with hw_can_pause), resume it now that Play() + has delivered new data and re-activated the I/O thread */ + if (snd_pcm_state(pcm) == SND_PCM_STATE_PAUSED) { + int err = snd_pcm_pause(pcm, /* disable */ 0); + if (err < 0) + throw Alsa::MakeError(err, "snd_pcm_pause() failed"); + } + return non_block.PrepareSockets(*this, pcm); } catch (...) { ClearSocketList(); From b7f489300febe716ae2ce2d738987e2ffa5d1c74 Mon Sep 17 00:00:00 2001 From: Mikael Magnusson Date: Thu, 21 May 2026 17:57:16 +0200 Subject: [PATCH 2/2] review comments --- src/output/Filtered.cxx | 2 +- src/output/Interface.hxx | 18 ++++++++++++++---- src/output/plugins/AlsaOutputPlugin.cxx | 3 +-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/output/Filtered.cxx b/src/output/Filtered.cxx index 693783bded..ec820028f0 100644 --- a/src/output/Filtered.cxx +++ b/src/output/Filtered.cxx @@ -192,7 +192,7 @@ FilteredAudioOutput::Cancel() noexcept void FilteredAudioOutput::BeginPause() noexcept { - if (!output->SupportsPauseWithoutCancel()) + if (!output->SupportsHardwarePause()) Cancel(); } diff --git a/src/output/Interface.hxx b/src/output/Interface.hxx index 7fed7a1cc2..ae6011c7f6 100644 --- a/src/output/Interface.hxx +++ b/src/output/Interface.hxx @@ -40,14 +40,24 @@ public: return flags & FLAG_PAUSE; } - virtual bool SupportsPauseWithoutCancel() const noexcept { - return false; - } - bool GetNeedFullyDefinedAudioFormat() const noexcept { return flags & FLAG_NEED_FULLY_DEFINED_AUDIO_FORMAT; } + /** + * Returns true if this plugin can pause playback without first + * discarding buffered audio via Cancel(). When this returns true, + * #FilteredAudioOutput::BeginPause() will skip the Cancel() call, + * allowing the plugin to preserve its hardware buffer across a pause + * and resume it intact when Play() is next called. + * + * Plugins that return true must handle buffer cleanup if + * Cancel() is called while paused (e.g. on stop or seek). + */ + virtual bool SupportsHardwarePause() const noexcept { + return false; + } + /** * Returns a map of runtime attributes. * diff --git a/src/output/plugins/AlsaOutputPlugin.cxx b/src/output/plugins/AlsaOutputPlugin.cxx index b703110783..e91026c5ff 100644 --- a/src/output/plugins/AlsaOutputPlugin.cxx +++ b/src/output/plugins/AlsaOutputPlugin.cxx @@ -400,7 +400,7 @@ class AlsaOutput final Event::Duration PrepareSockets() noexcept override; void DispatchSockets() noexcept override; - bool SupportsPauseWithoutCancel() const noexcept override { + bool SupportsHardwarePause() const noexcept override { return !close_on_pause && hw_can_pause; } @@ -1103,7 +1103,6 @@ AlsaOutput::CancelInternal() noexcept ring_buffer.Clear(); active = false; - paused = false; waiting = false; UnregisterSockets();