Commit fd85d5ac authored by David Reid's avatar David Reid

Improve the DirectSound backend by not using notifications exclusively to...

Improve the DirectSound backend by not using notifications exclusively to manage data retrieval and submission.
parent af5e5d48
...@@ -40,10 +40,6 @@ ...@@ -40,10 +40,6 @@
// //
// BACKEND NUANCES // BACKEND NUANCES
// =============== // ===============
// - For playback devices, the ALSA backend will pre-fill every fragment with sample data. The DirectSound
// backend only pre-fills the first fragment.
// - DirectSound has _bad_ latency compared to other backends. In my testing, a fragment size of 1024 frames
// is too small, but a size of 2048 seems to work.
#ifndef mini_al_h #ifndef mini_al_h
#define mini_al_h #define mini_al_h
...@@ -225,8 +221,9 @@ struct mal_device ...@@ -225,8 +221,9 @@ struct mal_device
/*LPDIRECTSOUNDNOTIFY*/ mal_ptr pNotify; /*LPDIRECTSOUNDNOTIFY*/ mal_ptr pNotify;
/*HANDLE*/ mal_handle pNotifyEvents[MAL_MAX_FRAGMENTS_DSOUND]; // One event handle for each fragment. /*HANDLE*/ mal_handle pNotifyEvents[MAL_MAX_FRAGMENTS_DSOUND]; // One event handle for each fragment.
/*HANDLE*/ mal_handle hStopEvent; /*HANDLE*/ mal_handle hStopEvent;
mal_uint32 ignoredFragmentCounter; // <-- This is used for a cheap hack to skip over some initial notifications when the device is first played. mal_uint32 ignoredFragmentCounter; // This is used for a cheap hack to skip over some initial notifications when the device is first played.
mal_uint32 lastProcessedFragment; mal_uint32 lastProcessedFrame; // This is circular.
mal_bool32 breakFromMainLoop;
} dsound; } dsound;
#endif #endif
...@@ -1403,6 +1400,8 @@ static mal_result mal_device__start_backend__dsound(mal_device* pDevice) ...@@ -1403,6 +1400,8 @@ static mal_result mal_device__start_backend__dsound(mal_device* pDevice)
return result; // The error will have already been posted. return result; // The error will have already been posted.
} }
pDevice->dsound.lastProcessedFrame = pDevice->fragmentSizeInFrames;
if (IDirectSoundBuffer_Play((LPDIRECTSOUNDBUFFER)pDevice->dsound.pPlaybackBuffer, 0, 0, DSBPLAY_LOOPING) != DS_OK) { if (IDirectSoundBuffer_Play((LPDIRECTSOUNDBUFFER)pDevice->dsound.pPlaybackBuffer, 0, 0, DSBPLAY_LOOPING) != DS_OK) {
return mal_post_error(pDevice, "[DirectSound] IDirectSoundBuffer_Play() failed.", MAL_FAILED_TO_START_BACKEND_DEVICE); return mal_post_error(pDevice, "[DirectSound] IDirectSoundBuffer_Play() failed.", MAL_FAILED_TO_START_BACKEND_DEVICE);
} }
...@@ -1440,82 +1439,163 @@ static mal_result mal_device__break_main_loop__dsound(mal_device* pDevice) ...@@ -1440,82 +1439,163 @@ static mal_result mal_device__break_main_loop__dsound(mal_device* pDevice)
// The main loop will be waiting on a bunch of events via the WaitForMultipleObjects() API. One of those events // 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. // is a special event we use for forcing that function to return.
pDevice->dsound.breakFromMainLoop = MAL_TRUE;
SetEvent(pDevice->dsound.hStopEvent); SetEvent(pDevice->dsound.hStopEvent);
return MAL_SUCCESS; return MAL_SUCCESS;
} }
static mal_result mal_device__main_loop__dsound(mal_device* pDevice) static mal_bool32 mal_device__get_current_frame__dsound(mal_device* pDevice, mal_uint32* pCurrentPos)
{ {
mal_assert(pDevice != NULL); mal_assert(pDevice != NULL);
mal_assert(pCurrentPos != NULL);
*pCurrentPos = 0;
// Make sure the stop event is not signaled to ensure we don't end up immediately returning from WaitForMultipleObjects(). DWORD dwCurrentPosition;
ResetEvent(pDevice->dsound.hStopEvent); if (pDevice->type == mal_device_type_playback) {
if (IDirectSoundBuffer_GetCurrentPosition((LPDIRECTSOUNDBUFFER)pDevice->dsound.pPlaybackBuffer, NULL, &dwCurrentPosition) != DS_OK) {
return MAL_FALSE;
}
} else {
if (IDirectSoundCaptureBuffer8_GetCurrentPosition((LPDIRECTSOUNDCAPTUREBUFFER8)pDevice->dsound.pCaptureBuffer, &dwCurrentPosition, NULL) != DS_OK) {
return MAL_FALSE;
}
}
// When the device is first started, there will be a few fragments that we need to skip over due to the way *pCurrentPos = (mal_uint32)dwCurrentPosition / mal_get_sample_size_in_bytes(pDevice->format) / pDevice->channels;
// they're handled by DirectSound. For a recording device it's the first fragment we need to ignore. return MAL_TRUE;
}
static mal_bool32 mal_device__get_available_frames__dsound(mal_device* pDevice)
{
mal_assert(pDevice != NULL);
mal_uint32 currentFrame;
if (!mal_device__get_current_frame__dsound(pDevice, &currentFrame)) {
return 0;
}
// In a playback device the last processed frame should always be ahead of the current frame. The space between
// the last processed and current frame (moving forward, starting from the last processed frame) is the amount
// of space available to write.
//
// 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->fragmentSizeInFrames*pDevice->fragmentCount;
if (pDevice->type == mal_device_type_playback) { if (pDevice->type == mal_device_type_playback) {
pDevice->dsound.ignoredFragmentCounter = 0; mal_uint32 committedBeg = currentFrame;
mal_uint32 committedEnd = pDevice->dsound.lastProcessedFrame;
if (committedEnd <= committedBeg) {
committedEnd += totalFrameCount; // Wrap around.
}
mal_uint32 committedSize = (committedEnd - committedBeg);
mal_assert(committedSize <= totalFrameCount);
return totalFrameCount - committedSize;
} else { } else {
pDevice->dsound.ignoredFragmentCounter = 1; mal_uint32 validBeg = pDevice->dsound.lastProcessedFrame;
mal_uint32 validEnd = currentFrame;
if (validEnd < validBeg) {
validEnd += totalFrameCount; // Wrap around.
} }
for (;;) { mal_uint32 validSize = (validEnd - validBeg);
// Wait for a notification. Notifications are tied to fragments. mal_assert(validSize <= totalFrameCount);
return validSize;
}
}
static mal_uint32 mal_device__wait_for_frames__dsound(mal_device* pDevice)
{
mal_assert(pDevice != NULL);
unsigned int eventCount = pDevice->fragmentCount + 1; unsigned int eventCount = pDevice->fragmentCount + 1;
HANDLE eventHandles[MAL_MAX_FRAGMENTS_DSOUND + 1]; // +1 for the stop event. HANDLE eventHandles[MAL_MAX_FRAGMENTS_DSOUND + 1]; // +1 for the stop event.
mal_copy_memory(eventHandles, pDevice->dsound.pNotifyEvents, sizeof(HANDLE) * pDevice->fragmentCount); mal_copy_memory(eventHandles, pDevice->dsound.pNotifyEvents, sizeof(HANDLE) * pDevice->fragmentCount);
eventHandles[eventCount-1] = pDevice->dsound.hStopEvent; eventHandles[eventCount-1] = pDevice->dsound.hStopEvent;
DWORD rc = WaitForMultipleObjects(eventCount, eventHandles, FALSE, INFINITE); while (!pDevice->dsound.breakFromMainLoop) {
if (rc < WAIT_OBJECT_0 || rc >= WAIT_OBJECT_0 + eventCount) { mal_uint32 framesAvailable = mal_device__get_available_frames__dsound(pDevice);
break; if (framesAvailable == 0) {
return 0;
} }
unsigned int eventIndex = rc - WAIT_OBJECT_0; // Never return more frames that will fit in a fragment.
HANDLE hEvent = eventHandles[eventIndex]; if (framesAvailable >= pDevice->fragmentSizeInFrames) {
return pDevice->fragmentSizeInFrames;
// Has the device been stopped? If so, need to get out of this loop. }
if (hEvent == pDevice->dsound.hStopEvent) {
// If it's a capture device there will be some leftover samples that need to be sent to the client. To
// calculate this we just look at the previously processed fragment and compare it to the current read
// position
if (pDevice->type == mal_device_type_capture) {
DWORD dwOffset = (((pDevice->dsound.lastProcessedFragment+1) * pDevice->fragmentSizeInFrames) % (pDevice->fragmentSizeInFrames*pDevice->fragmentCount)) * pDevice->channels * mal_get_sample_size_in_bytes(pDevice->format);
DWORD dwBytes = mal_device_get_fragment_size_in_bytes(pDevice);
DWORD dwPlayPosition; // If we get here it means we weren't able to find a full fragment. We'll just wait here for a bit.
if (IDirectSoundCaptureBuffer8_GetCurrentPosition((LPDIRECTSOUNDCAPTUREBUFFER8)pDevice->dsound.pCaptureBuffer, &dwPlayPosition, NULL) == DS_OK) { const DWORD timeoutInMilliseconds = 5; // <-- This affects latency with the DirectSound backend. Should this be a property?
DWORD sampleCount = (dwPlayPosition - dwOffset) / mal_get_sample_size_in_bytes(pDevice->format); DWORD rc = WaitForMultipleObjects(eventCount, eventHandles, FALSE, timeoutInMilliseconds);
if (sampleCount > 0) { if (rc < WAIT_OBJECT_0 || rc >= WAIT_OBJECT_0 + eventCount) {
void* pLockPtr; return 0;
DWORD lockSize;
if (IDirectSoundCaptureBuffer8_Lock((LPDIRECTSOUNDCAPTUREBUFFER8)pDevice->dsound.pCaptureBuffer, dwOffset, dwBytes, &pLockPtr, &lockSize, NULL, NULL, 0) == DS_OK) {
mal_device__send_samples_to_client(pDevice, sampleCount, pLockPtr);
IDirectSoundCaptureBuffer8_Unlock((LPDIRECTSOUNDCAPTUREBUFFER8)pDevice->dsound.pCaptureBuffer, pLockPtr, lockSize, NULL, 0);
} }
} }
// We'll get here if the loop was terminated. Just return whatever's available.
return mal_device__get_available_frames__dsound(pDevice);
}
static mal_result mal_device__main_loop__dsound(mal_device* pDevice)
{
mal_assert(pDevice != NULL);
// Make sure the stop event is not signaled to ensure we don't end up immediately returning from WaitForMultipleObjects().
ResetEvent(pDevice->dsound.hStopEvent);
// When the device is first started, there will be a few fragments that we need to skip over due to the way
// they're handled by DirectSound. For a recording device it's the first fragment we need to ignore.
if (pDevice->type == mal_device_type_playback) {
pDevice->dsound.ignoredFragmentCounter = 0;
} else {
pDevice->dsound.ignoredFragmentCounter = 1;
} }
pDevice->dsound.breakFromMainLoop = MAL_FALSE;
while (!pDevice->dsound.breakFromMainLoop) {
mal_uint32 framesAvailable = mal_device__wait_for_frames__dsound(pDevice);
if (framesAvailable == 0) {
continue;
} }
break; // If it's a playback device, don't bother grabbing more data if the device is being stopped.
if (pDevice->dsound.breakFromMainLoop && pDevice->type == mal_device_type_playback) {
return MAL_FALSE;
} }
// If we get here it means the event that's been signaled represents a fragment. DWORD lockOffset = pDevice->dsound.lastProcessedFrame * pDevice->channels * mal_get_sample_size_in_bytes(pDevice->format);
unsigned int fragmentIndex = eventIndex; // <-- Just for clarity. DWORD lockSize = framesAvailable * pDevice->channels * mal_get_sample_size_in_bytes(pDevice->format);
mal_assert(fragmentIndex < pDevice->fragmentCount);
pDevice->dsound.lastProcessedFragment = fragmentIndex; if (pDevice->type == mal_device_type_playback) {
if (pDevice->dsound.breakFromMainLoop) {
return MAL_FALSE;
}
// Some initial fragments need to be skipped over. void* pLockPtr;
if (pDevice->dsound.ignoredFragmentCounter > 0) { DWORD actualLockSize;
pDevice->dsound.ignoredFragmentCounter -= 1; if (FAILED(IDirectSoundBuffer_Lock((LPDIRECTSOUNDBUFFER)pDevice->dsound.pPlaybackBuffer, lockOffset, lockSize, &pLockPtr, &actualLockSize, NULL, NULL, 0))) {
continue; return mal_post_error(pDevice, "[DirectSound] IDirectSoundBuffer_Lock() failed.", MAL_FAILED_TO_MAP_DEVICE_BUFFER);
} }
if (pDevice->type == mal_device_type_playback) { mal_uint32 sampleCount = actualLockSize / mal_get_sample_size_in_bytes(pDevice->format);
mal_device__read_fragment_from_client__dsound(pDevice, (fragmentIndex + 1) % pDevice->fragmentCount); mal_device__read_samples_from_client(pDevice, sampleCount, pLockPtr);
pDevice->dsound.lastProcessedFrame = (pDevice->dsound.lastProcessedFrame + (sampleCount/pDevice->channels)) % (pDevice->fragmentSizeInFrames*pDevice->fragmentCount);
IDirectSoundBuffer_Unlock((LPDIRECTSOUNDBUFFER)pDevice->dsound.pPlaybackBuffer, pLockPtr, actualLockSize, NULL, 0);
} else { } else {
mal_device__send_fragment_to_client__dsound(pDevice, fragmentIndex); void* pLockPtr;
DWORD actualLockSize;
if (FAILED(IDirectSoundCaptureBuffer_Lock((LPDIRECTSOUNDCAPTUREBUFFER)pDevice->dsound.pCaptureBuffer, lockOffset, lockSize, &pLockPtr, &actualLockSize, NULL, NULL, 0))) {
return mal_post_error(pDevice, "[DirectSound] IDirectSoundCaptureBuffer_Lock() failed.", MAL_FAILED_TO_MAP_DEVICE_BUFFER);
}
mal_uint32 sampleCount = actualLockSize / mal_get_sample_size_in_bytes(pDevice->format);
mal_device__send_samples_to_client(pDevice, sampleCount, pLockPtr);
pDevice->dsound.lastProcessedFrame = (pDevice->dsound.lastProcessedFrame + (sampleCount/pDevice->channels)) % (pDevice->fragmentSizeInFrames*pDevice->fragmentCount);
IDirectSoundCaptureBuffer_Unlock((LPDIRECTSOUNDCAPTUREBUFFER)pDevice->dsound.pCaptureBuffer, pLockPtr, actualLockSize, NULL, 0);
} }
} }
...@@ -1538,7 +1618,7 @@ static mal_result mal_device__main_loop__dsound(mal_device* pDevice) ...@@ -1538,7 +1618,7 @@ static mal_result mal_device__main_loop__dsound(mal_device* pDevice)
// fragment size. // fragment size.
// //
// This will return early if the main loop is broken with mal_device__break_main_loop(). // This will return early if the main loop is broken with mal_device__break_main_loop().
mal_uint32 mal_device__wait_for_frames(mal_device* pDevice) mal_uint32 mal_device__wait_for_frames__alsa(mal_device* pDevice)
{ {
mal_assert(pDevice != NULL); mal_assert(pDevice != NULL);
...@@ -1601,7 +1681,7 @@ mal_bool32 mal_device_write__alsa(mal_device* pDevice) ...@@ -1601,7 +1681,7 @@ mal_bool32 mal_device_write__alsa(mal_device* pDevice)
} else { } else {
// readi/writei. // readi/writei.
while (!pDevice->alsa.breakFromMainLoop) { while (!pDevice->alsa.breakFromMainLoop) {
mal_uint32 framesAvailable = mal_device__wait_for_frames(pDevice); mal_uint32 framesAvailable = mal_device__wait_for_frames__alsa(pDevice);
if (framesAvailable == 0) { if (framesAvailable == 0) {
return MAL_FALSE; return MAL_FALSE;
} }
...@@ -1660,7 +1740,7 @@ mal_bool32 mal_device_read__alsa(mal_device* pDevice) ...@@ -1660,7 +1740,7 @@ mal_bool32 mal_device_read__alsa(mal_device* pDevice)
// readi/writei. // readi/writei.
snd_pcm_sframes_t framesRead = 0; snd_pcm_sframes_t framesRead = 0;
while (!pDevice->alsa.breakFromMainLoop) { while (!pDevice->alsa.breakFromMainLoop) {
mal_uint32 framesAvailable = mal_device__wait_for_frames(pDevice); mal_uint32 framesAvailable = mal_device__wait_for_frames__alsa(pDevice);
if (framesAvailable == 0) { if (framesAvailable == 0) {
return MAL_FALSE; return MAL_FALSE;
} }
...@@ -2227,9 +2307,11 @@ void mal_device_uninit(mal_device* pDevice) ...@@ -2227,9 +2307,11 @@ void mal_device_uninit(mal_device* pDevice)
// Make sure the device is stopped first. The backends will probably handle this naturally, // Make sure the device is stopped first. The backends will probably handle this naturally,
// but I like to do it explicitly for my own sanity. // but I like to do it explicitly for my own sanity.
if (mal_device_is_started(pDevice)) {
while (mal_device_stop(pDevice) == MAL_DEVICE_BUSY) { while (mal_device_stop(pDevice) == MAL_DEVICE_BUSY) {
mal_sleep(10); mal_sleep(10);
} }
}
// Putting the device into an uninitialized state will make the worker thread return. // Putting the device into an uninitialized state will make the worker thread return.
mal_device__set_state(pDevice, MAL_STATE_UNINITIALIZED); mal_device__set_state(pDevice, MAL_STATE_UNINITIALIZED);
......
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