Atlas - SDL_wasapi.c
Home / ext / SDL / src / audio / wasapi Lines: 1 | Size: 36886 bytes [Download] [Show on GitHub] [Search similar files] [Raw] [Raw (proxy)][FILE BEGIN]1/* 2 Simple DirectMedia Layer 3 Copyright (C) 1997-2025 Sam Lantinga <[email protected]> 4 5 This software is provided 'as-is', without any express or implied 6 warranty. In no event will the authors be held liable for any damages 7 arising from the use of this software. 8 9 Permission is granted to anyone to use this software for any purpose, 10 including commercial applications, and to alter it and redistribute it 11 freely, subject to the following restrictions: 12 13 1. The origin of this software must not be misrepresented; you must not 14 claim that you wrote the original software. If you use this software 15 in a product, an acknowledgment in the product documentation would be 16 appreciated but is not required. 17 2. Altered source versions must be plainly marked as such, and must not be 18 misrepresented as being the original software. 19 3. This notice may not be removed or altered from any source distribution. 20*/ 21#include "SDL_internal.h" 22 23#ifdef SDL_AUDIO_DRIVER_WASAPI 24 25#include "../../core/windows/SDL_windows.h" 26#include "../../core/windows/SDL_immdevice.h" 27#include "../../thread/SDL_systhread.h" 28#include "../SDL_sysaudio.h" 29 30#define COBJMACROS 31#include <audioclient.h> 32 33#include "SDL_wasapi.h" 34 35// These constants aren't available in older SDKs 36#ifndef AUDCLNT_STREAMFLAGS_RATEADJUST 37#define AUDCLNT_STREAMFLAGS_RATEADJUST 0x00100000 38#endif 39#ifndef AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY 40#define AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY 0x08000000 41#endif 42#ifndef AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM 43#define AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM 0x80000000 44#endif 45 46// handle to Avrt.dll--Vista and later!--for flagging the callback thread as "Pro Audio" (low latency). 47static HMODULE libavrt = NULL; 48typedef HANDLE (WINAPI *pfnAvSetMmThreadCharacteristicsW)(LPCWSTR, LPDWORD); 49typedef BOOL (WINAPI *pfnAvRevertMmThreadCharacteristics)(HANDLE); 50static pfnAvSetMmThreadCharacteristicsW pAvSetMmThreadCharacteristicsW = NULL; 51static pfnAvRevertMmThreadCharacteristics pAvRevertMmThreadCharacteristics = NULL; 52 53// Some GUIDs we need to know without linking to libraries that aren't available before Vista. 54static const IID SDL_IID_IAudioRenderClient = { 0xf294acfc, 0x3146, 0x4483, { 0xa7, 0xbf, 0xad, 0xdc, 0xa7, 0xc2, 0x60, 0xe2 } }; 55static const IID SDL_IID_IAudioCaptureClient = { 0xc8adbd64, 0xe71e, 0x48a0, { 0xa4, 0xde, 0x18, 0x5c, 0x39, 0x5c, 0xd3, 0x17 } }; 56static const IID SDL_IID_IAudioClient = { 0x1cb9ad4c, 0xdbfa, 0x4c32, { 0xb1, 0x78, 0xc2, 0xf5, 0x68, 0xa7, 0x03, 0xb2 } }; 57#ifdef __IAudioClient2_INTERFACE_DEFINED__ 58static const IID SDL_IID_IAudioClient2 = { 0x726778cd, 0xf60a, 0x4EDA, { 0x82, 0xde, 0xe4, 0x76, 0x10, 0xcd, 0x78, 0xaa } }; 59#endif // 60#ifdef __IAudioClient3_INTERFACE_DEFINED__ 61static const IID SDL_IID_IAudioClient3 = { 0x7ed4ee07, 0x8e67, 0x4cd4, { 0x8c, 0x1a, 0x2b, 0x7a, 0x59, 0x87, 0xad, 0x42 } }; 62#endif // 63 64static bool immdevice_initialized = false; 65static bool supports_recording_on_playback_devices = false; 66 67// WASAPI is _really_ particular about various things happening on the same thread, for COM and such, 68// so we proxy various stuff to a single background thread to manage. 69 70typedef struct ManagementThreadPendingTask 71{ 72 ManagementThreadTask fn; 73 void *userdata; 74 bool result; 75 SDL_Semaphore *task_complete_sem; 76 char *errorstr; 77 struct ManagementThreadPendingTask *next; 78} ManagementThreadPendingTask; 79 80static SDL_Thread *ManagementThread = NULL; 81static ManagementThreadPendingTask *ManagementThreadPendingTasks = NULL; 82static SDL_Mutex *ManagementThreadLock = NULL; 83static SDL_Condition *ManagementThreadCondition = NULL; 84static SDL_AtomicInt ManagementThreadShutdown; 85 86static void ManagementThreadMainloop(void) 87{ 88 SDL_LockMutex(ManagementThreadLock); 89 ManagementThreadPendingTask *task; 90 while (((task = (ManagementThreadPendingTask *)SDL_GetAtomicPointer((void **)&ManagementThreadPendingTasks)) != NULL) || !SDL_GetAtomicInt(&ManagementThreadShutdown)) { 91 if (!task) { 92 SDL_WaitCondition(ManagementThreadCondition, ManagementThreadLock); // block until there's something to do. 93 } else { 94 SDL_SetAtomicPointer((void **) &ManagementThreadPendingTasks, task->next); // take task off the pending list. 95 SDL_UnlockMutex(ManagementThreadLock); // let other things add to the list while we chew on this task. 96 task->result = task->fn(task->userdata); // run this task. 97 if (task->task_complete_sem) { // something waiting on result? 98 task->errorstr = SDL_strdup(SDL_GetError()); 99 SDL_SignalSemaphore(task->task_complete_sem); 100 } else { // nothing waiting, we're done, free it. 101 SDL_free(task); 102 } 103 SDL_LockMutex(ManagementThreadLock); // regrab the lock so we can get the next task; if nothing to do, we'll release the lock in SDL_WaitCondition. 104 } 105 } 106 SDL_UnlockMutex(ManagementThreadLock); // told to shut down and out of tasks, let go of the lock and return. 107} 108 109bool WASAPI_ProxyToManagementThread(ManagementThreadTask task, void *userdata, bool *wait_on_result) 110{ 111 // We want to block for a result, but we are already running from the management thread! Just run the task now so we don't deadlock. 112 if ((wait_on_result) && (SDL_GetCurrentThreadID() == SDL_GetThreadID(ManagementThread))) { 113 *wait_on_result = task(userdata); 114 return true; // completed! 115 } 116 117 if (SDL_GetAtomicInt(&ManagementThreadShutdown)) { 118 return SDL_SetError("Can't add task, we're shutting down"); 119 } 120 121 ManagementThreadPendingTask *pending = (ManagementThreadPendingTask *)SDL_calloc(1, sizeof(ManagementThreadPendingTask)); 122 if (!pending) { 123 return false; 124 } 125 126 pending->fn = task; 127 pending->userdata = userdata; 128 129 if (wait_on_result) { 130 pending->task_complete_sem = SDL_CreateSemaphore(0); 131 if (!pending->task_complete_sem) { 132 SDL_free(pending); 133 return false; 134 } 135 } 136 137 pending->next = NULL; 138 139 SDL_LockMutex(ManagementThreadLock); 140 141 // add to end of task list. 142 ManagementThreadPendingTask *prev = NULL; 143 for (ManagementThreadPendingTask *i = (ManagementThreadPendingTask *)SDL_GetAtomicPointer((void **)&ManagementThreadPendingTasks); i; i = i->next) { 144 prev = i; 145 } 146 147 if (prev) { 148 prev->next = pending; 149 } else { 150 SDL_SetAtomicPointer((void **) &ManagementThreadPendingTasks, pending); 151 } 152 153 // task is added to the end of the pending list, let management thread rip! 154 SDL_SignalCondition(ManagementThreadCondition); 155 SDL_UnlockMutex(ManagementThreadLock); 156 157 if (wait_on_result) { 158 SDL_WaitSemaphore(pending->task_complete_sem); 159 SDL_DestroySemaphore(pending->task_complete_sem); 160 *wait_on_result = pending->result; 161 if (pending->errorstr) { 162 SDL_SetError("%s", pending->errorstr); 163 SDL_free(pending->errorstr); 164 } 165 SDL_free(pending); 166 } 167 168 return true; // successfully added (and possibly executed)! 169} 170 171 172static void AudioDeviceDisconnected(SDL_AudioDevice *device) 173{ 174 WASAPI_DisconnectDevice(device); 175} 176 177static bool mgmtthrtask_DefaultAudioDeviceChanged(void *userdata) 178{ 179 SDL_AudioDevice *device = (SDL_AudioDevice *) userdata; 180 SDL_DefaultAudioDeviceChanged(device); 181 UnrefPhysicalAudioDevice(device); // make sure this lived until the task completes. 182 return true; 183} 184 185static void DefaultAudioDeviceChanged(SDL_AudioDevice *new_default_device) 186{ 187 // don't wait on this, IMMDevice's own thread needs to return or everything will deadlock. 188 if (new_default_device) { 189 RefPhysicalAudioDevice(new_default_device); // make sure this lives until the task completes. 190 WASAPI_ProxyToManagementThread(mgmtthrtask_DefaultAudioDeviceChanged, new_default_device, NULL); 191 } 192} 193 194static void StopWasapiHotplug(void) 195{ 196 if (immdevice_initialized) { 197 SDL_IMMDevice_Quit(); 198 immdevice_initialized = false; 199 } 200} 201 202static void Deinit(void) 203{ 204 if (libavrt) { 205 FreeLibrary(libavrt); 206 libavrt = NULL; 207 } 208 209 pAvSetMmThreadCharacteristicsW = NULL; 210 pAvRevertMmThreadCharacteristics = NULL; 211 212 StopWasapiHotplug(); 213 214 WIN_CoUninitialize(); 215} 216 217static bool ManagementThreadPrepare(void) 218{ 219 const SDL_IMMDevice_callbacks callbacks = { AudioDeviceDisconnected, DefaultAudioDeviceChanged }; 220 if (FAILED(WIN_CoInitialize())) { 221 return SDL_SetError("CoInitialize() failed"); 222 } else if (!SDL_IMMDevice_Init(&callbacks)) { 223 return false; // Error string is set by SDL_IMMDevice_Init 224 } 225 226 immdevice_initialized = true; 227 228 libavrt = LoadLibrary(TEXT("avrt.dll")); // this library is available in Vista and later. No WinXP, so have to LoadLibrary to use it for now! 229 if (libavrt) { 230 pAvSetMmThreadCharacteristicsW = (pfnAvSetMmThreadCharacteristicsW)GetProcAddress(libavrt, "AvSetMmThreadCharacteristicsW"); 231 pAvRevertMmThreadCharacteristics = (pfnAvRevertMmThreadCharacteristics)GetProcAddress(libavrt, "AvRevertMmThreadCharacteristics"); 232 } 233 234 ManagementThreadLock = SDL_CreateMutex(); 235 if (!ManagementThreadLock) { 236 Deinit(); 237 return false; 238 } 239 240 ManagementThreadCondition = SDL_CreateCondition(); 241 if (!ManagementThreadCondition) { 242 SDL_DestroyMutex(ManagementThreadLock); 243 ManagementThreadLock = NULL; 244 Deinit(); 245 return false; 246 } 247 248 return true; 249} 250 251typedef struct 252{ 253 char *errorstr; 254 SDL_Semaphore *ready_sem; 255} ManagementThreadEntryData; 256 257static int ManagementThreadEntry(void *userdata) 258{ 259 ManagementThreadEntryData *data = (ManagementThreadEntryData *)userdata; 260 261 if (!ManagementThreadPrepare()) { 262 data->errorstr = SDL_strdup(SDL_GetError()); 263 SDL_SignalSemaphore(data->ready_sem); // unblock calling thread. 264 return 0; 265 } 266 267 SDL_SignalSemaphore(data->ready_sem); // unblock calling thread. 268 ManagementThreadMainloop(); 269 270 Deinit(); 271 return 0; 272} 273 274static bool InitManagementThread(void) 275{ 276 ManagementThreadEntryData mgmtdata; 277 SDL_zero(mgmtdata); 278 mgmtdata.ready_sem = SDL_CreateSemaphore(0); 279 if (!mgmtdata.ready_sem) { 280 return false; 281 } 282 283 SDL_SetAtomicPointer((void **) &ManagementThreadPendingTasks, NULL); 284 SDL_SetAtomicInt(&ManagementThreadShutdown, 0); 285 ManagementThread = SDL_CreateThreadWithStackSize(ManagementThreadEntry, "SDLWASAPIMgmt", 256 * 1024, &mgmtdata); // !!! FIXME: maybe even smaller stack size? 286 if (!ManagementThread) { 287 return false; 288 } 289 290 SDL_WaitSemaphore(mgmtdata.ready_sem); 291 SDL_DestroySemaphore(mgmtdata.ready_sem); 292 293 if (mgmtdata.errorstr) { 294 SDL_WaitThread(ManagementThread, NULL); 295 ManagementThread = NULL; 296 SDL_SetError("%s", mgmtdata.errorstr); 297 SDL_free(mgmtdata.errorstr); 298 return false; 299 } 300 301 return true; 302} 303 304static void DeinitManagementThread(void) 305{ 306 if (ManagementThread) { 307 SDL_SetAtomicInt(&ManagementThreadShutdown, 1); 308 SDL_LockMutex(ManagementThreadLock); 309 SDL_SignalCondition(ManagementThreadCondition); 310 SDL_UnlockMutex(ManagementThreadLock); 311 SDL_WaitThread(ManagementThread, NULL); 312 ManagementThread = NULL; 313 } 314 315 SDL_assert(SDL_GetAtomicPointer((void **) &ManagementThreadPendingTasks) == NULL); 316 317 SDL_DestroyCondition(ManagementThreadCondition); 318 SDL_DestroyMutex(ManagementThreadLock); 319 ManagementThreadCondition = NULL; 320 ManagementThreadLock = NULL; 321 SDL_SetAtomicInt(&ManagementThreadShutdown, 0); 322} 323 324typedef struct 325{ 326 SDL_AudioDevice **default_playback; 327 SDL_AudioDevice **default_recording; 328} mgmtthrtask_DetectDevicesData; 329 330static bool mgmtthrtask_DetectDevices(void *userdata) 331{ 332 mgmtthrtask_DetectDevicesData *data = (mgmtthrtask_DetectDevicesData *)userdata; 333 SDL_IMMDevice_EnumerateEndpoints(data->default_playback, data->default_recording, SDL_AUDIO_F32, supports_recording_on_playback_devices); 334 return true; 335} 336 337static void WASAPI_DetectDevices(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording) 338{ 339 bool rc; 340 // this blocks because it needs to finish before the audio subsystem inits 341 mgmtthrtask_DetectDevicesData data; 342 data.default_playback = default_playback; 343 data.default_recording = default_recording; 344 WASAPI_ProxyToManagementThread(mgmtthrtask_DetectDevices, &data, &rc); 345} 346 347void WASAPI_DisconnectDevice(SDL_AudioDevice *device) 348{ 349 // don't block in here; IMMDevice's own thread needs to return or everything will deadlock. 350 if (device && (!device->hidden || SDL_CompareAndSwapAtomicInt(&device->hidden->device_disconnecting, 0, 1))) { 351 SDL_AudioDeviceDisconnected(device); // this proxies the work to the main thread now, so no point in proxying to the management thread. 352 } 353} 354 355static bool WasapiFailed(SDL_AudioDevice *device, const HRESULT err) 356{ 357 if (err == S_OK) { 358 return false; 359 } else if (err == AUDCLNT_E_DEVICE_INVALIDATED) { 360 device->hidden->device_lost = true; 361 } else { 362 device->hidden->device_dead = true; 363 } 364 365 return true; 366} 367 368static bool mgmtthrtask_StopAndReleaseClient(void *userdata) 369{ 370 IAudioClient *client = (IAudioClient *) userdata; 371 IAudioClient_Stop(client); 372 IAudioClient_Release(client); 373 return true; 374} 375 376static bool mgmtthrtask_ReleaseCaptureClient(void *userdata) 377{ 378 IAudioCaptureClient_Release((IAudioCaptureClient *)userdata); 379 return true; 380} 381 382static bool mgmtthrtask_ReleaseRenderClient(void *userdata) 383{ 384 IAudioRenderClient_Release((IAudioRenderClient *)userdata); 385 return true; 386} 387 388static bool mgmtthrtask_CoTaskMemFree(void *userdata) 389{ 390 CoTaskMemFree(userdata); 391 return true; 392} 393 394static bool mgmtthrtask_CloseHandle(void *userdata) 395{ 396 CloseHandle((HANDLE) userdata); 397 return true; 398} 399 400static void ResetWasapiDevice(SDL_AudioDevice *device) 401{ 402 if (!device || !device->hidden) { 403 return; 404 } 405 406 // just queue up all the tasks in the management thread and don't block. 407 // We don't care when any of these actually get free'd. 408 409 if (device->hidden->client) { 410 IAudioClient *client = device->hidden->client; 411 device->hidden->client = NULL; 412 WASAPI_ProxyToManagementThread(mgmtthrtask_StopAndReleaseClient, client, NULL); 413 } 414 415 if (device->hidden->render) { 416 IAudioRenderClient *render = device->hidden->render; 417 device->hidden->render = NULL; 418 WASAPI_ProxyToManagementThread(mgmtthrtask_ReleaseRenderClient, render, NULL); 419 } 420 421 if (device->hidden->capture) { 422 IAudioCaptureClient *capture = device->hidden->capture; 423 device->hidden->capture = NULL; 424 WASAPI_ProxyToManagementThread(mgmtthrtask_ReleaseCaptureClient, capture, NULL); 425 } 426 427 if (device->hidden->waveformat) { 428 void *ptr = device->hidden->waveformat; 429 device->hidden->waveformat = NULL; 430 WASAPI_ProxyToManagementThread(mgmtthrtask_CoTaskMemFree, ptr, NULL); 431 } 432 433 if (device->hidden->event) { 434 HANDLE event = device->hidden->event; 435 device->hidden->event = NULL; 436 WASAPI_ProxyToManagementThread(mgmtthrtask_CloseHandle, (void *) event, NULL); 437 } 438} 439 440static bool mgmtthrtask_ActivateDevice(void *userdata) 441{ 442 SDL_AudioDevice *device = (SDL_AudioDevice *) userdata; 443 444 IMMDevice *immdevice = NULL; 445 if (!SDL_IMMDevice_Get(device, &immdevice, device->recording)) { 446 device->hidden->client = NULL; 447 return false; // This is already set by SDL_IMMDevice_Get 448 } 449 450 device->hidden->isplayback = !SDL_IMMDevice_GetIsCapture(immdevice); 451 452 // this is _not_ async in standard win32, yay! 453 HRESULT ret = IMMDevice_Activate(immdevice, &SDL_IID_IAudioClient, CLSCTX_ALL, NULL, (void **)&device->hidden->client); 454 IMMDevice_Release(immdevice); 455 456 if (FAILED(ret)) { 457 SDL_assert(device->hidden->client == NULL); 458 return WIN_SetErrorFromHRESULT("WASAPI can't activate audio endpoint", ret); 459 } 460 461 SDL_assert(device->hidden->client != NULL); 462 if (!WASAPI_PrepDevice(device)) { // not async, fire it right away. 463 return false; 464 } 465 466 return true; // good to go. 467} 468 469static bool ActivateWasapiDevice(SDL_AudioDevice *device) 470{ 471 // this blocks because we're either being notified from a background thread or we're running during device open, 472 // both of which won't deadlock vs the device thread. 473 bool rc = false; 474 return (WASAPI_ProxyToManagementThread(mgmtthrtask_ActivateDevice, device, &rc) && rc); 475} 476 477// do not call when holding the device lock! 478static bool RecoverWasapiDevice(SDL_AudioDevice *device) 479{ 480 ResetWasapiDevice(device); // dump the lost device's handles. 481 482 // This handles a non-default device that simply had its format changed in the Windows Control Panel. 483 if (!ActivateWasapiDevice(device)) { 484 WASAPI_DisconnectDevice(device); 485 return false; 486 } 487 488 device->hidden->device_lost = false; 489 490 return true; // okay, carry on with new device details! 491} 492 493// do not call when holding the device lock! 494static bool RecoverWasapiIfLost(SDL_AudioDevice *device) 495{ 496 if (SDL_GetAtomicInt(&device->shutdown)) { 497 return false; // closing, stop trying. 498 } else if (SDL_GetAtomicInt(&device->hidden->device_disconnecting)) { 499 return false; // failing via the WASAPI management thread, stop trying. 500 } else if (device->hidden->device_dead) { // had a fatal error elsewhere, clean up and quit 501 IAudioClient_Stop(device->hidden->client); 502 WASAPI_DisconnectDevice(device); 503 SDL_assert(SDL_GetAtomicInt(&device->shutdown)); // so we don't come back through here. 504 return false; // already failed. 505 } else if (SDL_GetAtomicInt(&device->zombie)) { 506 return false; // we're already dead, so just leave and let the Zombie implementations take over. 507 } else if (!device->hidden->client) { 508 return true; // still waiting for activation. 509 } 510 511 return device->hidden->device_lost ? RecoverWasapiDevice(device) : true; 512} 513 514static Uint8 *WASAPI_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size) 515{ 516 // get an endpoint buffer from WASAPI. 517 BYTE *buffer = NULL; 518 519 if (device->hidden->render) { 520 const HRESULT ret = IAudioRenderClient_GetBuffer(device->hidden->render, device->sample_frames, &buffer); 521 if (ret == AUDCLNT_E_BUFFER_TOO_LARGE) { 522 SDL_assert(buffer == NULL); 523 *buffer_size = 0; // just go back to WaitDevice and try again after the hardware has consumed some more data. 524 } else if (WasapiFailed(device, ret)) { 525 SDL_assert(buffer == NULL); 526 if (device->hidden->device_lost) { // just use an available buffer, we won't be playing it anyhow. 527 *buffer_size = 0; // we'll recover during WaitDevice and try again. 528 } 529 } 530 } 531 532 return (Uint8 *)buffer; 533} 534 535static bool WASAPI_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buflen) 536{ 537 if (device->hidden->render && !SDL_GetAtomicInt(&device->hidden->device_disconnecting)) { // definitely activated? 538 // WasapiFailed() will mark the device for reacquisition or removal elsewhere. 539 WasapiFailed(device, IAudioRenderClient_ReleaseBuffer(device->hidden->render, device->sample_frames, 0)); 540 } 541 return true; 542} 543 544static bool WASAPI_WaitDevice(SDL_AudioDevice *device) 545{ 546 // WaitDevice does not hold the device lock, so check for recovery/disconnect details here. 547 while (RecoverWasapiIfLost(device) && device->hidden->client && device->hidden->event) { 548 if (device->recording) { 549 // Recording devices should return immediately if there is any data available 550 UINT32 padding = 0; 551 if (!WasapiFailed(device, IAudioClient_GetCurrentPadding(device->hidden->client, &padding))) { 552 //SDL_Log("WASAPI EVENT! padding=%u maxpadding=%u", (unsigned int)padding, (unsigned int)maxpadding); 553 if (padding > 0) { 554 break; 555 } 556 } 557 558 switch (WaitForSingleObjectEx(device->hidden->event, 200, FALSE)) { 559 case WAIT_OBJECT_0: 560 case WAIT_TIMEOUT: 561 break; 562 563 default: 564 //SDL_Log("WASAPI FAILED EVENT!"); 565 IAudioClient_Stop(device->hidden->client); 566 return false; 567 } 568 } else { 569 DWORD waitResult = WaitForSingleObjectEx(device->hidden->event, 200, FALSE); 570 if (waitResult == WAIT_OBJECT_0) { 571 UINT32 padding = 0; 572 if (!WasapiFailed(device, IAudioClient_GetCurrentPadding(device->hidden->client, &padding))) { 573 //SDL_Log("WASAPI EVENT! padding=%u maxpadding=%u", (unsigned int)padding, (unsigned int)maxpadding); 574 if (padding <= (UINT32)device->sample_frames) { 575 break; 576 } 577 } 578 } else if (waitResult != WAIT_TIMEOUT) { 579 //SDL_Log("WASAPI FAILED EVENT!");*/ 580 IAudioClient_Stop(device->hidden->client); 581 return false; 582 } 583 } 584 } 585 586 return true; 587} 588 589static int WASAPI_RecordDevice(SDL_AudioDevice *device, void *buffer, int buflen) 590{ 591 BYTE *ptr = NULL; 592 UINT32 frames = 0; 593 DWORD flags = 0; 594 595 while (device->hidden->capture) { 596 const HRESULT ret = IAudioCaptureClient_GetBuffer(device->hidden->capture, &ptr, &frames, &flags, NULL, NULL); 597 if (ret == AUDCLNT_S_BUFFER_EMPTY) { 598 return 0; // in theory we should have waited until there was data, but oh well, we'll go back to waiting. Returning 0 is safe in SDL3. 599 } 600 601 WasapiFailed(device, ret); // mark device lost/failed if necessary. 602 603 if (ret == S_OK) { 604 const int total = ((int)frames) * device->hidden->framesize; 605 const int cpy = SDL_min(buflen, total); 606 const int leftover = total - cpy; 607 const bool silent = (flags & AUDCLNT_BUFFERFLAGS_SILENT) ? true : false; 608 609 SDL_assert(leftover == 0); // according to MSDN, this isn't everything available, just one "packet" of data per-GetBuffer call. 610 611 if (silent) { 612 SDL_memset(buffer, device->silence_value, cpy); 613 } else { 614 SDL_memcpy(buffer, ptr, cpy); 615 } 616 617 WasapiFailed(device, IAudioCaptureClient_ReleaseBuffer(device->hidden->capture, frames)); 618 619 return cpy; 620 } 621 } 622 623 return -1; // unrecoverable error. 624} 625 626static void WASAPI_FlushRecording(SDL_AudioDevice *device) 627{ 628 BYTE *ptr = NULL; 629 UINT32 frames = 0; 630 DWORD flags = 0; 631 632 // just read until we stop getting packets, throwing them away. 633 while (!SDL_GetAtomicInt(&device->shutdown) && device->hidden->capture) { 634 const HRESULT ret = IAudioCaptureClient_GetBuffer(device->hidden->capture, &ptr, &frames, &flags, NULL, NULL); 635 if (ret == AUDCLNT_S_BUFFER_EMPTY) { 636 break; // no more buffered data; we're done. 637 } else if (WasapiFailed(device, ret)) { 638 break; // failed for some other reason, abort. 639 } else if (WasapiFailed(device, IAudioCaptureClient_ReleaseBuffer(device->hidden->capture, frames))) { 640 break; // something broke. 641 } 642 } 643} 644 645static void WASAPI_CloseDevice(SDL_AudioDevice *device) 646{ 647 if (device->hidden) { 648 ResetWasapiDevice(device); 649 SDL_free(device->hidden->devid); 650 SDL_free(device->hidden); 651 device->hidden = NULL; 652 } 653} 654 655static bool mgmtthrtask_PrepDevice(void *userdata) 656{ 657 SDL_AudioDevice *device = (SDL_AudioDevice *)userdata; 658 659 /* !!! FIXME: we could request an exclusive mode stream, which is lower latency; 660 !!! it will write into the kernel's audio buffer directly instead of 661 !!! shared memory that a user-mode mixer then writes to the kernel with 662 !!! everything else. Doing this means any other sound using this device will 663 !!! stop playing, including the user's MP3 player and system notification 664 !!! sounds. You'd probably need to release the device when the app isn't in 665 !!! the foreground, to be a good citizen of the system. It's doable, but it's 666 !!! more work and causes some annoyances, and I don't know what the latency 667 !!! wins actually look like. Maybe add a hint to force exclusive mode at 668 !!! some point. To be sure, defaulting to shared mode is the right thing to 669 !!! do in any case. */ 670 const AUDCLNT_SHAREMODE sharemode = AUDCLNT_SHAREMODE_SHARED; 671 672 IAudioClient *client = device->hidden->client; 673 SDL_assert(client != NULL); 674 675 device->hidden->event = CreateEvent(NULL, FALSE, FALSE, NULL); 676 if (!device->hidden->event) { 677 return WIN_SetError("WASAPI can't create an event handle"); 678 } 679 680 HRESULT ret; 681 682 WAVEFORMATEX *waveformat = NULL; 683 ret = IAudioClient_GetMixFormat(client, &waveformat); 684 if (FAILED(ret)) { 685 return WIN_SetErrorFromHRESULT("WASAPI can't determine mix format", ret); 686 } 687 SDL_assert(waveformat != NULL); 688 device->hidden->waveformat = waveformat; 689 690 SDL_AudioSpec newspec; 691 newspec.channels = (Uint8)waveformat->nChannels; 692 693 // Make sure we have a valid format that we can convert to whatever WASAPI wants. 694 const SDL_AudioFormat wasapi_format = SDL_WaveFormatExToSDLFormat(waveformat); 695 696 SDL_AudioFormat test_format; 697 const SDL_AudioFormat *closefmts = SDL_ClosestAudioFormats(device->spec.format); 698 while ((test_format = *(closefmts++)) != 0) { 699 if (test_format == wasapi_format) { 700 newspec.format = test_format; 701 break; 702 } 703 } 704 705 if (!test_format) { 706 return SDL_SetError("%s: Unsupported audio format", "wasapi"); 707 } 708 709 REFERENCE_TIME default_period = 0; 710 ret = IAudioClient_GetDevicePeriod(client, &default_period, NULL); 711 if (FAILED(ret)) { 712 return WIN_SetErrorFromHRESULT("WASAPI can't determine minimum device period", ret); 713 } 714 715 DWORD streamflags = 0; 716 717 /* we've gotten reports that WASAPI's resampler introduces distortions, but in the short term 718 it fixes some other WASAPI-specific quirks we haven't quite tracked down. 719 Refer to bug #6326 for the immediate concern. */ 720#if 1 721 // favor WASAPI's resampler over our own 722 if ((DWORD)device->spec.freq != waveformat->nSamplesPerSec) { 723 streamflags |= (AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY); 724 waveformat->nSamplesPerSec = device->spec.freq; 725 waveformat->nAvgBytesPerSec = waveformat->nSamplesPerSec * waveformat->nChannels * (waveformat->wBitsPerSample / 8); 726 } 727#endif 728 729 newspec.freq = waveformat->nSamplesPerSec; 730 731 if (device->recording && device->hidden->isplayback) { 732 streamflags |= AUDCLNT_STREAMFLAGS_LOOPBACK; 733 } 734 735 streamflags |= AUDCLNT_STREAMFLAGS_EVENTCALLBACK; 736 737 int new_sample_frames = 0; 738 bool iaudioclient3_initialized = false; 739 740#ifdef __IAudioClient2_INTERFACE_DEFINED__ 741 IAudioClient2 *client2 = NULL; 742 ret = IAudioClient_QueryInterface(client, &SDL_IID_IAudioClient2, (void **)&client2); 743 if (SUCCEEDED(ret)) { 744 AudioClientProperties audioProps; 745 746 SDL_zero(audioProps); 747 audioProps.cbSize = sizeof(audioProps); 748 749// Setting AudioCategory_GameChat breaks audio on several devices, including Behringer U-PHORIA UM2 and RODE NT-USB Mini. 750// We'll disable this for now until we understand more about what's happening. 751#if 0 752 const char *hint = SDL_GetHint(SDL_HINT_AUDIO_DEVICE_STREAM_ROLE); 753 if (hint && *hint) { 754 if (SDL_strcasecmp(hint, "Communications") == 0) { 755 audioProps.eCategory = AudioCategory_Communications; 756 } else if (SDL_strcasecmp(hint, "Game") == 0) { 757 // We'll add support for GameEffects as distinct from GameMedia later when we add stream roles 758 audioProps.eCategory = AudioCategory_GameEffects; 759 } else if (SDL_strcasecmp(hint, "GameChat") == 0) { 760 audioProps.eCategory = AudioCategory_GameChat; 761 } else if (SDL_strcasecmp(hint, "Movie") == 0) { 762 audioProps.eCategory = AudioCategory_Movie; 763 } else if (SDL_strcasecmp(hint, "Media") == 0) { 764 audioProps.eCategory = AudioCategory_Media; 765 } 766 } 767#endif 768 769 if (SDL_GetHintBoolean(SDL_HINT_AUDIO_DEVICE_RAW_STREAM, false)) { 770 audioProps.Options = AUDCLNT_STREAMOPTIONS_RAW; 771 } 772 773 ret = IAudioClient2_SetClientProperties(client2, &audioProps); 774 if (FAILED(ret)) { 775 // This isn't fatal, let's log it instead of failing 776 SDL_LogWarn(SDL_LOG_CATEGORY_AUDIO, "IAudioClient2_SetClientProperties failed: 0x%lx", ret); 777 } 778 IAudioClient2_Release(client2); 779 } 780#endif 781 782#ifdef __IAudioClient3_INTERFACE_DEFINED__ 783 // Try querying IAudioClient3 if sharemode is AUDCLNT_SHAREMODE_SHARED 784 if (sharemode == AUDCLNT_SHAREMODE_SHARED) { 785 IAudioClient3 *client3 = NULL; 786 ret = IAudioClient_QueryInterface(client, &SDL_IID_IAudioClient3, (void **)&client3); 787 if (SUCCEEDED(ret)) { 788 UINT32 default_period_in_frames = 0; 789 UINT32 fundamental_period_in_frames = 0; 790 UINT32 min_period_in_frames = 0; 791 UINT32 max_period_in_frames = 0; 792 ret = IAudioClient3_GetSharedModeEnginePeriod(client3, waveformat, 793 &default_period_in_frames, &fundamental_period_in_frames, &min_period_in_frames, &max_period_in_frames); 794 if (SUCCEEDED(ret)) { 795 // IAudioClient3_InitializeSharedAudioStream only accepts the integral multiple of fundamental_period_in_frames 796 UINT32 period_in_frames = fundamental_period_in_frames * (UINT32)SDL_round((double)device->sample_frames / fundamental_period_in_frames); 797 period_in_frames = SDL_clamp(period_in_frames, min_period_in_frames, max_period_in_frames); 798 799 ret = IAudioClient3_InitializeSharedAudioStream(client3, streamflags, period_in_frames, waveformat, NULL); 800 if (SUCCEEDED(ret)) { 801 new_sample_frames = (int)period_in_frames; 802 iaudioclient3_initialized = true; 803 } 804 } 805 806 IAudioClient3_Release(client3); 807 } 808 } 809#endif 810 811 if (!iaudioclient3_initialized) 812 ret = IAudioClient_Initialize(client, sharemode, streamflags, 0, 0, waveformat, NULL); 813 814 if (FAILED(ret)) { 815 return WIN_SetErrorFromHRESULT("WASAPI can't initialize audio client", ret); 816 } 817 818 ret = IAudioClient_SetEventHandle(client, device->hidden->event); 819 if (FAILED(ret)) { 820 return WIN_SetErrorFromHRESULT("WASAPI can't set event handle", ret); 821 } 822 823 UINT32 bufsize = 0; // this is in sample frames, not samples, not bytes. 824 ret = IAudioClient_GetBufferSize(client, &bufsize); 825 if (FAILED(ret)) { 826 return WIN_SetErrorFromHRESULT("WASAPI can't determine buffer size", ret); 827 } 828 829 // Match the callback size to the period size to cut down on the number of 830 // interrupts waited for in each call to WaitDevice 831 if (new_sample_frames <= 0) { 832 const float period_millis = default_period / 10000.0f; 833 const float period_frames = period_millis * newspec.freq / 1000.0f; 834 new_sample_frames = (int) SDL_ceilf(period_frames); 835 } 836 837 // regardless of what we calculated for the period size, clamp it to the expected hardware buffer size. 838 if (new_sample_frames > (int) bufsize) { 839 new_sample_frames = (int) bufsize; 840 } 841 842 // Update the fragment size as size in bytes 843 if (!SDL_AudioDeviceFormatChangedAlreadyLocked(device, &newspec, new_sample_frames)) { 844 return false; 845 } 846 847 device->hidden->framesize = SDL_AUDIO_FRAMESIZE(device->spec); 848 849 if (device->recording) { 850 IAudioCaptureClient *capture = NULL; 851 ret = IAudioClient_GetService(client, &SDL_IID_IAudioCaptureClient, (void **)&capture); 852 if (FAILED(ret)) { 853 return WIN_SetErrorFromHRESULT("WASAPI can't get capture client service", ret); 854 } 855 856 SDL_assert(capture != NULL); 857 device->hidden->capture = capture; 858 ret = IAudioClient_Start(client); 859 if (FAILED(ret)) { 860 return WIN_SetErrorFromHRESULT("WASAPI can't start capture", ret); 861 } 862 863 WASAPI_FlushRecording(device); // MSDN says you should flush the recording endpoint right after startup. 864 } else { 865 IAudioRenderClient *render = NULL; 866 ret = IAudioClient_GetService(client, &SDL_IID_IAudioRenderClient, (void **)&render); 867 if (FAILED(ret)) { 868 return WIN_SetErrorFromHRESULT("WASAPI can't get render client service", ret); 869 } 870 871 SDL_assert(render != NULL); 872 device->hidden->render = render; 873 ret = IAudioClient_Start(client); 874 if (FAILED(ret)) { 875 return WIN_SetErrorFromHRESULT("WASAPI can't start playback", ret); 876 } 877 } 878 879 return true; // good to go. 880} 881 882// This is called once a device is activated, possibly asynchronously. 883bool WASAPI_PrepDevice(SDL_AudioDevice *device) 884{ 885 bool rc = true; 886 return (WASAPI_ProxyToManagementThread(mgmtthrtask_PrepDevice, device, &rc) && rc); 887} 888 889static bool WASAPI_OpenDevice(SDL_AudioDevice *device) 890{ 891 // Initialize all variables that we clean on shutdown 892 device->hidden = (struct SDL_PrivateAudioData *) SDL_calloc(1, sizeof(*device->hidden)); 893 if (!device->hidden) { 894 return false; 895 } else if (!ActivateWasapiDevice(device)) { 896 return false; // already set error. 897 } 898 899 /* Ready, but possibly waiting for async device activation. 900 Until activation is successful, we will report silence from recording 901 devices and ignore data on playback devices. Upon activation, we'll make 902 sure any bound audio streams are adjusted for the final device format. */ 903 904 return true; 905} 906 907static void WASAPI_ThreadInit(SDL_AudioDevice *device) 908{ 909 // this thread uses COM. 910 if (SUCCEEDED(WIN_CoInitialize())) { // can't report errors, hope it worked! 911 device->hidden->coinitialized = true; 912 } 913 914 // Set this thread to very high "Pro Audio" priority. 915 if (pAvSetMmThreadCharacteristicsW) { 916 DWORD idx = 0; 917 device->hidden->task = pAvSetMmThreadCharacteristicsW(L"Pro Audio", &idx); 918 } else { 919 SDL_SetCurrentThreadPriority(device->recording ? SDL_THREAD_PRIORITY_HIGH : SDL_THREAD_PRIORITY_TIME_CRITICAL); 920 } 921} 922 923static void WASAPI_ThreadDeinit(SDL_AudioDevice *device) 924{ 925 // Set this thread back to normal priority. 926 if (device->hidden->task && pAvRevertMmThreadCharacteristics) { 927 pAvRevertMmThreadCharacteristics(device->hidden->task); 928 device->hidden->task = NULL; 929 } 930 931 if (device->hidden->coinitialized) { 932 WIN_CoUninitialize(); 933 device->hidden->coinitialized = false; 934 } 935} 936 937static bool mgmtthrtask_FreeDeviceHandle(void *userdata) 938{ 939 SDL_IMMDevice_FreeDeviceHandle((SDL_AudioDevice *) userdata); 940 return true; 941} 942 943static void WASAPI_FreeDeviceHandle(SDL_AudioDevice *device) 944{ 945 bool rc; 946 WASAPI_ProxyToManagementThread(mgmtthrtask_FreeDeviceHandle, device, &rc); 947} 948 949static bool mgmtthrtask_DeinitializeStart(void *userdata) 950{ 951 StopWasapiHotplug(); 952 return true; 953} 954 955static void WASAPI_DeinitializeStart(void) 956{ 957 bool rc; 958 WASAPI_ProxyToManagementThread(mgmtthrtask_DeinitializeStart, NULL, &rc); 959} 960 961static void WASAPI_Deinitialize(void) 962{ 963 DeinitManagementThread(); 964} 965 966static bool WASAPI_Init(SDL_AudioDriverImpl *impl) 967{ 968 if (!InitManagementThread()) { 969 return false; 970 } 971 972 impl->DetectDevices = WASAPI_DetectDevices; 973 impl->ThreadInit = WASAPI_ThreadInit; 974 impl->ThreadDeinit = WASAPI_ThreadDeinit; 975 impl->OpenDevice = WASAPI_OpenDevice; 976 impl->PlayDevice = WASAPI_PlayDevice; 977 impl->WaitDevice = WASAPI_WaitDevice; 978 impl->GetDeviceBuf = WASAPI_GetDeviceBuf; 979 impl->WaitRecordingDevice = WASAPI_WaitDevice; 980 impl->RecordDevice = WASAPI_RecordDevice; 981 impl->FlushRecording = WASAPI_FlushRecording; 982 impl->CloseDevice = WASAPI_CloseDevice; 983 impl->DeinitializeStart = WASAPI_DeinitializeStart; 984 impl->Deinitialize = WASAPI_Deinitialize; 985 impl->FreeDeviceHandle = WASAPI_FreeDeviceHandle; 986 987 impl->HasRecordingSupport = true; 988 supports_recording_on_playback_devices = SDL_GetHintBoolean(SDL_HINT_AUDIO_INCLUDE_MONITORS, false); 989 990 return true; 991} 992 993AudioBootStrap WASAPI_bootstrap = { 994 "wasapi", "WASAPI", WASAPI_Init, false, false 995}; 996 997#endif // SDL_AUDIO_DRIVER_WASAPI 998[FILE END](C) 2025 0x4248 (C) 2025 4248 Media and 4248 Systems, All part of 0x4248 See LICENCE files for more information. Not all files are by 0x4248 always check Licencing.