Advertisement

sound mixer tutorial

Started by August 03, 2009 07:29 AM
1 comment, last by hunguptodry 15 years, 3 months ago
here is a tutorial i wrote about making a sound mixer. hope u can use it. is there a proper way to submit tutorials? This tutorial will show u how to implement a sound mixer. The mixer will allow your game to play up to 8 sounds at the same time. We will use the Wndows waveout API to implement this. The first thing to note about modelling sound is to observe the difference between sound and silence. The answer to this question is that there is little difference. You see, silence is simply a very soft sound that u can barely hear. Now that u are convinced that sound goes on continuously, all we need is a way to play this continuous stream much like we render video frames. The mixer will have 2 sound buffers. It will render the first buffer, then the second and repeat. Initially the buffers will contain silence so your mixer will render silence and u wil hear nothing. To play something u can hear. we modify one buffer while the mixer is rendering the other. This process is called mixing. Usually mixing is done in a separate accumulation buffer instead of the sound buffer. It is only copied to the sound buffer when everything is fully mixed-in. Mixing can be very complex. You can combine sound information from multiple sources or even modify the content on the fly. It can also be quite simple. Let us see what happens when we play a single sound stream. First we break up our stream into byte sized blocks that will fit nicely into our buffers. Recall that While our mixer is rendering one buffer we will work on the other. We first zero our buffer. Then we mix in our 1st block. When the mixer finds its way round to render our recently worked on buffer, we zero the other buffer and mix in the next block. This goes on until we run out of blocks. But regardless we should continue to zero out fresh buffers. The process above is all that is needed to render our sound stream or silence. Remember, there is no difference. Now on to some code. Here is the function InitWave that sets everything in motion.

int InitWave() { WAVEFORMATEX wfx; MMRESULT r;
    wfx.nSamplesPerSec = 44100; wfx.wBitsPerSample = 16; wfx.nChannels = 2;
    wfx.nBlockAlign = (wfx.wBitsPerSample*wfx.nChannels) >> 3;
    wfx.nAvgBytesPerSec = wfx.nBlockAlign*wfx.nSamplesPerSec;
    wfx.wFormatTag = WAVE_FORMAT_PCM; wfx.cbSize = 0; 
    r = waveOutOpen(&_hWaveOut,WAVE_MAPPER,&wfx,
        (DWORD)hWnd,0,CALLBACK_WINDOW
    );
    if (r == MMSYSERR_NOERROR) { int n;
        int i; for (i=0; i< 2; i++) { 
            ZeroMemory(&_wvhdr,sizeof(WAVEHDR));
            char *buff = (char *) malloc(8192);
            _wvhdr.dwBufferLength = 8192;
            _wvhdr.lpData = buff;
            memset(buff,0,8192);
        }
        for (i=0; i< 4096; i++) {
            _wvbuff = 0;
        } 
        for (i=0; i< 8; i++) {
            _wvf.fp = 0;
        }
        for (i=0; i< 2; i++) {
            n = i; _wvhdr[n].dwFlags = 0;
            r = waveOutPrepareHeader(
                _hWaveOut,&_wvhdr[n],
                sizeof(WAVEHDR)
            );
            r = waveOutWrite(
                _hWaveOut,&_wvhdr[n],
                sizeof(WAVEHDR)
            );
        }
    }
    else {
        _hWaveOut = 0;
    }
    return 1;
}


The following code fragment opens the sound device. The interesting thing here is we tell the waveout system to send a message to our window when it is done playing a buffer.

    r = waveOutOpen(&_hWaveOut,WAVE_MAPPER,&wfx,
        (DWORD)hWnd,0,CALLBACK_WINDOW
    );

It will send the WOM_DONE message to our WndProc. So we'll add a little code fragment to our WndProc like so ...

        case WOM_DONE:
             FillBuffer(lParam);
             return 0;
        case WM ....

Lets go back to our function InitWave. The code fragment below creates the 2 sound buffers that we need in _wvhdr[0] and _wvhdr[1]. Note that the buffer is 8192 bytes but each sample is 16-bits so we essentially have 4096 samples.

        int i; for (i=0; i&lt; 2; i++) { 
            ZeroMemory(&_wvhdr,sizeof(WAVEHDR));
            char *buff = (char *) malloc(8192);
            _wvhdr.dwBufferLength = 8192;
            _wvhdr.lpData = buff;
            memset(buff,0,8192);
        }

Here, our accumulation buffer is being initialized. All 4096 samples.

        for (i=0; i&lt; 4096; i++) {
            _wvbuff = 0;
        }

We have 8 slots to keep track of up to 8 opened files.

        for (i=0; i&lt; 8; i++) {
            _wvf.fp = 0;
        }

This is where we set things in motion by asking waveout to render the buffers. _wvhdr[0] is rendered first so it'll finish first and send the WOM_DONE message to our WndProc.

        for (i=0; i&lt; 2; i++) {
            n = i; _wvhdr[n].dwFlags = 0;
            r = waveOutPrepareHeader(
                _hWaveOut,&_wvhdr[n],
                sizeof(WAVEHDR)
            );
            r = waveOutWrite(
                _hWaveOut,&_wvhdr[n],
                sizeof(WAVEHDR)
            );
        }

Thats about it for InitWave. Lets talk about what to do when waveout has finished rendering a buffer. Recall the code fragment that we stuffed into our WndProc.

        case WOM_DONE:
             FillBuffer(lParam);
             return 0;

lParam simply points to the buffer that was just rendered. So we should fill it with more sound and set it off again. This all is done by FillBuffer.

int FillBuffer(WAVEHDR *wh) {
    int i; short *dest; if (_hWaveOut != 0) {
        waveOutUnprepareHeader(_hWaveOut,wh,sizeof(WAVEHDR));
        dest = (short *) wh-&gt;lpData;
        for (i=0; i&lt;4096; i++) {
            dest = _wvbuff;
        }
        waveOutPrepareHeader(
            _hWaveOut,wh,sizeof(WAVEHDR)
        );
        waveOutWrite(_hWaveOut,wh,
            sizeof(WAVEHDR)
        );
    }
    int n; for (i=0; i&lt;4096; i++) { _wvbuff = 0; }
    int j; for (j=0; j&lt;8; j++) {
        if (_wvf[j].fp != 0) { float vol; int dt;
            n = fread(_wtbuff,1,8192,_wvf[j].fp);
            n = n/2; vol = _wvf[j].vol;
            if (n &gt; 0) {
                dest = (short *) (_wtbuff+0);
                for (i=0; i&lt;n; i++) {
                    dest = vol*(0.5+dest);
                    dt = _wvbuff+dest;
                    _wvbuff = dt;
                }
            }
            else {
                if (_wvf[j].rep &gt; 0) {
                    fseek(_wvf[j].fp,0,0);
                }
                else {
                    fclose(_wvf[j].fp);
                    _wvf[j].fp = 0;
                }
            }
        }
    }
    return 1;
}

The first thing to do is to clean out the buffer that was returned to us.

        waveOutUnprepareHeader(_hWaveOut,wh,sizeof(WAVEHDR));

Next, we copy our accumulation buffer to the sound buffer.

        dest = (short *) wh-&gt;lpData;
        for (i=0; i&lt;4096; i++) {
            dest = _wvbuff;
        }

And prepare the sound buffer for the next render.

        waveOutPrepareHeader(
            _hWaveOut,wh,sizeof(WAVEHDR)
        );

Then, set it off again.

        waveOutWrite(_hWaveOut,wh,
            sizeof(WAVEHDR)
        );

We haven't done our mixing yet. So here we go. First thing, zero out our accumulation buffer with silence.

    int n; for (i=0; i&lt;4096; i++) { _wvbuff = 0; }

Check out our 8 slots. See if any of them has an opened stream. If so, mix-in with our accumulation buffer.

    int j; for (j=0; j&lt;8; j++) {
        if (_wvf[j].fp != 0) { float vol; int dt;
            n = fread(_wtbuff,1,8192,_wvf[j].fp);
            n = n/2; vol = _wvf[j].vol;
            if (n &gt; 0) {
                dest = (short *) (_wtbuff+0);
                for (i=0; i&lt;n; i++) {
                    dest = vol*(0.5+dest);
                    dt = _wvbuff+dest;
                    _wvbuff = dt;
                }
            }
            else {
                if (_wvf[j].rep &gt; 0) {
                    fseek(_wvf[j].fp,0,0);
                }
                else {
                    fclose(_wvf[j].fp);
                    _wvf[j].fp = 0;
                }
            }
        }
    }

That's it! We have our sound mixer. Here's how u would use it. Call PlayWave with a raw audio file. Set rep=1 to make the sound repeat. Set vol for rudimentary volume control. And use id to tag the sound stream. Note that PlayWave does not block. It should return immediately. for example: PlayWave("./ding.raw",0,1.0f,1001);

int PlayWave(char *ffname,int rep,float vol,int id) {
    MMRESULT r; FILE *fp = fopen(ffname,"rb");
    int i; if (fp != 0) {
        for (i=0; i&lt;8; i++) {
            if (_wvf.fp == 0) {
                _wvf.vol = vol;
                _wvf.rep = rep;
                _wvf.id = id;
                _wvf.fp = fp;
                break;
            }
        }
    }
    return i;
}

KillWave is the complement of InitWave. Shuts down the mixer.

int KillWave() {
    if (_hWaveOut != 0) { 
        int i; for(i=0; i&lt;2; i++) {
            while (!(_wvhdr.dwFlags & WHDR_DONE));
            waveOutUnprepareHeader(_hWaveOut,
                &_wvhdr,sizeof(WAVEHDR)
            );
            free(_wvhdr.lpData);
        }
        waveOutClose(_hWaveOut);
        _hWaveOut = 0;
        for(i=0; i&lt;8; i++) {
            if (_wvf.fp) {
                fclose(_wvf.fp);
                _wvf.fp = 0;
            }
        }
    }
    return 1;
}

Use StopWave to stop a sound stream prematurely. for example: StopWave(1001);

int StopWave(int id) {
    int i; for (i=0; i&lt;8; i++) {
        if (_wvf.id != id) {
            continue;
        }
        if (_wvf.fp != 0) {
            fclose(_wvf.fp);
            _wvf.fp = 0;
        }
    }
    return 1;
}

And here are the structs and variables we have been using.

typedef struct wave {
    int rep,id;
    float vol;
    FILE *fp;
} wave;

HWAVEOUT _hWaveOut;
WAVEHDR _wvhdr[2];

char _wtbuff[8192];
int _wvbuff[4096];
wave _wvf[8];

About raw audio files The system uses raw audio files. These are simply wav files with their headers stripped. unstripped wav files will probably work as raw files too. To be absolutely technical about this, raw files are just streams of unsigned 16-bit integers. Thanks for reading. wk.leong@trilobytetech.com aka hunguptodry [Edited by - hunguptodry on August 5, 2009 5:39:14 AM]
Hey there,

thanks for your tutorial.. Although I'd need to set up a new section for misc topics, as NeHe mostly deals with the graphics part of game development.

Btw, enclose your source in [ source lang = "cpp" ] [ / source ] tags to make it highlighted.
Advertisement
hi,

1) happy that u can use this.
2) the code tags are funky but they screw up all the for loops.

see ya

This topic is closed to new replies.

Advertisement