views:

457

answers:

2

Right-o, I'm working on implementing DirectSound in our Delphi voip app (the app allows for radios to be used by multiple users over a network connection) Data comes in via UDP broadcasts. As it is now, we go down on a raw data level and do the audio mixing from multiple sources ourselves and have a centralized component that is used to play all this back.

The app itself is a Delphi 5 app and I'm tasked with porting it to Delphi 2010. Once I got to this audio playback part, we concluded that it is best if we can get rid of this old code and replace it with directsound.

So, the idea is to have one SecondaryBuffer per radio (we have one 'panel' each per radio connection, based on a set of components that we create for every specific radio) and just let these add data to their respective SecondaryBuffers whenever they get data, only pausing to fill up half a second worth of audio data in the buffer if it runs out of data.

Now, I'm stuck at the part where I'm adding data to the buffers in my test-application where I'm just trying to get this to work properly before I start writing a component to utilize it the way we want.

I'm using the ported DirectX headers for Delphi (http://www.clootie.ru/delphi/download_dx92.html)

The point of these headers is to port over the regular DirectSound interface to Delphi, so hopefully non-Delphi programmers with DirectSound may know what the cause of my problem is as well.

My SecondaryBuffer (IDirectSoundBuffer) was created as follows:

var
  BufferDesc: DSBUFFERDESC;
  wfx: tWAVEFORMATEX;


wfx.wFormatTag := WAVE_FORMAT_PCM;
wfx.nChannels := 1;
wfx.nSamplesPerSec := 8000;
wfx.wBitsPerSample := 16;
wfx.nBlockAlign := 2; // Channels * (BitsPerSample/2)
wfx.nAvgBytesPerSec := 8000 * 2; // SamplesPerSec * BlockAlign

BufferDesc.dwSize := SizeOf(DSBUFFERDESC);
BufferDesc.dwFlags := (DSBCAPS_GLOBALFOCUS or DSBCAPS_GETCURRENTPOSITION2 or DSBCAPS_CTRLPOSITIONNOTIFY);
BufferDesc.dwBufferBytes := wfx.nAvgBytesPerSec * 4; //Which should land at 64000
BufferDesc.lpwfxFormat  := @wfx;


case DSInterface.CreateSoundBuffer(BufferDesc, DSCurrentBuffer, nil) of
  DS_OK: ;
  DSERR_BADFORMAT: ShowMessage('DSERR_BADFORMAT');
  DSERR_INVALIDPARAM: ShowMessage('DSERR_INVALIDPARAM');
  end;

I left out the parts where I defined my PrimaryBuffer (it's set to play with the looping flag and was created exactly as MSDN says it should be) and the DSInterface, but it is as you might imagine an IDirectSoundInterface.

Now, every time I get an audio message (detected, decoded and converted to the appropriate audio format by other components we have made that have been confirmed to work for over seven years), I do the following:

DSCurrentBuffer.Lock(0, 512, @FirstPart, @FirstLength, @SecondPart, @SecondLength, DSBLOCK_FROMWRITECURSOR);
Move(AudioData, FirstPart^, FirstLength);
if SecondLength > 0 then
  Move(AudioData[FirstLength], SecondPart^, SecondLength);

DSCurrentBuffer.GetStatus(Status);
DSCurrentBuffer.GetCurrentPosition(@PlayCursorPosition, @WriteCursorPosition);
if (FirstPart <> nil) or (SecondPart <> nil) then
  begin
    Memo1.Lines.Add('FirstLength = ' + IntToStr(FirstLength));
    Memo1.Lines.Add('PlayCursorPosition = ' + IntToStr(PlayCursorPosition));
    Memo1.Lines.Add('WriteCursorPosition = ' + IntToStr(WriteCursorPosition));
  end;
DSCurrentBuffer.Unlock(@FirstPart, FirstLength, @SecondPart, SecondLength);

AudioData contains the data in my message. Messages always contain 512 bytes of audio data. I added the Memo1.Lines.Add lines to be able to get some debug output (since using breakpoints doesn't quite work, as directsound keeps playing the contents of the primary buffer regardless)

Now, when I'm playing my DSCurrentBuffer using the looping flag (which according to hte MSDN docs is enough to make it a Streaming Buffer) and having this code work out as it wants, my output text in the Memo show that I am being allowed to write up until the end of the buffer... But it doesn't wrap.

SecondPart is always nil. It never ever wraps around to the beginning of the buffer, which means I get the same few seconds of audio data playing over and over.

And yes, I have scoured the net for components that can do this stuff for us and have concluded that the only reliable way is to do it ourselves like this.

And yes, the audio data that this app plays is choppy. I'm holding off on writing the half-a-second buffering code until I can get the write-to-buffer code to wrap as it should :/

I have been reading that people suggest keeping track of your own write cursor, but from what I read Lock and Unlock should help me bypass that need. I'd also rather avoid having to have two buffers that I alternate between back and forth (or a split-buffer, which would essentially be the same thing, only a bit more complex in writing)

Any help greatly appreciated!

+1  A: 

A few things that might cause this:

  1. Memo1.Lines.Add should only be called from the main thread (the thread that initialized the VCL GUI). Use TThread.Synchronize for this (easier), or an intermediate buffer that is thread safe and preferably lock-free (faster; thanks mghie for this hint).

  2. Unlock should be in a finally section like below, because if an exception gets raised, you never unlock the buffer, see code sample below.

  3. You should log any exceptions taking place.

Sample code:

  DSCurrentBuffer.Lock(0, 512, @FirstPart, @FirstLength, @SecondPart, @SecondLength, DSBLOCK_FROMWRITECURSOR);
  try
    //...
  finally
    DSCurrentBuffer.Unlock(@FirstPart, FirstLength, @SecondPart, SecondLength);
  end;

--jeroen

Jeroen Pluimers
There is a typo in the code (finally). And I'd definitely stay away from `Synchronize()`. In every case, but especially in this one, as it negates the advantages of doing sound output in a background thread. There's no control over how long this will take. Much better to add the log messages to a thread-safe list and have the VCL thread add the queued messages to the memo asynchronously. This will still keep the order of the message, and will scale **much** better with multiple threads.
mghie
thx for the comment; modifed answer because of it.
Jeroen Pluimers
Ah, thank you and sorry, I figured out why it didn't want to wrap the buffer.It was incorrect to give it pointers to FirstPart and SecondPart in the Unlock() call.So now it wraps around and works as it should.This is a test application btw. The Memo is only there because there are no threads to mess with in this test application.No worries, I know how to keep things thread safe when it matters ^^I should indeed put in a try/catch there though. Really gotta make a habit of that.
Michael Stahre
+1  A: 

So I figured out the problem ^^;

Pretty simple too.

DSCurrentBuffer.Unlock(@FirstPart, FirstLength, @SecondPart, SecondLength);

I thought I was supposed to just pass along the same pointers to Pointers that Lock() had required.

Changing it to

DSCurrentBuffer.Unlock(FirstPart, FirstLength, SecondPart, SecondLength);

Solved the issue and the buffer now wraps correctly.

Sorry for wasting your time, but thanks anyway ^^;

Michael Stahre