Programming articles
Part 2 : DirectX Sound Server
 
Note: This article was published on GameDev.net website !
Part 2 : Using DirectX API

Few days ago we learn to make a SoundServer using the windows WaveOut API. (Read the first part for more infos about SoundServer) Now we'll use the today famous Direct-Sound API.

 
Is DirectSound better than WaveOut ??

As all simple questions, answer is quite not simple ! :-) In fact, you have to know exactly what's important for your sound server. If your program have to be very accurate (I mean game, demo, or anything requiring high visual/sound synchronisation), use DirectSound. The drawback is that it's a bit more complicated to use (thread usage) and user should have the DirectX API installed. If you only want to play a streaming audio in the background in a tool, just use the WaveOut version.

How does it works

If you read the previous article, you're familiar with the multi-buffering. With DirectSound, we don't use the same technic. Basicaly DirectSound provides a set of sample buffers, and mix them together. If you want some sound fx in your next generation video game, just create a DirectSoundBuffer for each sample, and play them. DirectSound manages all the mixing, polling etc.. for you !

So you say, "great", that's quite easy ! Yes, but we're speaking of a sound server, for streaming purpose ! So we have the same problem: we want a short sound buffer, and we want the sound server call our own callback periodicaly. Unfortunatly, DirectX7 does not provide streaming sound buffer (maybe in DX8). So we'll use that sheme:

  1. Create a DirectSoundBuffer
  2. Create and launch a thread rout, wich goal is to poll the SoundBuffer without end. Each time we have a little space in it, we fill the buffer with our own data, and so on.
Some words about DirectSound buffers...

DirectSound uses SoundBuffer to play sound. You can use one sound buffer for each sound effect you have to play. All these sounds are mixed into an special sound buffer called the primary sound buffer. All DirectSound app must create a primary sound buffer. For our SoundServer, we can fill directly the primary sound buffer with our data, but writing to the primary sound buffer is not allowed on all drivers or operating system (NT). So we'll use a second buffer, wich is not a primary one. We can lock/unlock and write data in that new buffer without trouble. So our SoundServer will contain a primary sound buffer and a classic sound buffer.

Let's do the code

All the sound server is encapsulated in a class called CDXSoundServer. You have to send the handle of your main window to the constructor, because DirectX need it. Then you can start the server by calling the "open" method. Open method gets your own callback function as an argument. Let's see the open method in detail:

1) Create a DirectSound object.

HRESULT hRes = ::DirectSoundCreate(0, &m_pDS, 0);

WARNING: In our sample I simply check if all is ok. If not, open returns FALSE. You have to add some better error message handler. As an example, if DirectSoundCreate returns an error, maybe DirectSound is not installed on the machine.

2) At that point, m_pDS is a valid LPDIRECTSOUND. Now we set a cooperative level. We choose DSSCL_EXCLUSIVE because we want our app to be the only one to play sound (others apps stops playing sound if they don't have the focus) and DSSCL_PRIORITY allowing us to set our own sound buffer format. (This is only for easy enderstanding, because DSSCL_EXCLUSIVE includes DSSCL_PRIORITY).

hRes = m_pDS->SetCooperativeLevel(m_hWnd,DSSCL_EXCLUSIVE | DSSCL_PRIORITY);

3) Now we can create the primary sound buffer and set its internal format.

DSBUFFERDESC bufferDesc;
memset(&bufferDesc, 0, sizeof(DSBUFFERDESC));
bufferDesc.dwSize = sizeof(DSBUFFERDESC);
bufferDesc.dwFlags = DSBCAPS_PRIMARYBUFFER|DSBCAPS_STICKYFOCUS;
bufferDesc.dwBufferBytes = 0;
bufferDesc.lpwfxFormat = NULL;
hRes = m_pDS->CreateSoundBuffer(&bufferDesc,&m_pPrimary, NULL);
if (hRes == DS_OK)
{

WAVEFORMATEX format;
memset(&format, 0, sizeof(WAVEFORMATEX));
format.wFormatTag = WAVE_FORMAT_PCM;
format.nChannels = 1; // mono
format.nSamplesPerSec = DXREPLAY_RATE;
format.nAvgBytesPerSec = DXREPLAY_SAMPLESIZE * DXREPLAY_RATE;
format.nBlockAlign = DXREPLAY_SAMPLESIZE;
format.wBitsPerSample = DXREPLAY_DEPTH;
format.cbSize = 0;
hRes = m_pPrimary->SetFormat(&format);

4) Now create a normal sound buffer (to be filled by our rout) and set the same format as the primary. Of course you can set another format, but in that case you'll get a speed penalty.

DSBUFFERDESC bufferDesc;
memset(&bufferDesc,0,sizeof(bufferDesc));
bufferDesc.dwSize = sizeof(bufferDesc);
bufferDesc.dwFlags = DSBCAPS_GETCURRENTPOSITION2|DSBCAPS_STICKYFOCUS;
bufferDesc.dwBufferBytes = DXREPLAY_BUFFERLEN;
bufferDesc.lpwfxFormat = &format; // Same format as primary
hRes = m_pDS->CreateSoundBuffer(&bufferDesc,&m_pBuffer,NULL);

WARNING: Please notice the DSBCAPS_STICKYFOCUS flags. This flags allow our app to play sound even if we don't have have focus. Very usefull if you write a sound player. The DSBCAPS_GETCURRENTPOSITION2 tells DirectSound we'll use the GetPosition method later on that sound buffer.

5) And finally, play the empty sound buffer in loop mode, and launch a new thread to fill it:

hRes = m_pBuffer->Play(0, 0, DSBPLAY_LOOPING);
m_hThread = (HANDLE)CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)threadRout,(void *)this,0,&tmp);

Some words about threads...

What's a thread ?? A thread is another task of your program. That is, you have benefit of multi-tasking AND memory sharing ! Our thread have to check the sound buffer all the time. So let's imagine we have only two threads running: our app and our sound thread. All threads will share 50% of CPU each. But I'm sure you don't want your SoundServer takes 50% of CPU time !! :-) So we'll use the "sleep" function. Sleep tells window to forgot the thread for a given amount of time. Sleep(20) suspends the thread for 20ms, so the app have 100% of CPU in that time ! 20ms is a good timing for a sound server. Of course, in practice your app will never have exactly 100% CPU, because of the operating system himself. Our thread routs looks like:

static DWORD WINAPI __stdcall threadRout(void *pObject)
{

CDXSoundServer *pDS = (CDXSoundServer *)pObject;
if (pDS)
{

while ( pDS->update() )
{

Sleep(20);

}

}

return 0;

}

NOTE: You may have notice the m_bThreadRunning is "volatile". Don't forget thread uses shared memory, so the m_bThreadRunning member can be changed by another task. That's why we don't want the compiler uses registers. Volatile tells compiler the memory can be changed by an interrupt routine.

How to fill a sound buffer...

Our thread rout calls DCXSoundServer::update() as often as possible. That function have to check where the sound buffer is currently playing (sound buffer are circular). We keep an internal position (m_writePos) wich is our own position. Let's imagine the sound buffer is 8192 bytes len and we already computed 120 bytes. so m_writePos = 120. At the same time, let's say the playing position is 4120. So we can compute safely 4000 bytes of new data from m_writePos to playPos. (because we can't write over the playing cursor without hear nastly glitchs). So, first we get the playing position, and we compute the data size to be generated from m_writePos to playPos (don't forget we're in a circular buffer)

HRESULT hRes = m_pBuffer->GetCurrentPosition(&playPos,&unusedWriteCursor);
if (m_writePos < playPos) writeLen = playPos - m_writePos;
else writeLen = DXREPLAY_BUFFERLEN - (m_writePos - playPos);

Now we can safely compute"wrileLen" bytes of data at the m_writePos. To fill a DirectSoundBuffer, we have to lock it:

while (DS_OK != m_pBuffer->Lock(m_writePos,writeLen,&p1,&l1,&p2,&l2,0))
{

m_pBuffer->Restore();
m_pBuffer->Play(0, 0, DSBPLAY_LOOPING);

}

Please notice that lock can returns an error when SoundBuffer have to be restored. Our error check is very lame, because we don't check the DSERR_BUFFERLOST error code. But that's quite enough for our article ! Finally we can call our user callback with valid pointer and size:

if (m_pUserCallback)
{

if ((p1) && (l1>0)) m_pUserCallback(p1,l1);
if ((p2) && (l2>0)) m_pUserCallback(p2,l2);

}

 

Source code and sample project...

As always, the source code of a very "short and simple" sound server using DirectSound API. If you want to use it in your project, just use DXSoundServer.cpp and DXSoundServer.h

As a sample, you can download a complete project containing a sin wave generator, using both WaveOut or DirectSound API.

WARNING: Do not forget to link your project with WINMM.LIB to use the WaveOut Sound Server,
and DSOUND.LIB to use the DirectSound API.

Download the Sound Server source code AND a sample project, playing a generated sin-wave.

Hope you like the article !
 

This programming article is written by Arnaud Carré and is part of the *Leonard alternative programming homepage*