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..ec820028f0 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->SupportsHardwarePause()) + Cancel(); } bool diff --git a/src/output/Interface.hxx b/src/output/Interface.hxx index c3aeef2c1c..ae6011c7f6 100644 --- a/src/output/Interface.hxx +++ b/src/output/Interface.hxx @@ -44,6 +44,20 @@ public: 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/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..e91026c5ff 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 SupportsHardwarePause() 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 @@ -1139,13 +1147,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; - // TODO use snd_pcm_pause()? + if (!hw_can_pause) { + paused = true; + return true; + } + } + + 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 +1256,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 +1288,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();