diff --git a/NEWS b/NEWS
index ae4534789..3e54113e8 100644
--- a/NEWS
+++ b/NEWS
@@ -15,6 +15,7 @@ ver 0.22.7 (not yet released)
   - curl: don't use glibc extension
 * output
   - wasapi: add algorithm for finding usable audio format
+  - wasapi: use default device only if none was configured
 
 ver 0.22.6 (2021/02/16)
 * fix missing tags on songs in queue
diff --git a/src/mixer/plugins/WasapiMixerPlugin.cxx b/src/mixer/plugins/WasapiMixerPlugin.cxx
index bc2604633..18c862f29 100644
--- a/src/mixer/plugins/WasapiMixerPlugin.cxx
+++ b/src/mixer/plugins/WasapiMixerPlugin.cxx
@@ -17,15 +17,21 @@
  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  */
 
+#include "output/plugins/wasapi/ForMixer.hxx"
+#include "output/plugins/wasapi/AudioClient.hxx"
+#include "output/plugins/wasapi/Device.hxx"
 #include "mixer/MixerInternal.hxx"
-#include "output/plugins/WasapiOutputPlugin.hxx"
+#include "win32/ComPtr.hxx"
 #include "win32/ComWorker.hxx"
 #include "win32/HResult.hxx"
 
 #include <cmath>
-#include <endpointvolume.h>
 #include <optional>
 
+#include <audioclient.h>
+#include <endpointvolume.h>
+#include <mmdeviceapi.h>
+
 class WasapiMixer final : public Mixer {
 	WasapiOutput &output;
 
@@ -43,15 +49,8 @@ public:
 			float volume_level;
 
 			if (wasapi_is_exclusive(output)) {
-				ComPtr<IAudioEndpointVolume> endpoint_volume;
-				result = wasapi_output_get_device(output)->Activate(
-					__uuidof(IAudioEndpointVolume), CLSCTX_ALL,
-					nullptr, endpoint_volume.AddressCast());
-				if (FAILED(result)) {
-					throw FormatHResultError(result,
-								 "Unable to get device "
-								 "endpoint volume");
-				}
+				auto endpoint_volume =
+					Activate<IAudioEndpointVolume>(*wasapi_output_get_device(output));
 
 				result = endpoint_volume->GetMasterVolumeLevelScalar(
 					&volume_level);
@@ -61,15 +60,8 @@ public:
 								 "volume level");
 				}
 			} else {
-				ComPtr<ISimpleAudioVolume> session_volume;
-				result = wasapi_output_get_client(output)->GetService(
-					__uuidof(ISimpleAudioVolume),
-					session_volume.AddressCast<void>());
-				if (FAILED(result)) {
-					throw FormatHResultError(result,
-								 "Unable to get client "
-								 "session volume");
-				}
+				auto session_volume =
+					GetService<ISimpleAudioVolume>(*wasapi_output_get_client(output));
 
 				result = session_volume->GetMasterVolume(&volume_level);
 				if (FAILED(result)) {
@@ -89,15 +81,8 @@ public:
 			const float volume_level = volume / 100.0f;
 
 			if (wasapi_is_exclusive(output)) {
-				ComPtr<IAudioEndpointVolume> endpoint_volume;
-				result = wasapi_output_get_device(output)->Activate(
-					__uuidof(IAudioEndpointVolume), CLSCTX_ALL,
-					nullptr, endpoint_volume.AddressCast());
-				if (FAILED(result)) {
-					throw FormatHResultError(
-						result,
-						"Unable to get device endpoint volume");
-				}
+				auto endpoint_volume =
+					Activate<IAudioEndpointVolume>(*wasapi_output_get_device(output));
 
 				result = endpoint_volume->SetMasterVolumeLevelScalar(
 					volume_level, nullptr);
@@ -107,15 +92,8 @@ public:
 						"Unable to set master volume level");
 				}
 			} else {
-				ComPtr<ISimpleAudioVolume> session_volume;
-				result = wasapi_output_get_client(output)->GetService(
-					__uuidof(ISimpleAudioVolume),
-					session_volume.AddressCast<void>());
-				if (FAILED(result)) {
-					throw FormatHResultError(
-						result,
-						"Unable to get client session volume");
-				}
+				auto session_volume =
+					GetService<ISimpleAudioVolume>(*wasapi_output_get_client(output));
 
 				result = session_volume->SetMasterVolume(volume_level,
 									 nullptr);
diff --git a/src/output/Registry.cxx b/src/output/Registry.cxx
index 5059998ed..768093125 100644
--- a/src/output/Registry.cxx
+++ b/src/output/Registry.cxx
@@ -44,7 +44,7 @@
 #include "plugins/WinmmOutputPlugin.hxx"
 #endif
 #ifdef ENABLE_WASAPI_OUTPUT
-#include "plugins/WasapiOutputPlugin.hxx"
+#include "plugins/wasapi/WasapiOutputPlugin.hxx"
 #endif
 #include "util/StringAPI.hxx"
 
diff --git a/src/output/plugins/meson.build b/src/output/plugins/meson.build
index d06f55141..0e51d86b5 100644
--- a/src/output/plugins/meson.build
+++ b/src/output/plugins/meson.build
@@ -154,7 +154,7 @@ endif
 output_features.set('ENABLE_WASAPI_OUTPUT', is_windows)
 if is_windows
   output_plugins_sources += [
-    'WasapiOutputPlugin.cxx',
+    'wasapi/WasapiOutputPlugin.cxx',
   ]
   wasapi_dep = [
     c_compiler.find_library('ksuser', required: true),
diff --git a/src/output/plugins/wasapi/AudioClient.hxx b/src/output/plugins/wasapi/AudioClient.hxx
new file mode 100644
index 000000000..3ed3da319
--- /dev/null
+++ b/src/output/plugins/wasapi/AudioClient.hxx
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2020-2021 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_WASAPI_AUDIO_CLIENT_HXX
+#define MPD_WASAPI_AUDIO_CLIENT_HXX
+
+#include "win32/ComHeapPtr.hxx"
+#include "win32/ComPtr.hxx"
+#include "win32/HResult.hxx"
+
+#include <audioclient.h>
+
+inline UINT32
+GetBufferSizeInFrames(IAudioClient &client)
+{
+	UINT32 buffer_size_in_frames;
+
+	HRESULT result = client.GetBufferSize(&buffer_size_in_frames);
+	if (FAILED(result))
+		throw FormatHResultError(result,
+					 "Unable to get audio client buffer size");
+
+	return buffer_size_in_frames;
+}
+
+inline UINT32
+GetCurrentPaddingFrames(IAudioClient &client)
+{
+	UINT32 padding_frames;
+
+	HRESULT result = client.GetCurrentPadding(&padding_frames);
+	if (FAILED(result))
+		throw FormatHResultError(result,
+					 "Failed to get current padding");
+
+	return padding_frames;
+}
+
+inline ComHeapPtr<WAVEFORMATEX>
+GetMixFormat(IAudioClient &client)
+{
+	WAVEFORMATEX *f;
+
+	HRESULT result = client.GetMixFormat(&f);
+	if (FAILED(result))
+		throw FormatHResultError(result, "GetMixFormat failed");
+
+	return ComHeapPtr{f};
+}
+
+inline void
+Start(IAudioClient &client)
+{
+	HRESULT result = client.Start();
+	if (FAILED(result))
+		throw FormatHResultError(result, "Failed to start client");
+}
+
+inline void
+Stop(IAudioClient &client)
+{
+	HRESULT result = client.Stop();
+	if (FAILED(result))
+		throw FormatHResultError(result, "Failed to stop client");
+}
+
+inline void
+SetEventHandle(IAudioClient &client, HANDLE h)
+{
+	HRESULT result = client.SetEventHandle(h);
+	if (FAILED(result))
+		throw FormatHResultError(result, "Unable to set event handle");
+}
+
+template<typename T>
+inline ComPtr<T>
+GetService(IAudioClient &client)
+{
+	T *p = nullptr;
+	HRESULT result = client.GetService(IID_PPV_ARGS(&p));
+	if (FAILED(result))
+		throw FormatHResultError(result, "Unable to get service");
+
+	return ComPtr{p};
+}
+
+#endif
diff --git a/src/output/plugins/wasapi/Device.hxx b/src/output/plugins/wasapi/Device.hxx
new file mode 100644
index 000000000..863f327a7
--- /dev/null
+++ b/src/output/plugins/wasapi/Device.hxx
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2020-2021 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_WASAPI_DEVICE_COLLECTION_HXX
+#define MPD_WASAPI_DEVICE_COLLECTION_HXX
+
+#include "win32/ComPtr.hxx"
+#include "win32/HResult.hxx"
+
+#include <mmdeviceapi.h>
+
+inline ComPtr<IMMDevice>
+GetDefaultAudioEndpoint(IMMDeviceEnumerator &e)
+{
+	IMMDevice *device = nullptr;
+
+	HRESULT result = e.GetDefaultAudioEndpoint(eRender, eMultimedia,
+						   &device);
+	if (FAILED(result))
+		throw FormatHResultError(result,
+					 "Unable to get default device for multimedia");
+
+	return ComPtr{device};
+}
+
+inline ComPtr<IMMDeviceCollection>
+EnumAudioEndpoints(IMMDeviceEnumerator &e)
+{
+	IMMDeviceCollection *dc = nullptr;
+
+	HRESULT result = e.EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE,
+					      &dc);
+	if (FAILED(result))
+		throw FormatHResultError(result, "Unable to enumerate devices");
+
+	return ComPtr{dc};
+}
+
+inline UINT
+GetCount(IMMDeviceCollection &dc)
+{
+	UINT count;
+
+	HRESULT result = dc.GetCount(&count);
+	if (FAILED(result))
+		throw FormatHResultError(result, "Collection->GetCount failed");
+
+	return count;
+}
+
+inline ComPtr<IMMDevice>
+Item(IMMDeviceCollection &dc, UINT i)
+{
+	IMMDevice *device = nullptr;
+
+	auto result = dc.Item(i, &device);
+	if (FAILED(result))
+		throw FormatHResultError(result, "Collection->Item failed");
+
+	return ComPtr{device};
+}
+
+inline DWORD
+GetState(IMMDevice &device)
+{
+	DWORD state;
+
+	HRESULT result = device.GetState(&state);;
+	if (FAILED(result))
+		throw FormatHResultError(result, "Unable to get device status");
+
+	return state;
+}
+
+template<typename T>
+inline ComPtr<T>
+Activate(IMMDevice &device)
+{
+	T *p = nullptr;
+	HRESULT result = device.Activate(__uuidof(T), CLSCTX_ALL,
+					 nullptr, (void **)&p);
+	if (FAILED(result))
+		throw FormatHResultError(result, "Unable to activate device");
+
+	return ComPtr{p};
+}
+
+inline ComPtr<IPropertyStore>
+OpenPropertyStore(IMMDevice &device)
+{
+	IPropertyStore *property_store = nullptr;
+
+	HRESULT result = device.OpenPropertyStore(STGM_READ, &property_store);
+	if (FAILED(result))
+		throw FormatHResultError(result,
+					 "Device->OpenPropertyStore failed");
+
+	return ComPtr{property_store};
+}
+
+#endif
diff --git a/src/output/plugins/wasapi/ForMixer.hxx b/src/output/plugins/wasapi/ForMixer.hxx
new file mode 100644
index 000000000..2d815ce61
--- /dev/null
+++ b/src/output/plugins/wasapi/ForMixer.hxx
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2020-2021 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_WASAPI_OUTPUT_FOR_MIXER_HXX
+#define MPD_WASAPI_OUTPUT_FOR_MIXER_HXX
+
+struct IMMDevice;
+struct IAudioClient;
+class AudioOutput;
+class WasapiOutput;
+
+[[gnu::pure]]
+WasapiOutput &
+wasapi_output_downcast(AudioOutput &output) noexcept;
+
+[[gnu::pure]]
+bool
+wasapi_is_exclusive(WasapiOutput &output) noexcept;
+
+[[gnu::pure]]
+IMMDevice *
+wasapi_output_get_device(WasapiOutput &output) noexcept;
+
+[[gnu::pure]]
+IAudioClient *
+wasapi_output_get_client(WasapiOutput &output) noexcept;
+
+#endif
diff --git a/src/output/plugins/wasapi/PropertyStore.hxx b/src/output/plugins/wasapi/PropertyStore.hxx
new file mode 100644
index 000000000..6d974087f
--- /dev/null
+++ b/src/output/plugins/wasapi/PropertyStore.hxx
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2020-2021 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_WASAPI_PROPERTY_STORE_HXX
+#define MPD_WASAPI_PROPERTY_STORE_HXX
+
+#include "win32/PropVariant.hxx"
+#include "util/AllocatedString.hxx"
+#include "util/ScopeExit.hxx"
+
+#include <propsys.h>
+
+[[gnu::pure]]
+inline AllocatedString
+GetString(IPropertyStore &ps, REFPROPERTYKEY key) noexcept
+{
+	PROPVARIANT pv;
+	PropVariantInit(&pv);
+
+	HRESULT result = ps.GetValue(key, &pv);
+	if (FAILED(result))
+		return nullptr;
+
+	AtScopeExit(&) { PropVariantClear(&pv); };
+	return ToString(pv);
+}
+
+#endif
diff --git a/src/output/plugins/WasapiOutputPlugin.cxx b/src/output/plugins/wasapi/WasapiOutputPlugin.cxx
similarity index 79%
rename from src/output/plugins/WasapiOutputPlugin.cxx
rename to src/output/plugins/wasapi/WasapiOutputPlugin.cxx
index 84d23dd12..9a3616817 100644
--- a/src/output/plugins/WasapiOutputPlugin.cxx
+++ b/src/output/plugins/wasapi/WasapiOutputPlugin.cxx
@@ -16,10 +16,13 @@
  * with this program; if not, write to the Free Software Foundation, Inc.,
  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  */
-#include <initguid.h>
 
-#include "Log.hxx"
 #include "WasapiOutputPlugin.hxx"
+#include "ForMixer.hxx"
+#include "AudioClient.hxx"
+#include "Device.hxx"
+#include "PropertyStore.hxx"
+#include "output/OutputAPI.hxx"
 #include "lib/icu/Win32.hxx"
 #include "mixer/MixerList.hxx"
 #include "output/Error.hxx"
@@ -35,19 +38,26 @@
 #include "util/ScopeExit.hxx"
 #include "util/StringBuffer.hxx"
 #include "win32/Com.hxx"
-#include "win32/ComHeapPtr.hxx"
+#include "win32/ComPtr.hxx"
 #include "win32/ComWorker.hxx"
 #include "win32/HResult.hxx"
 #include "win32/WinEvent.hxx"
+#include "Log.hxx"
+#include "config.h"
+
+#include <boost/lockfree/spsc_queue.hpp>
 
 #include <algorithm>
-#include <boost/lockfree/spsc_queue.hpp>
 #include <cinttypes>
 #include <cmath>
-#include <functiondiscoverykeys_devpkey.h>
 #include <optional>
 #include <variant>
 
+#include <audioclient.h>
+#include <initguid.h>
+#include <functiondiscoverykeys_devpkey.h>
+#include <mmdeviceapi.h>
+
 namespace {
 static constexpr Domain wasapi_output_domain("wasapi_output");
 
@@ -80,8 +90,8 @@ inline bool SafeTry(Functor &&functor) {
 	try {
 		functor();
 		return true;
-	} catch (std::runtime_error &err) {
-		FormatError(wasapi_output_domain, "%s", err.what());
+	} catch (...) {
+		FormatError(std::current_exception(), "%s");
 		return false;
 	}
 }
@@ -91,7 +101,7 @@ inline bool SafeSilenceTry(Functor &&functor) {
 	try {
 		functor();
 		return true;
-	} catch (std::runtime_error &err) {
+	} catch (...) {
 		return false;
 	}
 }
@@ -195,7 +205,13 @@ public:
 	WasapiOutput(const ConfigBlock &block);
 	void Enable() override {
 		COMWorker::Aquire();
-		COMWorker::Async([&]() { OpenDevice(); }).get();
+
+		try {
+			COMWorker::Async([&]() { OpenDevice(); }).get();
+		} catch (...) {
+			COMWorker::Release();
+			throw;
+		}
 	}
 	void Disable() noexcept override {
 		COMWorker::Async([&]() { DoDisable(); }).get();
@@ -223,7 +239,6 @@ private:
 	bool is_exclusive;
 	bool enumerate_devices;
 	std::string device_config;
-	std::vector<std::pair<unsigned int, AllocatedString>> device_desc;
 	ComPtr<IMMDeviceEnumerator> enumerator;
 	ComPtr<IMMDevice> device;
 	ComPtr<IAudioClient> client;
@@ -244,9 +259,8 @@ private:
 	void FindExclusiveFormatSupported(AudioFormat &audio_format);
 	void FindSharedFormatSupported(AudioFormat &audio_format);
 	void EnumerateDevices();
-	void GetDevice(unsigned int index);
-	unsigned int SearchDevice(std::string_view name);
-	void GetDefaultDevice();
+	ComPtr<IMMDevice> GetDevice(unsigned int index);
+	ComPtr<IMMDevice> SearchDevice(std::string_view name);
 };
 
 WasapiOutput &wasapi_output_downcast(AudioOutput &output) noexcept {
@@ -280,13 +294,8 @@ void WasapiOutputThread::Work() noexcept {
 
 			UINT32 write_in_frames = buffer_size_in_frames;
 			if (!is_exclusive) {
-				UINT32 data_in_frames;
-				if (HRESULT result =
-					    client->GetCurrentPadding(&data_in_frames);
-				    FAILED(result)) {
-					throw FormatHResultError(
-						result, "Failed to get current padding");
-				}
+				UINT32 data_in_frames =
+					GetCurrentPaddingFrames(*client);
 
 				if (data_in_frames >= buffer_size_in_frames) {
 					continue;
@@ -358,20 +367,12 @@ void WasapiOutput::DoDisable() noexcept {
 void WasapiOutput::DoOpen(AudioFormat &audio_format) {
 	client.reset();
 
-	DWORD state;
-	if (HRESULT result = device->GetState(&state); FAILED(result)) {
-		throw FormatHResultError(result, "Unable to get device status");
-	}
-	if (state != DEVICE_STATE_ACTIVE) {
+	if (GetState(*device) != DEVICE_STATE_ACTIVE) {
 		device.reset();
 		OpenDevice();
 	}
 
-	if (HRESULT result = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr,
-					      client.AddressCast());
-	    FAILED(result)) {
-		throw FormatHResultError(result, "Unable to activate audio client");
-	}
+	client = Activate<IAudioClient>(*device);
 
 	if (audio_format.channels > 8) {
 		audio_format.channels = 8;
@@ -445,13 +446,8 @@ void WasapiOutput::DoOpen(AudioFormat &audio_format) {
 		    FAILED(result)) {
 			if (result == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED) {
 				// https://docs.microsoft.com/en-us/windows/win32/api/audioclient/nf-audioclient-iaudioclient-initialize
-				UINT32 buffer_size_in_frames = 0;
-				result = client->GetBufferSize(&buffer_size_in_frames);
-				if (FAILED(result)) {
-					throw FormatHResultError(
-						result,
-						"Unable to get audio client buffer size");
-				}
+				UINT32 buffer_size_in_frames =
+					GetBufferSizeInFrames(*client);
 				buffer_duration =
 					std::ceil(double(buffer_size_in_frames *
 							 hundred_ns(s(1)).count()) /
@@ -461,14 +457,7 @@ void WasapiOutput::DoOpen(AudioFormat &audio_format) {
 					"Aligned buffer duration: %I64u ns",
 					size_t(ns(hundred_ns(buffer_duration)).count()));
 				client.reset();
-				result = device->Activate(__uuidof(IAudioClient),
-							  CLSCTX_ALL, nullptr,
-							  client.AddressCast());
-				if (FAILED(result)) {
-					throw FormatHResultError(
-						result,
-						"Unable to activate audio client");
-				}
+				client = Activate<IAudioClient>(*device);
 				result = client->Initialize(
 					AUDCLNT_SHAREMODE_EXCLUSIVE,
 					AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
@@ -493,27 +482,15 @@ void WasapiOutput::DoOpen(AudioFormat &audio_format) {
 		}
 	}
 
-	ComPtr<IAudioRenderClient> render_client;
-	if (HRESULT result = client->GetService(IID_PPV_ARGS(render_client.Address()));
-	    FAILED(result)) {
-		throw FormatHResultError(result, "Unable to get new render client");
-	}
+	auto render_client = GetService<IAudioRenderClient>(*client);
 
-	UINT32 buffer_size_in_frames;
-	if (HRESULT result = client->GetBufferSize(&buffer_size_in_frames);
-	    FAILED(result)) {
-		throw FormatHResultError(result,
-					 "Unable to get audio client buffer size");
-	}
+	const UINT32 buffer_size_in_frames = GetBufferSizeInFrames(*client);
 
 	watermark = buffer_size_in_frames * 3 * FrameSize();
 	thread.emplace(client.get(), std::move(render_client), FrameSize(),
 		       buffer_size_in_frames, is_exclusive);
 
-	if (HRESULT result = client->SetEventHandle(thread->event.handle());
-	    FAILED(result)) {
-		throw FormatHResultError(result, "Unable to set event handler");
-	}
+	SetEventHandle(*client, thread->event.handle());
 
 	thread->Start();
 }
@@ -523,9 +500,7 @@ void WasapiOutput::Close() noexcept {
 
 	try {
 		COMWorker::Async([&]() {
-			if (HRESULT result = client->Stop(); FAILED(result)) {
-				throw FormatHResultError(result, "Failed to stop client");
-			}
+			Stop(*client);
 		}).get();
 		thread->CheckException();
 	} catch (std::exception &err) {
@@ -587,10 +562,7 @@ size_t WasapiOutput::Play(const void *chunk, size_t size) {
 			is_started = true;
 			thread->Play();
 			COMWorker::Async([&]() {
-				if (HRESULT result = client->Start(); FAILED(result)) {
-					throw FormatHResultError(
-						result, "Failed to start client");
-				}
+				Start(*client);
 			}).wait();
 		}
 
@@ -631,31 +603,26 @@ void WasapiOutput::OpenDevice() {
 	enumerator.CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr,
 				    CLSCTX_INPROC_SERVER);
 
-	if (enumerate_devices && SafeTry([this]() { EnumerateDevices(); })) {
-		for (const auto &[device, desc] : device_desc) {
-			FormatNotice(wasapi_output_domain,
-				     "Device \"%u\" \"%s\"",
-				     device,
-				     desc.c_str());
+	if (enumerate_devices) {
+		try {
+			EnumerateDevices();
+		} catch (...) {
+			LogError(std::current_exception());
 		}
 	}
 
 	unsigned int id = kErrorId;
 	if (!device_config.empty()) {
 		if (!SafeSilenceTry([this, &id]() { id = std::stoul(device_config); })) {
-			id = SearchDevice(device_config);
-		}
+			device = SearchDevice(device_config);
+			if (!device)
+				throw FormatRuntimeError("Device '%s' not found",
+							 device_config.c_str());
+		} else
+			device = GetDevice(id);
+	} else {
+		device = GetDefaultAudioEndpoint(*enumerator);
 	}
-
-	if (id != kErrorId) {
-		SafeTry([this, id]() { GetDevice(id); });
-	}
-
-	if (!device) {
-		GetDefaultDevice();
-	}
-
-	device_desc.clear();
 }
 
 /// run inside COMWorkerThread
@@ -675,6 +642,9 @@ bool WasapiOutput::TryFormatExclusive(const AudioFormat &audio_format) {
 			device_format = test_format;
 			return true;
 		}
+
+		if (result == AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED)
+			throw std::runtime_error("Exclusive mode not allowed");
 	}
 	return false;
 }
@@ -727,13 +697,10 @@ void WasapiOutput::FindExclusiveFormatSupported(AudioFormat &audio_format) {
 /// run inside COMWorkerThread
 void WasapiOutput::FindSharedFormatSupported(AudioFormat &audio_format) {
 	HRESULT result;
-	ComHeapPtr<WAVEFORMATEX> mixer_format;
 
 	// In shared mode, different sample rate is always unsupported.
-	result = client->GetMixFormat(mixer_format.Address());
-	if (FAILED(result)) {
-		throw FormatHResultError(result, "GetMixFormat failed");
-	}
+	auto mixer_format = GetMixFormat(*client);
+
 	audio_format.sample_rate = mixer_format->nSamplesPerSec;
 	device_format = GetFormats(audio_format).front();
 
@@ -832,101 +799,50 @@ void WasapiOutput::FindSharedFormatSupported(AudioFormat &audio_format) {
 
 /// run inside COMWorkerThread
 void WasapiOutput::EnumerateDevices() {
-	if (!device_desc.empty()) {
-		return;
-	}
+	const auto device_collection = EnumAudioEndpoints(*enumerator);
 
-	HRESULT result;
-
-	ComPtr<IMMDeviceCollection> device_collection;
-	result = enumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE,
-						device_collection.Address());
-	if (FAILED(result)) {
-		throw FormatHResultError(result, "Unable to enumerate devices");
-	}
-
-	UINT count;
-	result = device_collection->GetCount(&count);
-	if (FAILED(result)) {
-		throw FormatHResultError(result, "Collection->GetCount failed");
-	}
-
-	device_desc.reserve(count);
+	const UINT count = GetCount(*device_collection);
 	for (UINT i = 0; i < count; ++i) {
-		ComPtr<IMMDevice> enumerated_device;
-		result = device_collection->Item(i, enumerated_device.Address());
-		if (FAILED(result)) {
-			throw FormatHResultError(result, "Collection->Item failed");
-		}
+		const auto enumerated_device = Item(*device_collection, i);
 
-		ComPtr<IPropertyStore> property_store;
-		result = enumerated_device->OpenPropertyStore(STGM_READ,
-							      property_store.Address());
-		if (FAILED(result)) {
-			throw FormatHResultError(result,
-						 "Device->OpenPropertyStore failed");
-		}
+		const auto property_store =
+			OpenPropertyStore(*enumerated_device);
 
-		PROPVARIANT var_name;
-		PropVariantInit(&var_name);
-		AtScopeExit(&) { PropVariantClear(&var_name); };
+		auto name = GetString(*property_store,
+				      PKEY_Device_FriendlyName);
+		if (name == nullptr)
+			continue;
 
-		result = property_store->GetValue(PKEY_Device_FriendlyName, &var_name);
-		if (FAILED(result)) {
-			throw FormatHResultError(result,
-						 "PropertyStore->GetValue failed");
-		}
-
-		device_desc.emplace_back(
-			i, WideCharToMultiByte(CP_UTF8,
-					       std::wstring_view(var_name.pwszVal)));
+		FormatNotice(wasapi_output_domain,
+			     "Device \"%u\" \"%s\"", i, name.c_str());
 	}
 }
 
 /// run inside COMWorkerThread
-void WasapiOutput::GetDevice(unsigned int index) {
-	HRESULT result;
-
-	ComPtr<IMMDeviceCollection> device_collection;
-	result = enumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE,
-						device_collection.Address());
-	if (FAILED(result)) {
-		throw FormatHResultError(result, "Unable to enumerate devices");
-	}
-
-	result = device_collection->Item(index, device.Address());
-	if (FAILED(result)) {
-		throw FormatHResultError(result, "Collection->Item failed");
-	}
+ComPtr<IMMDevice>
+WasapiOutput::GetDevice(unsigned int index)
+{
+	const auto device_collection = EnumAudioEndpoints(*enumerator);
+	return Item(*device_collection, index);
 }
 
 /// run inside COMWorkerThread
-unsigned int WasapiOutput::SearchDevice(std::string_view name) {
-	if (!SafeTry([this]() { EnumerateDevices(); })) {
-		return kErrorId;
-	}
-	auto iter =
-		std::find_if(device_desc.cbegin(), device_desc.cend(),
-			     [&name](const auto &desc) { return desc.second == name; });
-	if (iter == device_desc.cend()) {
-		FormatError(wasapi_output_domain, "Device %.*s not founded.",
-			    int(name.size()), name.data());
-		return kErrorId;
-	}
-	FormatInfo(wasapi_output_domain, "Select device \"%u\" \"%s\"", iter->first,
-		   iter->second.c_str());
-	return iter->first;
-}
+ComPtr<IMMDevice>
+WasapiOutput::SearchDevice(std::string_view name)
+{
+	const auto device_collection = EnumAudioEndpoints(*enumerator);
 
-/// run inside COMWorkerThread
-void WasapiOutput::GetDefaultDevice() {
-	HRESULT result;
-	result = enumerator->GetDefaultAudioEndpoint(eRender, eMultimedia,
-						     device.Address());
-	if (FAILED(result)) {
-		throw FormatHResultError(result,
-					 "Unable to get default device for multimedia");
+	const UINT count = GetCount(*device_collection);
+	for (UINT i = 0; i < count; ++i) {
+		auto d = Item(*device_collection, i);
+
+		const auto property_store = OpenPropertyStore(*d);
+		auto n = GetString(*property_store, PKEY_Device_FriendlyName);
+		if (n != nullptr && name.compare(n) == 0)
+			return d;
 	}
+
+	return nullptr;
 }
 
 static bool wasapi_output_test_default_device() { return true; }
diff --git a/src/output/plugins/WasapiOutputPlugin.hxx b/src/output/plugins/wasapi/WasapiOutputPlugin.hxx
similarity index 66%
rename from src/output/plugins/WasapiOutputPlugin.hxx
rename to src/output/plugins/wasapi/WasapiOutputPlugin.hxx
index 3b17bcb9f..9194b5059 100644
--- a/src/output/plugins/WasapiOutputPlugin.hxx
+++ b/src/output/plugins/wasapi/WasapiOutputPlugin.hxx
@@ -20,25 +20,6 @@
 #ifndef MPD_WASAPI_OUTPUT_PLUGIN_HXX
 #define MPD_WASAPI_OUTPUT_PLUGIN_HXX
 
-#include "output/Features.h"
-
-#include "../OutputAPI.hxx"
-#include "util/Compiler.h"
-#include "win32/ComPtr.hxx"
-
-#include <audioclient.h>
-#include <mmdeviceapi.h>
-
 extern const struct AudioOutputPlugin wasapi_output_plugin;
 
-class WasapiOutput;
-
-gcc_pure WasapiOutput &wasapi_output_downcast(AudioOutput &output) noexcept;
-
-gcc_pure bool wasapi_is_exclusive(WasapiOutput &output) noexcept;
-
-gcc_pure IMMDevice *wasapi_output_get_device(WasapiOutput &output) noexcept;
-
-gcc_pure IAudioClient *wasapi_output_get_client(WasapiOutput &output) noexcept;
-
 #endif
diff --git a/src/win32/ComPtr.hxx b/src/win32/ComPtr.hxx
index 325c06c10..01a8ef525 100644
--- a/src/win32/ComPtr.hxx
+++ b/src/win32/ComPtr.hxx
@@ -32,6 +32,7 @@ template <typename T>
 class ComPtr {
 public:
 	using pointer = T *;
+	using reference = T &;
 	using element_type = T;
 
 	constexpr ComPtr() noexcept : ptr(nullptr) {}
@@ -75,7 +76,7 @@ public:
 	pointer get() const noexcept { return ptr; }
 	explicit operator bool() const noexcept { return ptr; }
 
-	auto operator*() const { return *ptr; }
+	reference operator*() const noexcept { return *ptr; }
 	pointer operator->() const noexcept { return ptr; }
 
 	void CoCreateInstance(REFCLSID class_id, LPUNKNOWN unknown_outer = nullptr,
diff --git a/src/win32/PropVariant.cxx b/src/win32/PropVariant.cxx
new file mode 100644
index 000000000..5fa728eeb
--- /dev/null
+++ b/src/win32/PropVariant.cxx
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2020-2021 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "PropVariant.hxx"
+#include "lib/icu/Win32.hxx"
+#include "util/AllocatedString.hxx"
+#include "util/ScopeExit.hxx"
+
+AllocatedString
+ToString(const PROPVARIANT &pv) noexcept
+{
+	// TODO: VT_BSTR
+
+	switch (pv.vt) {
+	case VT_LPSTR:
+		return AllocatedString{static_cast<const char *>(pv.pszVal)};
+
+	case VT_LPWSTR:
+		return WideCharToMultiByte(CP_UTF8, pv.pwszVal);
+
+	default:
+		return nullptr;
+	}
+}
diff --git a/src/win32/PropVariant.hxx b/src/win32/PropVariant.hxx
new file mode 100644
index 000000000..54da5e988
--- /dev/null
+++ b/src/win32/PropVariant.hxx
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2020-2021 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_WIN32_PROPVARIANT_HXX
+#define MPD_WIN32_PROPVARIANT_HXX
+
+#include <propidl.h>
+
+class AllocatedString;
+
+[[gnu::pure]]
+AllocatedString
+ToString(const PROPVARIANT &pv) noexcept;
+
+#endif
diff --git a/src/win32/meson.build b/src/win32/meson.build
index 5f8eaf6b7..28f958a8a 100644
--- a/src/win32/meson.build
+++ b/src/win32/meson.build
@@ -7,6 +7,7 @@ win32 = static_library(
   'win32',
   'ComWorker.cxx',
   'HResult.cxx',
+  'PropVariant.cxx',
   'WinEvent.cxx',
   include_directories: inc,
 )
diff --git a/test/run_output.cxx b/test/run_output.cxx
index db567cab9..bb7ba71b8 100644
--- a/test/run_output.cxx
+++ b/test/run_output.cxx
@@ -26,10 +26,13 @@
 #include "fs/NarrowPath.hxx"
 #include "pcm/AudioParser.hxx"
 #include "pcm/AudioFormat.hxx"
+#include "util/OptionDef.hxx"
+#include "util/OptionParser.hxx"
 #include "util/StringBuffer.hxx"
 #include "util/RuntimeError.hxx"
 #include "util/ScopeExit.hxx"
 #include "util/PrintException.hxx"
+#include "LogBackend.hxx"
 
 #include <cassert>
 #include <memory>
@@ -39,6 +42,51 @@
 #include <stdlib.h>
 #include <stdio.h>
 
+struct CommandLine {
+	FromNarrowPath config_path;
+
+	const char *output_name = nullptr;
+
+	AudioFormat audio_format{44100, SampleFormat::S16, 2};
+
+	bool verbose = false;
+};
+
+enum Option {
+	OPTION_VERBOSE,
+};
+
+static constexpr OptionDef option_defs[] = {
+	{"verbose", 'v', false, "Verbose logging"},
+};
+
+static CommandLine
+ParseCommandLine(int argc, char **argv)
+{
+	CommandLine c;
+
+	OptionParser option_parser(option_defs, argc, argv);
+	while (auto o = option_parser.Next()) {
+		switch (Option(o.index)) {
+		case OPTION_VERBOSE:
+			c.verbose = true;
+			break;
+		}
+	}
+
+	auto args = option_parser.GetRemaining();
+	if (args.size < 2 || args.size > 3)
+		throw std::runtime_error("Usage: run_output CONFIG NAME [FORMAT] <IN");
+
+	c.config_path = args[0];
+	c.output_name = args[1];
+
+	if (args.size > 2)
+		c.audio_format = ParseAudioFormat(args[2], false);
+
+	return c;
+}
+
 static std::unique_ptr<AudioOutput>
 LoadAudioOutput(const ConfigData &config, EventLoop &event_loop,
 		const char *name)
@@ -57,6 +105,8 @@ LoadAudioOutput(const ConfigData &config, EventLoop &event_loop,
 	if (plugin == nullptr)
 		throw FormatRuntimeError("No such audio output plugin: %s",
 					 plugin_name);
+#include "util/OptionDef.hxx"
+#include "util/OptionParser.hxx"
 
 	return std::unique_ptr<AudioOutput>(ao_plugin_init(event_loop, *plugin,
 							   *block));
@@ -107,34 +157,24 @@ run_output(AudioOutput &ao, AudioFormat audio_format)
 
 int main(int argc, char **argv)
 try {
-	if (argc < 3 || argc > 4) {
-		fprintf(stderr, "Usage: run_output CONFIG NAME [FORMAT] <IN\n");
-		return EXIT_FAILURE;
-	}
-
-	const FromNarrowPath config_path = argv[1];
-
-	AudioFormat audio_format(44100, SampleFormat::S16, 2);
+	const auto c = ParseCommandLine(argc, argv);
+	SetLogThreshold(c.verbose ? LogLevel::DEBUG : LogLevel::INFO);
 
 	/* read configuration file (mpd.conf) */
 
-	const auto config = AutoLoadConfigFile(config_path);
+	const auto config = AutoLoadConfigFile(c.config_path);
 
 	EventThread io_thread;
 	io_thread.Start();
 
 	/* initialize the audio output */
 
-	auto ao = LoadAudioOutput(config, io_thread.GetEventLoop(), argv[2]);
-
-	/* parse the audio format */
-
-	if (argc > 3)
-		audio_format = ParseAudioFormat(argv[3], false);
+	auto ao = LoadAudioOutput(config, io_thread.GetEventLoop(),
+				  c.output_name);
 
 	/* do it */
 
-	run_output(*ao, audio_format);
+	run_output(*ao, c.audio_format);
 
 	/* cleanup and exit */