Back again with yet another DirectSound question, this one regarding the ways DirectSound Buffers can be used:
I have packets coming in over the network at approximately 30ms intervals containing audio data that is decoded into raw wav data by other parts of the application.
When the Indata event is triggered by these other pieces of code, I'm essentially dropped into a procedure with the Audio Data as a parameter.
DSCurrentBuffer was initialized as follows:
ZeroMemory(@BufferDesc, SizeOf(DSBUFFERDESC));
wfx.wFormatTag := WAVE_FORMAT_PCM;
wfx.nChannels := 1;
wfx.nSamplesPerSec := fFrequency;
wfx.wBitsPerSample := 16;
wfx.nBlockAlign := 2; // Channels * (BitsPerSample/8)
wfx.nAvgBytesPerSec := fFrequency * 2; // SamplesPerSec * BlockAlign
BufferDesc.dwSize := SizeOf(DSBUFFERDESC);
BufferDesc.dwFlags := (DSBCAPS_GLOBALFOCUS or DSBCAPS_GETCURRENTPOSITION2 or
DSBCAPS_CTRLPOSITIONNOTIFY);
BufferDesc.dwBufferBytes := BufferSize;
BufferDesc.lpwfxFormat := @wfx;
case DSInterface.CreateSoundBuffer(BufferDesc, DSCurrentBuffer, nil) of
DS_OK:
;
DSERR_BADFORMAT:
ShowMessage('DSERR_BADFORMAT');
DSERR_INVALIDPARAM:
ShowMessage('DSERR_INVALIDPARAM');
end;
I write this data to my Secondary Buffer as follows:
var
FirstPart, SecondPart: Pointer;
FirstLength, SecondLength: DWORD;
AudioData: Array [0 .. 511] of Byte;
I, K: Integer;
Status: Cardinal;
begin
Input data is converted to audio data here, not relevant to the question itself.
DSCurrentBuffer.GetStatus(Status);
if (Status and DSBSTATUS_PLAYING) = DSBSTATUS_PLAYING then // If it is playing, request the next segment of the buffer for writing
begin
DSCurrentBuffer.Lock(LastWrittenByte, 512, @FirstPart, @FirstLength,
@SecondPart, @SecondLength, DSBLOCK_FROMWRITECURSOR);
move(AudioData, FirstPart^, FirstLength);
LastWrittenByte := LastWrittenByte + FirstLength;
if SecondLength > 0 then
begin
move(AudioData[FirstLength], SecondPart^, SecondLength);
LastWrittenByte := SecondLength;
end;
DSCurrentBuffer.GetCurrentPosition(@PlayCursorPosition,
@WriteCursorPosition);
DSCurrentBuffer.Unlock(FirstPart, FirstLength, SecondPart, SecondLength);
end
else // If it isn't playing, set play cursor position to the start of buffer and lock the entire buffer
begin
if LastWrittenByte = 0 then
DSCurrentBuffer.SetCurrentPosition(0);
LockResult := DSCurrentBuffer.Lock(LastWrittenByte, 512, @FirstPart, @FirstLength,
@SecondPart, @SecondLength, DSBLOCK_ENTIREBUFFER);
move(AudioData, FirstPart^, 512);
LastWrittenByte := LastWrittenByte + 512;
DSCurrentBuffer.Unlock(FirstPart, 512, SecondPart, 0);
end;
The above is the code that I run in my OnAudioData-event (defined by our proprietary component that decodes messages sent with our protocol.) Basically, the code is run whenever I get an UDP message with audio data.
After writing to the buffer, I do the following to start playback once enough data is in the buffer: BufferSize is, by the way, set to the equivalent of 1 second at the moment.
if ((Status and DSBSTATUS_PLAYING) <> DSBSTATUS_PLAYING) and
(LastWrittenByte >= BufferSize div 2) then
DSCurrentBuffer.Play(0, 0, DSCBSTART_LOOPING);
So far so good, though the audio playback is slightly choppy. Unfortunately, this code isn't enough.
I also need to stop playback and wait for the buffer to fill up again when it runs out of data. This is where I'm running into problems.
Basically, I need to be able to find out when audio playback has reached the point where I last wrote into the buffer and stop it when it does. Otherwise any delays in the audio data I am receiving will mess up the audio, of course. I unfortunately can't just stop writing into the buffer because if I let the buffer keep playing, it will just play the old data left there from one second ago (what with being circular and all.) So I need to know if the PlayCursorPosition has reached the LastWrittenByte value that I keep track of in my buffer-writing code.
DirectSound Notifications seemed to be able to do this for me, but stopping and then starting the buffer anew every time I write data to it (SetNotificationPositions() requires the buffer to be stopped) has a notable impact on the playback itself, so the audio that plays back sounds even more broken than before.
I added this to the end of my writing code to get notifications. I kind of expected setting a new Notification every time I write data into the buffer probably wouldn't work all too well... But hey, I figured it couldn't hurt to try:
if (Status and DSBStatus_PLAYING) = DSBSTATUS_PLAYING then //If it's playing, create a notification
begin
DSCurrentBuffer.QueryInterface(IID_IDirectSoundNotify, DSNotify);
NotifyDesc.dwOffset := LastWrittenByte;
NotifyDesc.hEventNotify := NotificationThread.CreateEvent(ReachedLastWrittenByte);
DSCurrentBuffer.Stop;
LockResult := DSNotify.SetNotificationPositions(1, @NotifyDesc);
DSCurrentBuffer.Play(0, 0, DSBPLAY_LOOPING);
end;
NotificationThread is a thread that does WaitForSingleObject. CreateEvent creates a new event handle and makes it so that WaitForSingleObject will start waiting for that instead of the previous one. ReachedLastWrittenByte is a procedure defined in my application. The thread starts a criticalsection and calls it when a notification is triggered. (WaitForSingleObject is called with a timeout of 20ms so that I can update the handle whenever CreateEvent is called, of course.)
ReachedLastWrittenByte() does the following:
DSCurrentBuffer.Stop;
LastWrittenByte := 0;
When my notification is triggered and I call Stop on the Secondary Buffer I'm using, the audio still keeps on looping what seems to be leftover data in the primary buffer...
And even then, the notifications aren't properly triggered. If I stop the broadcast of audio data from the other application that is sending these audio messages, it just keeps looping over the leftovers in the buffer. So basically, it's going past the latest notification I set (the lastwrittenbyte) regardless. When in playback, it just occasionally stops up, fills the buffer and then starts playing... And skipping the half second of data it just buffered, just going ahead to play the data that is coming in after the buffer is filled (so it fills up the buffer, but apparently does not care to play its contents before it starts filling new data in there. Yeah. I have no idea either.)
What do I seem to be missing here? Is the idea to use DirectSound Notificatiosn to find out when the the most recently written byte is played a fruitless effort? You'd think there'd be SOME way of doing this kind of buffering with a streaming buffer.
It always sucks finding people asking the same question as you only to eventually go "nevermind I solved it" and not leave an answer.
<---- +1. – Convince