diff --git a/src/backends/WASAPI/CMakeLists.txt b/src/backends/WASAPI/CMakeLists.txt index ff0ece7..0aee874 100644 --- a/src/backends/WASAPI/CMakeLists.txt +++ b/src/backends/WASAPI/CMakeLists.txt @@ -22,6 +22,10 @@ target_include_directories(be_wasapi target_sources(be_wasapi PRIVATE + "Device.cpp" + "Device.hpp" + "EventManager.cpp" + "EventManager.hpp" "WASAPI.cpp" "WASAPI.hpp" ) diff --git a/src/backends/WASAPI/Device.cpp b/src/backends/WASAPI/Device.cpp new file mode 100644 index 0000000..acec9a8 --- /dev/null +++ b/src/backends/WASAPI/Device.cpp @@ -0,0 +1,118 @@ +// Copyright 2024 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#include "Device.hpp" + +#include "Node.h" + +#include +#include + +#include + +#include +#include +#include + +using Direction = CrossAudio_Direction; + +namespace wasapi { +static Direction getDirection(IMMDevice &device) { + IMMEndpoint *endpoint; + if (device.QueryInterface(__uuidof(IMMEndpoint), reinterpret_cast< void ** >(&endpoint)) != S_OK) { + return CROSSAUDIO_DIR_NONE; + } + + Direction direction = CROSSAUDIO_DIR_NONE; + + EDataFlow dataflow; + if (endpoint->GetDataFlow(&dataflow) == S_OK) { + switch (dataflow) { + case eRender: + direction = CROSSAUDIO_DIR_OUT; + break; + case eCapture: + direction = CROSSAUDIO_DIR_IN; + break; + case eAll: + direction = CROSSAUDIO_DIR_BOTH; + break; + } + } + + endpoint->Release(); + + return direction; +} + +static char *getID(IMMDevice &device) { + wchar_t *deviceID; + if (device.GetId(&deviceID) != S_OK) { + return nullptr; + } + + char *id = utf16To8(deviceID); + + CoTaskMemFree(deviceID); + + return id; +} + +static char *getName(IMMDevice &device) { + IPropertyStore *store; + if (device.OpenPropertyStore(STGM_READ, &store) != S_OK) { + return nullptr; + } + + char *name = nullptr; + + PROPVARIANT varName{}; + if (store->GetValue(PKEY_Device_FriendlyName, &varName) == S_OK) { + name = utf16To8(varName.pwszVal); + PropVariantClear(&varName); + } + + store->Release(); + + return name; +} + +bool populateNode(Node &node, IMMDevice &device, const wchar_t *id) { + if (!(node.id = id ? utf16To8(id) : getID(device))) { + return false; + } + + node.name = getName(device); + node.direction = getDirection(device); + + return true; +} + +char *utf16To8(const wchar_t *utf16) { + const auto utf16Size = static_cast< int >(wcslen(utf16) + 1); + + const auto utf8Size = WideCharToMultiByte(CP_UTF8, 0, utf16, utf16Size, nullptr, 0, nullptr, nullptr); + auto utf8 = static_cast< char *>(malloc(utf8Size)); + if (WideCharToMultiByte(CP_UTF8, 0, utf16, utf16Size, utf8, utf8Size, nullptr, nullptr) <= 0) { + free(utf8); + return nullptr; + } + + return utf8; +} + +wchar_t *utf8To16(const char *utf8) { + const auto utf8Size = static_cast< int >(strlen(utf8) + 1); + + const int utf16Size = sizeof(wchar_t) * MultiByteToWideChar(CP_UTF8, 0, utf8, utf8Size, nullptr, 0); + auto utf16 = static_cast< wchar_t *>(malloc(utf16Size)); + if (MultiByteToWideChar(CP_UTF8, 0, utf8, utf8Size, utf16, utf16Size / sizeof(wchar_t)) <= 0) { + free(utf16); + return nullptr; + } + + return utf16; +} +} // namespace wasapi diff --git a/src/backends/WASAPI/Device.hpp b/src/backends/WASAPI/Device.hpp new file mode 100644 index 0000000..8b5576c --- /dev/null +++ b/src/backends/WASAPI/Device.hpp @@ -0,0 +1,20 @@ +// Copyright 2024 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#ifndef CROSSAUDIO_SRC_BACKENDS_WASAPI_DEVICE_HPP +#define CROSSAUDIO_SRC_BACKENDS_WASAPI_DEVICE_HPP + +using Node = struct CrossAudio_Node; + +struct IMMDevice; + +namespace wasapi { +bool populateNode(Node &node, IMMDevice &device, const wchar_t *id); + +char *utf16To8(const wchar_t *utf16); +wchar_t *utf8To16(const char *utf8); +} // namespace wasapi + +#endif diff --git a/src/backends/WASAPI/EventManager.cpp b/src/backends/WASAPI/EventManager.cpp new file mode 100644 index 0000000..58ade40 --- /dev/null +++ b/src/backends/WASAPI/EventManager.cpp @@ -0,0 +1,111 @@ +// Copyright 2024 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#include "EventManager.hpp" + +#include "Device.hpp" + +#include "Node.h" + +#include + +using namespace wasapi; + +EventManager::EventManager(IMMDeviceEnumerator *enumerator, const Feedback &feedback) + : m_feedback(feedback), m_ref(0), m_enumerator(enumerator) { + m_enumerator->AddRef(); + m_enumerator->RegisterEndpointNotificationCallback(this); +} + +EventManager::~EventManager() { + m_enumerator->UnregisterEndpointNotificationCallback(this); + m_enumerator->Release(); +} + +ULONG STDMETHODCALLTYPE EventManager::AddRef() { + return InterlockedIncrement(&m_ref); +} + +ULONG STDMETHODCALLTYPE EventManager::Release() { + return InterlockedDecrement(&m_ref); +} + +HRESULT STDMETHODCALLTYPE EventManager::QueryInterface(const IID &iid, void **iface) { + if (iid == IID_IUnknown) { + *iface = static_cast< IUnknown * >(this); + AddRef(); + } else if (iid == __uuidof(IMMNotificationClient)) { + *iface = static_cast< IMMNotificationClient * >(this); + AddRef(); + } else { + *iface = nullptr; + return E_NOINTERFACE; + } + + return S_OK; +} + +HRESULT STDMETHODCALLTYPE EventManager::OnDeviceAdded(const wchar_t *id) { + wprintf(L"OnDeviceAdded: %s\n", id); + + IMMDevice *device; + if (m_enumerator->GetDevice(id, &device) != S_OK) { + return S_OK; + } + + Node *node = nodeNew(); + if (populateNode(*node, *device, id)) { + m_feedback.nodeAdded(node); + } + + device->Release(); + + return S_OK; +} + +HRESULT STDMETHODCALLTYPE EventManager::OnDeviceRemoved(const wchar_t *id) { + wprintf(L"OnDeviceRemoved: %s\n", id); + + IMMDevice *device; + if (m_enumerator->GetDevice(id, &device) != S_OK) { + return S_OK; + } + + Node *node = nodeNew(); + if (populateNode(*node, *device, id)) { + m_feedback.nodeRemoved(node); + } + + device->Release(); + + return S_OK; +} + +HRESULT STDMETHODCALLTYPE EventManager::OnDeviceStateChanged(const wchar_t *id, DWORD state) { + wprintf(L"OnDeviceStateChanged: %s\n", id); + + switch (state) { + case DEVICE_STATE_ACTIVE: + return OnDeviceAdded(id); + case DEVICE_STATE_DISABLED: + case DEVICE_STATE_NOTPRESENT: + case DEVICE_STATE_UNPLUGGED: + return OnDeviceRemoved(id); + } + + return S_OK; +} + +HRESULT STDMETHODCALLTYPE EventManager::OnDefaultDeviceChanged(EDataFlow, ERole, const wchar_t *id) { + wprintf(L"OnDefaultDeviceChanged: %s\n", id); + + return S_OK; +} + +HRESULT STDMETHODCALLTYPE EventManager::OnPropertyValueChanged(const wchar_t *id, const PROPERTYKEY) { + wprintf(L"OnPropertyValueChanged: %s\n", id); + + return S_OK; +} diff --git a/src/backends/WASAPI/EventManager.hpp b/src/backends/WASAPI/EventManager.hpp new file mode 100644 index 0000000..3665ce5 --- /dev/null +++ b/src/backends/WASAPI/EventManager.hpp @@ -0,0 +1,46 @@ +// Copyright 2024 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#ifndef CROSSAUDIO_SRC_BACKENDS_WASAPI_EVENTMANAGER_HPP +#define CROSSAUDIO_SRC_BACKENDS_WASAPI_EVENTMANAGER_HPP + +#include + +#include + +using Node = struct CrossAudio_Node; + +namespace wasapi { +class EventManager : public IMMNotificationClient { +public: + struct Feedback { + std::function< void(Node *node) > nodeAdded; + std::function< void(Node *node) > nodeRemoved; + }; + + EventManager(IMMDeviceEnumerator *enumerator, const Feedback &feedback); + ~EventManager(); + +private: + EventManager(const EventManager &) = delete; + EventManager &operator=(const EventManager &) = delete; + + ULONG STDMETHODCALLTYPE AddRef() override; + ULONG STDMETHODCALLTYPE Release() override; + + HRESULT STDMETHODCALLTYPE QueryInterface(const IID &iid, void **iface) override; + HRESULT STDMETHODCALLTYPE OnDeviceAdded(const wchar_t *id) override; + HRESULT STDMETHODCALLTYPE OnDeviceRemoved(const wchar_t *id) override; + HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(const wchar_t *id, DWORD state) override; + HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, const wchar_t *id) override; + HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(const wchar_t *id, const PROPERTYKEY key) override; + + Feedback m_feedback; + LONG m_ref; + IMMDeviceEnumerator *m_enumerator; +}; +} // namespace wasapi + +#endif diff --git a/src/backends/WASAPI/WASAPI.cpp b/src/backends/WASAPI/WASAPI.cpp index ea9ddf2..a7c3a73 100755 --- a/src/backends/WASAPI/WASAPI.cpp +++ b/src/backends/WASAPI/WASAPI.cpp @@ -5,6 +5,9 @@ #include "WASAPI.hpp" +#include "Device.hpp" +#include "EventManager.hpp" + #include "Backend.h" #include "Node.h" @@ -18,7 +21,6 @@ #include #include -#include #include using namespace wasapi; @@ -114,8 +116,8 @@ static ErrorCode engineFree(BE_Engine *engine) { return CROSSAUDIO_EC_OK; } -static ErrorCode engineStart(BE_Engine *engine, const EngineFeedback *) { - return toImpl(engine)->start(); +static ErrorCode engineStart(BE_Engine *engine, const EngineFeedback *feedback) { + return toImpl(engine)->start(feedback ? *feedback : EngineFeedback()); } static ErrorCode engineStop(BE_Engine *engine) { @@ -211,11 +213,22 @@ Engine::~Engine() { } } -ErrorCode Engine::start() { +ErrorCode Engine::start(const EngineFeedback &feedback) { + m_feedback = feedback; + + const EventManager::Feedback eventManagerFeedback{ + .nodeAdded = [this](Node *node) { m_feedback.nodeAdded(m_feedback.userData, node); }, + .nodeRemoved = [this](Node *node) { m_feedback.nodeRemoved(m_feedback.userData, node); } + }; + + m_eventManager = std::make_unique< EventManager >(m_enumerator, eventManagerFeedback); + return CROSSAUDIO_EC_OK; } ErrorCode Engine::stop() { + m_eventManager.reset(); + return CROSSAUDIO_EC_OK; } @@ -246,50 +259,8 @@ Nodes *Engine::engineNodesGet() { continue; } - IPropertyStore *store; - if (device->OpenPropertyStore(STGM_READ, &store) != S_OK) { - device->Release(); - continue; - } - - LPWSTR id; - if (device->GetId(&id) == S_OK) { - auto &node = nodes->items[i]; - - IMMEndpoint *endpoint; - if (device->QueryInterface(__uuidof(IMMEndpoint), reinterpret_cast< void ** >(&endpoint)) == S_OK) { - EDataFlow dataflow; - if (endpoint->GetDataFlow(&dataflow) == S_OK) { - switch (dataflow) { - case eRender: - node.direction = CROSSAUDIO_DIR_OUT; - break; - case eCapture: - node.direction = CROSSAUDIO_DIR_IN; - break; - case eAll: - node.direction = CROSSAUDIO_DIR_BOTH; - break; - default: - node.direction = CROSSAUDIO_DIR_NONE; - break; - } - } - - endpoint->Release(); - } - - node.id = utf16To8(id); - CoTaskMemFree(id); - - PROPVARIANT varName{}; - if (store->GetValue(PKEY_Device_FriendlyName, &varName) == S_OK) { - node.name = utf16To8(varName.pwszVal); - PropVariantClear(&varName); - } - } + populateNode(nodes->items[i], *device, nullptr); - store->Release(); device->Release(); } @@ -595,32 +566,6 @@ void Flux::processOutput() { deinit(); } -static char *utf16To8(const wchar_t *utf16) { - const auto utf16Size = static_cast< int >(wcslen(utf16) + 1); - - const auto utf8Size = WideCharToMultiByte(CP_UTF8, 0, utf16, utf16Size, nullptr, 0, nullptr, nullptr); - auto utf8 = static_cast< char *>(malloc(utf8Size)); - if (WideCharToMultiByte(CP_UTF8, 0, utf16, utf16Size, utf8, utf8Size, nullptr, nullptr) <= 0) { - free(utf8); - return nullptr; - } - - return utf8; -} - -static wchar_t *utf8To16(const char *utf8) { - const auto utf8Size = static_cast< int >(strlen(utf8) + 1); - - const int utf16Size = sizeof(wchar_t) * MultiByteToWideChar(CP_UTF8, 0, utf8, utf8Size, nullptr, 0); - auto utf16 = static_cast< wchar_t *>(malloc(utf16Size)); - if (MultiByteToWideChar(CP_UTF8, 0, utf8, utf8Size, utf16, utf16Size / sizeof(wchar_t)) <= 0) { - free(utf16); - return nullptr; - } - - return utf16; -} - static constexpr WAVEFORMATEXTENSIBLE configToWaveFormat(const FluxConfig &config) { WAVEFORMATEXTENSIBLE fmt{}; diff --git a/src/backends/WASAPI/WASAPI.hpp b/src/backends/WASAPI/WASAPI.hpp index 4b14be8..3ad3ece 100644 --- a/src/backends/WASAPI/WASAPI.hpp +++ b/src/backends/WASAPI/WASAPI.hpp @@ -6,6 +6,7 @@ #ifndef CROSSAUDIO_SRC_BACKENDS_WASAPI_WASAPI_HPP #define CROSSAUDIO_SRC_BACKENDS_WASAPI_WASAPI_HPP +#include "crossaudio/Engine.h" #include "crossaudio/ErrorCode.h" #include "crossaudio/Flux.h" #include "crossaudio/Node.h" @@ -14,6 +15,8 @@ #include #include +typedef CrossAudio_EngineFeedback EngineFeedback; + typedef CrossAudio_FluxConfig FluxConfig; typedef CrossAudio_FluxFeedback FluxFeedback; @@ -31,6 +34,8 @@ struct IMMDevice; struct IMMDeviceEnumerator; namespace wasapi { +class EventManager; + class Engine { public: struct SessionID { @@ -50,7 +55,7 @@ class Engine { Nodes *engineNodesGet(); - ErrorCode start(); + ErrorCode start(const EngineFeedback &feedback); ErrorCode stop(); std::string m_name; @@ -60,6 +65,9 @@ class Engine { private: Engine(const Engine &) = delete; Engine &operator=(const Engine &) = delete; + + EngineFeedback m_feedback; + std::unique_ptr< EventManager > m_eventManager; }; class Flux { @@ -101,9 +109,4 @@ extern "C" { extern const BE_Impl WASAPI_Impl; } -// Internal functions - -static char *utf16To8(const wchar_t *utf16); -static wchar_t *utf8To16(const char *utf8); - #endif