Ring Daemon 16.0.0
Loading...
Searching...
No Matches
audio_device_monitor.h
Go to the documentation of this file.
1/*
2 * Copyright (C) 2025 Savoir-faire Linux Inc.
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
18#include "string_utils.h"
19#include "logger.h"
20
21#include <atlbase.h>
22#include <conio.h>
23#include <mmdeviceapi.h>
24#include <mmreg.h>
25#include <string>
26#include <windows.h>
27#include <Functiondiscoverykeys_devpkey.h>
28
29#include <functional>
30#include <mutex>
31#include <unordered_set>
32
33// Note: the granularity of these events is not currently required because we end up
34// restarting the audio layer regardless of the event type. However, let's keep it like
35// this for now in case a future PortAudio bump or layer improvement can make use of it
36// to avoid unnecessary stream restarts.
38inline std::string
40{
41 switch (type) {
43 return "BecameActive";
45 return "BecameInactive";
47 return "DefaultChanged";
49 return "PropertyChanged";
50 }
51 return "Unknown";
52}
53using DeviceEventCallback = std::function<void(const std::string& deviceName, DeviceEventType)>;
54
55std::string
56GetFriendlyNameFromIMMDeviceId(CComPtr<IMMDeviceEnumerator> enumerator, LPCWSTR deviceId)
57{
58 if (!enumerator || !deviceId)
59 return {};
60
61 CComPtr<IMMDevice> device;
62 CComPtr<IPropertyStore> props;
63
64 if (FAILED(enumerator->GetDevice(deviceId, &device)))
65 return {};
66
67 if (FAILED(device->OpenPropertyStore(STGM_READ, &props)))
68 return {};
69
70 PROPVARIANT varName;
71 PropVariantInit(&varName);
72 HRESULT hr = props->GetValue(PKEY_Device_FriendlyName, &varName);
73
74 std::string result;
75 if (SUCCEEDED(hr) && varName.vt == VT_LPWSTR && varName.pwszVal) {
76 result = jami::to_string(varName.pwszVal);
77 }
78 PropVariantClear(&varName);
79 return result;
80}
81
82class AudioDeviceNotificationClient : public IMMNotificationClient
83{
84 LONG _refCount;
85
86public:
88 : _refCount(1)
89 {
90 if (FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator),
91 nullptr,
92 CLSCTX_ALL,
93 __uuidof(IMMDeviceEnumerator),
94 (void**) &deviceEnumerator_))) {
95 JAMI_ERR("Failed to create device enumerator");
96 } else {
97 // Fill our list of active devices (render + capture)
98 enumerateDevices();
99 }
100 }
101
103 {
104 if (deviceEnumerator_) {
105 deviceEnumerator_->UnregisterEndpointNotificationCallback(this);
106 }
107 }
108
110 {
111 if (!deviceEnumerator_) {
112 JAMI_ERR("Device enumerator not initialized");
113 return;
114 }
115
116 {
117 std::lock_guard<std::mutex> lock(mutex_);
118 deviceEventCallback_ = callback;
119 }
120
121 // Now we can start monitoring
122 deviceEnumerator_->RegisterEndpointNotificationCallback(this);
123 }
124
125 // // IUnknown methods
126 ULONG STDMETHODCALLTYPE AddRef() override
127 {
128 auto count = InterlockedIncrement(&_refCount);
129 return count;
130 }
131
132 ULONG STDMETHODCALLTYPE Release() override
133 {
134 auto count = InterlockedDecrement(&_refCount);
135 if (count == 0) {
136 delete this;
137 }
138 return count;
139 }
140
141 HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override
142 {
143 if (riid == __uuidof(IUnknown) || riid == __uuidof(IMMNotificationClient)) {
144 *ppv = static_cast<IMMNotificationClient*>(this);
145 AddRef();
146 return S_OK;
147 }
148 *ppv = nullptr;
149 return E_NOINTERFACE;
150 }
151
152 // IMMNotificationClient methods
153 HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR deviceId, DWORD newState) override
154 {
155 handleStateChanged(deviceId, newState);
156 return S_OK;
157 }
158
159 HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR deviceId) override
160 {
161 // Treat addition as transition to active - we'll validate the state in handler
162 handleStateChanged(deviceId, DEVICE_STATE_ACTIVE);
163 return S_OK;
164 }
165
166 HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR deviceId) override
167 {
168 // Removed devices should be treated as inactive/unavailable
169 handleStateChanged(deviceId, DEVICE_STATE_NOTPRESENT);
170 return S_OK;
171 }
172
173 HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow,
174 ERole role,
175 LPCWSTR deviceId) override
176 {
177 // If the default communication device changes, we should restart the layer
178 // to ensure the new device is used. We only care about the default communication
179 // device, so we ignore other roles.
180 if (role == eCommunications && deviceId && deviceEventCallback_) {
181 std::string friendlyName = GetFriendlyNameFromIMMDeviceId(deviceEnumerator_, deviceId);
182 deviceEventCallback_(friendlyName, DeviceEventType::DefaultChanged);
183 }
184 return S_OK;
185 }
186
187 HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR deviceId,
188 const PROPERTYKEY key) override
189 {
190 // Limit this to samplerate changes
191 if (key == PKEY_AudioEngine_DeviceFormat) {
192 // Fetch updated sample rate
193 IMMDevice* pDevice;
194 deviceEnumerator_->GetDevice(deviceId, &pDevice);
195
196 IPropertyStore* pProps;
197 pDevice->OpenPropertyStore(STGM_READ, &pProps);
198
199 PROPVARIANT var;
200 PropVariantInit(&var);
201 pProps->GetValue(PKEY_AudioEngine_DeviceFormat, &var);
202
203 if (var.vt == VT_BLOB) {
204 auto* pWaveFormat = reinterpret_cast<WAVEFORMATEXTENSIBLE*>(var.blob.pBlobData);
205 UINT sampleRate = pWaveFormat->Format.nSamplesPerSec;
206 JAMI_DBG("Sample rate changed to %u", sampleRate);
207 std::string friendlyName = GetFriendlyNameFromIMMDeviceId(deviceEnumerator_,
208 deviceId);
209 deviceEventCallback_(friendlyName, DeviceEventType::PropertyChanged);
210 }
211
212 PropVariantClear(&var);
213 pProps->Release();
214 pDevice->Release();
215 }
216 return S_OK;
217 }
218
219private:
220 CComPtr<IMMDeviceEnumerator> deviceEnumerator_;
221
222 DeviceEventCallback deviceEventCallback_;
223 std::unordered_set<std::wstring> activeDevices_;
224
225 // Notifications are invoked on system-managed threads, so we need a lock
226 // to protect deviceEventCallback_ and activeDevices_
227 std::mutex mutex_;
228
229 // Enumerates both render (playback) and capture (recording) devices
230 void enumerateDevices()
231 {
232 if (!deviceEnumerator_)
233 return;
234
235 std::lock_guard<std::mutex> lock(mutex_);
236 activeDevices_.clear();
237
238 enumerateDevicesOfType(eRender);
239 enumerateDevicesOfType(eCapture);
240 }
241
242 void enumerateDevicesOfType(EDataFlow flow)
243 {
244 CComPtr<IMMDeviceCollection> devices;
245 CComPtr<IMMDevice> device;
246 UINT deviceCount = 0;
247
248 if (FAILED(deviceEnumerator_->EnumAudioEndpoints(flow, DEVICE_STATE_ACTIVE, &devices))) {
249 JAMI_ERR("Failed to enumerate devices");
250 return;
251 }
252
253 if (FAILED(devices->GetCount(&deviceCount))) {
254 JAMI_ERR("Failed to get device count");
255 return;
256 }
257
258 for (UINT i = 0; i < deviceCount; ++i) {
259 if (FAILED(devices->Item(i, &device)))
260 continue;
261
262 LPWSTR deviceId = nullptr;
263 if (FAILED(device->GetId(&deviceId)))
264 continue;
265
266 DWORD deviceState = 0;
267 if (SUCCEEDED(device->GetState(&deviceState)) && deviceState == DEVICE_STATE_ACTIVE) {
268 activeDevices_.insert(deviceId);
269 }
270
271 CoTaskMemFree(deviceId);
272 }
273 }
274
275 void handleStateChanged(LPCWSTR deviceId, DWORD newState)
276 {
277 if (!deviceId || !deviceEnumerator_ || !deviceEventCallback_)
278 return;
279
280 std::lock_guard<std::mutex> lock(mutex_);
281
282 std::wstring wsId(deviceId);
283 std::string friendlyName = GetFriendlyNameFromIMMDeviceId(deviceEnumerator_, deviceId);
284
285 // We only care if a device is entering or leaving the active state
286 bool isActive = (newState == DEVICE_STATE_ACTIVE);
287 auto it = activeDevices_.find(wsId);
288
289 if (isActive && it == activeDevices_.end()) {
290 // Device is active and not in our list? Add it and notify
291 activeDevices_.insert(wsId);
292 deviceEventCallback_(friendlyName, DeviceEventType::BecameActive);
293 } else if (!isActive && it != activeDevices_.end()) {
294 // Device is inactive and in our list? Remove it and notify
295 activeDevices_.erase(it);
296 deviceEventCallback_(friendlyName, DeviceEventType::BecameInactive);
297 }
298 }
299};
300
301// RAII COM wrapper for AudioDeviceNotificationClient
306
307typedef std::unique_ptr<AudioDeviceNotificationClient, AudioDeviceNotificationClient_deleter>
std::string GetFriendlyNameFromIMMDeviceId(CComPtr< IMMDeviceEnumerator > enumerator, LPCWSTR deviceId)
std::unique_ptr< AudioDeviceNotificationClient, AudioDeviceNotificationClient_deleter > AudioDeviceNotificationClientPtr
std::string to_string(DeviceEventType type)
std::function< void(const std::string &deviceName, DeviceEventType)> DeviceEventCallback
HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR deviceId, const PROPERTYKEY key) override
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) override
ULONG STDMETHODCALLTYPE AddRef() override
void setDeviceEventCallback(DeviceEventCallback callback)
ULONG STDMETHODCALLTYPE Release() override
HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR deviceId, DWORD newState) override
HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR deviceId) override
HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR deviceId) override
HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR deviceId) override
#define JAMI_ERR(...)
Definition logger.h:218
#define JAMI_DBG(...)
Definition logger.h:216
std::string to_string(double value)
void operator()(AudioDeviceNotificationClient *client) const