From b80a135cf3cab19dfaef2ddc95a72e67d6c55f2a Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Fri, 2 Oct 2020 10:39:55 +0200
Subject: [PATCH] output/pulse: implement Interrupt()

---
 NEWS                                     |  1 +
 src/output/plugins/PulseOutputPlugin.cxx | 36 ++++++++++++++++++++++++
 2 files changed, 37 insertions(+)

diff --git a/NEWS b/NEWS
index 8e438ec1a..2e8dc14a6 100644
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,7 @@
 ver 0.22.1 (not yet released)
 * output
   - alsa: don't deadlock when the ALSA driver is buggy
+  - pulse: reduce the delay when stopping or pausing playback
 
 ver 0.22 (2020/09/23)
 * protocol
diff --git a/src/output/plugins/PulseOutputPlugin.cxx b/src/output/plugins/PulseOutputPlugin.cxx
index 6558232d0..c2e691b76 100644
--- a/src/output/plugins/PulseOutputPlugin.cxx
+++ b/src/output/plugins/PulseOutputPlugin.cxx
@@ -22,6 +22,7 @@
 #include "lib/pulse/LogError.hxx"
 #include "lib/pulse/LockGuard.hxx"
 #include "../OutputAPI.hxx"
+#include "../Error.hxx"
 #include "mixer/MixerList.hxx"
 #include "mixer/plugins/PulseMixerPlugin.hxx"
 #include "util/ScopeExit.hxx"
@@ -57,6 +58,15 @@ class PulseOutput final : AudioOutput {
 
 	bool pause;
 
+	/**
+	 * Was Interrupt() called?  This will unblock Play().  It will
+	 * be reset by Cancel() and Pause(), as documented by the
+	 * #AudioOutput interface.
+	 *
+	 * Only initialized while the output is open.
+	 */
+	bool interrupted;
+
 	explicit PulseOutput(const ConfigBlock &block);
 
 public:
@@ -99,6 +109,8 @@ public:
 	void Open(AudioFormat &audio_format) override;
 	void Close() noexcept override;
 
+	void Interrupt() noexcept override;
+
 	[[nodiscard]] std::chrono::steady_clock::duration Delay() const noexcept override;
 	size_t Play(const void *chunk, size_t size) override;
 	void Cancel() noexcept override;
@@ -677,6 +689,7 @@ PulseOutput::Open(AudioFormat &audio_format)
 	}
 
 	pause = false;
+	interrupted = false;
 }
 
 void
@@ -704,6 +717,21 @@ PulseOutput::Close() noexcept
 		DeleteContext();
 }
 
+void
+PulseOutput::Interrupt() noexcept
+{
+	if (mainloop == nullptr)
+		return;
+
+	const Pulse::LockGuard lock(mainloop);
+
+	/* the "interrupted" flag will prevent Play() from blocking,
+	   and will instead throw AudioOutputInterrupted */
+	interrupted = true;
+
+	Signal();
+}
+
 void
 PulseOutput::WaitStream()
 {
@@ -719,6 +747,9 @@ PulseOutput::WaitStream()
 					     "failed to connect the stream");
 
 		case PA_STREAM_CREATING:
+			if (interrupted)
+				throw AudioOutputInterrupted{};
+
 			pa_threaded_mainloop_wait(mainloop);
 			break;
 		}
@@ -784,6 +815,9 @@ PulseOutput::Play(const void *chunk, size_t size)
 		if (pa_stream_is_suspended(stream))
 			throw std::runtime_error("suspended");
 
+		if (interrupted)
+			throw AudioOutputInterrupted{};
+
 		pa_threaded_mainloop_wait(mainloop);
 
 		if (pa_stream_get_state(stream) != PA_STREAM_READY)
@@ -813,6 +847,7 @@ PulseOutput::Cancel() noexcept
 	assert(stream != nullptr);
 
 	Pulse::LockGuard lock(mainloop);
+	interrupted = false;
 
 	if (pa_stream_get_state(stream) != PA_STREAM_READY) {
 		/* no need to flush when the stream isn't connected
@@ -842,6 +877,7 @@ PulseOutput::Pause()
 	Pulse::LockGuard lock(mainloop);
 
 	pause = true;
+	interrupted = false;
 
 	/* check if the stream is (already/still) connected */