summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMax Kellermann <max@musicpd.org>2021-03-05 16:47:26 +0100
committerMax Kellermann <max@musicpd.org>2021-03-05 18:33:31 +0100
commit9ff790b7bb794b0828e034f14ae532680f349d0c (patch)
treef22962b78daaa66f86b3505d3d5f72ab26125419
parentebc1fe2821e152aca453466978848f0d698697cd (diff)
output/wasapi: move COM utilities to separate headers
-rw-r--r--src/mixer/plugins/WasapiMixerPlugin.cxx46
-rw-r--r--src/output/plugins/wasapi/AudioClient.hxx103
-rw-r--r--src/output/plugins/wasapi/Device.hxx117
-rw-r--r--src/output/plugins/wasapi/PropertyStore.hxx44
-rw-r--r--src/output/plugins/wasapi/WasapiOutputPlugin.cxx151
-rw-r--r--src/win32/PropVariant.cxx40
-rw-r--r--src/win32/PropVariant.hxx31
-rw-r--r--src/win32/meson.build1
8 files changed, 376 insertions, 157 deletions
diff --git a/src/mixer/plugins/WasapiMixerPlugin.cxx b/src/mixer/plugins/WasapiMixerPlugin.cxx
index 1b8190c12..18c862f29 100644
--- a/src/mixer/plugins/WasapiMixerPlugin.cxx
+++ b/src/mixer/plugins/WasapiMixerPlugin.cxx
@@ -18,6 +18,8 @@
*/
#include "output/plugins/wasapi/ForMixer.hxx"
+#include "output/plugins/wasapi/AudioClient.hxx"
+#include "output/plugins/wasapi/Device.hxx"
#include "mixer/MixerInternal.hxx"
#include "win32/ComPtr.hxx"
#include "win32/ComWorker.hxx"
@@ -47,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);
@@ -65,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)) {
@@ -93,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);
@@ -111,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/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/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/wasapi/WasapiOutputPlugin.cxx b/src/output/plugins/wasapi/WasapiOutputPlugin.cxx
index 70add3c08..58a12918d 100644
--- a/src/output/plugins/wasapi/WasapiOutputPlugin.cxx
+++ b/src/output/plugins/wasapi/WasapiOutputPlugin.cxx
@@ -19,6 +19,9 @@
#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"
@@ -35,7 +38,6 @@
#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"
@@ -254,7 +256,6 @@ private:
void EnumerateDevices();
void GetDevice(unsigned int index);
unsigned int SearchDevice(std::string_view name);
- void GetDefaultDevice();
};
WasapiOutput &wasapi_output_downcast(AudioOutput &output) noexcept {
@@ -288,13 +289,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;
@@ -366,20 +362,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;
@@ -453,13 +441,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()) /
@@ -469,14 +452,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,
@@ -501,27 +477,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();
}
@@ -531,9 +495,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) {
@@ -595,10 +557,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();
}
@@ -660,7 +619,7 @@ void WasapiOutput::OpenDevice() {
}
if (!device) {
- GetDefaultDevice();
+ device = GetDefaultAudioEndpoint(*enumerator);
}
device_desc.clear();
@@ -735,13 +694,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();
@@ -846,66 +802,30 @@ void WasapiOutput::EnumerateDevices() {
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");
- }
+ const auto device_collection = EnumAudioEndpoints(*enumerator);
- UINT count;
- result = device_collection->GetCount(&count);
- if (FAILED(result)) {
- throw FormatHResultError(result, "Collection->GetCount failed");
- }
+ const UINT count = GetCount(*device_collection);
device_desc.reserve(count);
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");
- }
-
- ComPtr<IPropertyStore> property_store;
- result = enumerated_device->OpenPropertyStore(STGM_READ,
- property_store.Address());
- if (FAILED(result)) {
- throw FormatHResultError(result,
- "Device->OpenPropertyStore failed");
- }
+ const auto enumerated_device = Item(*device_collection, i);
- PROPVARIANT var_name;
- PropVariantInit(&var_name);
- AtScopeExit(&) { PropVariantClear(&var_name); };
+ const auto property_store =
+ OpenPropertyStore(*enumerated_device);
- result = property_store->GetValue(PKEY_Device_FriendlyName, &var_name);
- if (FAILED(result)) {
- throw FormatHResultError(result,
- "PropertyStore->GetValue failed");
- }
+ auto name = GetString(*property_store,
+ PKEY_Device_FriendlyName);
+ if (name == nullptr)
+ continue;
- device_desc.emplace_back(
- i, WideCharToMultiByte(CP_UTF8,
- std::wstring_view(var_name.pwszVal)));
+ device_desc.emplace_back(i, std::move(name));
}
}
/// 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");
- }
+ const auto device_collection = EnumAudioEndpoints(*enumerator);
+ device = Item(*device_collection, index);
}
/// run inside COMWorkerThread
@@ -926,17 +846,6 @@ unsigned int WasapiOutput::SearchDevice(std::string_view name) {
return iter->first;
}
-/// 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");
- }
-}
-
static bool wasapi_output_test_default_device() { return true; }
const struct AudioOutputPlugin wasapi_output_plugin = {
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,
)