Introduction
Have you ever desired to play Waves in your Windows application or game without being lost with SDK / MFC anomalies which give you only a part of something, which is in fact useless if you're going to do some serious work? Here, I'll collect all needed parts and upgrade it to the higher level so you can think about more serious work with sound.
The CWaveBox
class relies on the SDK 'Waveform Audio Interface' API to play / produce (PCM uncompressed) multiwaves at once. Mixing and timing of multiple waves is required in games for adding interactive special/dimensional sound effects into. This class enables you possibility for that through the Windows WAI driver, by wrapping it with only one worker thread for multiwave support. I've seen a lots of examples about this topic concentrated about queuing / streaming WAI with two or more Wave data blocks continuously to avoid gap when switching, but few or just one of them only has properly freed / closed WAI interface which prevents unstoppable waste of memory if not anything else. Memory waste / leak of that (or any other) type is unknown with CWaveBox
. Playing performance is enhanced by caching Wave and playing from RAM, so, this class isn't for 'classic players' from file, which costs additional time for 'on the play' caching when CPU time is not critical. If you want to, feel free and recode it for such a thing, I won't be mad ;-). This class if fully portable to Windows CE, so you can play within at Pocket PC also.
Background
Whole idea for coding this class came from 'need for a sound' better than that from 'PlaySound
', 'sndPlaySound
' or any other partial solution. This class is already implemented in one board game, with great AI, for Pocket PC (which my friend Vrx and I have coded). Special thanks to people who have been translating the game tutorial into eight world languages, and some of them have 'stuck into translation' ;-). Here is a great tutorial at this topic: Using the Windows waveOut Interface.
Special thanks to the guy who wrote and coded the tutorial and WinAmp waveOut
plug-in. I hope he wouldn't criticize me, because I have borrowed a few functions (allocateBlocks
, freeBlocks
and waveOutProc
) from his tutorial / code and some of the variables notation. I'm a lazy ass. ;-).
All Waves mixed in the demo are downloaded from here.
Code, brief overview:
The CWaveBox
class is structured with the WAVE model which is used for storing wave data
, size
, wfx
(format info) loaded by the Load
method and Wave message WMSG
set by Play
method, to provide INTERFACE model with all required information and serves PlayThread
as sub interface to the WAI driver in order to successfully establish the WAI instance and play for every Wave.
WaveBox.h
#if !defined(AFX_WAVEBOX_H__DE24CFE1_7501_4DA3_AF18_667A845AAE49__INCLUDED_)
#define AFX_WAVEBOX_H__DE24CFE1_7501_4DA3_AF18_667A845AAE49__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif
#include <windows.h>
#include <mmsystem.h>
#define WAVE_FILE_MARK "RIFF"
#define WAVE_HEAD_MARK "WAVEfmt "
#define WAVE_DATA_MARK "data"
#define WAVE_PCM_16 16
#define WAVE_PCM_1 1
#define OFFSET_FILE_LEFT 4
#define OFFSET_HEAD_MARK 8
#define OFFSET_WAVE_PCM1 16
#define OFFSET_WAVE_PCM2 20
#define OFFSET_CHANNELS 22
#define OFFSET_SAMPLESPERSEC 24
#define OFFSET_AVGBYTESPERSEC 28
#define OFFSET_BLOCKALIGN 32
#define OFFSET_BITSPERSAMPLE 34
#define OFFSET_DATA_MARK 36
#define OFFSET_DATA_SIZE 40
#define OFFSET_WAVEDATA 44
#define HEADER_SIZE OFFSET_WAVEDATA
#define EOF_EXTRA_INFO 60
typedef unsigned int WMsg;
typedef unsigned int TMsg;
#define WMSG_WAIT 0
#define WMSG_START 1
#define TMSG_ALIVE 1
#define TMSG_CLOSE 0
#define INT_FREE 0
#define INT_USED 1
#define THREAD_EXIT 0xABECEDA
#define SUPPORT_WAVES 10
#define SUPPORT_INTERFACES 10
#define READ_BLOCK 8192
#define BLOCK_SIZE 8192
#define BLOCK_COUNT 20
#define BP_TURN 1
static CRITICAL_SECTION cs;
static unsigned int __stdcall
PlayThread( LPVOID lp );
static void CALLBACK waveOutProc( HWAVEOUT hWaveOut,
UINT uMsg,
DWORD dwInstance,
DWORD dwParam1,
DWORD dwParam2);
class CWaveBox
{
struct WAVE
{
char *data;
unsigned long size;
WAVEFORMATEX wfx;
WMsg WMSG;
};
struct INTERFACE
{
HWAVEOUT dev;
unsigned int state;
WAVE *wave;
unsigned long wpos;
WAVEHDR* wblock;
volatile int wfreeblock;
int wcurrblock;
};
public:
INTERFACE I[SUPPORT_INTERFACES];
WAVE W[SUPPORT_WAVES];
unsigned int wload;
TMsg TMSG;
int Load( TCHAR *file );
int Play( unsigned int wave );
CWaveBox();
virtual ~CWaveBox();
int AddInterface( HWAVEOUT *dev,
WAVEFORMATEX *wfx,
volatile int *wfreeblock );
int RemoveInterface( HWAVEOUT dev );
protected:
HANDLE thread;
unsigned int run;
WAVEHDR* allocateBlocks( int size, int count );
void freeBlocks( WAVEHDR* blockArray );
};
#endif
CWaveBox()
In the constructor, PlayThread
is created in suspended mode, so will be resumed at first play. All supported interfaces are set to initial state and Wave block headers are created at heap (queuing blocks). A LPVOID
argument class instance is passed so PlayThread
can easily access any of the public methods/members inside the class.
CWaveBox::CWaveBox()
{
wload = 0;
run = 0;
thread = CreateThread( NULL,
0,
(LPTHREAD_START_ROUTINE)PlayThread,
(LPVOID)this,
CREATE_SUSPENDED,
NULL );
for( unsigned int i = 0; i < SUPPORT_INTERFACES; i++ )
{
I[i].wblock = allocateBlocks( BLOCK_SIZE, BLOCK_COUNT );
I[i].wfreeblock = BLOCK_COUNT;
I[i].wcurrblock = 0;
I[i].state = INT_FREE;
I[i].wpos = 0;
}
for( i = 0; i < SUPPORT_WAVES; i++ ) W[i].WMSG = WMSG_WAIT;
InitializeCriticalSection( &cs );
}
Load
First of all, Wave header is read from file and must pass several verifications to satisfy the 'PCM wave' model. Then, Wave wfx
structure is filled and the whole data
block is loaded into memory.
Note: Load all waves (you want to play) before starting the Play
method, because there is no additional Load
supported if PlayThread
is resumed by starting at first Play
. (See wload
, it's also used in PlayThread
and is not protected by critical section.)
int CWaveBox::Load( TCHAR *file )
{
if( wload == SUPPORT_WAVES )
return -1;
HANDLE hFile;
if((hFile = CreateFile( file,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
0,
NULL )) == INVALID_HANDLE_VALUE) return -1;
char header[HEADER_SIZE];
unsigned long rbytes = 0;
if( !ReadFile(hFile, header, sizeof(header), &rbytes, NULL) )
{ CloseHandle(hFile); return -1; }
if( !rbytes || rbytes < sizeof(header) )
{ CloseHandle(hFile); return -1; }
if( strncmp( header, WAVE_FILE_MARK, strlen( WAVE_FILE_MARK )) )
{ CloseHandle(hFile); return -1; }
if( strncmp( header + OFFSET_HEAD_MARK,
WAVE_HEAD_MARK, strlen( WAVE_HEAD_MARK )) )
{ CloseHandle(hFile); return -1; }
if ( ((*(DWORD*)(header + OFFSET_WAVE_PCM1)) != WAVE_PCM_16 )
|| ((*(WORD *)(header + OFFSET_WAVE_PCM2)) != WAVE_PCM_1 ))
{CloseHandle(hFile); return -1; }
if( !strncmp( header + OFFSET_DATA_MARK,
WAVE_DATA_MARK, strlen( WAVE_DATA_MARK )) )
W[wload].size = *((DWORD*)(header + OFFSET_DATA_SIZE ));
else
{
W[wload].size = *((DWORD*)(header + OFFSET_FILE_LEFT ));
W[wload].size -= ( HEADER_SIZE - EOF_EXTRA_INFO );
}
W[wload].wfx.nSamplesPerSec =
*((DWORD*)(header + OFFSET_SAMPLESPERSEC ));
W[wload].wfx.wBitsPerSample =
*((WORD *)(header + OFFSET_BITSPERSAMPLE ));
W[wload].wfx.nChannels =
*((WORD *)(header + OFFSET_CHANNELS ));
W[wload].wfx.cbSize = 0;
W[wload].wfx.wFormatTag = WAVE_FORMAT_PCM;
W[wload].wfx.nBlockAlign =
*((WORD *)(header + OFFSET_BLOCKALIGN ));
W[wload].wfx.nAvgBytesPerSec =
*((DWORD*)(header + OFFSET_AVGBYTESPERSEC));
if((W[wload].data = ( char *) calloc( W[wload].size,
sizeof( char ))) == NULL)
{ CloseHandle(hFile); return -1; }
char buffer[READ_BLOCK];
unsigned long size = W[wload].size;
unsigned long read_block = 0;
rbytes = 0;
do
{
if( ( size -= rbytes ) >= READ_BLOCK ) read_block = READ_BLOCK;
else
if( size && size < READ_BLOCK ) read_block = size;
else break;
if( !ReadFile(hFile, buffer, read_block, &rbytes, NULL) )
break;
if( rbytes == 0 )
break;
if( rbytes < sizeof(buffer) )
memset(buffer + rbytes, 0, sizeof(buffer) - rbytes);
memcpy( &W[wload].data[W[wload].size - size], buffer, rbytes );
}while( 1 );
CloseHandle(hFile);
return ++wload;
}
Play
Play
method is pretty simple, all what it's doing is to set WMSG_START
for every wave
index which is passed as an argument so PlayThread
can start work. Only when first Play
is started, PlayThread
will be resumed and thread message TMSG_ALIVE
set.
int CWaveBox::Play( unsigned int wave )
{
if( wave < 0 || wave >= wload )
return -1;
EnterCriticalSection(&cs);
W[wave].WMSG = WMSG_START;
LeaveCriticalSection(&cs);
if( !run ){ run = 1; TMSG = TMSG_ALIVE; ResumeThread( thread ); }
return 1;
}
Queue Model (streaming multiple waves)
The PlayThread
model works in order to serve as a WAI server for preparing and sending as many buffers of BLOCK_SIZE
to the current WAI queue, as is limited with BLOCK_COUNT
constant. There can be SUPPORT_INTERFACES
queues, or WAI driver threads working. For example, if Wave of 1 second is played 10 times with 10ms of delay between each Play
, PlayThread
will link Wave with 10 different INTERFACE
s in order to play the Wave 10 times. Each thread INTERFACE
serves each WAI queue/thread required for playing current Wave linked with current INTERFACE
. So, eventually effect will be new delayed Wave of approx. 1.1 seconds consisting of 10 Waves with latency of 10ms between each.
To provide continuous and synchronous buffering without noise or gap as side effects, the idea is to control buffers queuing / streaming. When WAVE
is linked to the thread INTERFACE
it must be linked with the WAI interface or the device for playing (see HWAVEOUT
). By calling AddInterface
method, pointer of INTERFACE
's wfreeblock
counter is passed as an argument so it will be returned in callback waveOutProc
, when buffer is played from the WAI queue, or WOM_DONE
message arrives. By decreasing wfreeblock
counter in case of buffer is sent to the WAI queue for playing, and increasing inside of waveOutProc
when buffer is finished / played, keeping of limited BLOCK_COUNT
queued buffers is simply controlled. If any of the wfreeblock
buffers is freed, a new one is sent to the queue, so audio streaming will be continuous and synchronous over all INTERFACE
s. You can turn on PlayThread
to its limits by playing n SUPPORT_WAVES
at m SUPPORT_INTERFACES
inside of other CPU time spending processes (for ex., GUI related). I'm sure that will satisfy any of the proper coded tasks and play without gap for a reasonably count of waves / interfaces at once.
#define SUPPORT_WAVES 10
#define SUPPORT_INTERFACES 10
#define READ_BLOCK 8192
#define BLOCK_SIZE 8192
#define BLOCK_COUNT 20
#define BP_TURN 1
int CWaveBox::AddInterface( HWAVEOUT *dev,
WAVEFORMATEX *wfx,
volatile int *wfreeblock )
{
if( !waveOutGetNumDevs() )
return -1;
if(waveOutOpen( dev,
WAVE_MAPPER,
wfx,
(DWORD)waveOutProc,
(DWORD)wfreeblock,
CALLBACK_FUNCTION ) != MMSYSERR_NOERROR ) return -1;
return 1;
}
static void CALLBACK waveOutProc( HWAVEOUT hWaveOut,
UINT uMsg,
DWORD dwInstance,
DWORD dwParam1,
DWORD dwParam2 )
{
int* freeBlockCounter = (int*)dwInstance;
if(uMsg != WOM_DONE)
return;
EnterCriticalSection(&cs);
(*freeBlockCounter)++;
LeaveCriticalSection(&cs);
}
PlayThread
You should read the 'Queue Model' paragraph if you have skipped it, because it is an intro for you to completely understand how PlayThread
is modelled to interact with the WAI driver and play multiple Waves. The PlayThread
responses for two types of messages. First types are Wave WMsg
messages set by Play
method, so let's first discuss what they trigger.
When WMSG_START
arrives for a particularl wave wb->W[i];
, the thread continues searching for the first INT_FREE
interface to link the Wave within. When free interface is matched and HWAVEOUT
handle of device is opened by AddInterface
method and assigned to the current interface, WAVE
pointer is saved so interface can work with. Then, interface is marked as INT_USED
, so you'll see later, the thread will know to play it and current attached Waves. wb->W[i].WMSG;
message is set back to WMSG_WAIT
state, so if requested, it can be played again at same or another INTERFACE
. This linking model gives the CWaveBox
possibility for playing one Wave as well as for playing multiple Waves.
Now, it goes to the 'main playing loop' scope, where, first, for every INT_USED
interface wb->I[k].wfreeblock;
counter is checked to see if there is a free block for queuing on the current interface. If not, loop continues to search first one where it has. Blocks per turn, or BP_TURN
constant is nothing but the multiplier for BLOCK_SIZE
and is used for queuing more than one block per turn. You can avoid this by enlarging BLOCK_SIZE
. I really don't know if it is better queuing one bigger or many smaller blocks per turn, so this can be further tested. ;-)
Inside the 'block per turn' loop, first, we check for Wave data header WAVEHDR
block to be unprepared in case of previous queuing (ring queuing / buffering) and complete the WAVEHDR
block by copying Wave data
block at Wave header member pData
and set the size of block in dwBufferLength
. After preparations, header is sent for playing at current interface wb->I[k].dev;
via waveOutWrite
WAI method. Remaining job is to decrease interface wfreeblock
counter and to round up circular wcurrblock
counter so it will point on the next block.
In case the last block is queued and there is no data
left for queuing on current INTERFACE
, it again goes to check for wfreeblock
counter, but this time it must be equal to BLOCK_COUNT
which means that all buffers are played so all prepared buffers can be successfully unprepared and RemoveInterface
method invoked which closes current WAI driver thread and releases all memory used. Closing of WAI interfaces must be done in these steps or may fail and produce memory leak! Remaining job is to set the interface wb->I[k].state;
message to INT_FREE
so it can link another WAVE
if needed.
Another type of messages are thread TMsg
messages. First message TMSG_ALIVE
is set by Play
method and keeps the thread alive until TMSG_CLOSE
is set from the CWaveBox
destructor. When TMSG_CLOSE
is arrived, thread breaks from the 'main thread' or while
loop and continues to the bottom of PlayThread
. Destructor can be invoked while there is 'who knows how many waves' still playing at WAI subsystem, so there is a last check to see on which interface is state
flag set to INT_USED
so it can be reset with waveOutReset
and closed by RemoveInterface
method. Without releasing of WAI subsystem, there can be problems (primarily Windows CE) on next WAI start-up so this procedure is necessary.
And, last thing to do, is to return EXIT_THREAD
code, so it will signalize the destructor to continue with the destruction of the remaining CWaveBox
members. ;-)
static unsigned int __stdcall PlayThread( LPVOID lp )
{
CWaveBox *wb = ( CWaveBox *)lp;
register WMsg wmsg = WMSG_WAIT;
register TMsg tmsg = TMSG_ALIVE;
register unsigned int i = 0;
while( tmsg )
{
for( i = 0; i < wb->wload; i++ )
{
EnterCriticalSection( &cs );
wmsg = wb->W[i].WMSG;
LeaveCriticalSection( &cs );
if( wmsg == WMSG_START ) break;
}
if( wmsg == WMSG_START )
for( unsigned int j = 0; j < SUPPORT_INTERFACES; j++ )
if( wb->I[j].state == INT_FREE )
if( wb->AddInterface( &wb->I[j].dev,
&wb->W[i].wfx,
&wb->I[j].wfreeblock ) )
{
wb->I[j].wave = &wb->W[i];
wb->I[j].state = INT_USED;
EnterCriticalSection( &cs );
wb->W[i].WMSG = WMSG_WAIT;
LeaveCriticalSection( &cs );
break;
}
for( unsigned int k = 0; k < SUPPORT_INTERFACES; k++ )
{
if( wb->I[k].state == INT_FREE ) continue;
EnterCriticalSection( &cs );
int free = wb->I[k].wfreeblock;
LeaveCriticalSection( &cs );
if( free < BP_TURN ) continue;
WAVEHDR *current = NULL;
for( unsigned int m = 0; m < BP_TURN; m++ )
{
current = &wb->I[k].wblock[wb->I[k].wcurrblock];
if( current->dwFlags & WHDR_PREPARED )
waveOutUnprepareHeader( wb->I[k].dev,
current,
sizeof(WAVEHDR) );
unsigned long left = wb->I[k].wave->size - wb->I[k].wpos;
unsigned long chunk = 0;
if( left >= BLOCK_SIZE )
chunk = BLOCK_SIZE;
else
if( left && left < BLOCK_SIZE )
chunk = left;
else
{
EnterCriticalSection( &cs );
int free = wb->I[k].wfreeblock;
LeaveCriticalSection( &cs );
if( free == BLOCK_COUNT )
{
for( int i = 0; i < wb->I[k].wfreeblock; i++)
if( wb->I[k].wblock[i].dwFlags & WHDR_PREPARED )
waveOutUnprepareHeader( wb->I[k].dev,
&wb->I[k].wblock[i],
sizeof(WAVEHDR));
if( wb->RemoveInterface( wb->I[k].dev ) )
{
wb->I[k].wcurrblock = 0;
wb->I[k].state = INT_FREE;
wb->I[k].wpos = 0;
wb->I[k].wave = NULL;
}
}
break;
}
memcpy( current->lpData,
&wb->I[k].wave->data[wb->I[k].wpos], chunk );
current->dwBufferLength = chunk;
wb->I[k].wpos += chunk;
waveOutPrepareHeader( wb->I[k].dev,
current, sizeof(WAVEHDR) );
waveOutWrite(wb->I[k].dev, current, sizeof(WAVEHDR));
EnterCriticalSection( &cs );
wb->I[k].wfreeblock--;
LeaveCriticalSection( &cs );
wb->I[k].wcurrblock++;
wb->I[k].wcurrblock %= BLOCK_COUNT;
}
}
Sleep( 10 );
EnterCriticalSection( &cs );
tmsg = wb->TMSG;
LeaveCriticalSection( &cs );
}
for( i = 0; i < SUPPORT_INTERFACES; i++ )
if( wb->I[i].state == INT_USED )
if( waveOutReset( wb->I[i].dev ) == MMSYSERR_NOERROR )
wb->RemoveInterface( wb->I[i].dev );
return THREAD_EXIT;
}
~CWaveBox()
Within the destructor, for resumed thread, TMSG_CLOSE
message is set and looped until THREAD_EXIT
code will not arrive or 'thread soft close', so all playing interfaces are properly forced to be reset and closed. For suspended thread, 'thread hard close' or TerminateThread
is called. After that, all required buffers and critical section are released.
CWaveBox::~CWaveBox()
{
unsigned long exit = 0;
if( run )
{
EnterCriticalSection( &cs );
TMSG = TMSG_CLOSE;
LeaveCriticalSection( &cs );
do
{
GetExitCodeThread( thread, &exit );
Sleep( 10 );
}while( exit != THREAD_EXIT );
}else
{
GetExitCodeThread( thread, &exit );
TerminateThread( thread, exit );
}
for( unsigned int i = 0; i < wload; i++ )
free( W[i].data );
for( i = 0; i < SUPPORT_INTERFACES; i++ )
freeBlocks( I[i].wblock );
DeleteCriticalSection( &cs );
}
The Demo
This demo is a simple and effective example of how you can use this class and play with timed Waves. All you have to do is to Load
a Wave file, choose/set timing, and Play
it. Turn your volume on and enjoy! ;-)
Note: for compiling on Windows XP, link 'Winmm.lib' library, while on Windows CE it is already linked.
#include "stdafx.h"
#include "wavebox.h"
#include <stdio.h>
#include <conio.h>
int main(int argc, char* argv[])
{
CWaveBox w;
w.Load("twilightzone.wav");
w.Load("badfeeling.wav");
w.Load("wolfcall.wav");
w.Load("heart.wav");
w.Load("gusting_winds.wav");
w.Load("newmail.wav");
w.Load("dumbass.wav");
w.Load("porky.wav");
w.Load("system_alert.wav");
printf("<CWaveBox v0.95 demo>:\n\n");
printf("You will listen 9 waves in this demo, approx. 30 sec.\n");
for( int f = 0; f < 5; f++ ){ w.Play(8); Sleep(50); } Sleep(2000);
for( f = 0; f < 5; f++ ){ w.Play(8); Sleep(75);}
for( int k = 0; k < 1000; k += 50 )
{
w.Play(3); Sleep( 300 + 1000 - k );
if( !( k % 200 ) ) w.Play(2);
if( k == 400 || k == 700 ) w.Play(1);
if( k == 950 )
{ w.Play(4); Sleep( 500 );}
}
w.Play(0);Sleep(2500);
w.Play(5);Sleep(1500);
w.Play(6);Sleep(4500);
w.Play(7);
printf("thats all folks! ;-) \n");
getch();
return 1;
}
Points of Interest
This class can be extended by UnLoad
method, so any Wave can be Load
ed and UnLoad
ed any time. Also, playing from file 'on the fly' which has lower memory usage, but has bigger CPU usage, wouldn't be a bad extension. Then, events can replace the current pooling model and extend Play
for compressed Waves too. I will like to see this class attached to the MPEG3 'licence free' and 'commercially usable without fee' decoder on this zone so we can freely implement it in our games and applications.