diff options
Diffstat (limited to 'alc/backends/wasapi.cpp')
-rw-r--r-- | alc/backends/wasapi.cpp | 1813 |
1 files changed, 1290 insertions, 523 deletions
diff --git a/alc/backends/wasapi.cpp b/alc/backends/wasapi.cpp index e834eef4..3ee98457 100644 --- a/alc/backends/wasapi.cpp +++ b/alc/backends/wasapi.cpp @@ -31,7 +31,9 @@ #include <wtypes.h> #include <mmdeviceapi.h> +#include <audiosessiontypes.h> #include <audioclient.h> +#include <spatialaudioclient.h> #include <cguid.h> #include <devpropdef.h> #include <mmreg.h> @@ -59,6 +61,8 @@ #include "albit.h" #include "alc/alconfig.h" #include "alnumeric.h" +#include "alspan.h" +#include "althrd_setname.h" #include "comptr.h" #include "core/converter.h" #include "core/device.h" @@ -66,8 +70,22 @@ #include "core/logging.h" #include "ringbuffer.h" #include "strutils.h" -#include "threads.h" +#if defined(ALSOFT_UWP) + +#include <winrt/Windows.Media.Core.h> // !!This is important!! +#include <winrt/Windows.Foundation.Collections.h> +#include <winrt/Windows.Devices.h> +#include <winrt/Windows.Foundation.h> +#include <winrt/Windows.Devices.Enumeration.h> +#include <winrt/Windows.Media.Devices.h> + +using namespace winrt; +using namespace Windows::Foundation; +using namespace Windows::Media::Devices; +using namespace Windows::Devices::Enumeration; +using namespace Windows::Media::Devices; +#endif /* Some headers seem to define these as macros for __uuidof, which is annoying * since some headers don't declare them at all. Hopefully the ifdef is enough @@ -79,11 +97,11 @@ DEFINE_GUID(KSDATAFORMAT_SUBTYPE_PCM, 0x00000001, 0x0000, 0x0010, 0x80, 0x00, 0x #ifndef KSDATAFORMAT_SUBTYPE_IEEE_FLOAT DEFINE_GUID(KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, 0x00000003, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71); #endif - +#if !defined(ALSOFT_UWP) DEFINE_DEVPROPKEY(DEVPKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80,0x20, 0x67,0xd1,0x46,0xa8,0x50,0xe0, 14); DEFINE_PROPERTYKEY(PKEY_AudioEndpoint_FormFactor, 0x1da5d803, 0xd492, 0x4edd, 0x8c,0x23, 0xe0,0xc0,0xff,0xee,0x7f,0x0e, 0); DEFINE_PROPERTYKEY(PKEY_AudioEndpoint_GUID, 0x1da5d803, 0xd492, 0x4edd, 0x8c, 0x23,0xe0, 0xc0,0xff,0xee,0x7f,0x0e, 4 ); - +#endif namespace { @@ -91,9 +109,9 @@ using std::chrono::nanoseconds; using std::chrono::milliseconds; using std::chrono::seconds; -using ReferenceTime = std::chrono::duration<REFERENCE_TIME,std::ratio<1,10000000>>; +using ReferenceTime = std::chrono::duration<REFERENCE_TIME,std::ratio<1,10'000'000>>; -inline constexpr ReferenceTime operator "" _reftime(unsigned long long int n) noexcept +constexpr ReferenceTime operator "" _reftime(unsigned long long int n) noexcept { return ReferenceTime{static_cast<REFERENCE_TIME>(n)}; } @@ -124,15 +142,61 @@ constexpr DWORD X61Mask{MaskFromTopBits(X6DOT1)}; constexpr DWORD X71Mask{MaskFromTopBits(X7DOT1)}; constexpr DWORD X714Mask{MaskFromTopBits(X7DOT1DOT4)}; + +#ifndef _MSC_VER +constexpr AudioObjectType operator|(AudioObjectType lhs, AudioObjectType rhs) noexcept +{ return static_cast<AudioObjectType>(lhs | al::to_underlying(rhs)); } +#endif + +constexpr AudioObjectType ChannelMask_Mono{AudioObjectType_FrontCenter}; +constexpr AudioObjectType ChannelMask_Stereo{AudioObjectType_FrontLeft + | AudioObjectType_FrontRight}; +constexpr AudioObjectType ChannelMask_Quad{AudioObjectType_FrontLeft | AudioObjectType_FrontRight + | AudioObjectType_BackLeft | AudioObjectType_BackRight}; +constexpr AudioObjectType ChannelMask_X51{AudioObjectType_FrontLeft | AudioObjectType_FrontRight + | AudioObjectType_FrontCenter | AudioObjectType_LowFrequency | AudioObjectType_SideLeft + | AudioObjectType_SideRight}; +constexpr AudioObjectType ChannelMask_X51Rear{AudioObjectType_FrontLeft + | AudioObjectType_FrontRight | AudioObjectType_FrontCenter | AudioObjectType_LowFrequency + | AudioObjectType_BackLeft | AudioObjectType_BackRight}; +constexpr AudioObjectType ChannelMask_X61{AudioObjectType_FrontLeft | AudioObjectType_FrontRight + | AudioObjectType_FrontCenter | AudioObjectType_LowFrequency | AudioObjectType_SideLeft + | AudioObjectType_SideRight | AudioObjectType_BackCenter}; +constexpr AudioObjectType ChannelMask_X71{AudioObjectType_FrontLeft | AudioObjectType_FrontRight + | AudioObjectType_FrontCenter | AudioObjectType_LowFrequency | AudioObjectType_SideLeft + | AudioObjectType_SideRight | AudioObjectType_BackLeft | AudioObjectType_BackRight}; +constexpr AudioObjectType ChannelMask_X714{AudioObjectType_FrontLeft | AudioObjectType_FrontRight + | AudioObjectType_FrontCenter | AudioObjectType_LowFrequency | AudioObjectType_SideLeft + | AudioObjectType_SideRight | AudioObjectType_BackLeft | AudioObjectType_BackRight + | AudioObjectType_TopFrontLeft | AudioObjectType_TopFrontRight | AudioObjectType_TopBackLeft + | AudioObjectType_TopBackRight}; + + constexpr char DevNameHead[] = "OpenAL Soft on "; -constexpr size_t DevNameHeadLen{al::size(DevNameHead) - 1}; +constexpr size_t DevNameHeadLen{std::size(DevNameHead) - 1}; + + +template<typename... Ts> +struct overloaded : Ts... { using Ts::operator()...; }; + +template<typename... Ts> +overloaded(Ts...) -> overloaded<Ts...>; + + +template<typename T> +constexpr auto as_unsigned(T value) noexcept +{ + using UT = std::make_unsigned_t<T>; + return static_cast<UT>(value); +} /* Scales the given reftime value, rounding the result. */ -inline uint RefTime2Samples(const ReferenceTime &val, uint srate) +template<typename T> +constexpr uint RefTime2Samples(const ReferenceTime &val, T srate) noexcept { const auto retval = (val*srate + ReferenceTime{seconds{1}}/2) / seconds{1}; - return static_cast<uint>(mini64(retval, std::numeric_limits<uint>::max())); + return static_cast<uint>(std::min<decltype(retval)>(retval, std::numeric_limits<uint>::max())); } @@ -142,7 +206,7 @@ class GuidPrinter { public: GuidPrinter(const GUID &guid) { - std::snprintf(mMsg, al::size(mMsg), "{%08lx-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}", + std::snprintf(mMsg, std::size(mMsg), "{%08lx-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}", DWORD{guid.Data1}, guid.Data2, guid.Data3, guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]); } @@ -178,28 +242,73 @@ struct DevMap { , endpoint_guid{std::forward<T1>(guid_)} , devid{std::forward<T2>(devid_)} { } + /* To prevent GCC from complaining it doesn't want to inline this. */ + ~DevMap(); }; +DevMap::~DevMap() = default; -bool checkName(const al::vector<DevMap> &list, const std::string &name) +bool checkName(const al::span<DevMap> list, const std::string_view name) { - auto match_name = [&name](const DevMap &entry) -> bool - { return entry.name == name; }; + auto match_name = [name](const DevMap &entry) -> bool { return entry.name == name; }; return std::find_if(list.cbegin(), list.cend(), match_name) != list.cend(); } -al::vector<DevMap> PlaybackDevices; -al::vector<DevMap> CaptureDevices; + +struct DeviceList { + auto lock() noexcept(noexcept(mMutex.lock())) { return mMutex.lock(); } + auto unlock() noexcept(noexcept(mMutex.unlock())) { return mMutex.unlock(); } + +private: + std::mutex mMutex; + std::vector<DevMap> mPlayback; + std::vector<DevMap> mCapture; + std::wstring mPlaybackDefaultId; + std::wstring mCaptureDefaultId; + + friend struct DeviceListLock; +}; +struct DeviceListLock : public std::unique_lock<DeviceList> { + using std::unique_lock<DeviceList>::unique_lock; + + auto& getPlaybackList() const noexcept { return mutex()->mPlayback; } + auto& getCaptureList() const noexcept { return mutex()->mCapture; } + + void setPlaybackDefaultId(std::wstring_view devid) const { mutex()->mPlaybackDefaultId = devid; } + std::wstring_view getPlaybackDefaultId() const noexcept { return mutex()->mPlaybackDefaultId; } + void setCaptureDefaultId(std::wstring_view devid) const { mutex()->mCaptureDefaultId = devid; } + std::wstring_view getCaptureDefaultId() const noexcept { return mutex()->mCaptureDefaultId; } +}; + +DeviceList gDeviceList; + + +#if defined(ALSOFT_UWP) +enum EDataFlow { + eRender = 0, + eCapture = (eRender + 1), + eAll = (eCapture + 1), + EDataFlow_enum_count = (eAll + 1) +}; +#endif + +#if defined(ALSOFT_UWP) +using DeviceHandle = Windows::Devices::Enumeration::DeviceInformation; +using EventRegistrationToken = winrt::event_token; +#else +using DeviceHandle = ComPtr<IMMDevice>; +#endif using NameGUIDPair = std::pair<std::string,std::string>; -NameGUIDPair get_device_name_and_guid(IMMDevice *device) +static NameGUIDPair GetDeviceNameAndGuid(const DeviceHandle &device) { static constexpr char UnknownName[]{"Unknown Device Name"}; static constexpr char UnknownGuid[]{"Unknown Device GUID"}; +#if !defined(ALSOFT_UWP) std::string name, guid; ComPtr<IPropertyStore> ps; - HRESULT hr = device->OpenPropertyStore(STGM_READ, ps.getPtr()); + HRESULT hr{device->OpenPropertyStore(STGM_READ, al::out_ptr(ps))}; if(FAILED(hr)) { WARN("OpenPropertyStore failed: 0x%08lx\n", hr); @@ -207,42 +316,48 @@ NameGUIDPair get_device_name_and_guid(IMMDevice *device) } PropVariant pvprop; - hr = ps->GetValue(reinterpret_cast<const PROPERTYKEY&>(DEVPKEY_Device_FriendlyName), pvprop.get()); + hr = ps->GetValue(al::bit_cast<PROPERTYKEY>(DEVPKEY_Device_FriendlyName), pvprop.get()); if(FAILED(hr)) - { WARN("GetValue Device_FriendlyName failed: 0x%08lx\n", hr); - name += UnknownName; - } else if(pvprop->vt == VT_LPWSTR) - name += wstr_to_utf8(pvprop->pwszVal); + name = wstr_to_utf8(pvprop->pwszVal); else - { - WARN("Unexpected PROPVARIANT type: 0x%04x\n", pvprop->vt); - name += UnknownName; - } + WARN("Unexpected Device_FriendlyName PROPVARIANT type: 0x%04x\n", pvprop->vt); pvprop.clear(); - hr = ps->GetValue(reinterpret_cast<const PROPERTYKEY&>(PKEY_AudioEndpoint_GUID), pvprop.get()); + hr = ps->GetValue(al::bit_cast<PROPERTYKEY>(PKEY_AudioEndpoint_GUID), pvprop.get()); if(FAILED(hr)) - { WARN("GetValue AudioEndpoint_GUID failed: 0x%08lx\n", hr); - guid = UnknownGuid; - } else if(pvprop->vt == VT_LPWSTR) guid = wstr_to_utf8(pvprop->pwszVal); else - { - WARN("Unexpected PROPVARIANT type: 0x%04x\n", pvprop->vt); - guid = UnknownGuid; + WARN("Unexpected AudioEndpoint_GUID PROPVARIANT type: 0x%04x\n", pvprop->vt); +#else + std::string name{wstr_to_utf8(device.Name())}; + std::string guid; + // device->Id is DeviceInterfacePath: \\?\SWD#MMDEVAPI#{0.0.0.00000000}.{a21c17a0-fc1d-405e-ab5a-b513422b57d1}#{e6327cad-dcec-4949-ae8a-991e976a79d2} + auto devIfPath = device.Id(); + if(auto devIdStart = wcsstr(devIfPath.data(), L"}.")) + { + devIdStart += 2; // L"}." + if(auto devIdStartEnd = wcschr(devIdStart, L'#')) + { + std::wstring wDevId{devIdStart, static_cast<size_t>(devIdStartEnd - devIdStart)}; + guid = wstr_to_utf8(wDevId.c_str()); + std::transform(guid.begin(), guid.end(), guid.begin(), + [](char ch) { return static_cast<char>(std::toupper(ch)); }); + } } - +#endif + if(name.empty()) name = UnknownName; + if(guid.empty()) guid = UnknownGuid; return std::make_pair(std::move(name), std::move(guid)); } - -EndpointFormFactor get_device_formfactor(IMMDevice *device) +#if !defined(ALSOFT_UWP) +EndpointFormFactor GetDeviceFormfactor(IMMDevice *device) { ComPtr<IPropertyStore> ps; - HRESULT hr{device->OpenPropertyStore(STGM_READ, ps.getPtr())}; + HRESULT hr{device->OpenPropertyStore(STGM_READ, al::out_ptr(ps))}; if(FAILED(hr)) { WARN("OpenPropertyStore failed: 0x%08lx\n", hr); @@ -260,90 +375,422 @@ EndpointFormFactor get_device_formfactor(IMMDevice *device) WARN("Unexpected PROPVARIANT type: 0x%04x\n", pvform->vt); return formfactor; } +#endif -void add_device(IMMDevice *device, const WCHAR *devid, al::vector<DevMap> &list) +#if defined(ALSOFT_UWP) +struct DeviceHelper final : public IActivateAudioInterfaceCompletionHandler +#else +struct DeviceHelper final : private IMMNotificationClient +#endif { - for(auto &entry : list) + DeviceHelper() { - if(entry.devid == devid) - return; +#if defined(ALSOFT_UWP) + /* TODO: UWP also needs to watch for device added/removed events and + * dynamically add/remove devices from the lists. + */ + mActiveClientEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr); + + mRenderDeviceChangedToken = MediaDevice::DefaultAudioRenderDeviceChanged([this](const IInspectable& /*sender*/, const DefaultAudioRenderDeviceChangedEventArgs& args) { + if (args.Role() == AudioDeviceRole::Default) + { + const std::string msg{ "Default playback device changed: " + + wstr_to_utf8(args.Id())}; + alc::Event(alc::EventType::DefaultDeviceChanged, alc::DeviceType::Playback, + msg); + } + }); + + mCaptureDeviceChangedToken = MediaDevice::DefaultAudioCaptureDeviceChanged([this](const IInspectable& /*sender*/, const DefaultAudioCaptureDeviceChangedEventArgs& args) { + if (args.Role() == AudioDeviceRole::Default) + { + const std::string msg{ "Default capture device changed: " + + wstr_to_utf8(args.Id()) }; + alc::Event(alc::EventType::DefaultDeviceChanged, alc::DeviceType::Capture, + msg); + } + }); +#endif + } + ~DeviceHelper() + { +#if defined(ALSOFT_UWP) + MediaDevice::DefaultAudioRenderDeviceChanged(mRenderDeviceChangedToken); + MediaDevice::DefaultAudioCaptureDeviceChanged(mCaptureDeviceChangedToken); + + if(mActiveClientEvent != nullptr) + CloseHandle(mActiveClientEvent); + mActiveClientEvent = nullptr; +#else + if(mEnumerator) + mEnumerator->UnregisterEndpointNotificationCallback(this); + mEnumerator = nullptr; +#endif } - auto name_guid = get_device_name_and_guid(device); + /** -------------------------- IUnknown ----------------------------- */ + std::atomic<ULONG> mRefCount{1}; + STDMETHODIMP_(ULONG) AddRef() noexcept override { return mRefCount.fetch_add(1u) + 1u; } + STDMETHODIMP_(ULONG) Release() noexcept override { return mRefCount.fetch_sub(1u) - 1u; } + + STDMETHODIMP QueryInterface(const IID& IId, void **UnknownPtrPtr) noexcept override + { + // Three rules of QueryInterface: + // https://docs.microsoft.com/en-us/windows/win32/com/rules-for-implementing-queryinterface + // 1. Objects must have identity. + // 2. The set of interfaces on an object instance must be static. + // 3. It must be possible to query successfully for any interface on an object from any other interface. + + // If ppvObject(the address) is nullptr, then this method returns E_POINTER. + if(!UnknownPtrPtr) + return E_POINTER; + + // https://docs.microsoft.com/en-us/windows/win32/com/implementing-reference-counting + // Whenever a client calls a method(or API function), such as QueryInterface, that returns a new interface + // pointer, the method being called is responsible for incrementing the reference count through the returned + // pointer. For example, when a client first creates an object, it receives an interface pointer to an object + // that, from the client's point of view, has a reference count of one. If the client then calls AddRef on the + // interface pointer, the reference count becomes two. The client must call Release twice on the interface + // pointer to drop all of its references to the object. +#if defined(ALSOFT_UWP) + if(IId == __uuidof(IActivateAudioInterfaceCompletionHandler)) + { + *UnknownPtrPtr = static_cast<IActivateAudioInterfaceCompletionHandler*>(this); + AddRef(); + return S_OK; + } +#else + if(IId == __uuidof(IMMNotificationClient)) + { + *UnknownPtrPtr = static_cast<IMMNotificationClient*>(this); + AddRef(); + return S_OK; + } +#endif + else if(IId == __uuidof(IAgileObject) || IId == __uuidof(IUnknown)) + { + *UnknownPtrPtr = static_cast<IUnknown*>(this); + AddRef(); + return S_OK; + } + + // This method returns S_OK if the interface is supported, and E_NOINTERFACE otherwise. + *UnknownPtrPtr = nullptr; + return E_NOINTERFACE; + } - int count{1}; - std::string newname{name_guid.first}; - while(checkName(list, newname)) +#if defined(ALSOFT_UWP) + /** ----------------------- IActivateAudioInterfaceCompletionHandler ------------ */ + HRESULT ActivateCompleted(IActivateAudioInterfaceAsyncOperation*) override { - newname = name_guid.first; - newname += " #"; - newname += std::to_string(++count); + SetEvent(mActiveClientEvent); + + // Need to return S_OK + return S_OK; } - list.emplace_back(std::move(newname), std::move(name_guid.second), devid); - const DevMap &newentry = list.back(); +#else + /** ----------------------- IMMNotificationClient ------------ */ + STDMETHODIMP OnDeviceStateChanged(LPCWSTR /*pwstrDeviceId*/, DWORD /*dwNewState*/) noexcept override { return S_OK; } - TRACE("Got device \"%s\", \"%s\", \"%ls\"\n", newentry.name.c_str(), - newentry.endpoint_guid.c_str(), newentry.devid.c_str()); -} + STDMETHODIMP OnDeviceAdded(LPCWSTR pwstrDeviceId) noexcept override + { + ComPtr<IMMDevice> device; + HRESULT hr{mEnumerator->GetDevice(pwstrDeviceId, al::out_ptr(device))}; + if(FAILED(hr)) + { + ERR("Failed to get device: 0x%08lx\n", hr); + return S_OK; + } -WCHAR *get_device_id(IMMDevice *device) -{ - WCHAR *devid; + ComPtr<IMMEndpoint> endpoint; + hr = device->QueryInterface(__uuidof(IMMEndpoint), al::out_ptr(endpoint)); + if(FAILED(hr)) + { + ERR("Failed to get device endpoint: 0x%08lx\n", hr); + return S_OK; + } - const HRESULT hr{device->GetId(&devid)}; - if(FAILED(hr)) + EDataFlow flowdir{}; + hr = endpoint->GetDataFlow(&flowdir); + if(FAILED(hr)) + { + ERR("Failed to get endpoint data flow: 0x%08lx\n", hr); + return S_OK; + } + + auto devlock = DeviceListLock{gDeviceList}; + auto &list = (flowdir==eRender) ? devlock.getPlaybackList() : devlock.getCaptureList(); + + if(AddDevice(device, pwstrDeviceId, list)) + { + const auto devtype = (flowdir==eRender) ? alc::DeviceType::Playback + : alc::DeviceType::Capture; + const std::string msg{"Device added: "+list.back().name}; + alc::Event(alc::EventType::DeviceAdded, devtype, msg); + } + + return S_OK; + } + + STDMETHODIMP OnDeviceRemoved(LPCWSTR pwstrDeviceId) noexcept override { - ERR("Failed to get device id: %lx\n", hr); - return nullptr; + auto devlock = DeviceListLock{gDeviceList}; + for(auto flowdir : std::array{eRender, eCapture}) + { + auto &list = (flowdir==eRender) ? devlock.getPlaybackList() : devlock.getCaptureList(); + auto devtype = (flowdir==eRender)?alc::DeviceType::Playback : alc::DeviceType::Capture; + + /* Find the ID in the list to remove. */ + auto iter = std::find_if(list.begin(), list.end(), + [pwstrDeviceId](const DevMap &entry) noexcept + { return pwstrDeviceId == entry.devid; }); + if(iter == list.end()) continue; + + TRACE("Removing device \"%s\", \"%s\", \"%ls\"\n", iter->name.c_str(), + iter->endpoint_guid.c_str(), iter->devid.c_str()); + + std::string msg{"Device removed: "+std::move(iter->name)}; + list.erase(iter); + + alc::Event(alc::EventType::DeviceRemoved, devtype, msg); + } + return S_OK; } - return devid; -} + STDMETHODIMP OnPropertyValueChanged(LPCWSTR /*pwstrDeviceId*/, const PROPERTYKEY /*key*/) noexcept override { return S_OK; } -void probe_devices(IMMDeviceEnumerator *devenum, EDataFlow flowdir, al::vector<DevMap> &list) -{ - al::vector<DevMap>{}.swap(list); + STDMETHODIMP OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDefaultDeviceId) noexcept override + { + if(role != eMultimedia) + return S_OK; - ComPtr<IMMDeviceCollection> coll; - HRESULT hr{devenum->EnumAudioEndpoints(flowdir, DEVICE_STATE_ACTIVE, coll.getPtr())}; - if(FAILED(hr)) + const std::wstring_view devid{pwstrDefaultDeviceId ? pwstrDefaultDeviceId + : std::wstring_view{}}; + if(flow == eRender) + { + DeviceListLock{gDeviceList}.setPlaybackDefaultId(devid); + const std::string msg{"Default playback device changed: " + wstr_to_utf8(devid)}; + alc::Event(alc::EventType::DefaultDeviceChanged, alc::DeviceType::Playback, msg); + } + else if(flow == eCapture) + { + DeviceListLock{gDeviceList}.setCaptureDefaultId(devid); + const std::string msg{"Default capture device changed: " + wstr_to_utf8(devid)}; + alc::Event(alc::EventType::DefaultDeviceChanged, alc::DeviceType::Capture, msg); + } + return S_OK; + } +#endif + + /** -------------------------- DeviceHelper ----------------------------- */ + HRESULT init() { - ERR("Failed to enumerate audio endpoints: 0x%08lx\n", hr); - return; +#if !defined(ALSOFT_UWP) + HRESULT hr{CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_INPROC_SERVER, + __uuidof(IMMDeviceEnumerator), al::out_ptr(mEnumerator))}; + if(SUCCEEDED(hr)) + mEnumerator->RegisterEndpointNotificationCallback(this); + else + WARN("Failed to create IMMDeviceEnumerator instance: 0x%08lx\n", hr); + return hr; +#else + return S_OK; +#endif } - UINT count{0}; - hr = coll->GetCount(&count); - if(SUCCEEDED(hr) && count > 0) - list.reserve(count); + HRESULT openDevice(std::wstring_view devid, EDataFlow flow, DeviceHandle& device) + { +#if !defined(ALSOFT_UWP) + HRESULT hr{E_FAIL}; + if(mEnumerator) + { + if(devid.empty()) + hr = mEnumerator->GetDefaultAudioEndpoint(flow, eMultimedia, al::out_ptr(device)); + else + hr = mEnumerator->GetDevice(devid.data(), al::out_ptr(device)); + } + return hr; +#else + const auto deviceRole = Windows::Media::Devices::AudioDeviceRole::Default; + auto devIfPath = + devid.empty() ? (flow == eRender ? MediaDevice::GetDefaultAudioRenderId(deviceRole) : MediaDevice::GetDefaultAudioCaptureId(deviceRole)) + : winrt::hstring(devid.data()); + if (devIfPath.empty()) + return E_POINTER; + + auto&& deviceInfo = DeviceInformation::CreateFromIdAsync(devIfPath, nullptr, DeviceInformationKind::DeviceInterface).get(); + if (!deviceInfo) + return E_NOINTERFACE; + device = deviceInfo; + return S_OK; +#endif + } - ComPtr<IMMDevice> device; - hr = devenum->GetDefaultAudioEndpoint(flowdir, eMultimedia, device.getPtr()); - if(SUCCEEDED(hr)) +#if !defined(ALSOFT_UWP) + static HRESULT activateAudioClient(_In_ DeviceHandle &device, REFIID iid, void **ppv) + { return device->Activate(iid, CLSCTX_INPROC_SERVER, nullptr, ppv); } +#else + HRESULT activateAudioClient(_In_ DeviceHandle &device, _In_ REFIID iid, void **ppv) { - if(WCHAR *devid{get_device_id(device.get())}) + ComPtr<IActivateAudioInterfaceAsyncOperation> asyncOp; + HRESULT hr{ActivateAudioInterfaceAsync(device.Id().data(), iid, nullptr, this, + al::out_ptr(asyncOp))}; + if(FAILED(hr)) + return hr; + + /* I don't like waiting for INFINITE time, but the activate operation + * can take an indefinite amount of time since it can require user + * input. + */ + DWORD res{WaitForSingleObjectEx(mActiveClientEvent, INFINITE, FALSE)}; + if(res != WAIT_OBJECT_0) { - add_device(device.get(), devid, list); - CoTaskMemFree(devid); + ERR("WaitForSingleObjectEx error: 0x%lx\n", res); + return E_FAIL; } - device = nullptr; + + HRESULT hrActivateRes{E_FAIL}; + ComPtr<IUnknown> punkAudioIface; + hr = asyncOp->GetActivateResult(&hrActivateRes, al::out_ptr(punkAudioIface)); + if(SUCCEEDED(hr)) hr = hrActivateRes; + if(FAILED(hr)) return hr; + + return punkAudioIface->QueryInterface(iid, ppv); } +#endif - for(UINT i{0};i < count;++i) + std::wstring probeDevices(EDataFlow flowdir, std::vector<DevMap> &list) { - hr = coll->Item(i, device.getPtr()); - if(FAILED(hr)) continue; + std::wstring defaultId; + std::vector<DevMap>{}.swap(list); + +#if !defined(ALSOFT_UWP) + ComPtr<IMMDeviceCollection> coll; + HRESULT hr{mEnumerator->EnumAudioEndpoints(flowdir, DEVICE_STATE_ACTIVE, + al::out_ptr(coll))}; + if(FAILED(hr)) + { + ERR("Failed to enumerate audio endpoints: 0x%08lx\n", hr); + return defaultId; + } + + UINT count{0}; + hr = coll->GetCount(&count); + if(SUCCEEDED(hr) && count > 0) + list.reserve(count); + + ComPtr<IMMDevice> device; + hr = mEnumerator->GetDefaultAudioEndpoint(flowdir, eMultimedia, al::out_ptr(device)); + if(SUCCEEDED(hr)) + { + if(WCHAR *devid{GetDeviceId(device.get())}) + { + defaultId = devid; + CoTaskMemFree(devid); + } + device = nullptr; + } + + for(UINT i{0};i < count;++i) + { + hr = coll->Item(i, al::out_ptr(device)); + if(FAILED(hr)) + continue; - if(WCHAR *devid{get_device_id(device.get())}) + if(WCHAR *devid{GetDeviceId(device.get())}) + { + std::ignore = AddDevice(device, devid, list); + CoTaskMemFree(devid); + } + device = nullptr; + } +#else + const auto deviceRole = Windows::Media::Devices::AudioDeviceRole::Default; + auto DefaultAudioId = flowdir == eRender ? MediaDevice::GetDefaultAudioRenderId(deviceRole) + : MediaDevice::GetDefaultAudioCaptureId(deviceRole); + if(!DefaultAudioId.empty()) { - add_device(device.get(), devid, list); - CoTaskMemFree(devid); + auto deviceInfo = DeviceInformation::CreateFromIdAsync(DefaultAudioId, nullptr, + DeviceInformationKind::DeviceInterface).get(); + if(deviceInfo) + defaultId = deviceInfo.Id().data(); } - device = nullptr; + + // Get the string identifier of the audio renderer + auto AudioSelector = flowdir == eRender ? MediaDevice::GetAudioRenderSelector() : MediaDevice::GetAudioCaptureSelector(); + + // Setup the asynchronous callback + auto&& DeviceInfoCollection = DeviceInformation::FindAllAsync(AudioSelector, /*PropertyList*/nullptr, DeviceInformationKind::DeviceInterface).get(); + if(DeviceInfoCollection) + { + try { + auto deviceCount = DeviceInfoCollection.Size(); + for(unsigned int i{0};i < deviceCount;++i) + { + auto deviceInfo = DeviceInfoCollection.GetAt(i); + if(deviceInfo) + std::ignore = AddDevice(deviceInfo, deviceInfo.Id().data(), list); + } + } + catch (const winrt::hresult_error& /*ex*/) { + } + } +#endif + + return defaultId; } -} +private: + static bool AddDevice(const DeviceHandle &device, const WCHAR *devid, std::vector<DevMap> &list) + { + for(auto &entry : list) + { + if(entry.devid == devid) + return false; + } + + auto name_guid = GetDeviceNameAndGuid(device); + int count{1}; + std::string newname{name_guid.first}; + while(checkName(list, newname)) + { + newname = name_guid.first; + newname += " #"; + newname += std::to_string(++count); + } + list.emplace_back(std::move(newname), std::move(name_guid.second), devid); + const DevMap &newentry = list.back(); + + TRACE("Got device \"%s\", \"%s\", \"%ls\"\n", newentry.name.c_str(), + newentry.endpoint_guid.c_str(), newentry.devid.c_str()); + return true; + } + +#if !defined(ALSOFT_UWP) + static WCHAR *GetDeviceId(IMMDevice *device) + { + WCHAR *devid; + + const HRESULT hr{device->GetId(&devid)}; + if(FAILED(hr)) + { + ERR("Failed to get device id: %lx\n", hr); + return nullptr; + } + + return devid; + } + ComPtr<IMMDeviceEnumerator> mEnumerator{nullptr}; + +#else + + HANDLE mActiveClientEvent{nullptr}; + + EventRegistrationToken mRenderDeviceChangedToken; + EventRegistrationToken mCaptureDeviceChangedToken; +#endif +}; bool MakeExtensible(WAVEFORMATEXTENSIBLE *out, const WAVEFORMATEX *in) { @@ -430,51 +877,51 @@ enum class MsgType { StartDevice, StopDevice, CloseDevice, - EnumeratePlayback, - EnumerateCapture, - Count, - QuitThread = Count + QuitThread }; -constexpr char MessageStr[static_cast<size_t>(MsgType::Count)][20]{ - "Open Device", - "Reset Device", - "Start Device", - "Stop Device", - "Close Device", - "Enumerate Playback", - "Enumerate Capture" -}; +constexpr const char *GetMessageTypeName(MsgType type) noexcept +{ + switch(type) + { + case MsgType::OpenDevice: return "Open Device"; + case MsgType::ResetDevice: return "Reset Device"; + case MsgType::StartDevice: return "Start Device"; + case MsgType::StopDevice: return "Stop Device"; + case MsgType::CloseDevice: return "Close Device"; + case MsgType::QuitThread: break; + } + return ""; +} /* Proxy interface used by the message handler. */ struct WasapiProxy { virtual ~WasapiProxy() = default; - virtual HRESULT openProxy(const char *name) = 0; + virtual HRESULT openProxy(std::string_view name) = 0; virtual void closeProxy() = 0; virtual HRESULT resetProxy() = 0; virtual HRESULT startProxy() = 0; - virtual void stopProxy() = 0; + virtual void stopProxy() = 0; struct Msg { MsgType mType; WasapiProxy *mProxy; - const char *mParam; + std::string_view mParam; std::promise<HRESULT> mPromise; explicit operator bool() const noexcept { return mType != MsgType::QuitThread; } }; - static std::thread sThread; - static std::deque<Msg> mMsgQueue; - static std::mutex mMsgQueueLock; - static std::condition_variable mMsgQueueCond; - static std::mutex sThreadLock; - static size_t sInitCount; + static inline std::deque<Msg> mMsgQueue; + static inline std::mutex mMsgQueueLock; + static inline std::condition_variable mMsgQueueCond; + + static inline std::optional<DeviceHelper> sDeviceHelper; - std::future<HRESULT> pushMessage(MsgType type, const char *param=nullptr) + std::future<HRESULT> pushMessage(MsgType type, std::string_view param={}) { std::promise<HRESULT> promise; std::future<HRESULT> future{promise.get_future()}; @@ -492,7 +939,7 @@ struct WasapiProxy { std::future<HRESULT> future{promise.get_future()}; { std::lock_guard<std::mutex> _{mMsgQueueLock}; - mMsgQueue.emplace_back(Msg{type, nullptr, nullptr, std::move(promise)}); + mMsgQueue.emplace_back(Msg{type, nullptr, {}, std::move(promise)}); } mMsgQueueCond.notify_one(); return future; @@ -508,65 +955,41 @@ struct WasapiProxy { } static int messageHandler(std::promise<HRESULT> *promise); - - static HRESULT InitThread() - { - std::lock_guard<std::mutex> _{sThreadLock}; - HRESULT res{S_OK}; - if(!sThread.joinable()) - { - std::promise<HRESULT> promise; - auto future = promise.get_future(); - - sThread = std::thread{&WasapiProxy::messageHandler, &promise}; - res = future.get(); - if(FAILED(res)) - { - sThread.join(); - return res; - } - } - ++sInitCount; - return res; - } - - static void DeinitThread() - { - std::lock_guard<std::mutex> _{sThreadLock}; - if(!--sInitCount && sThread.joinable()) - { - pushMessageStatic(MsgType::QuitThread); - sThread.join(); - } - } }; -std::thread WasapiProxy::sThread; -std::deque<WasapiProxy::Msg> WasapiProxy::mMsgQueue; -std::mutex WasapiProxy::mMsgQueueLock; -std::condition_variable WasapiProxy::mMsgQueueCond; -std::mutex WasapiProxy::sThreadLock; -size_t WasapiProxy::sInitCount{0}; int WasapiProxy::messageHandler(std::promise<HRESULT> *promise) { TRACE("Starting message thread\n"); - HRESULT hr{CoInitializeEx(nullptr, COINIT_MULTITHREADED)}; - if(FAILED(hr)) + ComWrapper com{COINIT_MULTITHREADED}; + if(!com) { - WARN("Failed to initialize COM: 0x%08lx\n", hr); - promise->set_value(hr); + WARN("Failed to initialize COM: 0x%08lx\n", com.status()); + promise->set_value(com.status()); return 0; } - promise->set_value(S_OK); + + HRESULT hr{sDeviceHelper.emplace().init()}; + promise->set_value(hr); promise = nullptr; + if(FAILED(hr)) + goto skip_loop; + + { + auto devlock = DeviceListLock{gDeviceList}; + auto defaultId = sDeviceHelper->probeDevices(eRender, devlock.getPlaybackList()); + if(!defaultId.empty()) devlock.setPlaybackDefaultId(defaultId); + defaultId = sDeviceHelper->probeDevices(eCapture, devlock.getCaptureList()); + if(!defaultId.empty()) devlock.setCaptureDefaultId(defaultId); + } TRACE("Starting message loop\n"); while(Msg msg{popMessage()}) { - TRACE("Got message \"%s\" (0x%04x, this=%p, param=%p)\n", - MessageStr[static_cast<size_t>(msg.mType)], static_cast<uint>(msg.mType), - static_cast<void*>(msg.mProxy), static_cast<const void*>(msg.mParam)); + TRACE("Got message \"%s\" (0x%04x, this=%p, param=\"%.*s\")\n", + GetMessageTypeName(msg.mType), static_cast<uint>(msg.mType), + static_cast<void*>(msg.mProxy), static_cast<int>(msg.mParam.length()), + msg.mParam.data()); switch(msg.mType) { @@ -595,27 +1018,6 @@ int WasapiProxy::messageHandler(std::promise<HRESULT> *promise) msg.mPromise.set_value(S_OK); continue; - case MsgType::EnumeratePlayback: - case MsgType::EnumerateCapture: - { - void *ptr{}; - hr = CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_INPROC_SERVER, - IID_IMMDeviceEnumerator, &ptr); - if(FAILED(hr)) - msg.mPromise.set_value(hr); - else - { - ComPtr<IMMDeviceEnumerator> devenum{static_cast<IMMDeviceEnumerator*>(ptr)}; - - if(msg.mType == MsgType::EnumeratePlayback) - probe_devices(devenum.get(), eRender, PlaybackDevices); - else if(msg.mType == MsgType::EnumerateCapture) - probe_devices(devenum.get(), eCapture, CaptureDevices); - msg.mPromise.set_value(S_OK); - } - continue; - } - case MsgType::QuitThread: break; } @@ -623,20 +1025,22 @@ int WasapiProxy::messageHandler(std::promise<HRESULT> *promise) msg.mPromise.set_value(E_FAIL); } TRACE("Message loop finished\n"); - CoUninitialize(); + +skip_loop: + sDeviceHelper.reset(); return 0; } - struct WasapiPlayback final : public BackendBase, WasapiProxy { WasapiPlayback(DeviceBase *device) noexcept : BackendBase{device} { } ~WasapiPlayback() override; int mixerProc(); + int mixerSpatialProc(); - void open(const char *name) override; - HRESULT openProxy(const char *name) override; + void open(std::string_view name) override; + HRESULT openProxy(std::string_view name) override; void closeProxy() override; bool reset() override; @@ -648,10 +1052,22 @@ struct WasapiPlayback final : public BackendBase, WasapiProxy { ClockLatency getClockLatency() override; + void prepareFormat(WAVEFORMATEXTENSIBLE &OutputType); + void finalizeFormat(WAVEFORMATEXTENSIBLE &OutputType); + HRESULT mOpenStatus{E_FAIL}; - ComPtr<IMMDevice> mMMDev{nullptr}; - ComPtr<IAudioClient> mClient{nullptr}; - ComPtr<IAudioRenderClient> mRender{nullptr}; + DeviceHandle mMMDev{nullptr}; + + struct PlainDevice { + ComPtr<IAudioClient> mClient{nullptr}; + ComPtr<IAudioRenderClient> mRender{nullptr}; + }; + struct SpatialDevice { + ComPtr<ISpatialAudioClient> mClient{nullptr}; + ComPtr<ISpatialAudioObjectRenderStream> mRender{nullptr}; + AudioObjectType mStaticMask{}; + }; + std::variant<std::monostate,PlainDevice,SpatialDevice> mAudio; HANDLE mNotifyEvent{nullptr}; UINT32 mOrigBufferSize{}, mOrigUpdateSize{}; @@ -673,10 +1089,7 @@ struct WasapiPlayback final : public BackendBase, WasapiProxy { WasapiPlayback::~WasapiPlayback() { if(SUCCEEDED(mOpenStatus)) - { pushMessage(MsgType::CloseDevice).wait(); - DeinitThread(); - } mOpenStatus = E_FAIL; if(mNotifyEvent != nullptr) @@ -687,24 +1100,29 @@ WasapiPlayback::~WasapiPlayback() FORCE_ALIGN int WasapiPlayback::mixerProc() { - HRESULT hr{CoInitializeEx(nullptr, COINIT_MULTITHREADED)}; - if(FAILED(hr)) + ComWrapper com{COINIT_MULTITHREADED}; + if(!com) { - ERR("CoInitializeEx(nullptr, COINIT_MULTITHREADED) failed: 0x%08lx\n", hr); - mDevice->handleDisconnect("COM init failed: 0x%08lx", hr); + ERR("CoInitializeEx(nullptr, COINIT_MULTITHREADED) failed: 0x%08lx\n", com.status()); + mDevice->handleDisconnect("COM init failed: 0x%08lx", com.status()); return 1; } + auto &audio = std::get<PlainDevice>(mAudio); + SetRTPriority(); althrd_setname(MIXER_THREAD_NAME); const uint frame_size{mFormat.Format.nChannels * mFormat.Format.wBitsPerSample / 8u}; const uint update_size{mOrigUpdateSize}; const UINT32 buffer_len{mOrigBufferSize}; + const void *resbufferptr{}; + + mBufferFilled = 0; while(!mKillNow.load(std::memory_order_relaxed)) { UINT32 written; - hr = mClient->GetCurrentPadding(&written); + HRESULT hr{audio.mClient->GetCurrentPadding(&written)}; if(FAILED(hr)) { ERR("Failed to get padding: 0x%08lx\n", hr); @@ -723,7 +1141,7 @@ FORCE_ALIGN int WasapiPlayback::mixerProc() } BYTE *buffer; - hr = mRender->GetBuffer(len, &buffer); + hr = audio.mRender->GetBuffer(len, &buffer); if(SUCCEEDED(hr)) { if(mResampler) @@ -735,22 +1153,15 @@ FORCE_ALIGN int WasapiPlayback::mixerProc() { mDevice->renderSamples(mResampleBuffer.get(), mDevice->UpdateSize, mFormat.Format.nChannels); + resbufferptr = mResampleBuffer.get(); mBufferFilled = mDevice->UpdateSize; } - const void *src{mResampleBuffer.get()}; - uint srclen{mBufferFilled}; - uint got{mResampler->convert(&src, &srclen, buffer, len-done)}; + uint got{mResampler->convert(&resbufferptr, &mBufferFilled, buffer, len-done)}; buffer += got*frame_size; done += got; mPadding.store(written + done, std::memory_order_relaxed); - if(srclen) - { - const char *bsrc{static_cast<const char*>(src)}; - std::copy(bsrc, bsrc + srclen*frame_size, mResampleBuffer.get()); - } - mBufferFilled = srclen; } } else @@ -759,7 +1170,7 @@ FORCE_ALIGN int WasapiPlayback::mixerProc() mDevice->renderSamples(buffer, len, mFormat.Format.nChannels); mPadding.store(written + len, std::memory_order_relaxed); } - hr = mRender->ReleaseBuffer(len, 0); + hr = audio.mRender->ReleaseBuffer(len, 0); } if(FAILED(hr)) { @@ -770,12 +1181,126 @@ FORCE_ALIGN int WasapiPlayback::mixerProc() } mPadding.store(0u, std::memory_order_release); - CoUninitialize(); + return 0; +} + +FORCE_ALIGN int WasapiPlayback::mixerSpatialProc() +{ + ComWrapper com{COINIT_MULTITHREADED}; + if(!com) + { + ERR("CoInitializeEx(nullptr, COINIT_MULTITHREADED) failed: 0x%08lx\n", com.status()); + mDevice->handleDisconnect("COM init failed: 0x%08lx", com.status()); + return 1; + } + + auto &audio = std::get<SpatialDevice>(mAudio); + + SetRTPriority(); + althrd_setname(MIXER_THREAD_NAME); + + std::vector<ComPtr<ISpatialAudioObject>> channels; + std::vector<float*> buffers; + std::vector<float*> resbuffers; + std::vector<const void*> tmpbuffers; + + /* TODO: Set mPadding appropriately. There doesn't seem to be a way to + * update it dynamically based on the stream, so a fixed size may be the + * best we can do. + */ + mPadding.store(mOrigBufferSize-mOrigUpdateSize, std::memory_order_release); + + mBufferFilled = 0; + while(!mKillNow.load(std::memory_order_relaxed)) + { + if(DWORD res{WaitForSingleObjectEx(mNotifyEvent, 1000, FALSE)}; res != WAIT_OBJECT_0) + { + ERR("WaitForSingleObjectEx error: 0x%lx\n", res); + + HRESULT hr{audio.mRender->Reset()}; + if(FAILED(hr)) + { + ERR("ISpatialAudioObjectRenderStream::Reset failed: 0x%08lx\n", hr); + mDevice->handleDisconnect("Device lost: 0x%08lx", hr); + break; + } + } + + UINT32 dynamicCount{}, framesToDo{}; + HRESULT hr{audio.mRender->BeginUpdatingAudioObjects(&dynamicCount, &framesToDo)}; + if(SUCCEEDED(hr)) + { + if(channels.empty()) UNLIKELY + { + auto flags = as_unsigned(audio.mStaticMask); + channels.reserve(as_unsigned(al::popcount(flags))); + while(flags) + { + auto id = decltype(flags){1} << al::countr_zero(flags); + flags &= ~id; + + channels.emplace_back(); + audio.mRender->ActivateSpatialAudioObject(static_cast<AudioObjectType>(id), + al::out_ptr(channels.back())); + } + buffers.resize(channels.size()); + if(mResampler) + { + tmpbuffers.resize(buffers.size()); + resbuffers.resize(buffers.size()); + for(size_t i{0};i < tmpbuffers.size();++i) + resbuffers[i] = reinterpret_cast<float*>(mResampleBuffer.get()) + + mDevice->UpdateSize*i; + } + } + + /* We have to call to get each channel's buffer individually every + * update, unfortunately. + */ + std::transform(channels.cbegin(), channels.cend(), buffers.begin(), + [](const ComPtr<ISpatialAudioObject> &obj) -> float* + { + BYTE *buffer{}; + UINT32 size{}; + obj->GetBuffer(&buffer, &size); + return reinterpret_cast<float*>(buffer); + }); + + if(!mResampler) + mDevice->renderSamples(buffers, framesToDo); + else + { + std::lock_guard<std::mutex> _{mMutex}; + for(UINT32 pos{0};pos < framesToDo;) + { + if(mBufferFilled == 0) + { + mDevice->renderSamples(resbuffers, mDevice->UpdateSize); + std::copy(resbuffers.cbegin(), resbuffers.cend(), tmpbuffers.begin()); + mBufferFilled = mDevice->UpdateSize; + } + + const uint got{mResampler->convertPlanar(tmpbuffers.data(), &mBufferFilled, + reinterpret_cast<void*const*>(buffers.data()), framesToDo-pos)}; + for(auto &buf : buffers) + buf += got; + pos += got; + } + } + + hr = audio.mRender->EndUpdatingAudioObjects(); + } + + if(FAILED(hr)) + ERR("Failed to update playback objects: 0x%08lx\n", hr); + } + mPadding.store(0u, std::memory_order_release); + return 0; } -void WasapiPlayback::open(const char *name) +void WasapiPlayback::open(std::string_view name) { if(SUCCEEDED(mOpenStatus)) throw al::backend_exception{al::backend_error::DeviceError, @@ -789,132 +1314,69 @@ void WasapiPlayback::open(const char *name) "Failed to create notify events"}; } - HRESULT hr{InitThread()}; - if(FAILED(hr)) - { - throw al::backend_exception{al::backend_error::DeviceError, - "Failed to init COM thread: 0x%08lx", hr}; - } - - if(name) + if(name.length() >= DevNameHeadLen + && std::strncmp(name.data(), DevNameHead, DevNameHeadLen) == 0) { - if(PlaybackDevices.empty()) - pushMessage(MsgType::EnumeratePlayback); - if(std::strncmp(name, DevNameHead, DevNameHeadLen) == 0) - { - name += DevNameHeadLen; - if(*name == '\0') - name = nullptr; - } + name = name.substr(DevNameHeadLen); } mOpenStatus = pushMessage(MsgType::OpenDevice, name).get(); if(FAILED(mOpenStatus)) - { - DeinitThread(); throw al::backend_exception{al::backend_error::DeviceError, "Device init failed: 0x%08lx", mOpenStatus}; - } } -HRESULT WasapiPlayback::openProxy(const char *name) +HRESULT WasapiPlayback::openProxy(std::string_view name) { - const wchar_t *devid{nullptr}; - if(name) + std::string devname; + std::wstring devid; + if(!name.empty()) { - auto iter = std::find_if(PlaybackDevices.cbegin(), PlaybackDevices.cend(), + auto devlock = DeviceListLock{gDeviceList}; + auto list = al::span{devlock.getPlaybackList()}; + auto iter = std::find_if(list.cbegin(), list.cend(), [name](const DevMap &entry) -> bool { return entry.name == name || entry.endpoint_guid == name; }); - if(iter == PlaybackDevices.cend()) + if(iter == list.cend()) { const std::wstring wname{utf8_to_wstr(name)}; - iter = std::find_if(PlaybackDevices.cbegin(), PlaybackDevices.cend(), + iter = std::find_if(list.cbegin(), list.cend(), [&wname](const DevMap &entry) -> bool { return entry.devid == wname; }); } - if(iter == PlaybackDevices.cend()) + if(iter == list.cend()) { - WARN("Failed to find device name matching \"%s\"\n", name); + WARN("Failed to find device name matching \"%.*s\"\n", static_cast<int>(name.length()), + name.data()); return E_FAIL; } - name = iter->name.c_str(); - devid = iter->devid.c_str(); + devname = iter->name; + devid = iter->devid; } - void *ptr; - ComPtr<IMMDevice> mmdev; - HRESULT hr{CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_INPROC_SERVER, - IID_IMMDeviceEnumerator, &ptr)}; - if(SUCCEEDED(hr)) - { - ComPtr<IMMDeviceEnumerator> enumerator{static_cast<IMMDeviceEnumerator*>(ptr)}; - if(!devid) - hr = enumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, mmdev.getPtr()); - else - hr = enumerator->GetDevice(devid, mmdev.getPtr()); - } + HRESULT hr{sDeviceHelper->openDevice(devid, eRender, mMMDev)}; if(FAILED(hr)) { - WARN("Failed to open device \"%s\"\n", name?name:"(default)"); + WARN("Failed to open device \"%s\"\n", devname.empty() ? "(default)" : devname.c_str()); return hr; } + if(!devname.empty()) + mDevice->DeviceName = DevNameHead + std::move(devname); + else + mDevice->DeviceName = DevNameHead + GetDeviceNameAndGuid(mMMDev).first; - mClient = nullptr; - mMMDev = std::move(mmdev); - if(name) mDevice->DeviceName = std::string{DevNameHead} + name; - else mDevice->DeviceName = DevNameHead + get_device_name_and_guid(mMMDev.get()).first; - - return hr; + return S_OK; } void WasapiPlayback::closeProxy() { - mClient = nullptr; + mAudio.emplace<std::monostate>(); mMMDev = nullptr; } -bool WasapiPlayback::reset() -{ - HRESULT hr{pushMessage(MsgType::ResetDevice).get()}; - if(FAILED(hr)) - throw al::backend_exception{al::backend_error::DeviceError, "0x%08lx", hr}; - return true; -} - -HRESULT WasapiPlayback::resetProxy() +void WasapiPlayback::prepareFormat(WAVEFORMATEXTENSIBLE &OutputType) { - mClient = nullptr; - - void *ptr; - HRESULT hr{mMMDev->Activate(IID_IAudioClient, CLSCTX_INPROC_SERVER, nullptr, &ptr)}; - if(FAILED(hr)) - { - ERR("Failed to reactivate audio client: 0x%08lx\n", hr); - return hr; - } - mClient = ComPtr<IAudioClient>{static_cast<IAudioClient*>(ptr)}; - - WAVEFORMATEX *wfx; - hr = mClient->GetMixFormat(&wfx); - if(FAILED(hr)) - { - ERR("Failed to get mix format: 0x%08lx\n", hr); - return hr; - } - TraceFormat("Device mix format", wfx); - - WAVEFORMATEXTENSIBLE OutputType; - if(!MakeExtensible(&OutputType, wfx)) - { - CoTaskMemFree(wfx); - return E_FAIL; - } - CoTaskMemFree(wfx); - wfx = nullptr; - - const ReferenceTime per_time{ReferenceTime{seconds{mDevice->UpdateSize}} / mDevice->Frequency}; - const ReferenceTime buf_time{ReferenceTime{seconds{mDevice->BufferSize}} / mDevice->Frequency}; bool isRear51{false}; if(!mDevice->Flags.test(FrequencyRequest)) @@ -928,7 +1390,7 @@ HRESULT WasapiPlayback::resetProxy() const uint32_t chancount{OutputType.Format.nChannels}; const DWORD chanmask{OutputType.dwChannelMask}; if(chancount >= 12 && (chanmask&X714Mask) == X7DOT1DOT4) - mDevice->FmtChans = DevFmtX71; + mDevice->FmtChans = DevFmtX714; else if(chancount >= 8 && (chanmask&X71Mask) == X7DOT1) mDevice->FmtChans = DevFmtX71; else if(chancount >= 7 && (chanmask&X61Mask) == X6DOT1) @@ -1030,141 +1492,426 @@ HRESULT WasapiPlayback::resetProxy() OutputType.Format.wBitsPerSample / 8); OutputType.Format.nAvgBytesPerSec = OutputType.Format.nSamplesPerSec * OutputType.Format.nBlockAlign; +} - TraceFormat("Requesting playback format", &OutputType.Format); - hr = mClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &OutputType.Format, &wfx); - if(FAILED(hr)) +void WasapiPlayback::finalizeFormat(WAVEFORMATEXTENSIBLE &OutputType) +{ + if(!GetConfigValueBool(mDevice->DeviceName.c_str(), "wasapi", "allow-resampler", true)) + mDevice->Frequency = OutputType.Format.nSamplesPerSec; + else + mDevice->Frequency = minu(mDevice->Frequency, OutputType.Format.nSamplesPerSec); + + const uint32_t chancount{OutputType.Format.nChannels}; + const DWORD chanmask{OutputType.dwChannelMask}; + /* Don't update the channel format if the requested format fits what's + * supported. + */ + bool chansok{false}; + if(mDevice->Flags.test(ChannelsRequest)) + { + /* When requesting a channel configuration, make sure it fits the + * mask's lsb (to ensure no gaps in the output channels). If there's no + * mask, assume the request fits with enough channels. + */ + switch(mDevice->FmtChans) + { + case DevFmtMono: + chansok = (chancount >= 1 && ((chanmask&MonoMask) == MONO || !chanmask)); + break; + case DevFmtStereo: + chansok = (chancount >= 2 && ((chanmask&StereoMask) == STEREO || !chanmask)); + break; + case DevFmtQuad: + chansok = (chancount >= 4 && ((chanmask&QuadMask) == QUAD || !chanmask)); + break; + case DevFmtX51: + chansok = (chancount >= 6 && ((chanmask&X51Mask) == X5DOT1 + || (chanmask&X51RearMask) == X5DOT1REAR || !chanmask)); + break; + case DevFmtX61: + chansok = (chancount >= 7 && ((chanmask&X61Mask) == X6DOT1 || !chanmask)); + break; + case DevFmtX71: + case DevFmtX3D71: + chansok = (chancount >= 8 && ((chanmask&X71Mask) == X7DOT1 || !chanmask)); + break; + case DevFmtX714: + chansok = (chancount >= 12 && ((chanmask&X714Mask) == X7DOT1DOT4 || !chanmask)); + case DevFmtAmbi3D: + break; + } + } + if(!chansok) { - WARN("Failed to check format support: 0x%08lx\n", hr); - hr = mClient->GetMixFormat(&wfx); + if(chancount >= 12 && (chanmask&X714Mask) == X7DOT1DOT4) + mDevice->FmtChans = DevFmtX714; + else if(chancount >= 8 && (chanmask&X71Mask) == X7DOT1) + mDevice->FmtChans = DevFmtX71; + else if(chancount >= 7 && (chanmask&X61Mask) == X6DOT1) + mDevice->FmtChans = DevFmtX61; + else if(chancount >= 6 && ((chanmask&X51Mask) == X5DOT1 + || (chanmask&X51RearMask) == X5DOT1REAR)) + mDevice->FmtChans = DevFmtX51; + else if(chancount >= 4 && (chanmask&QuadMask) == QUAD) + mDevice->FmtChans = DevFmtQuad; + else if(chancount >= 2 && ((chanmask&StereoMask) == STEREO || !chanmask)) + mDevice->FmtChans = DevFmtStereo; + else if(chancount >= 1 && ((chanmask&MonoMask) == MONO || !chanmask)) + mDevice->FmtChans = DevFmtMono; + else + { + ERR("Unhandled extensible channels: %d -- 0x%08lx\n", OutputType.Format.nChannels, + OutputType.dwChannelMask); + mDevice->FmtChans = DevFmtStereo; + OutputType.Format.nChannels = 2; + OutputType.dwChannelMask = STEREO; + } } - if(FAILED(hr)) + + if(IsEqualGUID(OutputType.SubFormat, KSDATAFORMAT_SUBTYPE_PCM)) { - ERR("Failed to find a supported format: 0x%08lx\n", hr); - return hr; + if(OutputType.Format.wBitsPerSample == 8) + mDevice->FmtType = DevFmtUByte; + else if(OutputType.Format.wBitsPerSample == 16) + mDevice->FmtType = DevFmtShort; + else if(OutputType.Format.wBitsPerSample == 32) + mDevice->FmtType = DevFmtInt; + else + { + mDevice->FmtType = DevFmtShort; + OutputType.Format.wBitsPerSample = 16; + } + } + else if(IsEqualGUID(OutputType.SubFormat, KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)) + { + mDevice->FmtType = DevFmtFloat; + OutputType.Format.wBitsPerSample = 32; + } + else + { + ERR("Unhandled format sub-type: %s\n", GuidPrinter{OutputType.SubFormat}.c_str()); + mDevice->FmtType = DevFmtShort; + if(OutputType.Format.wFormatTag != WAVE_FORMAT_EXTENSIBLE) + OutputType.Format.wFormatTag = WAVE_FORMAT_PCM; + OutputType.Format.wBitsPerSample = 16; + OutputType.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; } + OutputType.Samples.wValidBitsPerSample = OutputType.Format.wBitsPerSample; +} - if(wfx != nullptr) + +bool WasapiPlayback::reset() +{ + HRESULT hr{pushMessage(MsgType::ResetDevice).get()}; + if(FAILED(hr)) + throw al::backend_exception{al::backend_error::DeviceError, "0x%08lx", hr}; + return true; +} + +HRESULT WasapiPlayback::resetProxy() +{ + if(GetConfigValueBool(mDevice->DeviceName.c_str(), "wasapi", "spatial-api", false)) { - TraceFormat("Got playback format", wfx); - if(!MakeExtensible(&OutputType, wfx)) + auto &audio = mAudio.emplace<SpatialDevice>(); + HRESULT hr{sDeviceHelper->activateAudioClient(mMMDev, __uuidof(ISpatialAudioClient), + al::out_ptr(audio.mClient))}; + if(FAILED(hr)) { - CoTaskMemFree(wfx); - return E_FAIL; + ERR("Failed to activate spatial audio client: 0x%08lx\n", hr); + goto no_spatial; } - CoTaskMemFree(wfx); - wfx = nullptr; - if(!GetConfigValueBool(mDevice->DeviceName.c_str(), "wasapi", "allow-resampler", true)) - mDevice->Frequency = OutputType.Format.nSamplesPerSec; - else - mDevice->Frequency = minu(mDevice->Frequency, OutputType.Format.nSamplesPerSec); + ComPtr<IAudioFormatEnumerator> fmtenum; + hr = audio.mClient->GetSupportedAudioObjectFormatEnumerator(al::out_ptr(fmtenum)); + if(FAILED(hr)) + { + ERR("Failed to get format enumerator: 0x%08lx\n", hr); + goto no_spatial; + } - const uint32_t chancount{OutputType.Format.nChannels}; - const DWORD chanmask{OutputType.dwChannelMask}; - /* Don't update the channel format if the requested format fits what's - * supported. - */ - bool chansok{false}; - if(mDevice->Flags.test(ChannelsRequest)) + UINT32 fmtcount{}; + hr = fmtenum->GetCount(&fmtcount); + if(FAILED(hr) || fmtcount == 0) { - /* When requesting a channel configuration, make sure it fits the - * mask's lsb (to ensure no gaps in the output channels). If - * there's no mask, assume the request fits with enough channels. - */ - switch(mDevice->FmtChans) + ERR("Failed to get format count: 0x%08lx\n", hr); + goto no_spatial; + } + + WAVEFORMATEX *preferredFormat{}; + hr = fmtenum->GetFormat(0, &preferredFormat); + if(FAILED(hr)) + { + ERR("Failed to get preferred format: 0x%08lx\n", hr); + goto no_spatial; + } + TraceFormat("Preferred mix format", preferredFormat); + + UINT32 maxFrames{}; + hr = audio.mClient->GetMaxFrameCount(preferredFormat, &maxFrames); + if(FAILED(hr)) + ERR("Failed to get max frames: 0x%08lx\n", hr); + else + TRACE("Max sample frames: %u\n", maxFrames); + for(UINT32 i{1};i < fmtcount;++i) + { + WAVEFORMATEX *otherFormat{}; + hr = fmtenum->GetFormat(i, &otherFormat); + if(FAILED(hr)) + ERR("Failed to format %u: 0x%08lx\n", i+1, hr); + else { - case DevFmtMono: - chansok = (chancount >= 1 && ((chanmask&MonoMask) == MONO || !chanmask)); - break; - case DevFmtStereo: - chansok = (chancount >= 2 && ((chanmask&StereoMask) == STEREO || !chanmask)); - break; - case DevFmtQuad: - chansok = (chancount >= 4 && ((chanmask&QuadMask) == QUAD || !chanmask)); - break; - case DevFmtX51: - chansok = (chancount >= 6 && ((chanmask&X51Mask) == X5DOT1 - || (chanmask&X51RearMask) == X5DOT1REAR || !chanmask)); - break; - case DevFmtX61: - chansok = (chancount >= 7 && ((chanmask&X61Mask) == X6DOT1 || !chanmask)); - break; - case DevFmtX71: - case DevFmtX3D71: - chansok = (chancount >= 8 && ((chanmask&X71Mask) == X7DOT1 || !chanmask)); - break; - case DevFmtX714: - chansok = (chancount >= 12 && ((chanmask&X714Mask) == X7DOT1DOT4 || !chanmask)); - case DevFmtAmbi3D: - break; + TraceFormat("Other mix format", otherFormat); + UINT32 otherMaxFrames{}; + hr = audio.mClient->GetMaxFrameCount(otherFormat, &otherMaxFrames); + if(FAILED(hr)) + ERR("Failed to get max frames: 0x%08lx\n", hr); + else + TRACE("Max sample frames: %u\n", otherMaxFrames); } } - if(!chansok) + + WAVEFORMATEXTENSIBLE OutputType; + if(!MakeExtensible(&OutputType, preferredFormat)) + goto no_spatial; + + /* Force 32-bit float. This is currently required for planar output. */ + if(OutputType.Format.wFormatTag != WAVE_FORMAT_EXTENSIBLE + && OutputType.Format.wFormatTag != WAVE_FORMAT_IEEE_FLOAT) + { + OutputType.Format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT; + OutputType.Format.cbSize = 0; + } + if(OutputType.Format.wBitsPerSample != 32) { + OutputType.Format.nAvgBytesPerSec = OutputType.Format.nAvgBytesPerSec * 32u + / OutputType.Format.wBitsPerSample; + OutputType.Format.nBlockAlign = static_cast<WORD>(OutputType.Format.nBlockAlign * 32 + / OutputType.Format.wBitsPerSample); + OutputType.Format.wBitsPerSample = 32; + } + OutputType.Samples.wValidBitsPerSample = OutputType.Format.wBitsPerSample; + OutputType.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; + + /* Match the output rate if not requesting anything specific. */ + if(!mDevice->Flags.test(FrequencyRequest)) + mDevice->Frequency = OutputType.Format.nSamplesPerSec; + + bool isRear51{false}; + if(!mDevice->Flags.test(ChannelsRequest)) + { + const uint32_t chancount{OutputType.Format.nChannels}; + const DWORD chanmask{OutputType.dwChannelMask}; if(chancount >= 12 && (chanmask&X714Mask) == X7DOT1DOT4) mDevice->FmtChans = DevFmtX714; else if(chancount >= 8 && (chanmask&X71Mask) == X7DOT1) mDevice->FmtChans = DevFmtX71; else if(chancount >= 7 && (chanmask&X61Mask) == X6DOT1) mDevice->FmtChans = DevFmtX61; - else if(chancount >= 6 && ((chanmask&X51Mask) == X5DOT1 - || (chanmask&X51RearMask) == X5DOT1REAR)) + else if(chancount >= 6 && (chanmask&X51Mask) == X5DOT1) mDevice->FmtChans = DevFmtX51; + else if(chancount >= 6 && (chanmask&X51RearMask) == X5DOT1REAR) + { + mDevice->FmtChans = DevFmtX51; + isRear51 = true; + } else if(chancount >= 4 && (chanmask&QuadMask) == QUAD) mDevice->FmtChans = DevFmtQuad; else if(chancount >= 2 && ((chanmask&StereoMask) == STEREO || !chanmask)) mDevice->FmtChans = DevFmtStereo; - else if(chancount >= 1 && ((chanmask&MonoMask) == MONO || !chanmask)) - mDevice->FmtChans = DevFmtMono; - else + /* HACK: Don't autoselect mono. Wine returns this and makes the + * audio terrible. + */ + else if(!(chancount >= 1 && ((chanmask&MonoMask) == MONO || !chanmask))) + ERR("Unhandled channel config: %d -- 0x%08lx\n", chancount, chanmask); + } + else + { + const uint32_t chancount{OutputType.Format.nChannels}; + const DWORD chanmask{OutputType.dwChannelMask}; + isRear51 = (chancount == 6 && (chanmask&X51RearMask) == X5DOT1REAR); + } + + auto getTypeMask = [isRear51](DevFmtChannels chans) noexcept + { + switch(chans) { - ERR("Unhandled extensible channels: %d -- 0x%08lx\n", OutputType.Format.nChannels, - OutputType.dwChannelMask); - mDevice->FmtChans = DevFmtStereo; - OutputType.Format.nChannels = 2; - OutputType.dwChannelMask = STEREO; + case DevFmtMono: return ChannelMask_Mono; + case DevFmtStereo: return ChannelMask_Stereo; + case DevFmtQuad: return ChannelMask_Quad; + case DevFmtX51: return isRear51 ? ChannelMask_X51Rear : ChannelMask_X51; + case DevFmtX61: return ChannelMask_X61; + case DevFmtX3D71: + case DevFmtX71: return ChannelMask_X71; + case DevFmtX714: return ChannelMask_X714; + case DevFmtAmbi3D: + break; } + return ChannelMask_Stereo; + }; + + SpatialAudioObjectRenderStreamActivationParams streamParams{}; + streamParams.ObjectFormat = &OutputType.Format; + streamParams.StaticObjectTypeMask = getTypeMask(mDevice->FmtChans); + streamParams.Category = AudioCategory_Media; + streamParams.EventHandle = mNotifyEvent; + + PropVariant paramProp{}; + paramProp->vt = VT_BLOB; + paramProp->blob.cbSize = sizeof(streamParams); + paramProp->blob.pBlobData = reinterpret_cast<BYTE*>(&streamParams); + + hr = audio.mClient->ActivateSpatialAudioStream(paramProp.get(), + __uuidof(ISpatialAudioObjectRenderStream), al::out_ptr(audio.mRender)); + if(FAILED(hr)) + { + ERR("Failed to activate spatial audio stream: 0x%08lx\n", hr); + goto no_spatial; } - if(IsEqualGUID(OutputType.SubFormat, KSDATAFORMAT_SUBTYPE_PCM)) + audio.mStaticMask = streamParams.StaticObjectTypeMask; + mFormat = OutputType; + + mDevice->FmtType = DevFmtFloat; + mDevice->Flags.reset(DirectEar).set(Virtualization); + if(streamParams.StaticObjectTypeMask == ChannelMask_Stereo) + mDevice->FmtChans = DevFmtStereo; + if(!GetConfigValueBool(mDevice->DeviceName.c_str(), "wasapi", "allow-resampler", true)) + mDevice->Frequency = OutputType.Format.nSamplesPerSec; + else + mDevice->Frequency = minu(mDevice->Frequency, OutputType.Format.nSamplesPerSec); + + setDefaultWFXChannelOrder(); + + /* FIXME: Get the real update and buffer size. Presumably the actual + * device is configured once ActivateSpatialAudioStream succeeds, and + * an IAudioClient from the same IMMDevice accesses the same device + * configuration. This isn't obviously correct, but for now assume + * IAudioClient::GetDevicePeriod returns the current device period time + * that ISpatialAudioObjectRenderStream will try to wake up at. + * + * Unfortunately this won't get the buffer size of the + * ISpatialAudioObjectRenderStream, so we only assume there's two + * periods. + */ + mOrigUpdateSize = mDevice->UpdateSize; + mOrigBufferSize = mOrigUpdateSize*2; + ReferenceTime per_time{ReferenceTime{seconds{mDevice->UpdateSize}} / mDevice->Frequency}; + + ComPtr<IAudioClient> tmpClient; + hr = sDeviceHelper->activateAudioClient(mMMDev, __uuidof(IAudioClient), + al::out_ptr(tmpClient)); + if(FAILED(hr)) + ERR("Failed to activate audio client: 0x%08lx\n", hr); + else { - if(OutputType.Format.wBitsPerSample == 8) - mDevice->FmtType = DevFmtUByte; - else if(OutputType.Format.wBitsPerSample == 16) - mDevice->FmtType = DevFmtShort; - else if(OutputType.Format.wBitsPerSample == 32) - mDevice->FmtType = DevFmtInt; + hr = tmpClient->GetDevicePeriod(&reinterpret_cast<REFERENCE_TIME&>(per_time), nullptr); + if(FAILED(hr)) + ERR("Failed to get device period: 0x%08lx\n", hr); else { - mDevice->FmtType = DevFmtShort; - OutputType.Format.wBitsPerSample = 16; + mOrigUpdateSize = RefTime2Samples(per_time, mFormat.Format.nSamplesPerSec); + mOrigBufferSize = mOrigUpdateSize*2; } } - else if(IsEqualGUID(OutputType.SubFormat, KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)) + tmpClient = nullptr; + + mDevice->UpdateSize = RefTime2Samples(per_time, mDevice->Frequency); + mDevice->BufferSize = mDevice->UpdateSize*2; + + mResampler = nullptr; + mResampleBuffer = nullptr; + mBufferFilled = 0; + if(mDevice->Frequency != mFormat.Format.nSamplesPerSec) { - mDevice->FmtType = DevFmtFloat; - OutputType.Format.wBitsPerSample = 32; + const auto flags = as_unsigned(streamParams.StaticObjectTypeMask); + const auto channelCount = as_unsigned(al::popcount(flags)); + mResampler = SampleConverter::Create(mDevice->FmtType, mDevice->FmtType, + channelCount, mDevice->Frequency, mFormat.Format.nSamplesPerSec, + Resampler::FastBSinc24); + mResampleBuffer = std::make_unique<char[]>(size_t{mDevice->UpdateSize} * channelCount * + mFormat.Format.wBitsPerSample / 8); + + TRACE("Created converter for %s/%s format, dst: %luhz (%u), src: %uhz (%u)\n", + DevFmtChannelsString(mDevice->FmtChans), DevFmtTypeString(mDevice->FmtType), + mFormat.Format.nSamplesPerSec, mOrigUpdateSize, mDevice->Frequency, + mDevice->UpdateSize); } - else + + return S_OK; + } + +no_spatial: + mDevice->Flags.reset(Virtualization); + + auto &audio = mAudio.emplace<PlainDevice>(); + HRESULT hr{sDeviceHelper->activateAudioClient(mMMDev, __uuidof(IAudioClient), + al::out_ptr(audio.mClient))}; + if(FAILED(hr)) + { + ERR("Failed to reactivate audio client: 0x%08lx\n", hr); + return hr; + } + + WAVEFORMATEX *wfx; + hr = audio.mClient->GetMixFormat(&wfx); + if(FAILED(hr)) + { + ERR("Failed to get mix format: 0x%08lx\n", hr); + return hr; + } + TraceFormat("Device mix format", wfx); + + WAVEFORMATEXTENSIBLE OutputType; + if(!MakeExtensible(&OutputType, wfx)) + { + CoTaskMemFree(wfx); + return E_FAIL; + } + CoTaskMemFree(wfx); + wfx = nullptr; + + const ReferenceTime per_time{ReferenceTime{seconds{mDevice->UpdateSize}} / mDevice->Frequency}; + const ReferenceTime buf_time{ReferenceTime{seconds{mDevice->BufferSize}} / mDevice->Frequency}; + + prepareFormat(OutputType); + + TraceFormat("Requesting playback format", &OutputType.Format); + hr = audio.mClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &OutputType.Format, &wfx); + if(FAILED(hr)) + { + WARN("Failed to check format support: 0x%08lx\n", hr); + hr = audio.mClient->GetMixFormat(&wfx); + } + if(FAILED(hr)) + { + ERR("Failed to find a supported format: 0x%08lx\n", hr); + return hr; + } + + if(wfx != nullptr) + { + TraceFormat("Got playback format", wfx); + if(!MakeExtensible(&OutputType, wfx)) { - ERR("Unhandled format sub-type: %s\n", GuidPrinter{OutputType.SubFormat}.c_str()); - mDevice->FmtType = DevFmtShort; - if(OutputType.Format.wFormatTag != WAVE_FORMAT_EXTENSIBLE) - OutputType.Format.wFormatTag = WAVE_FORMAT_PCM; - OutputType.Format.wBitsPerSample = 16; - OutputType.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; + CoTaskMemFree(wfx); + return E_FAIL; } - OutputType.Samples.wValidBitsPerSample = OutputType.Format.wBitsPerSample; + CoTaskMemFree(wfx); + wfx = nullptr; + + finalizeFormat(OutputType); } mFormat = OutputType; - const EndpointFormFactor formfactor{get_device_formfactor(mMMDev.get())}; +#if !defined(ALSOFT_UWP) + const EndpointFormFactor formfactor{GetDeviceFormfactor(mMMDev.get())}; mDevice->Flags.set(DirectEar, (formfactor == Headphones || formfactor == Headset)); - +#else + mDevice->Flags.set(DirectEar, false); +#endif setDefaultWFXChannelOrder(); - hr = mClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + hr = audio.mClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, buf_time.count(), 0, &OutputType.Format, nullptr); if(FAILED(hr)) { @@ -1174,15 +1921,22 @@ HRESULT WasapiPlayback::resetProxy() UINT32 buffer_len{}; ReferenceTime min_per{}; - hr = mClient->GetDevicePeriod(&reinterpret_cast<REFERENCE_TIME&>(min_per), nullptr); + hr = audio.mClient->GetDevicePeriod(&reinterpret_cast<REFERENCE_TIME&>(min_per), nullptr); if(SUCCEEDED(hr)) - hr = mClient->GetBufferSize(&buffer_len); + hr = audio.mClient->GetBufferSize(&buffer_len); if(FAILED(hr)) { ERR("Failed to get audio buffer info: 0x%08lx\n", hr); return hr; } + hr = audio.mClient->SetEventHandle(mNotifyEvent); + if(FAILED(hr)) + { + ERR("Failed to set event handle: 0x%08lx\n", hr); + return hr; + } + /* Find the nearest multiple of the period size to the update size */ if(min_per < per_time) min_per *= maxi64((per_time + min_per/2) / min_per, 1); @@ -1212,13 +1966,6 @@ HRESULT WasapiPlayback::resetProxy() mDevice->UpdateSize); } - hr = mClient->SetEventHandle(mNotifyEvent); - if(FAILED(hr)) - { - ERR("Failed to set event handle: 0x%08lx\n", hr); - return hr; - } - return hr; } @@ -1235,33 +1982,61 @@ HRESULT WasapiPlayback::startProxy() { ResetEvent(mNotifyEvent); - HRESULT hr{mClient->Start()}; - if(FAILED(hr)) + auto mstate_fallback = [](std::monostate) -> HRESULT + { return E_FAIL; }; + auto start_plain = [&](PlainDevice &audio) -> HRESULT { - ERR("Failed to start audio client: 0x%08lx\n", hr); - return hr; - } + HRESULT hr{audio.mClient->Start()}; + if(FAILED(hr)) + { + ERR("Failed to start audio client: 0x%08lx\n", hr); + return hr; + } - void *ptr; - hr = mClient->GetService(IID_IAudioRenderClient, &ptr); - if(SUCCEEDED(hr)) + hr = audio.mClient->GetService(__uuidof(IAudioRenderClient), al::out_ptr(audio.mRender)); + if(SUCCEEDED(hr)) + { + try { + mKillNow.store(false, std::memory_order_release); + mThread = std::thread{std::mem_fn(&WasapiPlayback::mixerProc), this}; + } + catch(...) { + audio.mRender = nullptr; + ERR("Failed to start thread\n"); + hr = E_FAIL; + } + } + if(FAILED(hr)) + audio.mClient->Stop(); + return hr; + }; + auto start_spatial = [&](SpatialDevice &audio) -> HRESULT { - mRender = ComPtr<IAudioRenderClient>{static_cast<IAudioRenderClient*>(ptr)}; + HRESULT hr{audio.mRender->Start()}; + if(FAILED(hr)) + { + ERR("Failed to start spatial audio stream: 0x%08lx\n", hr); + return hr; + } + try { mKillNow.store(false, std::memory_order_release); - mThread = std::thread{std::mem_fn(&WasapiPlayback::mixerProc), this}; + mThread = std::thread{std::mem_fn(&WasapiPlayback::mixerSpatialProc), this}; } catch(...) { - mRender = nullptr; ERR("Failed to start thread\n"); hr = E_FAIL; } - } - if(FAILED(hr)) - mClient->Stop(); + if(FAILED(hr)) + { + audio.mRender->Stop(); + audio.mRender->Reset(); + } + return hr; + }; - return hr; + return std::visit(overloaded{mstate_fallback, start_plain, start_spatial}, mAudio); } @@ -1270,14 +2045,25 @@ void WasapiPlayback::stop() void WasapiPlayback::stopProxy() { - if(!mRender || !mThread.joinable()) + if(!mThread.joinable()) return; mKillNow.store(true, std::memory_order_release); mThread.join(); - mRender = nullptr; - mClient->Stop(); + auto mstate_fallback = [](std::monostate) -> void + { }; + auto stop_plain = [](PlainDevice &audio) -> void + { + audio.mRender = nullptr; + audio.mClient->Stop(); + }; + auto stop_spatial = [](SpatialDevice &audio) -> void + { + audio.mRender->Stop(); + audio.mRender->Reset(); + }; + std::visit(overloaded{mstate_fallback, stop_plain, stop_spatial}, mAudio); } @@ -1306,8 +2092,8 @@ struct WasapiCapture final : public BackendBase, WasapiProxy { int recordProc(); - void open(const char *name) override; - HRESULT openProxy(const char *name) override; + void open(std::string_view name) override; + HRESULT openProxy(std::string_view name) override; void closeProxy() override; HRESULT resetProxy() override; @@ -1316,11 +2102,11 @@ struct WasapiCapture final : public BackendBase, WasapiProxy { void stop() override; void stopProxy() override; - void captureSamples(al::byte *buffer, uint samples) override; + void captureSamples(std::byte *buffer, uint samples) override; uint availableSamples() override; HRESULT mOpenStatus{E_FAIL}; - ComPtr<IMMDevice> mMMDev{nullptr}; + DeviceHandle mMMDev{nullptr}; ComPtr<IAudioClient> mClient{nullptr}; ComPtr<IAudioCaptureClient> mCapture{nullptr}; HANDLE mNotifyEvent{nullptr}; @@ -1338,10 +2124,7 @@ struct WasapiCapture final : public BackendBase, WasapiProxy { WasapiCapture::~WasapiCapture() { if(SUCCEEDED(mOpenStatus)) - { pushMessage(MsgType::CloseDevice).wait(); - DeinitThread(); - } mOpenStatus = E_FAIL; if(mNotifyEvent != nullptr) @@ -1352,21 +2135,21 @@ WasapiCapture::~WasapiCapture() FORCE_ALIGN int WasapiCapture::recordProc() { - HRESULT hr{CoInitializeEx(nullptr, COINIT_MULTITHREADED)}; - if(FAILED(hr)) + ComWrapper com{COINIT_MULTITHREADED}; + if(!com) { - ERR("CoInitializeEx(nullptr, COINIT_MULTITHREADED) failed: 0x%08lx\n", hr); - mDevice->handleDisconnect("COM init failed: 0x%08lx", hr); + ERR("CoInitializeEx(nullptr, COINIT_MULTITHREADED) failed: 0x%08lx\n", com.status()); + mDevice->handleDisconnect("COM init failed: 0x%08lx", com.status()); return 1; } althrd_setname(RECORD_THREAD_NAME); - al::vector<float> samples; + std::vector<float> samples; while(!mKillNow.load(std::memory_order_relaxed)) { UINT32 avail; - hr = mCapture->GetNextPacketSize(&avail); + HRESULT hr{mCapture->GetNextPacketSize(&avail)}; if(FAILED(hr)) ERR("Failed to get next packet size: 0x%08lx\n", hr); else if(avail > 0) @@ -1437,12 +2220,11 @@ FORCE_ALIGN int WasapiCapture::recordProc() ERR("WaitForSingleObjectEx error: 0x%lx\n", res); } - CoUninitialize(); return 0; } -void WasapiCapture::open(const char *name) +void WasapiCapture::open(std::string_view name) { if(SUCCEEDED(mOpenStatus)) throw al::backend_exception{al::backend_error::DeviceError, @@ -1456,34 +2238,18 @@ void WasapiCapture::open(const char *name) "Failed to create notify events"}; } - HRESULT hr{InitThread()}; - if(FAILED(hr)) - { - throw al::backend_exception{al::backend_error::DeviceError, - "Failed to init COM thread: 0x%08lx", hr}; - } - - if(name) + if(name.length() >= DevNameHeadLen + && std::strncmp(name.data(), DevNameHead, DevNameHeadLen) == 0) { - if(CaptureDevices.empty()) - pushMessage(MsgType::EnumerateCapture); - if(std::strncmp(name, DevNameHead, DevNameHeadLen) == 0) - { - name += DevNameHeadLen; - if(*name == '\0') - name = nullptr; - } + name = name.substr(DevNameHeadLen); } mOpenStatus = pushMessage(MsgType::OpenDevice, name).get(); if(FAILED(mOpenStatus)) - { - DeinitThread(); throw al::backend_exception{al::backend_error::DeviceError, "Device init failed: 0x%08lx", mOpenStatus}; - } - hr = pushMessage(MsgType::ResetDevice).get(); + HRESULT hr{pushMessage(MsgType::ResetDevice).get()}; if(FAILED(hr)) { if(hr == E_OUTOFMEMORY) @@ -1492,52 +2258,47 @@ void WasapiCapture::open(const char *name) } } -HRESULT WasapiCapture::openProxy(const char *name) +HRESULT WasapiCapture::openProxy(std::string_view name) { - const wchar_t *devid{nullptr}; - if(name) + std::string devname; + std::wstring devid; + if(!name.empty()) { - auto iter = std::find_if(CaptureDevices.cbegin(), CaptureDevices.cend(), + auto devlock = DeviceListLock{gDeviceList}; + auto devlist = al::span{devlock.getCaptureList()}; + auto iter = std::find_if(devlist.cbegin(), devlist.cend(), [name](const DevMap &entry) -> bool { return entry.name == name || entry.endpoint_guid == name; }); - if(iter == CaptureDevices.cend()) + if(iter == devlist.cend()) { const std::wstring wname{utf8_to_wstr(name)}; - iter = std::find_if(CaptureDevices.cbegin(), CaptureDevices.cend(), + iter = std::find_if(devlist.cbegin(), devlist.cend(), [&wname](const DevMap &entry) -> bool { return entry.devid == wname; }); } - if(iter == CaptureDevices.cend()) + if(iter == devlist.cend()) { - WARN("Failed to find device name matching \"%s\"\n", name); + WARN("Failed to find device name matching \"%.*s\"\n", static_cast<int>(name.length()), + name.data()); return E_FAIL; } - name = iter->name.c_str(); - devid = iter->devid.c_str(); + devname = iter->name; + devid = iter->devid; } - void *ptr; - HRESULT hr{CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_INPROC_SERVER, - IID_IMMDeviceEnumerator, &ptr)}; - if(SUCCEEDED(hr)) - { - ComPtr<IMMDeviceEnumerator> enumerator{static_cast<IMMDeviceEnumerator*>(ptr)}; - if(!devid) - hr = enumerator->GetDefaultAudioEndpoint(eCapture, eMultimedia, mMMDev.getPtr()); - else - hr = enumerator->GetDevice(devid, mMMDev.getPtr()); - } + HRESULT hr{sDeviceHelper->openDevice(devid, eCapture, mMMDev)}; if(FAILED(hr)) { - WARN("Failed to open device \"%s\"\n", name?name:"(default)"); + WARN("Failed to open device \"%s\"\n", devname.empty() ? "(default)" : devname.c_str()); return hr; } - mClient = nullptr; - if(name) mDevice->DeviceName = std::string{DevNameHead} + name; - else mDevice->DeviceName = DevNameHead + get_device_name_and_guid(mMMDev.get()).first; + if(!devname.empty()) + mDevice->DeviceName = DevNameHead + std::move(devname); + else + mDevice->DeviceName = DevNameHead + GetDeviceNameAndGuid(mMMDev).first; - return hr; + return S_OK; } void WasapiCapture::closeProxy() @@ -1550,14 +2311,13 @@ HRESULT WasapiCapture::resetProxy() { mClient = nullptr; - void *ptr; - HRESULT hr{mMMDev->Activate(IID_IAudioClient, CLSCTX_INPROC_SERVER, nullptr, &ptr)}; + HRESULT hr{sDeviceHelper->activateAudioClient(mMMDev, __uuidof(IAudioClient), + al::out_ptr(mClient))}; if(FAILED(hr)) { ERR("Failed to reactivate audio client: 0x%08lx\n", hr); return hr; } - mClient = ComPtr<IAudioClient>{static_cast<IAudioClient*>(ptr)}; WAVEFORMATEX *wfx; hr = mClient->GetMixFormat(&wfx); @@ -1849,11 +2609,9 @@ HRESULT WasapiCapture::startProxy() return hr; } - void *ptr; - hr = mClient->GetService(IID_IAudioCaptureClient, &ptr); + hr = mClient->GetService(__uuidof(IAudioCaptureClient), al::out_ptr(mCapture)); if(SUCCEEDED(hr)) { - mCapture = ComPtr<IAudioCaptureClient>{static_cast<IAudioCaptureClient*>(ptr)}; try { mKillNow.store(false, std::memory_order_release); mThread = std::thread{std::mem_fn(&WasapiCapture::recordProc), this}; @@ -1892,7 +2650,7 @@ void WasapiCapture::stopProxy() } -void WasapiCapture::captureSamples(al::byte *buffer, uint samples) +void WasapiCapture::captureSamples(std::byte *buffer, uint samples) { mRing->read(buffer, samples); } uint WasapiCapture::availableSamples() @@ -1904,34 +2662,13 @@ uint WasapiCapture::availableSamples() bool WasapiBackendFactory::init() { static HRESULT InitResult{E_FAIL}; - if(FAILED(InitResult)) try { - auto res = std::async(std::launch::async, []() -> HRESULT - { - HRESULT hr{CoInitializeEx(nullptr, COINIT_MULTITHREADED)}; - if(FAILED(hr)) - { - WARN("Failed to initialize COM: 0x%08lx\n", hr); - return hr; - } - - void *ptr{}; - hr = CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_INPROC_SERVER, - IID_IMMDeviceEnumerator, &ptr); - if(FAILED(hr)) - { - WARN("Failed to create IMMDeviceEnumerator instance: 0x%08lx\n", hr); - CoUninitialize(); - return hr; - } - static_cast<IMMDeviceEnumerator*>(ptr)->Release(); - CoUninitialize(); - - return S_OK; - }); + std::promise<HRESULT> promise; + auto future = promise.get_future(); - InitResult = res.get(); + std::thread{&WasapiProxy::messageHandler, &promise}.detach(); + InitResult = future.get(); } catch(...) { } @@ -1944,34 +2681,45 @@ bool WasapiBackendFactory::querySupport(BackendType type) std::string WasapiBackendFactory::probe(BackendType type) { - struct ProxyControl { - HRESULT mResult{}; - ProxyControl() { mResult = WasapiProxy::InitThread(); } - ~ProxyControl() { if(SUCCEEDED(mResult)) WasapiProxy::DeinitThread(); } - }; - ProxyControl proxy; - std::string outnames; - if(FAILED(proxy.mResult)) - return outnames; + auto devlock = DeviceListLock{gDeviceList}; switch(type) { case BackendType::Playback: - WasapiProxy::pushMessageStatic(MsgType::EnumeratePlayback).wait(); - for(const DevMap &entry : PlaybackDevices) { - /* +1 to also append the null char (to ensure a null-separated list - * and double-null terminated list). - */ - outnames.append(DevNameHead).append(entry.name.c_str(), entry.name.length()+1); + auto defaultId = devlock.getPlaybackDefaultId(); + for(const DevMap &entry : devlock.getPlaybackList()) + { + if(entry.devid != defaultId) + { + /* +1 to also append the null char (to ensure a null- + * separated list and double-null terminated list). + */ + outnames.append(DevNameHead).append(entry.name.c_str(), entry.name.length()+1); + continue; + } + /* Default device goes first. */ + std::string name{DevNameHead + entry.name}; + outnames.insert(0, name.c_str(), name.length()+1); + } } break; case BackendType::Capture: - WasapiProxy::pushMessageStatic(MsgType::EnumerateCapture).wait(); - for(const DevMap &entry : CaptureDevices) - outnames.append(DevNameHead).append(entry.name.c_str(), entry.name.length()+1); + { + auto defaultId = devlock.getCaptureDefaultId(); + for(const DevMap &entry : devlock.getCaptureList()) + { + if(entry.devid != defaultId) + { + outnames.append(DevNameHead).append(entry.name.c_str(), entry.name.length()+1); + continue; + } + std::string name{DevNameHead + entry.name}; + outnames.insert(0, name.c_str(), name.length()+1); + } + } break; } @@ -1992,3 +2740,22 @@ BackendFactory &WasapiBackendFactory::getFactory() static WasapiBackendFactory factory{}; return factory; } + +alc::EventSupport WasapiBackendFactory::queryEventSupport(alc::EventType eventType, BackendType) +{ + switch(eventType) + { + case alc::EventType::DefaultDeviceChanged: + return alc::EventSupport::FullSupport; + + case alc::EventType::DeviceAdded: + case alc::EventType::DeviceRemoved: +#if !defined(ALSOFT_UWP) + return alc::EventSupport::FullSupport; +#endif + + case alc::EventType::Count: + break; + } + return alc::EventSupport::NoSupport; +} |