From a08a37643382d5136242971335c0344064479c21 Mon Sep 17 00:00:00 2001 From: David Reid Date: Mon, 31 Dec 2018 13:38:47 +1000 Subject: [PATCH] Experimental work on a blocking/synchronous API. This work is isolated to WASAPI for now while I figure out requirements and the API. This will be the basis for an improved main loop in the audio thread. --- mini_al.h | 402 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 385 insertions(+), 17 deletions(-) diff --git a/mini_al.h b/mini_al.h index 75774a57..fe56ebc6 100644 --- a/mini_al.h +++ b/mini_al.h @@ -1554,6 +1554,8 @@ struct mal_context mal_result (* onDeviceReinit )(mal_device* pDevice); mal_result (* onDeviceStart )(mal_device* pDevice); mal_result (* onDeviceStop )(mal_device* pDevice); + mal_result (* onDeviceWrite )(mal_device* pDevice, mal_uint32 pcmFrameCount, const void* pPCMFrames, mal_uint32* pPCMFramesWritten); /* Data is in internal device format. */ + mal_result (* onDeviceRead )(mal_device* pDevice, mal_uint32 pcmFrameCount, void* pPCMFrames, mal_uint32* pPCMFramesRead); /* Data is in internal device format. */ mal_result (* onDeviceBreakMainLoop)(mal_device* pDevice); mal_result (* onDeviceMainLoop )(mal_device* pDevice); @@ -1937,8 +1939,17 @@ MAL_ALIGNED_STRUCT(MAL_SIMD_ALIGNMENT) mal_device /*IAudioCaptureClient**/ mal_ptr pCaptureClient; /*IMMDeviceEnumerator**/ mal_ptr pDeviceEnumerator; /* <-- Used for IMMNotificationClient notifications. Required for detecting default device changes. */ mal_IMMNotificationClient notificationClient; + /*HANDLE*/ mal_handle hEventPlayback; /* Used with the blocking API. Manual reset. Initialized to signaled. */ + /*HANDLE*/ mal_handle hEventCapture; /* Used with the blocking API. Manual reset. Initialized to unsignaled. */ /*HANDLE*/ mal_handle hEvent; /*HANDLE*/ mal_handle hBreakEvent; /* <-- Used to break from WaitForMultipleObjects() in the main loop. */ + void* pDeviceBufferPlayback; + void* pDeviceBufferCapture; + mal_uint32 deviceBufferFramesRemainingPlayback; + mal_uint32 deviceBufferFramesRemainingCapture; + mal_uint32 deviceBufferFramesCapacityPlayback; + mal_uint32 deviceBufferFramesCapacityCapture; + mal_bool32 isStarted; mal_bool32 breakFromMainLoop; mal_bool32 hasDefaultDeviceChanged; /* <-- Make sure this is always a whole 32-bits because we use atomic assignments. */ } wasapi; @@ -2283,6 +2294,14 @@ mal_result mal_device_init_ex(const mal_backend backends[], mal_uint32 backendCo // try using the device at the same time as uninitializing it. void mal_device_uninit(mal_device* pDevice); + +// Writes PCM frames to the device. +mal_result mal_device_write(mal_device* pDevice, mal_uint32 pcmFrameCount, const void* pPCMFrames, mal_uint32* pPCMFramesWritten); + +// Reads PCM frames from the device. +mal_result mal_device_read(mal_device* pDevice, mal_uint32 pcmFrameCount, void* pPCMFrames, mal_uint32* pPCMFramesRead); + + // Sets the callback to use when the device has stopped, either explicitly or as a result of an error. // // Thread Safety: SAFE @@ -6393,6 +6412,13 @@ void mal_device_uninit__wasapi(mal_device* pDevice) mal_IAudioClient_Release((mal_IAudioClient*)pDevice->wasapi.pAudioClient); } + if (pDevice->wasapi.hEventPlayback) { + CloseHandle(pDevice->wasapi.hEventPlayback); + } + if (pDevice->wasapi.hEventCapture) { + CloseHandle(pDevice->wasapi.hEventCapture); + } + if (pDevice->wasapi.hEvent) { CloseHandle(pDevice->wasapi.hEvent); } @@ -6857,16 +6883,54 @@ mal_result mal_device_init__wasapi(mal_context* pContext, mal_device_type type, } #endif + /* Events. */ + mal_bool32 isSynchronous = (pConfig->onRecvCallback == NULL && pConfig->onSendCallback == NULL); - // We need to create and set the event for event-driven mode. This event is signalled whenever a new chunk of audio - // data needs to be written or read from the device. - pDevice->wasapi.hEvent = CreateEventA(NULL, FALSE, FALSE, NULL); - if (pDevice->wasapi.hEvent == NULL) { - errorMsg = "[WASAPI] Failed to create main event for main loop.", result = MAL_FAILED_TO_CREATE_EVENT; - goto done; + if (isSynchronous) { + /* + The event for playback is needs to be manual reset because we want to explicitly control the fact that it becomes signalled + only after the whole available space has been filled, never before. + + The playback event also needs to be initially set to a signaled state so that the first call to mal_device_write() is able + to get passed WaitForMultipleObjects(). + */ + if (type == mal_device_type_playback) { + pDevice->wasapi.hEventPlayback = CreateEventA(NULL, TRUE, TRUE, NULL); /* Manual reset, signaled by default. */ + if (pDevice->wasapi.hEventPlayback == NULL) { + errorMsg = "[WASAPI] Failed to create event for playback."; result = MAL_FAILED_TO_CREATE_EVENT; + goto done; + } + + mal_IAudioClient_SetEventHandle((mal_IAudioClient*)pDevice->wasapi.pAudioClient, pDevice->wasapi.hEventPlayback); + } + + /* + The event for capture needs to be manual reset for the same reason as playback. We keep the initial state set to unsignaled, + however, because we want to block until we actually have something for the first call to mal_device_read(). + */ + if (type == mal_device_type_capture) { + pDevice->wasapi.hEventCapture = CreateEventA(NULL, TRUE, FALSE, NULL); /* Manual reset, unsignaled by default. */ + if (pDevice->wasapi.hEventCapture == NULL) { + errorMsg = "[WASAPI] Failed to create event for capture."; result = MAL_FAILED_TO_CREATE_EVENT; + goto done; + } + + mal_IAudioClient_SetEventHandle((mal_IAudioClient*)pDevice->wasapi.pAudioClient, pDevice->wasapi.hEventCapture); + } + } else { + // We need to create and set the event for event-driven mode. This event is signaled whenever a new chunk of audio + // data needs to be written or read from the device. + pDevice->wasapi.hEvent = CreateEventA(NULL, FALSE, TRUE, NULL); + if (pDevice->wasapi.hEvent == NULL) { + errorMsg = "[WASAPI] Failed to create main event for main loop.", result = MAL_FAILED_TO_CREATE_EVENT; + goto done; + } + + mal_IAudioClient_SetEventHandle((mal_IAudioClient*)pDevice->wasapi.pAudioClient, pDevice->wasapi.hEvent); } - mal_IAudioClient_SetEventHandle((mal_IAudioClient*)pDevice->wasapi.pAudioClient, pDevice->wasapi.hEvent); + + // When the device is playing the worker thread will be waiting on a bunch of notification events. To return from @@ -6898,6 +6962,7 @@ mal_result mal_device_start__wasapi(mal_device* pDevice) return mal_post_error(pDevice, MAL_LOG_LEVEL_ERROR, "[WASAPI] Failed to start internal device.", MAL_FAILED_TO_START_BACKEND_DEVICE); } + pDevice->wasapi.isStarted = MAL_TRUE; return MAL_SUCCESS; } @@ -6914,19 +6979,10 @@ mal_result mal_device_stop__wasapi(mal_device* pDevice) return mal_post_error(pDevice, MAL_LOG_LEVEL_ERROR, "[WASAPI] Failed to stop internal device.", MAL_FAILED_TO_STOP_BACKEND_DEVICE); } + pDevice->wasapi.isStarted = MAL_FALSE; return MAL_SUCCESS; } -mal_result mal_device_break_main_loop__wasapi(mal_device* pDevice) -{ - mal_assert(pDevice != NULL); - - // The main loop will be waiting on a bunch of events via the WaitForMultipleObjects() API. One of those events - // is a special event we use for forcing that function to return. - pDevice->wasapi.breakFromMainLoop = MAL_TRUE; - SetEvent(pDevice->wasapi.hBreakEvent); - return MAL_SUCCESS; -} mal_result mal_device__get_available_frames__wasapi(mal_device* pDevice, mal_uint32* pFrameCount) { @@ -6955,6 +7011,210 @@ mal_result mal_device__get_available_frames__wasapi(mal_device* pDevice, mal_uin return MAL_SUCCESS; } + +mal_result mal_device_write__wasapi(mal_device* pDevice, mal_uint32 pcmFrameCount, const void* pPCMFrames, mal_uint32* pPCMFramesWritten) +{ + mal_result result; + mal_bool32 wasStartedOnEntry; + mal_uint32 totalPCMFramesWritten; + HRESULT hr; + DWORD waitResult; + HANDLE hEvents[1]; + hEvents[0] = pDevice->wasapi.hEventPlayback; + + wasStartedOnEntry = pDevice->wasapi.isStarted; + + /* Try to write every frame. */ + totalPCMFramesWritten = 0; + while (totalPCMFramesWritten < pcmFrameCount) { + /* + If we've already got a pointer to the device buffer we will want to fill that up first. Once it's consumed we'll want to reset + the event and set the cached pointer to NULL. + */ + if (pDevice->wasapi.pDeviceBufferPlayback != NULL && pDevice->wasapi.deviceBufferFramesRemainingPlayback > 0) { + mal_uint32 bpf = mal_get_bytes_per_frame(pDevice->internalFormat, pDevice->internalChannels); + mal_uint32 deviceBufferFramesConsumed = pDevice->wasapi.deviceBufferFramesCapacityPlayback - pDevice->wasapi.deviceBufferFramesRemainingPlayback; + + void* pDst = (mal_uint8*)pDevice->wasapi.pDeviceBufferPlayback + (deviceBufferFramesConsumed * bpf); + const void* pSrc = (const mal_uint8*)pPCMFrames + (totalPCMFramesWritten * bpf); + mal_uint32 framesToCopy = mal_min(pDevice->wasapi.deviceBufferFramesRemainingPlayback, (pcmFrameCount - totalPCMFramesWritten)); + mal_copy_memory(pDst, pSrc, framesToCopy * bpf); + + pDevice->wasapi.deviceBufferFramesRemainingPlayback -= framesToCopy; + totalPCMFramesWritten += framesToCopy; + } + + mal_assert(totalPCMFramesWritten <= pcmFrameCount); + if (totalPCMFramesWritten == pcmFrameCount) { + break; + } + + /* Getting here means we've consumed the device buffer and need to wait for more to become available. */ + if (pDevice->wasapi.deviceBufferFramesCapacityPlayback > 0) { + hr = mal_IAudioRenderClient_ReleaseBuffer((mal_IAudioRenderClient*)pDevice->wasapi.pRenderClient, pDevice->wasapi.deviceBufferFramesCapacityPlayback, 0); + pDevice->wasapi.pDeviceBufferPlayback = NULL; + pDevice->wasapi.deviceBufferFramesRemainingPlayback = 0; + pDevice->wasapi.deviceBufferFramesCapacityPlayback = 0; + + if (FAILED(hr)) { + result = MAL_FAILED_TO_UNMAP_DEVICE_BUFFER; + mal_post_error(pDevice, MAL_LOG_LEVEL_ERROR, "[WASAPI] Failed to release internal buffer from playback device after writing to the device.", result); + break; + } + + ResetEvent(pDevice->wasapi.hEventPlayback); + + /* + After releasing the buffer, if the device is not started we need to do so. Note from MSDN: + + The event handle should be in the nonsignaled state at the time that the client calls the Start method. + + This means we should start the device only after setting the event to non-signaled (after the call to ResetEvent()). + */ + if (!pDevice->wasapi.isStarted && !wasStartedOnEntry) { + result = mal_device_start__wasapi(pDevice); + if (result != MAL_SUCCESS) { + break; + } + } + } + + + /* Wait for data. */ + waitResult = WaitForMultipleObjects(mal_countof(hEvents), hEvents, FALSE, INFINITE); + if (waitResult == WAIT_FAILED) { + result = MAL_ERROR; + break; /* An error occurred while waiting for the event. */ + } + + /* If the device has been stopped don't continue. */ + if (!pDevice->wasapi.isStarted && wasStartedOnEntry) { + break; + } + + /* The device buffer has become available, so now we need to get a pointer to it. */ + result = mal_device__get_available_frames__wasapi(pDevice, &pDevice->wasapi.deviceBufferFramesCapacityPlayback); + if (result != MAL_SUCCESS) { + break; + } + + hr = mal_IAudioRenderClient_GetBuffer((mal_IAudioRenderClient*)pDevice->wasapi.pRenderClient, pDevice->wasapi.deviceBufferFramesCapacityPlayback, (BYTE**)&pDevice->wasapi.pDeviceBufferPlayback); + if (FAILED(hr)) { + result = MAL_FAILED_TO_MAP_DEVICE_BUFFER; + mal_post_error(pDevice, MAL_LOG_LEVEL_ERROR, "[WASAPI] Failed to retrieve internal buffer from playback device in preparation for writing to the device.", result); + break; + } + + pDevice->wasapi.deviceBufferFramesRemainingPlayback = pDevice->wasapi.deviceBufferFramesCapacityPlayback; + } + + *pPCMFramesWritten = totalPCMFramesWritten; + return MAL_SUCCESS; +} + +mal_result mal_device_read__wasapi(mal_device* pDevice, mal_uint32 pcmFrameCount, void* pPCMFrames, mal_uint32* pPCMFramesRead) +{ + mal_result result; + mal_uint32 totalPCMFramesRead; + HRESULT hr; + DWORD waitResult; + DWORD flags; /* Passed to IAudioCaptureClient_GetBuffer(). */ + HANDLE hEvents[1]; + hEvents[0] = pDevice->wasapi.hEventCapture; + + /* + This is mostly the same as mal_device_write__wasapi() with only a few exceptions: + - If the device is not already started, it's started immediately. + */ + if (!pDevice->wasapi.isStarted) { + result = mal_device_start__wasapi(pDevice); + if (result != MAL_SUCCESS) { + return result; /* Failed to auto-start device. */ + } + } + + /* Try to read every frame. */ + totalPCMFramesRead = 0; + while (totalPCMFramesRead < pcmFrameCount) { + /* Make sure we consume any cached data before waiting for more. */ + if (pDevice->wasapi.pDeviceBufferCapture != NULL && pDevice->wasapi.deviceBufferFramesRemainingCapture > 0) { + mal_uint32 bpf = mal_get_bytes_per_frame(pDevice->internalFormat, pDevice->internalChannels); + mal_uint32 deviceBufferFramesConsumed = pDevice->wasapi.deviceBufferFramesCapacityCapture - pDevice->wasapi.deviceBufferFramesRemainingCapture; + + void* pDst = (mal_uint8*)pPCMFrames + (totalPCMFramesRead * bpf); + const void* pSrc = (const mal_uint8*)pDevice->wasapi.pDeviceBufferCapture + (deviceBufferFramesConsumed * bpf); + mal_uint32 framesToCopy = mal_min(pDevice->wasapi.deviceBufferFramesRemainingCapture, (pcmFrameCount - totalPCMFramesRead)); + mal_copy_memory(pDst, pSrc, framesToCopy * bpf); + + pDevice->wasapi.deviceBufferFramesRemainingCapture -= framesToCopy; + totalPCMFramesRead += framesToCopy; + } + + mal_assert(totalPCMFramesRead <= pcmFrameCount); + if (totalPCMFramesRead == pcmFrameCount) { + break; + } + + /* Getting here means we've consumed the device buffer and need to wait for more to become available. */ + if (pDevice->wasapi.deviceBufferFramesCapacityCapture > 0) { + hr = mal_IAudioCaptureClient_ReleaseBuffer((mal_IAudioCaptureClient*)pDevice->wasapi.pCaptureClient, pDevice->wasapi.deviceBufferFramesCapacityCapture); + pDevice->wasapi.pDeviceBufferCapture = NULL; + pDevice->wasapi.deviceBufferFramesRemainingCapture = 0; + pDevice->wasapi.deviceBufferFramesCapacityCapture = 0; + + if (FAILED(hr)) { + result = MAL_FAILED_TO_UNMAP_DEVICE_BUFFER; + mal_post_error(pDevice, MAL_LOG_LEVEL_ERROR, "[WASAPI] Failed to release internal buffer from capture device after reading from the device.", result); + break; + } + + ResetEvent(pDevice->wasapi.hEventCapture); + } + + /* Wait for data. */ + waitResult = WaitForMultipleObjects(mal_countof(hEvents), hEvents, FALSE, INFINITE); + if (waitResult == WAIT_FAILED) { + result = MAL_ERROR; + break; /* An error occurred while waiting for the event. */ + } + + /* If the device has been stopped don't continue. */ + if (!pDevice->wasapi.isStarted) { + break; + } + + /* The device buffer has become available, so now we need to get a pointer to it. */ + result = mal_device__get_available_frames__wasapi(pDevice, &pDevice->wasapi.deviceBufferFramesCapacityCapture); + if (result != MAL_SUCCESS) { + break; + } + + hr = mal_IAudioCaptureClient_GetBuffer((mal_IAudioCaptureClient*)pDevice->wasapi.pCaptureClient, (BYTE**)&pDevice->wasapi.pDeviceBufferCapture, &pDevice->wasapi.deviceBufferFramesCapacityCapture, &flags, NULL, NULL); + if (FAILED(hr)) { + result = MAL_FAILED_TO_MAP_DEVICE_BUFFER; + mal_post_error(pDevice, MAL_LOG_LEVEL_ERROR, "[WASAPI] Failed to retrieve internal buffer from playback device in preparation for writing to the device.", result); + break; + } + + pDevice->wasapi.deviceBufferFramesRemainingCapture = pDevice->wasapi.deviceBufferFramesCapacityCapture; + } + + *pPCMFramesRead = totalPCMFramesRead; + return MAL_SUCCESS; +} + + +mal_result mal_device_break_main_loop__wasapi(mal_device* pDevice) +{ + mal_assert(pDevice != NULL); + + // The main loop will be waiting on a bunch of events via the WaitForMultipleObjects() API. One of those events + // is a special event we use for forcing that function to return. + pDevice->wasapi.breakFromMainLoop = MAL_TRUE; + SetEvent(pDevice->wasapi.hBreakEvent); + return MAL_SUCCESS; +} + mal_result mal_device__wait_for_frames__wasapi(mal_device* pDevice, mal_uint32* pFrameCount) { mal_assert(pDevice != NULL); @@ -7153,6 +7413,9 @@ mal_result mal_context_init__wasapi(mal_context* pContext) pContext->onDeviceReinit = mal_device_reinit__wasapi; pContext->onDeviceStart = mal_device_start__wasapi; pContext->onDeviceStop = mal_device_stop__wasapi; + pContext->onDeviceWrite = mal_device_write__wasapi; + pContext->onDeviceRead = mal_device_read__wasapi; + pContext->onDeviceBreakMainLoop = mal_device_break_main_loop__wasapi; pContext->onDeviceMainLoop = mal_device_main_loop__wasapi; @@ -20469,6 +20732,111 @@ void mal_device_uninit(mal_device* pDevice) mal_zero_object(pDevice); } +mal_result mal_device_write(mal_device* pDevice, mal_uint32 pcmFrameCount, const void* pPCMFrames, mal_uint32* pPCMFramesWritten) +{ + mal_result result; + mal_uint32 totalPCMFramesWritten = 0; + + if (pPCMFramesWritten != NULL) { + *pPCMFramesWritten = 0; /* Safety. */ + } + + if (pDevice == NULL || pPCMFrames == NULL) { + return MAL_INVALID_ARGS; + } + + /* Not allowed to call this in asynchronous mode. */ + if (pDevice->onRecv != NULL || pDevice->onSend != NULL) { + return MAL_INVALID_OPERATION; + } + + /* Backend must supporting synchronous writes. */ + if (pDevice->pContext->onDeviceWrite == NULL) { + return MAL_INVALID_OPERATION; + } + + /* If it's a passthrough we can call the backend directly, otherwise we need a data conversion into an intermediary buffer. */ + if (pDevice->dsp.isPassthrough) { + /* Fast path. Write directly to the device. */ + result = pDevice->pContext->onDeviceWrite(pDevice, pcmFrameCount, pPCMFrames, &totalPCMFramesWritten); + } else { + /* Slow path. Perform a data conversion. */ +#if 0 + mal_uint8 buffer[4096]; + while (totalPCMFramesWritten < pcmFrameCount) { + mal_uint32 framesJustWritten = 0; + mal_uint32 framesRemaining = (pcmFrameCount - totalPCMFramesWritten); + mal_uint32 framesToWrite = framesRemaining; + if (framesToWrite > (sizeof(buffer)/mal_get_bytes_per_frame(pDevice->internalFormat, pDevice->internalChannels))) { + framesToWrite = (sizeof(buffer)/mal_get_bytes_per_frame(pDevice->internalFormat, pDevice->internalChannels)); + } + + /* TODO: Convert the pPCMFrames to the device's internal format. */ + + result = pDevice->pContext->onDeviceWrite(pDevice, framesToWrite, buffer, &framesJustWritten); + totalPCMFramesWritten += framesJustWritten; + if (result != MAL_SUCCESS) { + break; + } + } +#endif + + result = MAL_INVALID_OPERATION; + /*result = MAL_SUCCESS;*/ + } + + + if (pPCMFramesWritten != NULL) { + *pPCMFramesWritten = totalPCMFramesWritten; + } + + return result; +} + +mal_result mal_device_read(mal_device* pDevice, mal_uint32 pcmFrameCount, void* pPCMFrames, mal_uint32* pPCMFramesRead) +{ + mal_result result; + mal_uint32 totalPCMFramesRead = 0; + + if (pPCMFramesRead != NULL) { + *pPCMFramesRead = 0; /* Safety. */ + } + + if (pDevice == NULL || pPCMFrames == NULL) { + return MAL_INVALID_ARGS; + } + + /* Not allowed to call this in asynchronous mode. */ + if (pDevice->onRecv != NULL || pDevice->onSend != NULL) { + return MAL_INVALID_OPERATION; + } + + /* Backend must supporting synchronous reads. */ + if (pDevice->pContext->onDeviceRead == NULL) { + return MAL_INVALID_OPERATION; + } + + + /* If it's a passthrough we can call the backend directly, otherwise we need a data conversion into an intermediary buffer. */ + if (pDevice->dsp.isPassthrough) { + /* Fast path. Write directly to the device. */ + result = pDevice->pContext->onDeviceRead(pDevice, pcmFrameCount, pPCMFrames, &totalPCMFramesRead); + } else { + /* Slow path. Perform a data conversion. */ + + + /* TODO: Implement me. */ + result = MAL_INVALID_OPERATION; + } + + + if (pPCMFramesRead != NULL) { + *pPCMFramesRead = totalPCMFramesRead; + } + + return result; +} + void mal_device_set_stop_callback(mal_device* pDevice, mal_stop_proc proc) { if (pDevice == NULL) return;