UltraGrid
UltraGrid Low-Latency High-Resolution Transmission System
Loading...
Searching...
No Matches
wasapi.cpp
Go to the documentation of this file.
1
5/*
6 * Copyright (c) 2019-2023 CESNET, z. s. p. o.
7 * All rights reserved.
8 *
9 * This program is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation, either version 3 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
21 */
31#include <audioclient.h>
32#include <audiosessiontypes.h> // for AUDCLNT_SHAREMODE_SHARED
33#include <basetsd.h> // for UINT32, HRESULT, UINT, LPWSTR, GUID..
34#include <cassert> // for assert
35#include <cctype> // for isdigit
36#include <combaseapi.h> // for CoTaskMemFree, CoCreateInstance
37#include <cstdio> // for snprintf
38#include <cstdlib> // for NULL, atoi, malloc, mbtowc, realloc
39#include <cstring> // for memset, strcmp, strlen, wcslen
40#include <iostream>
41#include <ksmedia.h> // for KSAUDIO_SPEAKER_5POINT1_SURROUND
42#include <mediaobj.h> // for REFERENCE_TIME
43#include <mmdeviceapi.h>
44#include <mmeapi.h> // for WAVEFORMATEX
45#include <mmreg.h> // for WAVEFORMATEXTENSIBLE, WAVE_FORMAT_...
46#include <objbase.h> // for STGM_READ
47#include <propidl.h> // for PropVariantClear, PropVariantInit
48#include <propsys.h> // for IPropertyStore
49#include <sstream>
50#include <string>
51#include <winerror.h> // for SUCCEEDED, S_OK, FAILED, S_FALSE
52
54#include "audio/types.h"
55#include "debug.h"
56#include "host.h" // for get_commandline_param, INIT_NOERR
57#include "lib_common.h"
58#include "types.h" // for device_info
59#include "ug_runtime_error.hpp"
60#include "utils/color_out.h"
61#include "utils/windows.h"
62
63#define DEFAULT_WASAPI_BUFLEN_MS 67
64#define MOD_NAME "[WASAPI play.] "
65#define REFTIMES_PER_SEC 10000000
66#define REFTIMES_PER_MILLISEC 10000
67
68const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
69const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
70const IID IID_IAudioClient = __uuidof(IAudioClient);
71const IID IID_IAudioRenderClient = __uuidof(IAudioRenderClient);
72const static GUID IDevice_FriendlyName = { 0xa45c254e, 0xdf1c, 0x4efd, { 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0 } };
73const static PROPERTYKEY PKEY_Device_FriendlyName = { IDevice_FriendlyName, 14 };
74#pragma GCC diagnostic push
75#pragma GCC diagnostic ignored "-Wmissing-braces" // not our issue - defined by Mingw-w64
76const static GUID UG_KSDATAFORMAT_SUBTYPE_PCM = { STATIC_KSDATAFORMAT_SUBTYPE_PCM };
77#pragma GCC diagnostic pop
78
79
80using std::cout;
81using std::ostringstream;
82using std::string;
83using std::wcout;
84using std::wstring;
85
89 IMMDevice *pDevice;
90 IAudioClient *pAudioClient;
91 IAudioRenderClient *pRenderClient;
92 UINT32 bufferSize;
93};
94
95string wasapi_get_default_device_id(EDataFlow dataFlow, IMMDeviceEnumerator *enumerator); // defined in WASAPI capture
96
97#define SAFE_RELEASE(u) \
98 do { if ((u) != NULL) (u)->Release(); (u) = NULL; } while(0)
99#undef THROW_IF_FAILED
100#define THROW_IF_FAILED(cmd) do { HRESULT hr = cmd; if (!SUCCEEDED(hr)) { ostringstream oss; oss << #cmd << ": " << hresult_to_str(hr); throw ug_runtime_error(oss.str()); } } while(0)
101
102static string get_name(IMMDevice *pDevice) {
103 wstring out;
104 IPropertyStore *pProps = NULL;
105 PROPVARIANT varName;
106 LPWSTR pwszID = NULL;
107 // Initialize container for property value.
108 PropVariantInit(&varName);
109 HRESULT hr = pDevice->OpenPropertyStore(STGM_READ, &pProps);
110 if (!FAILED(hr)) {
111 hr = pProps->GetValue(PKEY_Device_FriendlyName, &varName);
112 if (!FAILED(hr)) {
113 out = varName.pwszVal;
114 }
115 }
116 SAFE_RELEASE(pProps);
117 CoTaskMemFree(pwszID);
118 PropVariantClear(&varName);
119 return win_wstr_to_str(out.c_str());
120}
121
122static void audio_play_wasapi_probe(struct device_info **available_devices, int *dev_count, void (**deleter)(void *))
123{
124 *deleter = free;
125 *available_devices = (struct device_info *) malloc(0);
126 *dev_count = 0;
127
128 IMMDeviceEnumerator *enumerator = nullptr;
129 IMMDeviceCollection *pEndpoints = nullptr;
130 bool com_initialized = false;
131 if (!com_initialize(&com_initialized, MOD_NAME)) {
132 return;
133 }
134
135 try {
136 THROW_IF_FAILED(CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_IMMDeviceEnumerator,
137 (void **) &enumerator));
138
139 THROW_IF_FAILED(enumerator->EnumAudioEndpoints(eRender, DEVICE_STATEMASK_ALL, &pEndpoints));
140 UINT count;
141 THROW_IF_FAILED(pEndpoints->GetCount(&count));
142 for (UINT i = 0; i < count; ++i) {
143 IMMDevice *pDevice = nullptr;
144 LPWSTR pwszID = NULL;
145 try {
146 THROW_IF_FAILED(pEndpoints->Item(i, &pDevice));
147 THROW_IF_FAILED(pDevice->GetId(&pwszID));
148 *available_devices = (struct device_info *) realloc(*available_devices, (*dev_count + 1) * sizeof(struct device_info));
149 memset(&(*available_devices)[*dev_count], 0, sizeof(struct device_info));
150 snprintf((*available_devices)[*dev_count].dev, sizeof (*available_devices)[*dev_count].dev, ":%u", i);
151 snprintf((*available_devices)[*dev_count].name, sizeof (*available_devices)[*dev_count].name, "WASAPI %s", get_name(pDevice).c_str());
152 ++*dev_count;
153 } catch (ug_runtime_error &e) {
154 LOG(LOG_LEVEL_WARNING) << MOD_NAME << "Device " << i << ": " << e.what() << "\n";
155 }
156 SAFE_RELEASE(pDevice);
157 CoTaskMemFree(pwszID);
158 }
159 } catch (...) {
160 }
161 SAFE_RELEASE(enumerator);
162 SAFE_RELEASE(pEndpoints);
163 com_uninitialize(&com_initialized);
164}
165
167 col() << "Usage:\n" <<
168 SBOLD(SRED("\t-r wasapi") << "[:<index>|:<ID>] --param audio-buffer-len=<ms>") << "\n" <<
169 "\nAvailable devices:\n";
170
171 bool com_initialized = false;
172 if (!com_initialize(&com_initialized, MOD_NAME)) {
173 return;
174 }
175 IMMDeviceEnumerator *enumerator = nullptr;
176 IMMDeviceCollection *pEndpoints = nullptr;
177
178 try {
179 THROW_IF_FAILED(CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_IMMDeviceEnumerator,
180 (void **) &enumerator));
181
182 THROW_IF_FAILED(enumerator->EnumAudioEndpoints(eRender, DEVICE_STATEMASK_ALL, &pEndpoints));
183 string default_dev_id = wasapi_get_default_device_id(eRender, enumerator);
184 UINT count;
185 THROW_IF_FAILED(pEndpoints->GetCount(&count));
186 for (UINT i = 0; i < count; ++i) {
187 IMMDevice *pDevice = nullptr;
188 LPWSTR pwszID = NULL;
189 try {
190 THROW_IF_FAILED(pEndpoints->Item(i, &pDevice));
191 THROW_IF_FAILED(pDevice->GetId(&pwszID));
192 string dev_id = win_wstr_to_str(pwszID);
193 col() << (dev_id == default_dev_id ? "(*)" : "") << "\t" << SBOLD(i) << ") " << SBOLD(get_name(pDevice)) << " (ID: " << dev_id << ")\n";
194 } catch (ug_runtime_error &e) {
195 LOG(LOG_LEVEL_WARNING) << MOD_NAME << "Device " << i << ": " << e.what() << "\n";
196 }
197 SAFE_RELEASE(pDevice);
198 CoTaskMemFree(pwszID);
199 }
200 } catch (...) {
201 }
202 SAFE_RELEASE(enumerator);
203 SAFE_RELEASE(pEndpoints);
204 com_uninitialize(&com_initialized);
205}
206
207static void * audio_play_wasapi_init(const char *cfg)
208{
209 wchar_t deviceID[1024] = L"";
210 int index = -1;
211 if (strlen(cfg) > 0) {
212 if (strcmp(cfg, "help") == 0) {
214 return INIT_NOERR;
215 }
216 if (isdigit(cfg[0])) {
217 index = atoi(cfg);
218 } else {
219 mbtowc(deviceID, cfg, (sizeof deviceID / 2) - 1);
220 }
221 }
222 auto s = new state_aplay_wasapi();
223 if (!com_initialize(&s->com_initialized, MOD_NAME)) {
224 delete s;
225 return nullptr;
226 }
227 IMMDeviceEnumerator *enumerator = nullptr;
228 try {
229
230 THROW_IF_FAILED(CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_IMMDeviceEnumerator,
231 (void **) &enumerator));
232 if (wcslen(deviceID) > 0) {
233 THROW_IF_FAILED(enumerator->GetDevice(deviceID, &s->pDevice));
234 } else if (index != -1) {
235 IMMDeviceCollection *pEndpoints = nullptr;
236 try {
237 THROW_IF_FAILED(enumerator->EnumAudioEndpoints(eRender, DEVICE_STATEMASK_ALL, &pEndpoints));
238 UINT count;
239 THROW_IF_FAILED(pEndpoints->GetCount(&count));
240 for (UINT i = 0; i < count; ++i) {
241 if (i == (UINT) index) {
242 THROW_IF_FAILED(pEndpoints->Item(i, &s->pDevice));
243 break;
244 }
245 }
246 } catch (ug_runtime_error &e) { // just continue with the next
247 LOG(LOG_LEVEL_WARNING) << MOD_NAME << e.what() << "\n";
248 }
249 SAFE_RELEASE(pEndpoints);
250 } else { // default device
251 THROW_IF_FAILED(enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &s->pDevice));
252 }
253 if (!s->pDevice) {
254 throw ug_runtime_error("Device not found!");
255 }
256 THROW_IF_FAILED(s->pDevice->Activate(IID_IAudioClient, CLSCTX_ALL, NULL,
257 (void **)&s->pAudioClient));
258
259 auto friendlyName = get_name(s->pDevice);
260 if (!friendlyName.empty()) {
261 LOG(LOG_LEVEL_NOTICE) << MOD_NAME << "Using device: "
262 << friendlyName << "\n";
263 }
264 } catch (ug_runtime_error &e) {
265 LOG(LOG_LEVEL_ERROR) << MOD_NAME << e.what() << "\n";
266 com_uninitialize(&s->com_initialized);
267 delete s;
268 s = nullptr;
269 }
270 // print friendly name
271 SAFE_RELEASE(enumerator);
272 return s;
273}
274
276{
277 CoUninitialize();
278 delete static_cast<state_aplay_wasapi *>(state);
279}
280
281static DWORD get_channel_mask(int *count) {
282 assert(*count > 0);
283 switch (*count) {
284 case 1: return KSAUDIO_SPEAKER_MONO;
285 case 2: return KSAUDIO_SPEAKER_STEREO;
286 case 3: *count = 4; return KSAUDIO_SPEAKER_SURROUND;
287 case 4: return KSAUDIO_SPEAKER_QUAD; // or SURROUND
288 case 5: *count = 6; return KSAUDIO_SPEAKER_5POINT1_SURROUND;
289 case 6: return KSAUDIO_SPEAKER_5POINT1; // ditto
290 case 7: *count = 7; return KSAUDIO_SPEAKER_5POINT1_SURROUND;
291 case 8: return KSAUDIO_SPEAKER_7POINT1; // ditto
292 default: *count = 8; return KSAUDIO_SPEAKER_7POINT1;
293 }
294}
295
296static WAVEFORMATEXTENSIBLE audio_format_to_waveformatex(struct audio_desc *desc) {
297 assert(desc->codec == AC_PCM);
298 WAVEFORMATEXTENSIBLE fmt;
300 fmt.dwChannelMask = get_channel_mask(&desc->ch_count);
301 fmt.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
302 fmt.Format.nChannels = desc->ch_count;
303 fmt.Format.wBitsPerSample = desc->bps * 8;
304 fmt.Format.nSamplesPerSec = desc->sample_rate;
305 fmt.Format.nBlockAlign = fmt.Format.nChannels * fmt.Format.wBitsPerSample / 8;
306 fmt.Format.nAvgBytesPerSec = fmt.Format.nSamplesPerSec * fmt.Format.nBlockAlign;
307 fmt.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX); // at least 22 for extensible
308 fmt.Samples.wValidBitsPerSample = fmt.Format.wBitsPerSample;
309 return fmt;
310}
311
312static bool audio_play_wasapi_ctl(void *state, int request, void *data, size_t *len)
313{
314 auto s = static_cast<struct state_aplay_wasapi *>(state);
315 HRESULT hr;
316 WAVEFORMATEXTENSIBLE fmt;
317 WAVEFORMATEX *closestMatch;
318 switch (request) {
320 if (*len < sizeof(struct audio_desc)) {
321 return false;
322 }
323 struct audio_desc desc;
324 memcpy(&desc, data, sizeof desc);
325 desc.codec = AC_PCM;
326
328 hr = s->pAudioClient->IsFormatSupported(
329 AUDCLNT_SHAREMODE_SHARED,
330 (WAVEFORMATEX *) &fmt,
331 &closestMatch);
332 if (hr != S_OK && hr != S_FALSE) {
333 LOG(LOG_LEVEL_ERROR) << MOD_NAME "Unable to get format: " << hresult_to_str(hr) << "\n";
334 return false;
335 }
336 if (hr == S_FALSE) {
337 desc.ch_count = closestMatch->nChannels;
338 desc.bps = closestMatch->wBitsPerSample / 8;
339 desc.sample_rate = closestMatch->nSamplesPerSec;
340 CoTaskMemFree(closestMatch);
341 }
342
343 memcpy(data, &desc, sizeof desc);
344 *len = sizeof desc;
345 return true;
346 default:
347 return false;
348 }
349}
350
351#define FAIL_IF_NOT(cmd) do {HRESULT hr = cmd; if (hr != S_OK) { LOG(LOG_LEVEL_ERROR) << MOD_NAME << #cmd << ": " << hresult_to_str(hr) << "\n"; return false;}} while(0)
353{
354 auto s = static_cast<struct state_aplay_wasapi *>(state);
355 int buflen_ms = DEFAULT_WASAPI_BUFLEN_MS;
356 if (get_commandline_param("audio-buffer-len") != nullptr) {
357 buflen_ms = atoi(get_commandline_param("audio-buffer-len"));
358 assert(buflen_ms > 0);
359 }
360 REFERENCE_TIME bufferDuration = buflen_ms * REFTIMES_PER_MILLISEC;
361 WAVEFORMATEXTENSIBLE fmt = audio_format_to_waveformatex(&desc);
362 FAIL_IF_NOT(s->pAudioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, 0, bufferDuration, 0,
363 (WAVEFORMATEX *) &fmt, NULL));
364 FAIL_IF_NOT(s->pAudioClient->GetService(IID_IAudioRenderClient, (void **) &s->pRenderClient));
365
366 FAIL_IF_NOT(s->pAudioClient->GetBufferSize(&s->bufferSize));
367 LOG(LOG_LEVEL_INFO) << MOD_NAME "Buffer size: " << s->bufferSize << " frames\n";
368
369 FAIL_IF_NOT(s->pAudioClient->Start());
370
371 return true;
372}
373
374#define CHECK(cmd) do {HRESULT hr = cmd; if (hr != S_OK) { LOG(LOG_LEVEL_ERROR) << MOD_NAME << #cmd ": " << hresult_to_str(hr) << "\n"; return;}} while(0)
375static void audio_play_wasapi_put_frame(void *state, const struct audio_frame *buffer)
376{
377 auto s = static_cast<struct state_aplay_wasapi *>(state);
378 UINT32 numFramesPadding;
379
380 CHECK(s->pAudioClient->GetCurrentPadding(&numFramesPadding));
381
382 UINT32 NumFramesRequested = buffer->data_len / buffer->ch_count / buffer->bps;
383 if (s->bufferSize - numFramesPadding < NumFramesRequested) {
384 LOG(LOG_LEVEL_WARNING) << MOD_NAME "Buffer overflow!\n";
385 NumFramesRequested = s->bufferSize - numFramesPadding;
386 }
387 BYTE *data;
388 CHECK(s->pRenderClient->GetBuffer(NumFramesRequested, &data));
389 memcpy(data, buffer->data, NumFramesRequested * buffer->ch_count * buffer->bps);
390 CHECK(s->pRenderClient->ReleaseBuffer(NumFramesRequested, 0));
391
392}
393
402
404
405/* vim: set expandtab sw=8: */
406
void ** state
struct video_desc desc
const char * fmt
int void size_t * len
int void(** deleter)(void *))
This file contains definition of some common audio types.
@ AC_PCM
Definition types.h:53
#define AUDIO_PLAYBACK_CTL_QUERY_FORMAT
#define AUDIO_PLAYBACK_ABI_VERSION
#define SRED(x)
Definition color_out.h:85
#define SBOLD(x)
Definition color_out.h:80
unsigned char BYTE
Definition crypt_aes.h:71
u32 UINT
Definition cuda_dxt.cu:13
#define LOG_LEVEL_NOTICE
information that may be interesting
Definition debug.h:61
#define LOG_LEVEL_INFO
normal reporting
Definition debug.h:62
#define LOG_LEVEL_ERROR
general errors
Definition debug.h:59
#define LOG(level)
Definition debug.h:313
#define LOG_LEVEL_WARNING
less severe errors
Definition debug.h:60
int count
Definition hd-rum.c:44
const char * get_commandline_param(const char *key)
Definition host.cpp:894
#define INIT_NOERR
Definition host.h:73
@ LIBRARY_CLASS_AUDIO_PLAYBACK
Definition lib_common.h:77
#define REGISTER_MODULE(name, info, lclass, abi)
Registers module to global modules' registry.
Definition lib_common.h:144
static void * audio_play_wasapi_init(const char *cfg)
Definition wasapi.cpp:207
static void audio_play_wasapi_done(void *state)
Definition wasapi.cpp:275
#define DEFAULT_WASAPI_BUFLEN_MS
Definition wasapi.cpp:63
static const PROPERTYKEY PKEY_Device_FriendlyName
Definition wasapi.cpp:73
static const struct audio_playback_info aplay_wasapi_info
Definition wasapi.cpp:394
#define CHECK(cmd)
Definition wasapi.cpp:374
static bool audio_play_wasapi_reconfigure(void *state, struct audio_desc desc)
Definition wasapi.cpp:352
#define THROW_IF_FAILED(cmd)
Definition wasapi.cpp:100
static DWORD get_channel_mask(int *count)
Definition wasapi.cpp:281
static void audio_play_wasapi_put_frame(void *state, const struct audio_frame *buffer)
Definition wasapi.cpp:375
static void audio_play_wasapi_probe(struct device_info **available_devices, int *dev_count, void(**deleter)(void *))
Definition wasapi.cpp:122
#define SAFE_RELEASE(u)
Definition wasapi.cpp:97
const IID IID_IAudioRenderClient
Definition wasapi.cpp:71
static WAVEFORMATEXTENSIBLE audio_format_to_waveformatex(struct audio_desc *desc)
Definition wasapi.cpp:296
const CLSID CLSID_MMDeviceEnumerator
Definition wasapi.cpp:68
const IID IID_IAudioClient
Definition wasapi.cpp:70
#define MOD_NAME
Definition wasapi.cpp:64
string wasapi_get_default_device_id(EDataFlow dataFlow, IMMDeviceEnumerator *enumerator)
Definition wasapi.cpp:177
static const GUID IDevice_FriendlyName
Definition wasapi.cpp:72
#define FAIL_IF_NOT(cmd)
Definition wasapi.cpp:351
static const GUID UG_KSDATAFORMAT_SUBTYPE_PCM
Definition wasapi.cpp:76
static void audio_play_wasapi_help()
Definition wasapi.cpp:166
static bool audio_play_wasapi_ctl(void *state, int request, void *data, size_t *len)
Definition wasapi.cpp:312
const IID IID_IMMDeviceEnumerator
Definition wasapi.cpp:69
static string get_name(IMMDevice *pDevice)
Definition wasapi.cpp:102
#define REFTIMES_PER_MILLISEC
Definition wasapi.cpp:66
const char * name
Definition settings.cpp:193
struct used to comunicate with audio capture/playback device
Definition types.h:99
char * data
Definition types.h:102
int ch_count
Definition types.h:104
int data_len
Definition types.h:103
int bps
Definition types.h:100
char dev[1024]
Definition types.h:383
IAudioRenderClient * pRenderClient
Definition wasapi.cpp:91
struct audio_desc desc
Definition wasapi.cpp:88
IAudioClient * pAudioClient
Definition wasapi.cpp:90
IMMDevice * pDevice
Definition wasapi.cpp:89
This file contains some common data types.
@ FAILED
bool com_initialize(bool *com_initialized, const char *err_prefix)
Definition windows.c:65
void com_uninitialize(bool *com_initialized)
Definition windows.c:94