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.
And again I found myself answering my own question ^^;
There were three problems: First, I was looking at the Play Cursor position to determine if playback had reached the last written byte. That does not work quite right, because while the play cursor shows what is currently being played, it does not tell you what the last byte of data set to be played is. The write cursor is always positioned a little bit ahead of the play cursor. The area between the play cursor and the write cursor is locked for playback.
In other words, I was looking at the wrong cursor.
Second, and here's the main reason stuff didn't work as it should, is that I was writing data with the DSBLOCK_FROMWRITECURSOR flag. What this does is allow me to write data from the write cursor and onwards. Every time.
Which means that when I thought it was buffering data, it was actually just writing over the same data again and again. The write cursor, contrary to what I had assumed, does not move forward when you write data to the buffer. So what I do now is that I keep track of my last written byte manually and just do a DSBLOCK_ENTIREBUFFER on that offset. I handle wrapping by just keeping track of buffer size and my lastwrittenbyte var and confirm that it doesn't wrap. It helps that I only ever need to write exactly 512 bytes of data into the buffer at any time. Making my buffer-size a multiple of 512 means I only ever have to make sure that if lastwrittenbyte >= buffersize then lastwrittenbyte := 0 (I changed lastwrittenbyte to nextbyte, since it now rather shows the next byte to write to. That way I don't have to worry about uneven bytes, evening it out with a + 1 and all that.)
So the supposed method for streaming data into the buffer has you writing over your own data time and time again. How they intended you to properly use this for streaming data into the buffer... I really don't know. Perhaps they're thinking you could put a notification in the middle and treat it like a split buffer?
Which brings me to the third problem: I was trying to use notifications. It turns out that notifications are unreliable and people overall recommend that you avoid using them, and if you have to then you shouldn't use more than a few.
So I did away with notifications and tossed in a high-resolution timer that triggers every 30ms (the data packets I'm receiving and extracting the audio to be played from will according to our protocol always be sent at a 32 ms interval, so anything below 32ms works for this timer really.)
I keep track of how much data I have left to play with a simple BytesInBuffer variable. When my timer triggers, I check how much the write cursor (which as I said before essentially is the true play cursor) has advanced since the last time the timer triggered and remove this number from my BytesInBuffer var.
From there I can either look at if I've run out of bytes, or I can look at the writecursor position and check if it has exceeded my lastwrittenbyte (knowing how many bytes I have played since my last timer-event helps.
It always sucks finding people asking the same question as you only to eventually go "nevermind I solved it" and not leave an answer.
Here's to hoping this might enlighten someone else in the future.