Programming articles
Part 1 : WaveOut Sound Server
 
Note: This article was published on GameDev.net website !
 
What is a Sound Server ?

A Sound Server is some code to play sound. (What a general definition !). Each time you want to code computer sound, you have to use a Sound Server. A Sound Server can be represented as a single thread, managing the System sound device all the time. The user (you !) just send some sound data to the Sound Server regulary.

 
Why need I a sound Server ??

Let's imagine you just coded a great demo, and you want to add some music. You like these old soundchip tunes and you want to play an YM file (ST-Sound music file format). You download the package to play an YM file, but unfortunatly the package is only a "music rendering" package. That is, with the library, you can ONLY generate some samples in memory, not into the sound device ! Many music library are made like this. Tradionnaly, the library provides a function like:

MusicCompute(void *pSampleBuffer,long bufferSize)

So the SoundServer is for you !!! My SoundServer provides all Windows sound device managing, and call your callback regulary. Here is what your code should be:

#include <windows.h>
#include "SoundServer"

static CSoundServer soundServer;

void myCallback(void *pSampleBuffer,long bufferLen)
{
MusicCompute(pSampleBuffer,bufferLen); // original music package API
}

void main(void)
{
soundServer.open(myCallback);
// wait a key or anything you want
soundServer.close();
}

How does it works

Managing sound device under Windows can be done with various API. Today we'll use the classic Multimedia API called WaveOut. So our Sound Server will work properly even if you don't have DirectSound. We'll see a DirectSound version of the Sound Server in the next article.

The main problem is that we're speaking of sound, so we have some rules to respect to avoid listening some nasty *blips* in the sound stream. Let's imagine we want to play an YM file at 44100Khz, 16bits, mono, and we have internal buffers of 1 second. First, we fill our buffer with the start of the music, and we play the buffer through the Windows API. After one second, buffer is finished, so Windows tell us that the buffer is done, and we have to fill it again with the next of the song. We can fill our buffer again, and send back to the sound device. BUT, in this case, playback is stopped until we fill the buffer again, so we hear some *blips* !!

To avoid that problem, we'll use the queuing capability of the WaveOut API. Just imagine you have two buffers of one second each, already filled with valid sound data (let's call them buffer1 and buffer2). If you play buffer1 and IMMEDIATLY play buffer2, buffer1 is not cutted. Buffer2 is just "queued", and buffer1 is still playing. when buffer1 is finished, Windows starts IMMEDIATLY buffer2 so there is no *blips*, and inform you buffer1 is finished through a callback. So you have 1 second to fill buffer1 again and send it to the sound device. Quite simple, no ?

Let's do the code

All the sound server is encapsulated in a class called CSoundServer. You start the server by calling the "open" method. Open method gets your own callback function as an argument. Then we initialize the Windows WaveOut API by calling

waveOutOpen( &m_hWaveOut, WAVE_MAPPER, &wfx, (DWORD)waveOutProc, (DWORD)this, // User data. (DWORD)CALLBACK_FUNCTION);

Please note that waveOutProc is our internal callback. And this callback will call your user-callback.

Then we fill all our sound buffer (remember the multi-buffering to avoid *blips*).

for (i=0;i<REPLAY_NBSOUNDBUFFER,i++)
{
fillNextSoundBuffer();
}

Let's have a look to the most important function: "fillNextSoundBuffer". First, we have to call your user callback, to fill the sound buffer with real sample data.

// Call the user function to fill the buffer with anything you want ! :-)
if (m_pUserCallback) m_pUserCallback(m_pSoundBuffer[m_currentBuffer],m_bufferSize);

Then we have to prepare the buffer before sending it to the sound device:

// Prepare the buffer to be sent to the WaveOut API
m_waveHeader[m_currentBuffer].lpData = (char*)m_pSoundBuffer[m_currentBuffer];
m_waveHeader[m_currentBuffer].dwBufferLength = m_bufferSize; waveOutPrepareHeader(m_hWaveOut,&m_waveHeader[m_currentBuffer],sizeof(WAVEHDR));

and finnaly we can send it to the device with the waveOutWrite

// Send the buffer the the WaveOut queue
waveOutWrite(m_hWaveOut,&m_waveHeader[m_currentBuffer],sizeof(WAVEHDR));

That's all folks !! Quite easy, no ??

How can I use it ?

I like "clean and short" code. Traditionnaly, when I get a source code from the web, it's always a nightmare to compile and run it. So I try to do things as simple as possible. To use the sound server, just copy SoundServer.cpp and SoundServer.h files in your project directory.

WARNING: Do not forget to link your project with WINMM.LIB to use the Sound Server.

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


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