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,10 +1439,105 @@ static mal_result mal_device__break_main_loop__dsound(mal_device* pDevice) ...@@ -1440,10 +1439,105 @@ 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_bool32 mal_device__get_current_frame__dsound(mal_device* pDevice, mal_uint32* pCurrentPos)
{
mal_assert(pDevice != NULL);
mal_assert(pCurrentPos != NULL);
*pCurrentPos = 0;
DWORD dwCurrentPosition;
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;
}
}
*pCurrentPos = (mal_uint32)dwCurrentPosition / mal_get_sample_size_in_bytes(pDevice->format) / pDevice->channels;
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) {
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 {
mal_uint32 validBeg = pDevice->dsound.lastProcessedFrame;
mal_uint32 validEnd = currentFrame;
if (validEnd < validBeg) {
validEnd += totalFrameCount; // Wrap around.
}
mal_uint32 validSize = (validEnd - validBeg);
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;
HANDLE eventHandles[MAL_MAX_FRAGMENTS_DSOUND + 1]; // +1 for the stop event.
mal_copy_memory(eventHandles, pDevice->dsound.pNotifyEvents, sizeof(HANDLE) * pDevice->fragmentCount);
eventHandles[eventCount-1] = pDevice->dsound.hStopEvent;
while (!pDevice->dsound.breakFromMainLoop) {
mal_uint32 framesAvailable = mal_device__get_available_frames__dsound(pDevice);
if (framesAvailable == 0) {
return 0;
}
// Never return more frames that will fit in a fragment.
if (framesAvailable >= pDevice->fragmentSizeInFrames) {
return pDevice->fragmentSizeInFrames;
}
// If we get here it means we weren't able to find a full fragment. We'll just wait here for a bit.
const DWORD timeoutInMilliseconds = 5; // <-- This affects latency with the DirectSound backend. Should this be a property?
DWORD rc = WaitForMultipleObjects(eventCount, eventHandles, FALSE, timeoutInMilliseconds);
if (rc < WAIT_OBJECT_0 || rc >= WAIT_OBJECT_0 + eventCount) {
return 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) static mal_result mal_device__main_loop__dsound(mal_device* pDevice)
{ {
mal_assert(pDevice != NULL); mal_assert(pDevice != NULL);
...@@ -1459,63 +1553,49 @@ static mal_result mal_device__main_loop__dsound(mal_device* pDevice) ...@@ -1459,63 +1553,49 @@ static mal_result mal_device__main_loop__dsound(mal_device* pDevice)
pDevice->dsound.ignoredFragmentCounter = 1; pDevice->dsound.ignoredFragmentCounter = 1;
} }
for (;;) { pDevice->dsound.breakFromMainLoop = MAL_FALSE;
// Wait for a notification. Notifications are tied to fragments. while (!pDevice->dsound.breakFromMainLoop) {
unsigned int eventCount = pDevice->fragmentCount + 1; mal_uint32 framesAvailable = mal_device__wait_for_frames__dsound(pDevice);
HANDLE eventHandles[MAL_MAX_FRAGMENTS_DSOUND + 1]; // +1 for the stop event. if (framesAvailable == 0) {
mal_copy_memory(eventHandles, pDevice->dsound.pNotifyEvents, sizeof(HANDLE) * pDevice->fragmentCount); continue;
eventHandles[eventCount-1] = pDevice->dsound.hStopEvent; }
DWORD rc = WaitForMultipleObjects(eventCount, eventHandles, FALSE, INFINITE); // If it's a playback device, don't bother grabbing more data if the device is being stopped.
if (rc < WAIT_OBJECT_0 || rc >= WAIT_OBJECT_0 + eventCount) { if (pDevice->dsound.breakFromMainLoop && pDevice->type == mal_device_type_playback) {
break; return MAL_FALSE;
} }
unsigned int eventIndex = rc - WAIT_OBJECT_0; DWORD lockOffset = pDevice->dsound.lastProcessedFrame * pDevice->channels * mal_get_sample_size_in_bytes(pDevice->format);
HANDLE hEvent = eventHandles[eventIndex]; DWORD lockSize = framesAvailable * pDevice->channels * mal_get_sample_size_in_bytes(pDevice->format);
// 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 (IDirectSoundCaptureBuffer8_GetCurrentPosition((LPDIRECTSOUNDCAPTUREBUFFER8)pDevice->dsound.pCaptureBuffer, &dwPlayPosition, NULL) == DS_OK) {
DWORD sampleCount = (dwPlayPosition - dwOffset) / mal_get_sample_size_in_bytes(pDevice->format);
if (sampleCount > 0) {
void* pLockPtr;
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);
}
}
}
}
break; if (pDevice->type == mal_device_type_playback) {
} if (pDevice->dsound.breakFromMainLoop) {
return MAL_FALSE;
}
// If we get here it means the event that's been signaled represents a fragment. void* pLockPtr;
unsigned int fragmentIndex = eventIndex; // <-- Just for clarity. DWORD actualLockSize;
mal_assert(fragmentIndex < pDevice->fragmentCount); if (FAILED(IDirectSoundBuffer_Lock((LPDIRECTSOUNDBUFFER)pDevice->dsound.pPlaybackBuffer, lockOffset, lockSize, &pLockPtr, &actualLockSize, NULL, NULL, 0))) {
return mal_post_error(pDevice, "[DirectSound] IDirectSoundBuffer_Lock() failed.", MAL_FAILED_TO_MAP_DEVICE_BUFFER);
}
pDevice->dsound.lastProcessedFragment = fragmentIndex; mal_uint32 sampleCount = actualLockSize / mal_get_sample_size_in_bytes(pDevice->format);
mal_device__read_samples_from_client(pDevice, sampleCount, pLockPtr);
pDevice->dsound.lastProcessedFrame = (pDevice->dsound.lastProcessedFrame + (sampleCount/pDevice->channels)) % (pDevice->fragmentSizeInFrames*pDevice->fragmentCount);
// Some initial fragments need to be skipped over. IDirectSoundBuffer_Unlock((LPDIRECTSOUNDBUFFER)pDevice->dsound.pPlaybackBuffer, pLockPtr, actualLockSize, NULL, 0);
if (pDevice->dsound.ignoredFragmentCounter > 0) {
pDevice->dsound.ignoredFragmentCounter -= 1;
continue;
}
if (pDevice->type == mal_device_type_playback) {
mal_device__read_fragment_from_client__dsound(pDevice, (fragmentIndex + 1) % pDevice->fragmentCount);
} 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,8 +2307,10 @@ void mal_device_uninit(mal_device* pDevice) ...@@ -2227,8 +2307,10 @@ 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.
while (mal_device_stop(pDevice) == MAL_DEVICE_BUSY) { if (mal_device_is_started(pDevice)) {
mal_sleep(10); while (mal_device_stop(pDevice) == MAL_DEVICE_BUSY) {
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.
......
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