diff --git a/src/output/Control.cxx b/src/output/Control.cxx
index 4ae8f5e08..59736ecb2 100644
--- a/src/output/Control.cxx
+++ b/src/output/Control.cxx
@@ -38,7 +38,9 @@ static constexpr PeriodClock::Duration REOPEN_AFTER = std::chrono::seconds(10);
 struct notify audio_output_client_notify;
 
 AudioOutputControl::AudioOutputControl(AudioOutput *_output)
-	:output(_output), mutex(output->mutex)
+	:output(_output),
+	 thread(BIND_THIS_METHOD(Task)),
+	 mutex(output->mutex)
 {
 }
 
@@ -91,20 +93,8 @@ AudioOutputControl::IsOpen() const
 	return output->IsOpen();
 }
 
-bool
-AudioOutputControl::IsBusy() const
-{
-	return output->IsBusy();
-}
-
-const std::exception_ptr &
-AudioOutputControl::GetLastError() const
-{
-	return output->GetLastError();
-}
-
 void
-AudioOutput::WaitForCommand()
+AudioOutputControl::WaitForCommand()
 {
 	while (!IsCommandFinished()) {
 		mutex.unlock();
@@ -114,13 +104,7 @@ AudioOutput::WaitForCommand()
 }
 
 void
-AudioOutputControl::WaitForCommand()
-{
-	output->WaitForCommand();
-}
-
-void
-AudioOutput::CommandAsync(Command cmd)
+AudioOutputControl::CommandAsync(Command cmd)
 {
 	assert(IsCommandFinished());
 
@@ -129,28 +113,28 @@ AudioOutput::CommandAsync(Command cmd)
 }
 
 void
-AudioOutput::CommandWait(Command cmd)
+AudioOutputControl::CommandWait(Command cmd)
 {
 	CommandAsync(cmd);
 	WaitForCommand();
 }
 
 void
-AudioOutput::LockCommandWait(Command cmd)
+AudioOutputControl::LockCommandWait(Command cmd)
 {
 	const std::lock_guard<Mutex> protect(mutex);
 	CommandWait(cmd);
 }
 
 void
-AudioOutput::EnableAsync()
+AudioOutputControl::EnableAsync()
 {
 	if (!thread.IsDefined()) {
-		if (plugin.enable == nullptr) {
+		if (output->plugin.enable == nullptr) {
 			/* don't bother to start the thread now if the
 			   device doesn't even have a enable() method;
 			   just assign the variable and we're done */
-			really_enabled = true;
+			output->really_enabled = true;
 			return;
 		}
 
@@ -161,15 +145,15 @@ AudioOutput::EnableAsync()
 }
 
 void
-AudioOutput::DisableAsync()
+AudioOutputControl::DisableAsync()
 {
 	if (!thread.IsDefined()) {
-		if (plugin.disable == nullptr)
-			really_enabled = false;
+		if (output->plugin.disable == nullptr)
+			output->really_enabled = false;
 		else
 			/* if there's no thread yet, the device cannot
 			   be enabled */
-			assert(!really_enabled);
+			assert(!output->really_enabled);
 
 		return;
 	}
@@ -180,27 +164,27 @@ AudioOutput::DisableAsync()
 void
 AudioOutputControl::EnableDisableAsync()
 {
-	output->EnableDisableAsync();
-}
+	if (output->enabled == output->really_enabled)
+		return;
 
-void
-AudioOutputControl::LockPauseAsync()
-{
-	output->LockPauseAsync();
+	if (output->enabled)
+		EnableAsync();
+	else
+		DisableAsync();
 }
 
 inline bool
-AudioOutput::Open(const AudioFormat audio_format, const MusicPipe &mp)
+AudioOutputControl::Open(const AudioFormat audio_format, const MusicPipe &mp)
 {
 	assert(allow_play);
 	assert(audio_format.IsValid());
 
 	fail_timer.Reset();
 
-	if (open && audio_format == request.audio_format) {
-		assert(request.pipe == &mp || (always_on && pause));
+	if (output->open && audio_format == request.audio_format) {
+		assert(request.pipe == &mp || (output->always_on && output->pause));
 
-		if (!pause)
+		if (!output->pause)
 			/* already open, already the right parameters
 			   - nothing needs to be done */
 			return true;
@@ -213,13 +197,14 @@ AudioOutput::Open(const AudioFormat audio_format, const MusicPipe &mp)
 		StartThread();
 
 	CommandWait(Command::OPEN);
-	const bool open2 = open;
+	const bool open2 = output->open;
 
-	if (open2 && mixer != nullptr) {
+	if (open2 && output->mixer != nullptr) {
 		try {
-			mixer_open(mixer);
+			mixer_open(output->mixer);
 		} catch (const std::runtime_error &e) {
-			FormatError(e, "Failed to open mixer for '%s'", name);
+			FormatError(e, "Failed to open mixer for '%s'",
+				    GetName());
 		}
 	}
 
@@ -227,29 +212,29 @@ AudioOutput::Open(const AudioFormat audio_format, const MusicPipe &mp)
 }
 
 void
-AudioOutput::CloseWait()
+AudioOutputControl::CloseWait()
 {
 	assert(allow_play);
 
-	if (mixer != nullptr)
-		mixer_auto_close(mixer);
+	if (output->mixer != nullptr)
+		mixer_auto_close(output->mixer);
 
-	assert(!open || !fail_timer.IsDefined());
+	assert(!output->open || !fail_timer.IsDefined());
 
-	if (open)
+	if (output->open)
 		CommandWait(Command::CLOSE);
 	else
 		fail_timer.Reset();
 }
 
 bool
-AudioOutput::LockUpdate(const AudioFormat audio_format,
-			const MusicPipe &mp,
-			bool force)
+AudioOutputControl::LockUpdate(const AudioFormat audio_format,
+			       const MusicPipe &mp,
+			       bool force)
 {
 	const std::lock_guard<Mutex> protect(mutex);
 
-	if (enabled && really_enabled) {
+	if (output->enabled && output->really_enabled) {
 		if (force || !fail_timer.IsDefined() ||
 		    fail_timer.Check(REOPEN_AFTER * 1000)) {
 			return Open(audio_format, mp);
@@ -260,26 +245,6 @@ AudioOutput::LockUpdate(const AudioFormat audio_format,
 	return false;
 }
 
-void
-AudioOutputControl::LockRelease()
-{
-	output->LockRelease();
-}
-
-void
-AudioOutputControl::LockCloseWait()
-{
-	output->LockCloseWait();
-}
-
-bool
-AudioOutputControl::LockUpdate(const AudioFormat audio_format,
-			       const MusicPipe &mp,
-			       bool force)
-{
-	return output->LockUpdate(audio_format, mp, force);
-}
-
 bool
 AudioOutputControl::LockIsChunkConsumed(const MusicChunk &chunk) const
 {
@@ -293,7 +258,7 @@ AudioOutputControl::ClearTailChunk(const MusicChunk &chunk)
 }
 
 void
-AudioOutput::LockPlay()
+AudioOutputControl::LockPlay()
 {
 	const std::lock_guard<Mutex> protect(mutex);
 
@@ -306,13 +271,13 @@ AudioOutput::LockPlay()
 }
 
 void
-AudioOutput::LockPauseAsync()
+AudioOutputControl::LockPauseAsync()
 {
-	if (mixer != nullptr && plugin.pause == nullptr)
+	if (output->mixer != nullptr && output->plugin.pause == nullptr)
 		/* the device has no pause mode: close the mixer,
 		   unless its "global" flag is set (checked by
 		   mixer_auto_close()) */
-		mixer_auto_close(mixer);
+		mixer_auto_close(output->mixer);
 
 	const std::lock_guard<Mutex> protect(mutex);
 
@@ -322,7 +287,7 @@ AudioOutput::LockPauseAsync()
 }
 
 void
-AudioOutput::LockDrainAsync()
+AudioOutputControl::LockDrainAsync()
 {
 	const std::lock_guard<Mutex> protect(mutex);
 
@@ -332,7 +297,7 @@ AudioOutput::LockDrainAsync()
 }
 
 void
-AudioOutput::LockCancelAsync()
+AudioOutputControl::LockCancelAsync()
 {
 	const std::lock_guard<Mutex> protect(mutex);
 
@@ -343,7 +308,7 @@ AudioOutput::LockCancelAsync()
 }
 
 void
-AudioOutput::LockAllowPlay()
+AudioOutputControl::LockAllowPlay()
 {
 	const std::lock_guard<Mutex> protect(mutex);
 
@@ -353,18 +318,18 @@ AudioOutput::LockAllowPlay()
 }
 
 void
-AudioOutput::LockRelease()
+AudioOutputControl::LockRelease()
 {
-	if (always_on)
+	if (output->always_on)
 		LockPauseAsync();
 	else
 		LockCloseWait();
 }
 
 void
-AudioOutput::LockCloseWait()
+AudioOutputControl::LockCloseWait()
 {
-	assert(!open || !fail_timer.IsDefined());
+	assert(!output->open || !fail_timer.IsDefined());
 
 	const std::lock_guard<Mutex> protect(mutex);
 	CloseWait();
@@ -377,7 +342,7 @@ AudioOutputControl::SetReplayGainMode(ReplayGainMode _mode)
 }
 
 void
-AudioOutput::StopThread()
+AudioOutputControl::StopThread()
 {
 	assert(thread.IsDefined());
 	assert(allow_play);
@@ -391,6 +356,12 @@ AudioOutput::BeginDestroy()
 {
 	if (mixer != nullptr)
 		mixer_auto_close(mixer);
+}
+
+void
+AudioOutputControl::BeginDestroy()
+{
+	output->BeginDestroy();
 
 	if (thread.IsDefined()) {
 		const std::lock_guard<Mutex> protect(mutex);
@@ -398,48 +369,18 @@ AudioOutput::BeginDestroy()
 	}
 }
 
-void
-AudioOutputControl::BeginDestroy()
-{
-	output->BeginDestroy();
-}
-
 void
 AudioOutput::FinishDestroy()
 {
-	if (thread.IsDefined())
-		thread.Join();
-
 	audio_output_free(this);
 }
 
 void
 AudioOutputControl::FinishDestroy()
 {
+	if (thread.IsDefined())
+		thread.Join();
+
 	output->FinishDestroy();
 	output = nullptr;
 }
-
-void
-AudioOutputControl::LockPlay()
-{
-	output->LockPlay();
-}
-
-void
-AudioOutputControl::LockDrainAsync()
-{
-	output->LockDrainAsync();
-}
-
-void
-AudioOutputControl::LockCancelAsync()
-{
-	output->LockCancelAsync();
-}
-
-void
-AudioOutputControl::LockAllowPlay()
-{
-	output->LockAllowPlay();
-}
diff --git a/src/output/Control.hxx b/src/output/Control.hxx
index 8aa2161a1..74a90e4d9 100644
--- a/src/output/Control.hxx
+++ b/src/output/Control.hxx
@@ -20,6 +20,10 @@
 #ifndef MPD_OUTPUT_CONTROL_HXX
 #define MPD_OUTPUT_CONTROL_HXX
 
+#include "AudioFormat.hxx"
+#include "thread/Thread.hxx"
+#include "thread/Cond.hxx"
+#include "system/PeriodClock.hxx"
 #include "Compiler.h"
 
 #include <utility>
@@ -32,7 +36,6 @@
 #include <stdint.h>
 
 enum class ReplayGainMode : uint8_t;
-struct AudioFormat;
 struct AudioOutput;
 struct MusicChunk;
 class MusicPipe;
@@ -46,6 +49,99 @@ class AudioOutputClient;
 class AudioOutputControl {
 	AudioOutput *output;
 
+	/**
+	 * The error that occurred in the output thread.  It is
+	 * cleared whenever the output is opened successfully.
+	 *
+	 * Protected by #mutex.
+	 */
+	std::exception_ptr last_error;
+
+	/**
+	 * If not nullptr, the device has failed, and this timer is used
+	 * to estimate how long it should stay disabled (unless
+	 * explicitly reopened with "play").
+	 */
+	PeriodClock fail_timer;
+
+	/**
+	 * The thread handle, or nullptr if the output thread isn't
+	 * running.
+	 */
+	Thread thread;
+
+	/**
+	 * This condition object wakes up the output thread after
+	 * #command has been set.
+	 */
+	Cond cond;
+
+	/**
+	 * Additional data for #command.  Protected by #mutex.
+	 */
+	struct Request {
+		/**
+		 * The #AudioFormat requested by #Command::OPEN.
+		 */
+		AudioFormat audio_format;
+
+		/**
+		 * The #MusicPipe passed to #Command::OPEN.
+		 */
+		const MusicPipe *pipe;
+	} request;
+
+	/**
+	 * The next command to be performed by the output thread.
+	 */
+	enum class Command {
+		NONE,
+		ENABLE,
+		DISABLE,
+
+		/**
+		 * Open the output, or reopen it if it is already
+		 * open, adjusting for input #AudioFormat changes.
+		 */
+		OPEN,
+
+		CLOSE,
+		PAUSE,
+
+		/**
+		 * Drains the internal (hardware) buffers of the device.  This
+		 * operation may take a while to complete.
+		 */
+		DRAIN,
+
+		CANCEL,
+		KILL
+	} command = Command::NONE;
+
+	/**
+	 * When this flag is set, the output thread will not do any
+	 * playback.  It will wait until the flag is cleared.
+	 *
+	 * This is used to synchronize the "clear" operation on the
+	 * shared music pipe during the CANCEL command.
+	 */
+	bool allow_play = true;
+
+	/**
+	 * True while the OutputThread is inside ao_play().  This
+	 * means the PlayerThread does not need to wake up the
+	 * OutputThread when new chunks are added to the MusicPipe,
+	 * because the OutputThread is already watching that.
+	 */
+	bool in_playback_loop = false;
+
+	/**
+	 * Has the OutputThread been woken up to play more chunks?
+	 * This is set by audio_output_play() and reset by ao_play()
+	 * to reduce the number of duplicate wakeups.
+	 */
+	bool woken_for_play = false;
+
 public:
 	Mutex &mutex;
 
@@ -53,6 +149,8 @@ public:
 
 #ifndef NDEBUG
 	~AudioOutputControl() {
+		assert(!fail_timer.IsDefined());
+		assert(!thread.IsDefined());
 		assert(output == nullptr);
 	}
 #endif
@@ -84,28 +182,103 @@ public:
 	gcc_pure
 	bool IsOpen() const;
 
-	gcc_pure
-	bool IsBusy() const;
+	/**
+	 * Caller must lock the mutex.
+	 */
+	bool IsBusy() const {
+		return IsOpen() && !IsCommandFinished();
+	}
 
 	/**
 	 * Caller must lock the mutex.
 	 */
-	gcc_const
-	const std::exception_ptr &GetLastError() const;
+	const std::exception_ptr &GetLastError() const {
+		return last_error;
+	}
 
+	void StartThread();
+	void StopThread();
+
+	/**
+	 * Caller must lock the mutex.
+	 */
+	bool IsCommandFinished() const {
+		return command == Command::NONE;
+	}
+
+	void CommandFinished();
+
+	/**
+	 * Waits for command completion.
+	 *
+	 * Caller must lock the mutex.
+	 */
 	void WaitForCommand();
 
+	/**
+	 * Sends a command, but does not wait for completion.
+	 *
+	 * Caller must lock the mutex.
+	 */
+	void CommandAsync(Command cmd);
+
+	/**
+	 * Sends a command to the #AudioOutput object and waits for
+	 * completion.
+	 *
+	 * Caller must lock the mutex.
+	 */
+	void CommandWait(Command cmd);
+
+	/**
+	 * Lock the #AudioOutput object and execute the command
+	 * synchronously.
+	 */
+	void LockCommandWait(Command cmd);
+
 	void BeginDestroy();
 	void FinishDestroy();
 
+	/**
+	 * Enables the device, but don't wait for completion.
+	 *
+	 * Caller must lock the mutex.
+	 */
+	void EnableAsync();
+
+	/**
+	 * Disables the device, but don't wait for completion.
+	 *
+	 * Caller must lock the mutex.
+	 */
+	void DisableAsync();
+
+	/**
+	 * Attempt to enable or disable the device as specified by the
+	 * #enabled attribute; attempt to sync it with #really_enabled
+	 * (wrapper for EnableAsync() or DisableAsync()).
+	 *
+	 * Caller must lock the mutex.
+	 */
 	void EnableDisableAsync();
 	void LockPauseAsync();
 
+	void CloseWait();
 	void LockCloseWait();
+
+	/**
+	 * Closes the audio output, but if the "always_on" flag is set, put it
+	 * into pause mode instead.
+	 */
 	void LockRelease();
 
 	void SetReplayGainMode(ReplayGainMode _mode);
 
+	/**
+	 * Caller must lock the mutex.
+	 */
+	bool Open(const AudioFormat audio_format, const MusicPipe &mp);
+
 	/**
 	 * Opens or closes the device, depending on the "enabled"
 	 * flag.
@@ -124,8 +297,48 @@ public:
 
 	void LockPlay();
 	void LockDrainAsync();
+
+	/**
+	 * Clear the "allow_play" flag and send the "CANCEL" command
+	 * asynchronously.  To finish the operation, the caller has to
+	 * call LockAllowPlay().
+	 */
 	void LockCancelAsync();
+
+	/**
+	 * Set the "allow_play" and signal the thread.
+	 */
 	void LockAllowPlay();
+
+private:
+	/**
+	 * Wait until the output's delay reaches zero.
+	 *
+	 * @return true if playback should be continued, false if a
+	 * command was issued
+	 */
+	bool WaitForDelay();
+
+	bool FillSourceOrClose();
+
+	bool PlayChunk();
+
+	/**
+	 * Plays all remaining chunks, until the tail of the pipe has
+	 * been reached (and no more chunks are queued), or until a
+	 * command is received.
+	 *
+	 * @return true if at least one chunk has been available,
+	 * false if the tail of the pipe was already reached
+	 */
+	bool Play();
+
+	void Pause();
+
+	/**
+	 * The OutputThread.
+	 */
+	void Task();
 };
 
 #endif
diff --git a/src/output/Finish.cxx b/src/output/Finish.cxx
index d3f3ed665..3aa31b652 100644
--- a/src/output/Finish.cxx
+++ b/src/output/Finish.cxx
@@ -28,8 +28,6 @@
 AudioOutput::~AudioOutput()
 {
 	assert(!open);
-	assert(!fail_timer.IsDefined());
-	assert(!thread.IsDefined());
 
 	if (mixer != nullptr)
 		mixer_free(mixer);
@@ -43,8 +41,6 @@ void
 audio_output_free(AudioOutput *ao)
 {
 	assert(!ao->IsOpen());
-	assert(!ao->fail_timer.IsDefined());
-	assert(!ao->thread.IsDefined());
 
 	ao_plugin_finish(ao);
 }
diff --git a/src/output/Init.cxx b/src/output/Init.cxx
index 185971d8c..df28292e6 100644
--- a/src/output/Init.cxx
+++ b/src/output/Init.cxx
@@ -51,8 +51,7 @@
 
 AudioOutput::AudioOutput(const AudioOutputPlugin &_plugin,
 			 const ConfigBlock &block)
-	:plugin(_plugin),
-	 thread(BIND_THIS_METHOD(Task))
+	:plugin(_plugin)
 {
 	assert(plugin.finish != nullptr);
 	assert(plugin.open != nullptr);
diff --git a/src/output/Internal.hxx b/src/output/Internal.hxx
index bbc8a4ae1..45bd25420 100644
--- a/src/output/Internal.hxx
+++ b/src/output/Internal.hxx
@@ -21,15 +21,9 @@
 #define MPD_OUTPUT_INTERNAL_HXX
 
 #include "Source.hxx"
-#include "SharedPipeConsumer.hxx"
 #include "AudioFormat.hxx"
 #include "filter/Observer.hxx"
 #include "thread/Mutex.hxx"
-#include "thread/Cond.hxx"
-#include "thread/Thread.hxx"
-#include "system/PeriodClock.hxx"
-
-#include <exception>
 
 class PreparedFilter;
 class MusicPipe;
@@ -43,30 +37,6 @@ struct AudioOutputPlugin;
 struct ReplayGainConfig;
 
 struct AudioOutput {
-	enum class Command {
-		NONE,
-		ENABLE,
-		DISABLE,
-
-		/**
-		 * Open the output, or reopen it if it is already
-		 * open, adjusting for input #AudioFormat changes.
-		 */
-		OPEN,
-
-		CLOSE,
-		PAUSE,
-
-		/**
-		 * Drains the internal (hardware) buffers of the device.  This
-		 * operation may take a while to complete.
-		 */
-		DRAIN,
-
-		CANCEL,
-		KILL
-	};
-
 	/**
 	 * The device's configured display name.
 	 */
@@ -124,37 +94,6 @@ struct AudioOutput {
 	 */
 	bool pause = false;
 
-	/**
-	 * When this flag is set, the output thread will not do any
-	 * playback.  It will wait until the flag is cleared.
-	 *
-	 * This is used to synchronize the "clear" operation on the
-	 * shared music pipe during the CANCEL command.
-	 */
-	bool allow_play = true;
-
-	/**
-	 * True while the OutputThread is inside ao_play().  This
-	 * means the PlayerThread does not need to wake up the
-	 * OutputThread when new chunks are added to the MusicPipe,
-	 * because the OutputThread is already watching that.
-	 */
-	bool in_playback_loop = false;
-
-	/**
-	 * Has the OutputThread been woken up to play more chunks?
-	 * This is set by audio_output_play() and reset by ao_play()
-	 * to reduce the number of duplicate wakeups.
-	 */
-	bool woken_for_play = false;
-
-	/**
-	 * If not nullptr, the device has failed, and this timer is used
-	 * to estimate how long it should stay disabled (unless
-	 * explicitly reopened with "play").
-	 */
-	PeriodClock fail_timer;
-
 	/**
 	 * The configured audio format.
 	 */
@@ -209,43 +148,11 @@ struct AudioOutput {
 	 */
 	FilterObserver convert_filter;
 
-	/**
-	 * The thread handle, or nullptr if the output thread isn't
-	 * running.
-	 */
-	Thread thread;
-
-	/**
-	 * The next command to be performed by the output thread.
-	 */
-	Command command = Command::NONE;
-
-	/**
-	 * Additional data for #command.  Protected by #mutex.
-	 */
-	struct Request {
-		/**
-		 * The #AudioFormat requested by #Command::OPEN.
-		 */
-		AudioFormat audio_format;
-
-		/**
-		 * The #MusicPipe passed to #Command::OPEN.
-		 */
-		const MusicPipe *pipe;
-	} request;
-
 	/**
 	 * This mutex protects #open, #fail_timer, #pipe.
 	 */
 	mutable Mutex mutex;
 
-	/**
-	 * This condition object wakes up the output thread after
-	 * #command has been set.
-	 */
-	Cond cond;
-
 	/**
 	 * The PlayerControl object which "owns" this output.  This
 	 * object is needed to signal command completion.
@@ -257,14 +164,6 @@ struct AudioOutput {
 	 */
 	AudioOutputSource source;
 
-	/**
-	 * The error that occurred in the output thread.  It is
-	 * cleared whenever the output is opened successfully.
-	 *
-	 * Protected by #mutex.
-	 */
-	std::exception_ptr last_error;
-
 	/**
 	 * Throws #std::runtime_error on error.
 	 */
@@ -282,9 +181,6 @@ public:
 		   MixerListener &mixer_listener,
 		   const ConfigBlock &block);
 
-	void StartThread();
-	void StopThread();
-
 	void BeginDestroy();
 	void FinishDestroy();
 
@@ -306,137 +202,10 @@ public:
 		return open;
 	}
 
-	/**
-	 * Caller must lock the mutex.
-	 */
-	bool IsCommandFinished() const {
-		return command == Command::NONE;
-	}
-
-	/**
-	 * Caller must lock the mutex.
-	 */
-	bool IsBusy() const {
-		return IsOpen() && !IsCommandFinished();
-	}
-
-	/**
-	 * Caller must lock the mutex.
-	 */
-	const std::exception_ptr &GetLastError() const {
-		return last_error;
-	}
-
-	/**
-	 * Waits for command completion.
-	 *
-	 * Caller must lock the mutex.
-	 */
-	void WaitForCommand();
-
-	/**
-	 * Sends a command, but does not wait for completion.
-	 *
-	 * Caller must lock the mutex.
-	 */
-	void CommandAsync(Command cmd);
-
-	/**
-	 * Sends a command to the #AudioOutput object and waits for
-	 * completion.
-	 *
-	 * Caller must lock the mutex.
-	 */
-	void CommandWait(Command cmd);
-
-	/**
-	 * Lock the #AudioOutput object and execute the command
-	 * synchronously.
-	 */
-	void LockCommandWait(Command cmd);
-
-	/**
-	 * Enables the device, but don't wait for completion.
-	 *
-	 * Caller must lock the mutex.
-	 */
-	void EnableAsync();
-
-	/**
-	 * Disables the device, but don't wait for completion.
-	 *
-	 * Caller must lock the mutex.
-	 */
-	void DisableAsync();
-
-	/**
-	 * Attempt to enable or disable the device as specified by the
-	 * #enabled attribute; attempt to sync it with #really_enabled
-	 * (wrapper for EnableAsync() or DisableAsync()).
-	 *
-	 * Caller must lock the mutex.
-	 */
-	void EnableDisableAsync() {
-		if (enabled == really_enabled)
-			return;
-
-		if (enabled)
-			EnableAsync();
-		else
-			DisableAsync();
-	}
-
-	void LockPauseAsync();
-
-	/**
-	 * Same LockCloseWait(), but expects the lock to be
-	 * held by the caller.
-	 */
-	void CloseWait();
-	void LockCloseWait();
-
-	/**
-	 * Closes the audio output, but if the "always_on" flag is set, put it
-	 * into pause mode instead.
-	 */
-	void LockRelease();
-
 	void SetReplayGainMode(ReplayGainMode _mode) {
 		source.SetReplayGainMode(_mode);
 	}
 
-	/**
-	 * Caller must lock the mutex.
-	 */
-	bool Open(const AudioFormat audio_format, const MusicPipe &mp);
-
-	/**
-	 * Opens or closes the device, depending on the "enabled"
-	 * flag.
-	 *
-	 * @param force true to ignore the #fail_timer
-	 * @return true if the device is open
-	 */
-	bool LockUpdate(const AudioFormat audio_format,
-			const MusicPipe &mp,
-			bool force);
-
-	void LockPlay();
-
-	void LockDrainAsync();
-
-	/**
-	 * Clear the "allow_play" flag and send the "CANCEL" command
-	 * asynchronously.  To finish the operation, the caller has to
-	 * call LockAllowPlay().
-	 */
-	void LockCancelAsync();
-
-	/**
-	 * Set the "allow_play" and signal the thread.
-	 */
-	void LockAllowPlay();
-
 	/**
 	 * Did we already consumed this chunk?
 	 *
@@ -455,9 +224,6 @@ public:
 		source.ClearTailChunk(chunk);
 	}
 
-private:
-	void CommandFinished();
-
 	/**
 	 * Throws #std::runtime_error on error.
 	 */
@@ -468,8 +234,11 @@ private:
 	/**
 	 * Throws #std::runtime_error on error.
 	 */
-	void Open();
+	void Open(AudioFormat audio_format, const MusicPipe &pipe);
 
+	void Close(bool drain);
+
+private:
 	/**
 	 * Invoke OutputPlugin::open() and configure the
 	 * #ConvertFilter.
@@ -480,8 +249,6 @@ private:
 	 */
 	void OpenOutputAndConvert(AudioFormat audio_format);
 
-	void Close(bool drain);
-
 	/**
 	 * Close the output plugin.
 	 *
@@ -494,37 +261,13 @@ private:
 	 */
 	void CloseFilter();
 
-	/**
-	 * Wait until the output's delay reaches zero.
-	 *
-	 * @return true if playback should be continued, false if a
-	 * command was issued
-	 */
-	bool WaitForDelay();
-
-	bool FillSourceOrClose();
-
-	bool PlayChunk();
-
-	/**
-	 * Plays all remaining chunks, until the tail of the pipe has
-	 * been reached (and no more chunks are queued), or until a
-	 * command is received.
-	 *
-	 * @return true if at least one chunk has been available,
-	 * false if the tail of the pipe was already reached
-	 */
-	bool Play();
-
+public:
 	void BeginPause();
 	bool IteratePause();
 
-	void Pause();
-
-	/**
-	 * The OutputThread.
-	 */
-	void Task();
+	void EndPause() {
+		pause = false;
+	}
 };
 
 /**
diff --git a/src/output/Thread.cxx b/src/output/Thread.cxx
index 785ea947a..13b9079ac 100644
--- a/src/output/Thread.cxx
+++ b/src/output/Thread.cxx
@@ -48,7 +48,7 @@
 #include <string.h>
 
 void
-AudioOutput::CommandFinished()
+AudioOutputControl::CommandFinished()
 {
 	assert(command != Command::NONE);
 	command = Command::NONE;
@@ -98,11 +98,9 @@ AudioOutput::CloseFilter()
 }
 
 inline void
-AudioOutput::Open()
+AudioOutput::Open(AudioFormat audio_format, const MusicPipe &pipe)
 {
-	assert(request.audio_format.IsValid());
-
-	fail_timer.Reset();
+	assert(audio_format.IsValid());
 
 	/* enable the device (just in case the last enable has failed) */
 	Enable();
@@ -110,7 +108,7 @@ AudioOutput::Open()
 	AudioFormat f;
 
 	try {
-		f = source.Open(request.audio_format, *request.pipe,
+		f = source.Open(audio_format, pipe,
 				prepared_replay_gain_filter,
 				prepared_other_replay_gain_filter,
 				prepared_filter);
@@ -239,10 +237,10 @@ AudioOutput::CloseOutput(bool drain)
  * was issued
  */
 inline bool
-AudioOutput::WaitForDelay()
+AudioOutputControl::WaitForDelay()
 {
 	while (true) {
-		const auto delay = ao_plugin_delay(*this);
+		const auto delay = ao_plugin_delay(*output);
 		if (delay <= std::chrono::steady_clock::duration::zero())
 			return true;
 
@@ -254,14 +252,14 @@ AudioOutput::WaitForDelay()
 }
 
 bool
-AudioOutput::FillSourceOrClose()
+AudioOutputControl::FillSourceOrClose()
 try {
-	return source.Fill(mutex);
+	return output->source.Fill(mutex);
 } catch (const std::runtime_error &e) {
 	FormatError(e, "Failed to filter for output \"%s\" [%s]",
-		    name, plugin.name);
+		    GetName(), output->plugin.name);
 
-	Close(false);
+	output->Close(false);
 
 	/* don't automatically reopen this device for 10
 	   seconds */
@@ -270,23 +268,23 @@ try {
 }
 
 inline bool
-AudioOutput::PlayChunk()
+AudioOutputControl::PlayChunk()
 {
-	if (tags) {
-		const auto *tag = source.ReadTag();
+	if (output->tags) {
+		const auto *tag = output->source.ReadTag();
 		if (tag != nullptr) {
 			const ScopeUnlock unlock(mutex);
 			try {
-				ao_plugin_send_tag(*this, *tag);
+				ao_plugin_send_tag(*output, *tag);
 			} catch (const std::runtime_error &e) {
 				FormatError(e, "Failed to send tag to \"%s\" [%s]",
-					    name, plugin.name);
+					    GetName(), output->plugin.name);
 			}
 		}
 	}
 
 	while (command == Command::NONE) {
-		const auto data = source.PeekData();
+		const auto data = output->source.PeekData();
 		if (data.IsEmpty())
 			break;
 
@@ -297,17 +295,17 @@ AudioOutput::PlayChunk()
 
 		try {
 			const ScopeUnlock unlock(mutex);
-			nbytes = ao_plugin_play(*this, data.data, data.size);
+			nbytes = ao_plugin_play(*output, data.data, data.size);
 			assert(nbytes <= data.size);
 		} catch (const std::runtime_error &e) {
 			FormatError(e, "\"%s\" [%s] failed to play",
-				    name, plugin.name);
+				    GetName(), output->plugin.name);
 
 			nbytes = 0;
 		}
 
 		if (nbytes == 0) {
-			Close(false);
+			output->Close(false);
 
 			/* don't automatically reopen this device for
 			   10 seconds */
@@ -317,16 +315,16 @@ AudioOutput::PlayChunk()
 			return false;
 		}
 
-		assert(nbytes % out_audio_format.GetFrameSize() == 0);
+		assert(nbytes % output->out_audio_format.GetFrameSize() == 0);
 
-		source.ConsumeData(nbytes);
+		output->source.ConsumeData(nbytes);
 	}
 
 	return true;
 }
 
 inline bool
-AudioOutput::Play()
+AudioOutputControl::Play()
 {
 	if (!FillSourceOrClose())
 		/* no chunk available */
@@ -351,7 +349,7 @@ AudioOutput::Play()
 			   give it a chance to refill the pipe before
 			   it runs empty */
 			const ScopeUnlock unlock(mutex);
-			client->ChunksConsumed();
+			output->client->ChunksConsumed();
 			n = 0;
 		}
 
@@ -360,7 +358,7 @@ AudioOutput::Play()
 	} while (FillSourceOrClose());
 
 	const ScopeUnlock unlock(mutex);
-	client->ChunksConsumed();
+	output->client->ChunksConsumed();
 
 	return true;
 }
@@ -397,26 +395,26 @@ AudioOutput::IteratePause()
 }
 
 inline void
-AudioOutput::Pause()
+AudioOutputControl::Pause()
 {
-	BeginPause();
+	output->BeginPause();
 	CommandFinished();
 
 	do {
 		if (!WaitForDelay())
 			break;
 
-		if (!IteratePause())
+		if (!output->IteratePause())
 			break;
 	} while (command == Command::NONE);
 
-	pause = false;
+	output->EndPause();
 }
 
 void
-AudioOutput::Task()
+AudioOutputControl::Task()
 {
-	FormatThreadName("output:%s", name);
+	FormatThreadName("output:%s", GetName());
 
 	try {
 		SetThreadRealtime();
@@ -438,7 +436,7 @@ AudioOutput::Task()
 			last_error = nullptr;
 
 			try {
-				Enable();
+				output->Enable();
 			} catch (const std::runtime_error &e) {
 				LogError(e);
 				fail_timer.Update();
@@ -449,15 +447,17 @@ AudioOutput::Task()
 			break;
 
 		case Command::DISABLE:
-			Disable();
+			output->Disable();
 			CommandFinished();
 			break;
 
 		case Command::OPEN:
 			last_error = nullptr;
+			fail_timer.Reset();
 
 			try {
-				Open();
+				output->Open(request.audio_format,
+					     *request.pipe);
 			} catch (const std::runtime_error &e) {
 				LogError(e);
 				fail_timer.Update();
@@ -468,13 +468,13 @@ AudioOutput::Task()
 			break;
 
 		case Command::CLOSE:
-			if (open)
-				Close(false);
+			if (IsOpen())
+				output->Close(false);
 			CommandFinished();
 			break;
 
 		case Command::PAUSE:
-			if (!open) {
+			if (!output->open) {
 				/* the output has failed after
 				   audio_output_all_pause() has
 				   submitted the PAUSE command; bail
@@ -491,46 +491,46 @@ AudioOutput::Task()
 			continue;
 
 		case Command::DRAIN:
-			if (open) {
+			if (output->open) {
 				const ScopeUnlock unlock(mutex);
-				ao_plugin_drain(*this);
+				ao_plugin_drain(*output);
 			}
 
 			CommandFinished();
 			continue;
 
 		case Command::CANCEL:
-			source.Cancel();
+			output->source.Cancel();
 
-			if (open) {
+			if (output->open) {
 				const ScopeUnlock unlock(mutex);
-				ao_plugin_cancel(*this);
+				ao_plugin_cancel(*output);
 			}
 
 			CommandFinished();
 			continue;
 
 		case Command::KILL:
-			Disable();
-			source.Cancel();
+			output->Disable();
+			output->source.Cancel();
 			CommandFinished();
 			return;
 		}
 
-		if (open && allow_play && Play())
+		if (output->open && allow_play && Play())
 			/* don't wait for an event if there are more
 			   chunks in the pipe */
 			continue;
 
 		if (command == Command::NONE) {
 			woken_for_play = false;
-			cond.wait(mutex);
+			cond.wait(output->mutex);
 		}
 	}
 }
 
 void
-AudioOutput::StartThread()
+AudioOutputControl::StartThread()
 {
 	assert(command == Command::NONE);
 
diff --git a/src/output/plugins/AlsaOutputPlugin.cxx b/src/output/plugins/AlsaOutputPlugin.cxx
index 6b7f9a7f3..026429a5e 100644
--- a/src/output/plugins/AlsaOutputPlugin.cxx
+++ b/src/output/plugins/AlsaOutputPlugin.cxx
@@ -26,6 +26,7 @@
 #include "mixer/MixerList.hxx"
 #include "pcm/PcmExport.hxx"
 #include "system/ByteOrder.hxx"
+#include "thread/Cond.hxx"
 #include "util/Manual.hxx"
 #include "util/RuntimeError.hxx"
 #include "util/Domain.hxx"
diff --git a/src/output/plugins/httpd/HttpdInternal.hxx b/src/output/plugins/httpd/HttpdInternal.hxx
index 762c5385a..6c0c940d8 100644
--- a/src/output/plugins/httpd/HttpdInternal.hxx
+++ b/src/output/plugins/httpd/HttpdInternal.hxx
@@ -30,6 +30,7 @@
 #include "output/Internal.hxx"
 #include "output/Timer.hxx"
 #include "thread/Mutex.hxx"
+#include "thread/Cond.hxx"
 #include "event/ServerSocket.hxx"
 #include "event/DeferredMonitor.hxx"
 #include "util/Cast.hxx"
diff --git a/src/output/plugins/sles/SlesOutputPlugin.cxx b/src/output/plugins/sles/SlesOutputPlugin.cxx
index b00bf1909..96ed6a66d 100644
--- a/src/output/plugins/sles/SlesOutputPlugin.cxx
+++ b/src/output/plugins/sles/SlesOutputPlugin.cxx
@@ -25,6 +25,7 @@
 #include "AndroidSimpleBufferQueue.hxx"
 #include "../../OutputAPI.hxx"
 #include "../../Wrapper.hxx"
+#include "thread/Cond.hxx"
 #include "util/Macros.hxx"
 #include "util/Domain.hxx"
 #include "system/ByteOrder.hxx"
diff --git a/test/run_output.cxx b/test/run_output.cxx
index 13178957d..a4a39b826 100644
--- a/test/run_output.cxx
+++ b/test/run_output.cxx
@@ -43,8 +43,6 @@
 #include <stdlib.h>
 #include <stdio.h>
 
-void AudioOutput::Task() {}
-
 class DummyAudioOutputClient final : public AudioOutputClient {
 public:
 	/* virtual methods from AudioOutputClient */