truehear-audio-library-node 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +220 -0
- package/binding.gyp +45 -0
- package/dist/domain/audio-device.types.d.ts +240 -0
- package/dist/domain/audio-device.types.d.ts.map +1 -0
- package/dist/domain/audio-device.types.js +13 -0
- package/dist/domain/audio-device.types.js.map +1 -0
- package/dist/index.d.ts +123 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +154 -0
- package/dist/index.js.map +1 -0
- package/include/audio_device_types.h +169 -0
- package/include/ipolicy_config.h +420 -0
- package/include/mmdevice_guids.h +9 -0
- package/include/truehear_audio.h +348 -0
- package/package.json +85 -0
- package/prebuilds/win32-x64/truehear_audio_native.node +0 -0
- package/src/addon.c +612 -0
- package/src/mmdevice_guids.c +8 -0
- package/src/policy_config_guids.c +26 -0
- package/src/truehear_audio_win.c +888 -0
|
@@ -0,0 +1,888 @@
|
|
|
1
|
+
#include "truehear_audio.h"
|
|
2
|
+
|
|
3
|
+
#include "ipolicy_config.h"
|
|
4
|
+
#include "mmdevice_guids.h"
|
|
5
|
+
|
|
6
|
+
#include <stdio.h>
|
|
7
|
+
#include <stdlib.h>
|
|
8
|
+
#include <string.h>
|
|
9
|
+
#include <wchar.h>
|
|
10
|
+
|
|
11
|
+
#ifdef _WIN32
|
|
12
|
+
#include <windows.h>
|
|
13
|
+
#include <mmdeviceapi.h>
|
|
14
|
+
#include <propsys.h>
|
|
15
|
+
#include <propidl.h>
|
|
16
|
+
#else
|
|
17
|
+
#error "truehear_audio_win.c must only be built on Windows"
|
|
18
|
+
#endif
|
|
19
|
+
|
|
20
|
+
/* ---------------------------------------------------------------------------------------------- */
|
|
21
|
+
/* Windows property keys */
|
|
22
|
+
/* ---------------------------------------------------------------------------------------------- */
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
* PKEY_Device_FriendlyName
|
|
26
|
+
*
|
|
27
|
+
* Windows normally exposes this from functiondiscoverykeys_devpkey.h,
|
|
28
|
+
* but defining it locally keeps this file simple and avoids extra header
|
|
29
|
+
* dependency surprises.
|
|
30
|
+
*
|
|
31
|
+
* This property gives names like:
|
|
32
|
+
*
|
|
33
|
+
* "Speakers (Realtek(R) Audio)"
|
|
34
|
+
* "Microphone Array (Intel Smart Sound Technology)"
|
|
35
|
+
*/
|
|
36
|
+
static const PROPERTYKEY TRUEHEAR_PKEY_Device_FriendlyName = {
|
|
37
|
+
{0xa45c254e, 0xdf1c, 0x4efd, {0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0}},
|
|
38
|
+
14 };
|
|
39
|
+
|
|
40
|
+
/* ---------------------------------------------------------------------------------------------- */
|
|
41
|
+
/* Internal error state */
|
|
42
|
+
/* ---------------------------------------------------------------------------------------------- */
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
* Simple library-level last error buffer.
|
|
46
|
+
*
|
|
47
|
+
* For now this is enough.
|
|
48
|
+
* Later, if we need thread-safe errors, we can improve it.
|
|
49
|
+
*/
|
|
50
|
+
static char g_truehear_audio_last_error[512] = "No error";
|
|
51
|
+
|
|
52
|
+
/*
|
|
53
|
+
* Store a plain error message.
|
|
54
|
+
*/
|
|
55
|
+
static void truehear_audio_set_error(const char* message) {
|
|
56
|
+
if (message == NULL) {
|
|
57
|
+
snprintf(
|
|
58
|
+
g_truehear_audio_last_error,
|
|
59
|
+
sizeof(g_truehear_audio_last_error),
|
|
60
|
+
"Unknown error");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
snprintf(
|
|
65
|
+
g_truehear_audio_last_error,
|
|
66
|
+
sizeof(g_truehear_audio_last_error),
|
|
67
|
+
"%s",
|
|
68
|
+
message);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/*
|
|
72
|
+
* Store an HRESULT error message.
|
|
73
|
+
*
|
|
74
|
+
* Windows COM functions usually return HRESULT.
|
|
75
|
+
*/
|
|
76
|
+
static void truehear_audio_set_hresult_error(
|
|
77
|
+
const char* operation,
|
|
78
|
+
HRESULT hr) {
|
|
79
|
+
snprintf(
|
|
80
|
+
g_truehear_audio_last_error,
|
|
81
|
+
sizeof(g_truehear_audio_last_error),
|
|
82
|
+
"%s failed with HRESULT 0x%08lx",
|
|
83
|
+
operation,
|
|
84
|
+
(unsigned long)hr);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/*
|
|
88
|
+
* Public error getter.
|
|
89
|
+
*/
|
|
90
|
+
const char* truehear_audio_get_last_error(void) {
|
|
91
|
+
return g_truehear_audio_last_error;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* ---------------------------------------------------------------------------------------------- */
|
|
95
|
+
/* Small string helpers */
|
|
96
|
+
/* ---------------------------------------------------------------------------------------------- */
|
|
97
|
+
|
|
98
|
+
/*
|
|
99
|
+
* C standard strdup replacement.
|
|
100
|
+
*
|
|
101
|
+
* strdup is common, but not part of older ISO C standards everywhere.
|
|
102
|
+
*/
|
|
103
|
+
static char* truehear_audio_strdup(const char* value) {
|
|
104
|
+
if (value == NULL) {
|
|
105
|
+
return NULL;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
size_t length = strlen(value);
|
|
109
|
+
char* copy = (char*)malloc(length + 1);
|
|
110
|
+
|
|
111
|
+
if (copy == NULL) {
|
|
112
|
+
return NULL;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
memcpy(copy, value, length + 1);
|
|
116
|
+
return copy;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/*
|
|
120
|
+
* Convert a Windows wide string to UTF-8.
|
|
121
|
+
*
|
|
122
|
+
* Windows device IDs and friendly names usually come as wchar_t / LPWSTR.
|
|
123
|
+
* Our public C API uses UTF-8 char* because that maps cleanly to Node.js later.
|
|
124
|
+
*/
|
|
125
|
+
static char* truehear_audio_wide_to_utf8(const wchar_t* value) {
|
|
126
|
+
if (value == NULL) {
|
|
127
|
+
return NULL;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
int required_size = WideCharToMultiByte(
|
|
131
|
+
CP_UTF8,
|
|
132
|
+
0,
|
|
133
|
+
value,
|
|
134
|
+
-1,
|
|
135
|
+
NULL,
|
|
136
|
+
0,
|
|
137
|
+
NULL,
|
|
138
|
+
NULL);
|
|
139
|
+
|
|
140
|
+
if (required_size <= 0) {
|
|
141
|
+
return NULL;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
char* out = (char*)malloc((size_t)required_size);
|
|
145
|
+
|
|
146
|
+
if (out == NULL) {
|
|
147
|
+
return NULL;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
int written = WideCharToMultiByte(
|
|
151
|
+
CP_UTF8,
|
|
152
|
+
0,
|
|
153
|
+
value,
|
|
154
|
+
-1,
|
|
155
|
+
out,
|
|
156
|
+
required_size,
|
|
157
|
+
NULL,
|
|
158
|
+
NULL);
|
|
159
|
+
|
|
160
|
+
if (written <= 0) {
|
|
161
|
+
free(out);
|
|
162
|
+
return NULL;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/*
|
|
169
|
+
* Convert UTF-8 char* to Windows wide string.
|
|
170
|
+
*
|
|
171
|
+
* IPolicyConfig::SetDefaultEndpoint expects PCWSTR, so device IDs from our
|
|
172
|
+
* public API must be converted back to wide strings.
|
|
173
|
+
*/
|
|
174
|
+
static wchar_t* truehear_audio_utf8_to_wide(const char* value) {
|
|
175
|
+
if (value == NULL) {
|
|
176
|
+
return NULL;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
int required_size = MultiByteToWideChar(
|
|
180
|
+
CP_UTF8,
|
|
181
|
+
0,
|
|
182
|
+
value,
|
|
183
|
+
-1,
|
|
184
|
+
NULL,
|
|
185
|
+
0);
|
|
186
|
+
|
|
187
|
+
if (required_size <= 0) {
|
|
188
|
+
return NULL;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
wchar_t* out = (wchar_t*)malloc(sizeof(wchar_t) * (size_t)required_size);
|
|
192
|
+
|
|
193
|
+
if (out == NULL) {
|
|
194
|
+
return NULL;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
int written = MultiByteToWideChar(
|
|
198
|
+
CP_UTF8,
|
|
199
|
+
0,
|
|
200
|
+
value,
|
|
201
|
+
-1,
|
|
202
|
+
out,
|
|
203
|
+
required_size);
|
|
204
|
+
|
|
205
|
+
if (written <= 0) {
|
|
206
|
+
free(out);
|
|
207
|
+
return NULL;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* ---------------------------------------------------------------------------------------------- */
|
|
214
|
+
/* Type mapping helpers */
|
|
215
|
+
/* ---------------------------------------------------------------------------------------------- */
|
|
216
|
+
|
|
217
|
+
/*
|
|
218
|
+
* Map our public device type to Windows Core Audio flow.
|
|
219
|
+
*
|
|
220
|
+
* TRUEHEAR_AUDIO_DEVICE_TYPE_PLAYBACK -> eRender
|
|
221
|
+
* TRUEHEAR_AUDIO_DEVICE_TYPE_RECORDING -> eCapture
|
|
222
|
+
*/
|
|
223
|
+
static EDataFlow truehear_audio_to_windows_data_flow(
|
|
224
|
+
TruehearAudioDeviceType type) {
|
|
225
|
+
if (type == TRUEHEAR_AUDIO_DEVICE_TYPE_RECORDING) {
|
|
226
|
+
return eCapture;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return eRender;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/*
|
|
233
|
+
* Validate the public audio device type.
|
|
234
|
+
*/
|
|
235
|
+
static int truehear_audio_is_valid_device_type(
|
|
236
|
+
TruehearAudioDeviceType type) {
|
|
237
|
+
return type == TRUEHEAR_AUDIO_DEVICE_TYPE_PLAYBACK ||
|
|
238
|
+
type == TRUEHEAR_AUDIO_DEVICE_TYPE_RECORDING;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* ---------------------------------------------------------------------------------------------- */
|
|
242
|
+
/* COM lifecycle helpers */
|
|
243
|
+
/* ---------------------------------------------------------------------------------------------- */
|
|
244
|
+
|
|
245
|
+
/*
|
|
246
|
+
* Initialize COM for this thread.
|
|
247
|
+
*
|
|
248
|
+
* Windows Core Audio APIs are COM-based.
|
|
249
|
+
* Before using IMMDeviceEnumerator or IPolicyConfig, COM must be initialized.
|
|
250
|
+
*
|
|
251
|
+
* should_uninitialize:
|
|
252
|
+
* 1 = this function initialized COM, so we should call CoUninitialize later
|
|
253
|
+
* 0 = COM was already initialized in a compatible/usable way
|
|
254
|
+
*/
|
|
255
|
+
static int truehear_audio_com_initialize(int* should_uninitialize) {
|
|
256
|
+
if (should_uninitialize == NULL) {
|
|
257
|
+
truehear_audio_set_error("should_uninitialize pointer is NULL");
|
|
258
|
+
return -1;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
*should_uninitialize = 0;
|
|
262
|
+
|
|
263
|
+
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
|
|
264
|
+
|
|
265
|
+
if (SUCCEEDED(hr)) {
|
|
266
|
+
/*
|
|
267
|
+
* This includes S_OK and S_FALSE.
|
|
268
|
+
*
|
|
269
|
+
* S_FALSE means COM was already initialized on this thread with the same
|
|
270
|
+
* apartment model. A successful CoInitializeEx call should still be matched
|
|
271
|
+
* with CoUninitialize.
|
|
272
|
+
*/
|
|
273
|
+
*should_uninitialize = 1;
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/*
|
|
278
|
+
* RPC_E_CHANGED_MODE means COM was already initialized on this thread
|
|
279
|
+
* using another apartment model.
|
|
280
|
+
*
|
|
281
|
+
* This can happen if the host app already called CoInitializeEx.
|
|
282
|
+
*
|
|
283
|
+
* For this implementation, we accept this and try to continue using the
|
|
284
|
+
* existing COM context.
|
|
285
|
+
*/
|
|
286
|
+
if (hr == RPC_E_CHANGED_MODE) {
|
|
287
|
+
*should_uninitialize = 0;
|
|
288
|
+
return 0;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
truehear_audio_set_hresult_error("CoInitializeEx", hr);
|
|
292
|
+
return -1;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/*
|
|
296
|
+
* Uninitialize COM only when this library initialized it.
|
|
297
|
+
*/
|
|
298
|
+
static void truehear_audio_com_uninitialize(int should_uninitialize) {
|
|
299
|
+
if (should_uninitialize) {
|
|
300
|
+
CoUninitialize();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/* ---------------------------------------------------------------------------------------------- */
|
|
305
|
+
/* Device list cleanup helpers */
|
|
306
|
+
/* ---------------------------------------------------------------------------------------------- */
|
|
307
|
+
|
|
308
|
+
/*
|
|
309
|
+
* Free only the contents of one device item.
|
|
310
|
+
*/
|
|
311
|
+
static void truehear_audio_free_device_item(
|
|
312
|
+
TruehearAudioDeviceInfo* item) {
|
|
313
|
+
if (item == NULL) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
free(item->name);
|
|
318
|
+
free(item->id);
|
|
319
|
+
|
|
320
|
+
item->name = NULL;
|
|
321
|
+
item->id = NULL;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/*
|
|
325
|
+
* Free a partial device list inside this file.
|
|
326
|
+
*
|
|
327
|
+
* Useful when truehear_audio_list_devices fails halfway.
|
|
328
|
+
*/
|
|
329
|
+
static void truehear_audio_free_partial_device_list(
|
|
330
|
+
TruehearAudioDeviceInfoList* list) {
|
|
331
|
+
if (list == NULL || list->items == NULL) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
for (int i = 0; i < list->count; i++) {
|
|
336
|
+
truehear_audio_free_device_item(&list->items[i]);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
free(list->items);
|
|
340
|
+
|
|
341
|
+
list->items = NULL;
|
|
342
|
+
list->count = 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* ---------------------------------------------------------------------------------------------- */
|
|
346
|
+
/* Windows Core Audio helpers */
|
|
347
|
+
/* ---------------------------------------------------------------------------------------------- */
|
|
348
|
+
|
|
349
|
+
/*
|
|
350
|
+
* Create the Windows MMDevice enumerator.
|
|
351
|
+
*
|
|
352
|
+
* This is the entry point for listing and querying Windows audio devices.
|
|
353
|
+
*/
|
|
354
|
+
static int truehear_audio_create_device_enumerator(
|
|
355
|
+
IMMDeviceEnumerator** out_enumerator) {
|
|
356
|
+
if (out_enumerator == NULL) {
|
|
357
|
+
truehear_audio_set_error("out_enumerator pointer is NULL");
|
|
358
|
+
return -1;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
*out_enumerator = NULL;
|
|
362
|
+
|
|
363
|
+
HRESULT hr = CoCreateInstance(
|
|
364
|
+
&CLSID_MMDeviceEnumerator,
|
|
365
|
+
NULL,
|
|
366
|
+
CLSCTX_ALL,
|
|
367
|
+
&IID_IMMDeviceEnumerator,
|
|
368
|
+
(void**)out_enumerator);
|
|
369
|
+
|
|
370
|
+
if (FAILED(hr)) {
|
|
371
|
+
truehear_audio_set_hresult_error(
|
|
372
|
+
"CoCreateInstance(CLSID_MMDeviceEnumerator)",
|
|
373
|
+
hr);
|
|
374
|
+
return -1;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/*
|
|
381
|
+
* Get the endpoint ID for a default device role.
|
|
382
|
+
*
|
|
383
|
+
* The returned LPWSTR is allocated by COM.
|
|
384
|
+
* Caller must free it with CoTaskMemFree().
|
|
385
|
+
*/
|
|
386
|
+
static LPWSTR truehear_audio_get_default_endpoint_id(
|
|
387
|
+
IMMDeviceEnumerator* enumerator,
|
|
388
|
+
EDataFlow flow,
|
|
389
|
+
ERole role) {
|
|
390
|
+
if (enumerator == NULL) {
|
|
391
|
+
return NULL;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
IMMDevice* device = NULL;
|
|
395
|
+
LPWSTR device_id = NULL;
|
|
396
|
+
|
|
397
|
+
HRESULT hr = enumerator->lpVtbl->GetDefaultAudioEndpoint(
|
|
398
|
+
enumerator,
|
|
399
|
+
flow,
|
|
400
|
+
role,
|
|
401
|
+
&device);
|
|
402
|
+
|
|
403
|
+
if (FAILED(hr) || device == NULL) {
|
|
404
|
+
return NULL;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
hr = device->lpVtbl->GetId(
|
|
408
|
+
device,
|
|
409
|
+
&device_id);
|
|
410
|
+
|
|
411
|
+
device->lpVtbl->Release(device);
|
|
412
|
+
|
|
413
|
+
if (FAILED(hr)) {
|
|
414
|
+
return NULL;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return device_id;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/*
|
|
421
|
+
* Read the friendly name from an IMMDevice.
|
|
422
|
+
*
|
|
423
|
+
* Returns a newly allocated UTF-8 string.
|
|
424
|
+
* Caller owns the returned string and must free it.
|
|
425
|
+
*/
|
|
426
|
+
static char* truehear_audio_get_device_friendly_name(
|
|
427
|
+
IMMDevice* device) {
|
|
428
|
+
if (device == NULL) {
|
|
429
|
+
return NULL;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
IPropertyStore* property_store = NULL;
|
|
433
|
+
PROPVARIANT property_value;
|
|
434
|
+
char* name = NULL;
|
|
435
|
+
|
|
436
|
+
PropVariantInit(&property_value);
|
|
437
|
+
|
|
438
|
+
HRESULT hr = device->lpVtbl->OpenPropertyStore(
|
|
439
|
+
device,
|
|
440
|
+
STGM_READ,
|
|
441
|
+
&property_store);
|
|
442
|
+
|
|
443
|
+
if (FAILED(hr) || property_store == NULL) {
|
|
444
|
+
return NULL;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
hr = property_store->lpVtbl->GetValue(
|
|
448
|
+
property_store,
|
|
449
|
+
&TRUEHEAR_PKEY_Device_FriendlyName,
|
|
450
|
+
&property_value);
|
|
451
|
+
|
|
452
|
+
if (SUCCEEDED(hr) && property_value.vt == VT_LPWSTR) {
|
|
453
|
+
name = truehear_audio_wide_to_utf8(property_value.pwszVal);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
PropVariantClear(&property_value);
|
|
457
|
+
property_store->lpVtbl->Release(property_store);
|
|
458
|
+
|
|
459
|
+
return name;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/*
|
|
463
|
+
* Create an IPolicyConfig object.
|
|
464
|
+
*
|
|
465
|
+
* This is the undocumented COM object used to change the default endpoint.
|
|
466
|
+
*/
|
|
467
|
+
static int truehear_audio_create_policy_config(
|
|
468
|
+
IPolicyConfig** out_policy_config) {
|
|
469
|
+
if (out_policy_config == NULL) {
|
|
470
|
+
truehear_audio_set_error("out_policy_config pointer is NULL");
|
|
471
|
+
return -1;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
*out_policy_config = NULL;
|
|
475
|
+
|
|
476
|
+
HRESULT hr = CoCreateInstance(
|
|
477
|
+
&CLSID_CPolicyConfigClient,
|
|
478
|
+
NULL,
|
|
479
|
+
CLSCTX_ALL,
|
|
480
|
+
&IID_IPolicyConfig,
|
|
481
|
+
(void**)out_policy_config);
|
|
482
|
+
|
|
483
|
+
if (FAILED(hr)) {
|
|
484
|
+
truehear_audio_set_hresult_error(
|
|
485
|
+
"CoCreateInstance(CLSID_CPolicyConfigClient)",
|
|
486
|
+
hr);
|
|
487
|
+
return -1;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return 0;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/*
|
|
494
|
+
* Set a default endpoint for one Windows audio role.
|
|
495
|
+
*
|
|
496
|
+
* Windows roles:
|
|
497
|
+
*
|
|
498
|
+
* eConsole
|
|
499
|
+
* eMultimedia
|
|
500
|
+
* eCommunications
|
|
501
|
+
*/
|
|
502
|
+
static int truehear_audio_set_default_endpoint_for_role(
|
|
503
|
+
const char* device_id,
|
|
504
|
+
ERole role) {
|
|
505
|
+
if (device_id == NULL || device_id[0] == '\0') {
|
|
506
|
+
truehear_audio_set_error("device_id is empty");
|
|
507
|
+
return -1;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
wchar_t* wide_device_id = truehear_audio_utf8_to_wide(device_id);
|
|
511
|
+
|
|
512
|
+
if (wide_device_id == NULL) {
|
|
513
|
+
truehear_audio_set_error("Failed to convert device_id from UTF-8 to wide string");
|
|
514
|
+
return -1;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
IPolicyConfig* policy_config = NULL;
|
|
518
|
+
|
|
519
|
+
if (truehear_audio_create_policy_config(&policy_config) != 0) {
|
|
520
|
+
free(wide_device_id);
|
|
521
|
+
return -1;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
HRESULT hr = policy_config->lpVtbl->SetDefaultEndpoint(
|
|
525
|
+
policy_config,
|
|
526
|
+
wide_device_id,
|
|
527
|
+
role);
|
|
528
|
+
|
|
529
|
+
policy_config->lpVtbl->Release(policy_config);
|
|
530
|
+
free(wide_device_id);
|
|
531
|
+
|
|
532
|
+
if (FAILED(hr)) {
|
|
533
|
+
truehear_audio_set_hresult_error(
|
|
534
|
+
"IPolicyConfig::SetDefaultEndpoint",
|
|
535
|
+
hr);
|
|
536
|
+
return -1;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return 0;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/* ---------------------------------------------------------------------------------------------- */
|
|
543
|
+
/* Public API implementation */
|
|
544
|
+
/* ---------------------------------------------------------------------------------------------- */
|
|
545
|
+
|
|
546
|
+
int truehear_audio_list_devices(
|
|
547
|
+
TruehearAudioDeviceType type,
|
|
548
|
+
TruehearAudioDeviceInfoList* out_list) {
|
|
549
|
+
if (out_list == NULL) {
|
|
550
|
+
truehear_audio_set_error("out_list pointer is NULL");
|
|
551
|
+
return -1;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/*
|
|
555
|
+
* Always reset output first.
|
|
556
|
+
*
|
|
557
|
+
* This makes cleanup safe even if the function fails halfway later.
|
|
558
|
+
*/
|
|
559
|
+
out_list->items = NULL;
|
|
560
|
+
out_list->count = 0;
|
|
561
|
+
|
|
562
|
+
if (!truehear_audio_is_valid_device_type(type)) {
|
|
563
|
+
truehear_audio_set_error("Invalid audio device type");
|
|
564
|
+
return -1;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
int should_uninitialize = 0;
|
|
568
|
+
|
|
569
|
+
if (truehear_audio_com_initialize(&should_uninitialize) != 0) {
|
|
570
|
+
return -1;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
IMMDeviceEnumerator* enumerator = NULL;
|
|
574
|
+
IMMDeviceCollection* collection = NULL;
|
|
575
|
+
|
|
576
|
+
if (truehear_audio_create_device_enumerator(&enumerator) != 0) {
|
|
577
|
+
truehear_audio_com_uninitialize(should_uninitialize);
|
|
578
|
+
return -1;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
EDataFlow flow = truehear_audio_to_windows_data_flow(type);
|
|
582
|
+
|
|
583
|
+
HRESULT hr = enumerator->lpVtbl->EnumAudioEndpoints(
|
|
584
|
+
enumerator,
|
|
585
|
+
flow,
|
|
586
|
+
DEVICE_STATE_ACTIVE,
|
|
587
|
+
&collection);
|
|
588
|
+
|
|
589
|
+
if (FAILED(hr)) {
|
|
590
|
+
truehear_audio_set_hresult_error(
|
|
591
|
+
"IMMDeviceEnumerator::EnumAudioEndpoints",
|
|
592
|
+
hr);
|
|
593
|
+
|
|
594
|
+
enumerator->lpVtbl->Release(enumerator);
|
|
595
|
+
truehear_audio_com_uninitialize(should_uninitialize);
|
|
596
|
+
return -1;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
UINT raw_count = 0;
|
|
600
|
+
|
|
601
|
+
hr = collection->lpVtbl->GetCount(
|
|
602
|
+
collection,
|
|
603
|
+
&raw_count);
|
|
604
|
+
|
|
605
|
+
if (FAILED(hr)) {
|
|
606
|
+
truehear_audio_set_hresult_error(
|
|
607
|
+
"IMMDeviceCollection::GetCount",
|
|
608
|
+
hr);
|
|
609
|
+
|
|
610
|
+
collection->lpVtbl->Release(collection);
|
|
611
|
+
enumerator->lpVtbl->Release(enumerator);
|
|
612
|
+
truehear_audio_com_uninitialize(should_uninitialize);
|
|
613
|
+
return -1;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/*
|
|
617
|
+
* Allocate enough space for all devices.
|
|
618
|
+
*
|
|
619
|
+
* Some entries may be skipped if Windows gives an unusable item, so the
|
|
620
|
+
* final out_list->count may be smaller than raw_count.
|
|
621
|
+
*/
|
|
622
|
+
TruehearAudioDeviceInfo* items = NULL;
|
|
623
|
+
|
|
624
|
+
if (raw_count > 0) {
|
|
625
|
+
items = (TruehearAudioDeviceInfo*)calloc(
|
|
626
|
+
(size_t)raw_count,
|
|
627
|
+
sizeof(TruehearAudioDeviceInfo));
|
|
628
|
+
|
|
629
|
+
if (items == NULL) {
|
|
630
|
+
truehear_audio_set_error("Failed to allocate audio device list");
|
|
631
|
+
|
|
632
|
+
collection->lpVtbl->Release(collection);
|
|
633
|
+
enumerator->lpVtbl->Release(enumerator);
|
|
634
|
+
truehear_audio_com_uninitialize(should_uninitialize);
|
|
635
|
+
return -1;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/*
|
|
640
|
+
* Get default endpoint IDs for comparison.
|
|
641
|
+
*
|
|
642
|
+
* is_default:
|
|
643
|
+
* true if the device matches either eConsole or eMultimedia.
|
|
644
|
+
*
|
|
645
|
+
* is_default_communication:
|
|
646
|
+
* true if the device matches eCommunications.
|
|
647
|
+
*/
|
|
648
|
+
LPWSTR default_console_id = truehear_audio_get_default_endpoint_id(
|
|
649
|
+
enumerator,
|
|
650
|
+
flow,
|
|
651
|
+
eConsole);
|
|
652
|
+
|
|
653
|
+
LPWSTR default_multimedia_id = truehear_audio_get_default_endpoint_id(
|
|
654
|
+
enumerator,
|
|
655
|
+
flow,
|
|
656
|
+
eMultimedia);
|
|
657
|
+
|
|
658
|
+
LPWSTR default_communication_id = truehear_audio_get_default_endpoint_id(
|
|
659
|
+
enumerator,
|
|
660
|
+
flow,
|
|
661
|
+
eCommunications);
|
|
662
|
+
|
|
663
|
+
int actual_count = 0;
|
|
664
|
+
|
|
665
|
+
for (UINT i = 0; i < raw_count; i++) {
|
|
666
|
+
IMMDevice* device = NULL;
|
|
667
|
+
|
|
668
|
+
hr = collection->lpVtbl->Item(
|
|
669
|
+
collection,
|
|
670
|
+
i,
|
|
671
|
+
&device);
|
|
672
|
+
|
|
673
|
+
if (FAILED(hr) || device == NULL) {
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
LPWSTR wide_device_id = NULL;
|
|
678
|
+
|
|
679
|
+
hr = device->lpVtbl->GetId(
|
|
680
|
+
device,
|
|
681
|
+
&wide_device_id);
|
|
682
|
+
|
|
683
|
+
if (FAILED(hr) || wide_device_id == NULL) {
|
|
684
|
+
device->lpVtbl->Release(device);
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
char* id = truehear_audio_wide_to_utf8(wide_device_id);
|
|
689
|
+
char* name = truehear_audio_get_device_friendly_name(device);
|
|
690
|
+
|
|
691
|
+
if (name == NULL) {
|
|
692
|
+
/*
|
|
693
|
+
* Fallback: if Windows does not return a friendly name, use the id.
|
|
694
|
+
*/
|
|
695
|
+
name = truehear_audio_strdup(id != NULL ? id : "(unknown audio device)");
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (id == NULL || name == NULL) {
|
|
699
|
+
free(id);
|
|
700
|
+
free(name);
|
|
701
|
+
CoTaskMemFree(wide_device_id);
|
|
702
|
+
device->lpVtbl->Release(device);
|
|
703
|
+
|
|
704
|
+
TruehearAudioDeviceInfoList partial_list;
|
|
705
|
+
partial_list.items = items;
|
|
706
|
+
partial_list.count = actual_count;
|
|
707
|
+
|
|
708
|
+
truehear_audio_free_partial_device_list(&partial_list);
|
|
709
|
+
|
|
710
|
+
if (default_console_id != NULL) {
|
|
711
|
+
CoTaskMemFree(default_console_id);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (default_multimedia_id != NULL) {
|
|
715
|
+
CoTaskMemFree(default_multimedia_id);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (default_communication_id != NULL) {
|
|
719
|
+
CoTaskMemFree(default_communication_id);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
collection->lpVtbl->Release(collection);
|
|
723
|
+
enumerator->lpVtbl->Release(enumerator);
|
|
724
|
+
truehear_audio_com_uninitialize(should_uninitialize);
|
|
725
|
+
|
|
726
|
+
truehear_audio_set_error("Failed to allocate audio device strings");
|
|
727
|
+
return -1;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* We avoid doing this
|
|
732
|
+
* items[actual_count].type = type;
|
|
733
|
+
* items[actual_count].id = id;
|
|
734
|
+
* items[actual_count].name = name;
|
|
735
|
+
* items[actual_count].is_default = 0;
|
|
736
|
+
* items[actual_count].is_default_communication = 0;
|
|
737
|
+
*/
|
|
738
|
+
TruehearAudioDeviceInfo* item = &items[actual_count];
|
|
739
|
+
|
|
740
|
+
item->type = type;
|
|
741
|
+
item->id = id;
|
|
742
|
+
item->name = name;
|
|
743
|
+
|
|
744
|
+
item->is_default = 0;
|
|
745
|
+
item->is_default_communication = 0;
|
|
746
|
+
|
|
747
|
+
if (default_console_id != NULL &&
|
|
748
|
+
wcscmp(wide_device_id, default_console_id) == 0) {
|
|
749
|
+
item->is_default = 1;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (default_multimedia_id != NULL &&
|
|
753
|
+
wcscmp(wide_device_id, default_multimedia_id) == 0) {
|
|
754
|
+
item->is_default = 1;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (default_communication_id != NULL &&
|
|
758
|
+
wcscmp(wide_device_id, default_communication_id) == 0) {
|
|
759
|
+
item->is_default_communication = 1;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
actual_count++;
|
|
763
|
+
|
|
764
|
+
CoTaskMemFree(wide_device_id);
|
|
765
|
+
device->lpVtbl->Release(device);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (default_console_id != NULL) {
|
|
769
|
+
CoTaskMemFree(default_console_id);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (default_multimedia_id != NULL) {
|
|
773
|
+
CoTaskMemFree(default_multimedia_id);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (default_communication_id != NULL) {
|
|
777
|
+
CoTaskMemFree(default_communication_id);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
collection->lpVtbl->Release(collection);
|
|
781
|
+
enumerator->lpVtbl->Release(enumerator);
|
|
782
|
+
truehear_audio_com_uninitialize(should_uninitialize);
|
|
783
|
+
|
|
784
|
+
out_list->items = items;
|
|
785
|
+
out_list->count = actual_count;
|
|
786
|
+
|
|
787
|
+
truehear_audio_set_error("No error");
|
|
788
|
+
return 0;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
int truehear_audio_set_default_device(
|
|
792
|
+
TruehearAudioDeviceType type,
|
|
793
|
+
const char* device_id) {
|
|
794
|
+
if (!truehear_audio_is_valid_device_type(type)) {
|
|
795
|
+
truehear_audio_set_error("Invalid audio device type");
|
|
796
|
+
return -1;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (device_id == NULL || device_id[0] == '\0') {
|
|
800
|
+
truehear_audio_set_error("device_id is empty");
|
|
801
|
+
return -1;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
int should_uninitialize = 0;
|
|
805
|
+
|
|
806
|
+
if (truehear_audio_com_initialize(&should_uninitialize) != 0) {
|
|
807
|
+
return -1;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/*
|
|
811
|
+
* Normal default device:
|
|
812
|
+
*
|
|
813
|
+
* eConsole:
|
|
814
|
+
* system/default role
|
|
815
|
+
*
|
|
816
|
+
* eMultimedia:
|
|
817
|
+
* media playback role
|
|
818
|
+
*
|
|
819
|
+
* We intentionally do not set eCommunications here.
|
|
820
|
+
* That is handled by truehear_audio_set_default_communication_device().
|
|
821
|
+
*/
|
|
822
|
+
if (truehear_audio_set_default_endpoint_for_role(device_id, eConsole) != 0) {
|
|
823
|
+
truehear_audio_com_uninitialize(should_uninitialize);
|
|
824
|
+
return -1;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (truehear_audio_set_default_endpoint_for_role(device_id, eMultimedia) != 0) {
|
|
828
|
+
truehear_audio_com_uninitialize(should_uninitialize);
|
|
829
|
+
return -1;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
truehear_audio_com_uninitialize(should_uninitialize);
|
|
833
|
+
|
|
834
|
+
truehear_audio_set_error("No error");
|
|
835
|
+
return 0;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
int truehear_audio_set_default_communication_device(
|
|
839
|
+
TruehearAudioDeviceType type,
|
|
840
|
+
const char* device_id) {
|
|
841
|
+
if (!truehear_audio_is_valid_device_type(type)) {
|
|
842
|
+
truehear_audio_set_error("Invalid audio device type");
|
|
843
|
+
return -1;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (device_id == NULL || device_id[0] == '\0') {
|
|
847
|
+
truehear_audio_set_error("device_id is empty");
|
|
848
|
+
return -1;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
int should_uninitialize = 0;
|
|
852
|
+
|
|
853
|
+
if (truehear_audio_com_initialize(&should_uninitialize) != 0) {
|
|
854
|
+
return -1;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/*
|
|
858
|
+
* Communication default device:
|
|
859
|
+
*
|
|
860
|
+
* eCommunications:
|
|
861
|
+
* calls / Teams / Zoom / communication apps
|
|
862
|
+
*/
|
|
863
|
+
if (truehear_audio_set_default_endpoint_for_role(device_id, eCommunications) != 0) {
|
|
864
|
+
truehear_audio_com_uninitialize(should_uninitialize);
|
|
865
|
+
return -1;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
truehear_audio_com_uninitialize(should_uninitialize);
|
|
869
|
+
|
|
870
|
+
truehear_audio_set_error("No error");
|
|
871
|
+
return 0;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
void truehear_audio_free_device_list(
|
|
875
|
+
TruehearAudioDeviceInfoList* list) {
|
|
876
|
+
if (list == NULL || list->items == NULL) {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
for (int i = 0; i < list->count; i++) {
|
|
881
|
+
truehear_audio_free_device_item(&list->items[i]);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
free(list->items);
|
|
885
|
+
|
|
886
|
+
list->items = NULL;
|
|
887
|
+
list->count = 0;
|
|
888
|
+
}
|