Commit 7271b20d authored by David Reid's avatar David Reid

Improvements to the Null backend.

This change makes a device run it's own loop with a timer to better
simulate an actual device.
parent a9af26f7
...@@ -2080,11 +2080,18 @@ MAL_ALIGNED_STRUCT(MAL_SIMD_ALIGNMENT) mal_device ...@@ -2080,11 +2080,18 @@ MAL_ALIGNED_STRUCT(MAL_SIMD_ALIGNMENT) mal_device
#ifdef MAL_SUPPORT_NULL #ifdef MAL_SUPPORT_NULL
struct struct
{ {
mal_thread deviceThread;
mal_event operationEvent;
mal_event operationCompletionEvent;
mal_uint32 operation;
mal_result operationResult;
mal_timer timer; mal_timer timer;
mal_uint32 lastProcessedFrame; // This is circular. double priorRunTime;
mal_bool32 breakFromMainLoop; mal_uint32 currentPeriodFramesRemainingPlayback;
mal_uint8* pBuffer; // This is malloc()'d and is used as the destination for reading from the client. Typed as mal_uint8 for easy offsetting. mal_uint32 currentPeriodFramesRemainingCapture;
mal_bool32 hasStarted : 1; /* Keeps track of whether or not the device has been started at least once. */ mal_uint64 lastProcessedFramePlayback;
mal_uint32 lastProcessedFrameCapture;
mal_bool32 isStarted;
} null_device; } null_device;
#endif #endif
}; };
...@@ -4678,6 +4685,93 @@ void mal_device__post_init_setup(mal_device* pDevice); ...@@ -4678,6 +4685,93 @@ void mal_device__post_init_setup(mal_device* pDevice);
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
#ifdef MAL_HAS_NULL #ifdef MAL_HAS_NULL
#define MAL_DEVICE_OP_NONE__NULL 0
#define MAL_DEVICE_OP_START__NULL 1
#define MAL_DEVICE_OP_SUSPEND__NULL 2
#define MAL_DEVICE_OP_KILL__NULL 3
mal_thread_result MAL_THREADCALL mal_device_thread__null(void* pData)
{
mal_device* pDevice = (mal_device*)pData;
mal_assert(pDevice != NULL);
for (;;) { /* Keep the thread alive until the device is uninitialized. */
/* Wait for an operation to be requested. */
mal_event_wait(&pDevice->null_device.operationEvent);
/* At this point an event should have been triggered. */
/* Starting the device needs to put the thread into a loop. */
if (pDevice->null_device.operation == MAL_DEVICE_OP_START__NULL) {
mal_atomic_exchange_32(&pDevice->null_device.operation, MAL_DEVICE_OP_NONE__NULL);
/* Reset the timer just in case. */
mal_timer_init(&pDevice->null_device.timer);
/* Keep looping until an operation has been requested. */
while (pDevice->null_device.operation != MAL_DEVICE_OP_NONE__NULL && pDevice->null_device.operation != MAL_DEVICE_OP_START__NULL) {
mal_sleep(10); /* Don't hog the CPU. */
}
/* Getting here means a suspend or kill operation has been requested. */
mal_atomic_exchange_32(&pDevice->null_device.operationResult, MAL_SUCCESS);
mal_event_signal(&pDevice->null_device.operationCompletionEvent);
continue;
}
/* Suspending the device means we need to stop the timer and just continue the loop. */
if (pDevice->null_device.operation == MAL_DEVICE_OP_SUSPEND__NULL) {
mal_atomic_exchange_32(&pDevice->null_device.operation, MAL_DEVICE_OP_NONE__NULL);
/* We need to add the current run time to the prior run time, then reset the timer. */
pDevice->null_device.priorRunTime += mal_timer_get_time_in_seconds(&pDevice->null_device.timer);
mal_timer_init(&pDevice->null_device.timer);
/* We're done. */
mal_atomic_exchange_32(&pDevice->null_device.operationResult, MAL_SUCCESS);
mal_event_signal(&pDevice->null_device.operationCompletionEvent);
continue;
}
/* Killing the device means we need to get out of this loop so that this thread can terminate. */
if (pDevice->null_device.operation == MAL_DEVICE_OP_KILL__NULL) {
mal_atomic_exchange_32(&pDevice->null_device.operation, MAL_DEVICE_OP_NONE__NULL);
mal_atomic_exchange_32(&pDevice->null_device.operationResult, MAL_SUCCESS);
mal_event_signal(&pDevice->null_device.operationCompletionEvent);
break;
}
/* Getting a signal on a "none" operation probably means an error. Return invalid operation. */
if (pDevice->null_device.operation == MAL_DEVICE_OP_NONE__NULL) {
mal_assert(MAL_FALSE); /* <-- Trigger this in debug mode to ensure developers are aware they're doing something wrong (or there's a bug in a mini_al). */
mal_atomic_exchange_32(&pDevice->null_device.operationResult, MAL_INVALID_OPERATION);
mal_event_signal(&pDevice->null_device.operationCompletionEvent);
continue; /* Continue the loop. Don't terminate. */
}
}
return (mal_thread_result)0;
}
mal_result mal_device_do_operation__null(mal_device* pDevice, mal_uint32 operation)
{
mal_atomic_exchange_32(&pDevice->null_device.operation, operation);
if (!mal_event_signal(&pDevice->null_device.operationEvent)) {
return MAL_ERROR;
}
if (!mal_event_wait(&pDevice->null_device.operationCompletionEvent)) {
return MAL_ERROR;
}
return pDevice->null_device.operationResult;
}
mal_uint64 mal_device_get_total_run_time_in_frames__null(mal_device* pDevice)
{
return (mal_uint64)((pDevice->null_device.priorRunTime + mal_timer_get_time_in_seconds(&pDevice->null_device.timer)) * pDevice->internalSampleRate);
}
mal_bool32 mal_context_is_device_id_equal__null(mal_context* pContext, const mal_device_id* pID0, const mal_device_id* pID1) mal_bool32 mal_context_is_device_id_equal__null(mal_context* pContext, const mal_device_id* pID0, const mal_device_id* pID1)
{ {
mal_assert(pContext != NULL); mal_assert(pContext != NULL);
...@@ -4750,7 +4844,13 @@ mal_result mal_context_get_device_info__null(mal_context* pContext, mal_device_t ...@@ -4750,7 +4844,13 @@ mal_result mal_context_get_device_info__null(mal_context* pContext, mal_device_t
void mal_device_uninit__null(mal_device* pDevice) void mal_device_uninit__null(mal_device* pDevice)
{ {
mal_assert(pDevice != NULL); mal_assert(pDevice != NULL);
mal_free(pDevice->null_device.pBuffer);
/* Keep it clean and wait for the device thread to finish before returning. */
mal_device_do_operation__null(pDevice, MAL_DEVICE_OP_KILL__NULL);
/* At this point the loop in the device thread is as good as terminated so we can uninitialize our events. */
mal_event_uninit(&pDevice->null_device.operationCompletionEvent);
mal_event_uninit(&pDevice->null_device.operationEvent);
} }
mal_result mal_device_init__null(mal_context* pContext, mal_device_type type, const mal_device_id* pDeviceID, const mal_device_config* pConfig, mal_device* pDevice) mal_result mal_device_init__null(mal_context* pContext, mal_device_type type, const mal_device_id* pDeviceID, const mal_device_config* pConfig, mal_device* pDevice)
...@@ -4760,6 +4860,8 @@ mal_result mal_device_init__null(mal_context* pContext, mal_device_type type, co ...@@ -4760,6 +4860,8 @@ mal_result mal_device_init__null(mal_context* pContext, mal_device_type type, co
(void)pDeviceID; (void)pDeviceID;
(void)pConfig; (void)pConfig;
mal_result result;
mal_assert(pDevice != NULL); mal_assert(pDevice != NULL);
mal_zero_object(&pDevice->null_device); mal_zero_object(&pDevice->null_device);
...@@ -4774,13 +4876,26 @@ mal_result mal_device_init__null(mal_context* pContext, mal_device_type type, co ...@@ -4774,13 +4876,26 @@ mal_result mal_device_init__null(mal_context* pContext, mal_device_type type, co
bufferSizeInFrames = mal_calculate_buffer_size_in_frames_from_milliseconds(pConfig->bufferSizeInMilliseconds, pConfig->sampleRate); bufferSizeInFrames = mal_calculate_buffer_size_in_frames_from_milliseconds(pConfig->bufferSizeInMilliseconds, pConfig->sampleRate);
} }
pDevice->null_device.pBuffer = (mal_uint8*)mal_malloc(bufferSizeInFrames * pConfig->channels * mal_get_bytes_per_sample(pConfig->format)); pDevice->bufferSizeInFrames = bufferSizeInFrames;
if (pDevice->null_device.pBuffer == NULL) {
return MAL_OUT_OF_MEMORY; /*
In order to get timing right, we need to create a thread that does nothing but keeps track of the timer. This timer is started when the
first period is "written" to it, and then stopped in mal_device_stop__null().
*/
result = mal_event_init(pContext, &pDevice->null_device.operationEvent);
if (result != MAL_SUCCESS) {
return result;
} }
pDevice->bufferSizeInFrames = bufferSizeInFrames; result = mal_event_init(pContext, &pDevice->null_device.operationCompletionEvent);
mal_zero_memory(pDevice->null_device.pBuffer, mal_device_get_buffer_size_in_bytes(pDevice)); if (result != MAL_SUCCESS) {
return result;
}
result = mal_thread_create(pContext, &pDevice->thread, mal_device_thread__null, pDevice);
if (result != MAL_SUCCESS) {
return result;
}
return MAL_SUCCESS; return MAL_SUCCESS;
} }
...@@ -4789,143 +4904,157 @@ mal_result mal_device_start__null(mal_device* pDevice) ...@@ -4789,143 +4904,157 @@ mal_result mal_device_start__null(mal_device* pDevice)
{ {
mal_assert(pDevice != NULL); mal_assert(pDevice != NULL);
if (!pDevice->null_device.hasStarted) { mal_device_do_operation__null(pDevice, MAL_DEVICE_OP_START__NULL);
mal_timer_init(&pDevice->null_device.timer);
pDevice->null_device.lastProcessedFrame = 0;
pDevice->null_device.hasStarted = MAL_TRUE;
}
mal_atomic_exchange_32(&pDevice->null_device.isStarted, MAL_TRUE);
return MAL_SUCCESS; return MAL_SUCCESS;
} }
mal_result mal_device_stop__null(mal_device* pDevice) mal_result mal_device_stop__null(mal_device* pDevice)
{ {
mal_assert(pDevice != NULL); mal_assert(pDevice != NULL);
(void)pDevice;
mal_device_do_operation__null(pDevice, MAL_DEVICE_OP_SUSPEND__NULL);
mal_atomic_exchange_32(&pDevice->null_device.isStarted, MAL_FALSE);
return MAL_SUCCESS; return MAL_SUCCESS;
} }
mal_result mal_device_break_main_loop__null(mal_device* pDevice) mal_result mal_device_write__null(mal_device* pDevice, const void* pPCMFrames, mal_uint32 pcmFrameCount)
{ {
mal_assert(pDevice != NULL); mal_result result = MAL_SUCCESS;
mal_uint32 totalPCMFramesProcessed;
mal_bool32 wasStartedOnEntry;
pDevice->null_device.breakFromMainLoop = MAL_TRUE; wasStartedOnEntry = pDevice->null_device.isStarted;
return MAL_SUCCESS;
}
mal_bool32 mal_device__get_current_frame__null(mal_device* pDevice, mal_uint32* pCurrentPos) /* Keep going until everything has been read. */
{ totalPCMFramesProcessed = 0;
mal_assert(pDevice != NULL); while (totalPCMFramesProcessed < pcmFrameCount) {
mal_assert(pCurrentPos != NULL); /* If there are any frames remaining in the current period, consume those first. */
*pCurrentPos = 0; if (pDevice->null_device.currentPeriodFramesRemainingPlayback > 0) {
mal_uint32 framesRemaining = (pcmFrameCount - totalPCMFramesProcessed);
mal_uint32 framesToProcess = pDevice->null_device.currentPeriodFramesRemainingPlayback;
if (framesToProcess > framesRemaining) {
framesToProcess = framesRemaining;
}
mal_uint64 currentFrameAbs = (mal_uint64)(mal_timer_get_time_in_seconds(&pDevice->null_device.timer) * pDevice->sampleRate); /* We don't actually do anything with pPCMFrames, so just mark it as unused to prevent a warning. */
(void)pPCMFrames;
*pCurrentPos = (mal_uint32)(currentFrameAbs % pDevice->bufferSizeInFrames); pDevice->null_device.currentPeriodFramesRemainingPlayback -= framesToProcess;
return MAL_TRUE; totalPCMFramesProcessed += framesToProcess;
} }
mal_uint32 mal_device__get_available_frames__null(mal_device* pDevice) /* If we've consumed the current period we'll need to mark it as such an ensure the device is started if it's not already. */
{ if (pDevice->null_device.currentPeriodFramesRemainingPlayback == 0) {
mal_assert(pDevice != NULL); pDevice->null_device.currentPeriodFramesRemainingPlayback = 0;
mal_uint32 currentFrame; if (!pDevice->null_device.isStarted && !wasStartedOnEntry) {
if (!mal_device__get_current_frame__null(pDevice, &currentFrame)) { result = mal_device_start__null(pDevice);
return 0; if (result != MAL_SUCCESS) {
break;
}
}
} }
// In a playback device the last processed frame should always be ahead of the current frame. The space between /* If we've consumed the whole buffer we can return now. */
// the last processed and current frame (moving forward, starting from the last processed frame) is the amount mal_assert(totalPCMFramesProcessed <= pcmFrameCount);
// of space available to write. if (totalPCMFramesProcessed == pcmFrameCount) {
// break;
// For a recording device it's the other way around - the last processed frame is always _behind_ the current
// frame and the space between is the available space.
mal_uint32 totalFrameCount = pDevice->bufferSizeInFrames;
if (pDevice->type == mal_device_type_playback) {
mal_uint32 committedBeg = currentFrame;
mal_uint32 committedEnd = pDevice->null_device.lastProcessedFrame;
if (committedEnd <= committedBeg) {
committedEnd += totalFrameCount; // Wrap around.
} }
mal_uint32 committedSize = (committedEnd - committedBeg); /* Getting here means we've still got more frames to consume, we but need to wait for it to become available. */
mal_assert(committedSize <= totalFrameCount); mal_uint64 targetFrame = pDevice->null_device.lastProcessedFramePlayback;
for (;;) {
/* Stop waiting if the device has been stopped. */
if (!pDevice->null_device.isStarted) {
break;
}
return totalFrameCount - committedSize; mal_uint64 currentFrame = mal_device_get_total_run_time_in_frames__null(pDevice);
} else { if (currentFrame >= targetFrame) {
mal_uint32 validBeg = pDevice->null_device.lastProcessedFrame; break;
mal_uint32 validEnd = currentFrame;
if (validEnd < validBeg) {
validEnd += totalFrameCount; // Wrap around.
} }
mal_uint32 validSize = (validEnd - validBeg); /* Getting here means we haven't yet reached the target sample, so continue waiting. */
mal_assert(validSize <= totalFrameCount); mal_sleep(10);
}
return validSize; pDevice->null_device.lastProcessedFramePlayback += pDevice->bufferSizeInFrames/pDevice->periods;
pDevice->null_device.currentPeriodFramesRemainingPlayback = pDevice->bufferSizeInFrames/pDevice->periods;
} }
return result;
} }
mal_uint32 mal_device__wait_for_frames__null(mal_device* pDevice) mal_result mal_device_read__null(mal_device* pDevice, void* pPCMFrames, mal_uint32 pcmFrameCount)
{ {
mal_assert(pDevice != NULL); mal_result result = MAL_SUCCESS;
mal_uint32 totalPCMFramesProcessed;
while (!pDevice->null_device.breakFromMainLoop) { /* The device needs to be started immediately. */
mal_uint32 framesAvailable = mal_device__get_available_frames__null(pDevice); if (!pDevice->null_device.isStarted) {
if (framesAvailable >= (pDevice->bufferSizeInFrames/pDevice->periods)) { result = mal_device_start__null(pDevice);
return framesAvailable; if (result != MAL_SUCCESS) {
return result;
} }
mal_sleep(pDevice->bufferSizeInMilliseconds/pDevice->periods);
} }
// We'll get here if the loop was terminated. Just return whatever's available. /* Keep going until everything has been read. */
return mal_device__get_available_frames__null(pDevice); totalPCMFramesProcessed = 0;
} while (totalPCMFramesProcessed < pcmFrameCount) {
/* If there are any frames remaining in the current period, consume those first. */
if (pDevice->null_device.currentPeriodFramesRemainingCapture > 0) {
mal_uint32 bpf = mal_get_bytes_per_frame(pDevice->internalFormat, pDevice->internalChannels);
mal_uint32 framesRemaining = (pcmFrameCount - totalPCMFramesProcessed);
mal_uint32 framesToProcess = pDevice->null_device.currentPeriodFramesRemainingCapture;
if (framesToProcess > framesRemaining) {
framesToProcess = framesRemaining;
}
mal_result mal_device_main_loop__null(mal_device* pDevice) /* We need to ensured the output buffer is zeroed. */
{ mal_zero_memory(mal_offset_ptr(pPCMFrames, totalPCMFramesProcessed*bpf), framesToProcess*bpf);
mal_assert(pDevice != NULL);
pDevice->null_device.breakFromMainLoop = MAL_FALSE; pDevice->null_device.currentPeriodFramesRemainingCapture -= framesToProcess;
while (!pDevice->null_device.breakFromMainLoop) { totalPCMFramesProcessed += framesToProcess;
mal_uint32 framesAvailable = mal_device__wait_for_frames__null(pDevice);
if (framesAvailable == 0) {
continue;
} }
// If it's a playback device, don't bother grabbing more data if the device is being stopped. /* If we've consumed the current period we'll need to mark it as such an ensure the device is started if it's not already. */
if (pDevice->null_device.breakFromMainLoop && pDevice->type == mal_device_type_playback) { if (pDevice->null_device.currentPeriodFramesRemainingCapture == 0) {
return MAL_ERROR; pDevice->null_device.currentPeriodFramesRemainingCapture = 0;
} }
if (framesAvailable + pDevice->null_device.lastProcessedFrame > pDevice->bufferSizeInFrames) { /* If we've consumed the whole buffer we can return now. */
framesAvailable = pDevice->bufferSizeInFrames - pDevice->null_device.lastProcessedFrame; mal_assert(totalPCMFramesProcessed <= pcmFrameCount);
if (totalPCMFramesProcessed == pcmFrameCount) {
break;
} }
mal_uint32 sampleCount = framesAvailable * pDevice->channels; /* Getting here means we've still got more frames to consume, we but need to wait for it to become available. */
mal_uint32 lockOffset = pDevice->null_device.lastProcessedFrame * pDevice->channels * mal_get_bytes_per_sample(pDevice->format); mal_uint64 targetFrame = pDevice->null_device.lastProcessedFrameCapture + (pDevice->bufferSizeInFrames/pDevice->periods);
mal_uint32 lockSize = sampleCount * mal_get_bytes_per_sample(pDevice->format); for (;;) {
/* Stop waiting if the device has been stopped. */
if (!pDevice->null_device.isStarted) {
break;
}
if (pDevice->type == mal_device_type_playback) { mal_uint64 currentFrame = mal_device_get_total_run_time_in_frames__null(pDevice);
if (pDevice->null_device.breakFromMainLoop) { if (currentFrame >= targetFrame) {
return MAL_ERROR; break;
} }
mal_device__read_frames_from_client(pDevice, framesAvailable, pDevice->null_device.pBuffer + lockOffset); /* Getting here means we haven't yet reached the target sample, so continue waiting. */
} else { mal_sleep(10);
mal_zero_memory(pDevice->null_device.pBuffer + lockOffset, lockSize);
mal_device__send_frames_to_client(pDevice, framesAvailable, pDevice->null_device.pBuffer + lockOffset);
} }
pDevice->null_device.lastProcessedFrame = (pDevice->null_device.lastProcessedFrame + framesAvailable) % pDevice->bufferSizeInFrames; pDevice->null_device.lastProcessedFrameCapture += pDevice->bufferSizeInFrames/pDevice->periods;
pDevice->null_device.currentPeriodFramesRemainingCapture = pDevice->bufferSizeInFrames/pDevice->periods;
} }
return MAL_SUCCESS; return result;
} }
mal_result mal_context_uninit__null(mal_context* pContext) mal_result mal_context_uninit__null(mal_context* pContext)
{ {
mal_assert(pContext != NULL); mal_assert(pContext != NULL);
...@@ -4947,10 +5076,10 @@ mal_result mal_context_init__null(mal_context* pContext) ...@@ -4947,10 +5076,10 @@ mal_result mal_context_init__null(mal_context* pContext)
pContext->onDeviceUninit = mal_device_uninit__null; pContext->onDeviceUninit = mal_device_uninit__null;
pContext->onDeviceStart = mal_device_start__null; pContext->onDeviceStart = mal_device_start__null;
pContext->onDeviceStop = mal_device_stop__null; pContext->onDeviceStop = mal_device_stop__null;
pContext->onDeviceBreakMainLoop = mal_device_break_main_loop__null; pContext->onDeviceWrite = mal_device_write__null;
pContext->onDeviceMainLoop = mal_device_main_loop__null; pContext->onDeviceRead = mal_device_read__null;
// The null backend always works. /* The null backend always works. */
return MAL_SUCCESS; return MAL_SUCCESS;
} }
#endif #endif
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment