From 9ff790b7bb794b0828e034f14ae532680f349d0c Mon Sep 17 00:00:00 2001 From: Max Kellermann <max@musicpd.org> Date: Fri, 5 Mar 2021 16:47:26 +0100 Subject: [PATCH] output/wasapi: move COM utilities to separate headers --- src/mixer/plugins/WasapiMixerPlugin.cxx | 46 ++---- src/output/plugins/wasapi/AudioClient.hxx | 103 ++++++++++++ src/output/plugins/wasapi/Device.hxx | 117 ++++++++++++++ src/output/plugins/wasapi/PropertyStore.hxx | 44 +++++ .../plugins/wasapi/WasapiOutputPlugin.cxx | 151 ++++-------------- src/win32/PropVariant.cxx | 40 +++++ src/win32/PropVariant.hxx | 31 ++++ src/win32/meson.build | 1 + 8 files changed, 376 insertions(+), 157 deletions(-) create mode 100644 src/output/plugins/wasapi/AudioClient.hxx create mode 100644 src/output/plugins/wasapi/Device.hxx create mode 100644 src/output/plugins/wasapi/PropertyStore.hxx create mode 100644 src/win32/PropVariant.cxx create mode 100644 src/win32/PropVariant.hxx 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"); - } + 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))); + 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, )