From: http://www.planet-source-code.com/vb/scripts/ShowCode.asp?txtCodeId=4422&lngWId=3
Windows waveOut Tutorial
This tutorial is designed to help you use the windows waveOut interface for playing digital audio. I know from experience that the interface can be pretty difficult to get to grips with. Through this tutorial I will build a windows console application for playing raw digital audio. This is my first tutorial so I'll apologise for the mistakes in advance! Note: This tutorial assumes that you are competent with C programming and using the Windows API functions. A basic understanding of digital audio is useful but not completely necessary. Contents
Get The Documentation!
The first thing you'll need is some decent documentation on the waveOut interface. If you have the Microsoft Platform SDK (a worthwhile download) or a copy of Visual C++ then you already have the relevent information in the documentation provided. If you don't have either of these you can view the documentation online at Microsoft's Developer website (msdn.microsoft.com).
What is Digital Audio?
This bit is for people who have absolutely no idea how digital audio is stored. Skip this section if you know all about digital audio and you know the meaning of the terms 'Sample', 'Sampling Rate', 'Sample Size', and 'Channels'. It's all very well sending all these bytes to the sound card but what do these bytes mean? Audio is simply a series of moving pressure waves. In real life this is an analogue wave, but in the digital world we have to store it as a set of samples along this wave. A sample is a value that represents the amplitude of the wave at a given point in time - it's just a number. The sampling rate is how frequently we take a sample of the wave. It is measured in hertz (Hz) or 'samples per second'. Obviously the higher the sampling rate, the more like the analogue wave your sampled wave becomes, so the higher the quality of the sound. Another thing that contributes to the quality of the audio is the size of each sample. Yes, you guessed it. The larger the sample size the higher the quality of the audio. Sample size is measured in bits. Why is the quality better? Consider an 8 bit sample. It has 256 (2^8) possible values. This means that you may not be able to represent the exact amplitude of the wave with it. Now consider a 16 bit sample. It has 65536 possible values (2^16). This means that it is 256 times as accurate as the 8 bit sample and can thus represent the amplitude more accurately. The final thing I'll touch on here is the channels. On most systems you have two speakers, left and right. That's two channels. This means that you must store a sample for the left channel and the right channel. Fortunately this is easy for two channels (which is the most you'll encounter in this tutorial). The samples are interleaved. That is the samples are stored, left, right, left, right etc... CD quality audio is sampled at 44100 Hz, has a sample size of 16 bits and has 2 channels. This means that 1 MB of audio data lasts for approximately 6 seconds.
Opening the Sound Device
To open the sound device you use the waveOutOpen function (look this up in your documentation now). Like most Windows objects, you basically need a handle to anything to use it. When you act on a window you use a HWND handle. Similarly when you act on a waveOut device you use a HWAVEOUT handle. So now comes the first version of our application. This simply opens the wave device to a CD quality standard, reports what's happened and closes it again.
#include <windows.h> #include <mmsystem.h> #include <stdio.h> int main(int argc, char* argv[]) { HWAVEOUT hWaveOut; /* device handle */ WAVEFORMATEX wfx; /* look this up in your documentation */ MMRESULT result;/* for waveOut return values */ /* * first we need to set up the WAVEFORMATEX structure. * the structure describes the format of the audio. */ wfx.nSamplesPerSec = 44100; /* sample rate */ wfx.wBitsPerSample = 16; /* sample size */ wfx.nChannels = 2; /* channels*/ /* * WAVEFORMATEX also has other fields which need filling. * as long as the three fields above are filled this should * work for any PCM (pulse code modulation) format. */ wfx.cbSize = 0; /* size of _extra_ info */ wfx.wFormatTag = WAVE_FORMAT_PCM; wfx.nBlockAlign = (wfx.wBitsPerSample >> 3) * wfx.nChannels; wfx.nAvgBytesPerSec = wfx.nBlockAlign * wfx.nSamplesPerSec; /* * try to open the default wave device. WAVE_MAPPER is * a constant defined in mmsystem.h, it always points to the * default wave device on the system (some people have 2 or * more sound cards). */ if(waveOutOpen( &hWaveOut, WAVE_MAPPER, &wfx, 0, 0, CALLBACK_NULL ) != MMSYSERR_NOERROR) { fprintf(stderr, "unable to open WAVE_MAPPER device\n"); ExitProcess(1); } /* * device is now open so print the success message * and then close the device again. */ printf("The Wave Mapper device was opened successfully!\n"); waveOutClose(hWaveOut); return 0; } |
Playing a Sound
Opening and closing the device is fun for a while but it doesn't actuallydo that much. What we want is to hear a sound. We need to do two things beforethis can happen.
void writeAudioBlock(HWAVEOUT hWaveOut, LPSTR block, DWORD size) { WAVEHDR header; /* * initialise the block header with the size * and pointer. */ ZeroMemory(&header, sizeof(WAVEHDR)); header.dwBufferLength = size; header.lpData = block; /* * prepare the block for playback */ waveOutPrepareHeader(hWaveOut, &header, sizeof(WAVEHDR)); /* * write the block to the device. waveOutWrite returns immediately * unless a synchronous driver is used (not often). */ waveOutWrite(hWaveOut, &header, sizeof(WAVEHDR)); /* * wait a while for the block to play then start trying * to unprepare the header. this will fail until the block has * played. */ Sleep(500); while(waveOutUnprepareHeader( hWaveOut, &header, sizeof(WAVEHDR) ) == WAVERR_STILLPLAYING) Sleep(100); } |
LPSTR loadAudioBlock(const char* filename, DWORD* blockSize) { HANDLE hFile= INVALID_HANDLE_VALUE; DWORD size = 0; DWORD readBytes = 0; void* block = NULL; /* * open the file */ if((hFile = CreateFile( filename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL )) == INVALID_HANDLE_VALUE) return NULL; /* * get it's size, allocate memory and read the file * into memory. don't use this on large files! */ do { if((size = GetFileSize(hFile, NULL)) == 0) break; if((block = HeapAlloc(GetProcessHeap(), 0, size)) == NULL) break; ReadFile(hFile, block, size, &readBytes, NULL); } while(0); CloseHandle(hFile); *blockSize = size; return (LPSTR)block; } |
#include <windows.h> #include <mmsystem.h> #include <stdio.h> LPSTR loadAudioBlock(const char* filename, DWORD* blockSize); void writeAudioBlock(HWAVEOUT hWaveOut, LPSTR block, DWORD size); int main(int argc, char* argv[]) { HWAVEOUT hWaveOut; WAVEFORMATEX wfx; LPSTR block;/* pointer to the block */ DWORD blockSize;/* holds the size of the block */ . . (leave middle section as it was) . printf("The Wave Mapper device was opened successfully!\n"); /* * load and play the block of audio */ if((block = loadAudioBlock("c:\\temp\\ding.raw", &blockSize)) == NULL) { fprintf(stderr, "Unable to load file\n"); ExitProcess(1); } writeAudioBlock(hWaveOut, block, blockSize); waveOutClose(hWaveOut); return 0; } |
Streaming Audio to the Device
As you can probably see the above code has a number of fundamental flaws(note that this was deliberate :), the most evident of which are:
The Buffering Scheme
My buffering scheme works on a principle similar to that discussed above. It requires theuse of a variable that keeps count of the number of free buffers at any time (yes a semaphorewould be ideal here but we can't use one, I'll explain why later). This variable is initialisedto the number of blocks, decremented when a block is written and incremented when a blockcompletes. When no blocks are available we wait until the counter is at least 1 and thencontinue writing. This allows us to queue any number of blocks in a ring which is very effective.Rather than queuing 3 blocks, I queue more like 20, of about 8 kB each.
Now here's something you might have already guessed: waveOutProc is called from a different thread. Windows create a thread specifically for managing the audio playback.There are a number of restrictions on what you can do in this callback. To quote the MicrosoftDocumentation:
"Applications should not call any system-defined functions from inside a callback function, except for EnterCriticalSection, LeaveCriticalSection, midiOutLongMsg, midiOutShortMsg, OutputDebugString, PostMessage, PostThreadMessage, SetEvent, timeGetSystemTime, timeGetTime, timeKillEvent, and timeSetEvent. Calling other wave functions will cause deadlock."Which explains why we can't use a semaphore - it would require the use of ReleaseSemaphorewhich you shouldn't use. In practice it is a little more flexible than this - I have seencode that uses semaphores from the callback but what works on one Windows version may notwork on another. Also, calling waveOut functions from the callback does cause deadlock.Ideally we would also call waveOutUnprepareHeader in the callback but we can't do that (it doesn't deadlock until you call waveOutReset just for your information :)
static void CALLBACK waveOutProc( HWAVEOUT hWaveOut, UINT uMsg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2 ) { /* * pointer to free block counter */ int* freeBlockCounter = (int*)dwInstance; /* * ignore calls that occur due to openining and closing the * device. */ if(uMsg != WOM_DONE) return; EnterCriticalSection(&waveCriticalSection); (*freeBlockCounter)++; LeaveCriticalSection(&waveCriticalSection); } |
WAVEHDR* allocateBlocks(int size, int count) { unsigned char* buffer; int i; WAVEHDR* blocks; DWORD totalBufferSize = (size + sizeof(WAVEHDR)) * count; /* * allocate memory for the entire set in one go */ if((buffer = HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, totalBufferSize )) == NULL) { fprintf(stderr, "Memory allocation error\n"); ExitProcess(1); } /* * and set up the pointers to each bit */ blocks = (WAVEHDR*)buffer; buffer += sizeof(WAVEHDR) * count; for(i = 0; i < count; i++) { blocks[i].dwBufferLength = size; blocks[i].lpData = buffer; buffer += size; } return blocks; } void freeBlocks(WAVEHDR* blockArray) { /* * and this is why allocateBlocks works the way it does */ HeapFree(GetProcessHeap(), 0, blockArray); } |
While there's data available If the current free block is prepared Unprepare it End If If there's space in the current free block Write all the data to the block Exit the function Else Write as much data as is possible to fill the block Prepare the block Write it Decrement the free blocks counter Subtract however many bytes were written from the data available Wait for at least one block to become free Update the current block pointer End If End While |
void writeAudio(HWAVEOUT hWaveOut, LPSTR data, int size) { WAVEHDR* current; int remain; current = &waveBlocks[waveCurrentBlock]; while(size > 0) { /* * first make sure the header we're going to use is unprepared */ if(current->dwFlags & WHDR_PREPARED) waveOutUnprepareHeader(hWaveOut, current, sizeof(WAVEHDR)); if(size < (int)(BLOCK_SIZE - current->dwUser)) { memcpy(current->lpData + current->dwUser, data, size); current->dwUser += size; break; } remain = BLOCK_SIZE - current->dwUser; memcpy(current->lpData + current->dwUser, data, remain); size -= remain; data += remain; current->dwBufferLength = BLOCK_SIZE; waveOutPrepareHeader(hWaveOut, current, sizeof(WAVEHDR)); waveOutWrite(hWaveOut, current, sizeof(WAVEHDR)); EnterCriticalSection(&waveCriticalSection); waveFreeBlockCount--; LeaveCriticalSection(&waveCriticalSection); /* * wait for a block to become free */ while(!waveFreeBlockCount) Sleep(10); /* * point to the next block */ waveCurrentBlock++; waveCurrentBlock %= BLOCK_COUNT; current = &waveBlocks[waveCurrentBlock]; current->dwUser = 0; } } |
The Driver Program
If you've followed this tutorial right though you will now have a C file containingthe following functions:
#include <windows.h> #include <mmsystem.h> #include <stdio.h> /* * some good values for block size and count */ #define BLOCK_SIZE 8192 #define BLOCK_COUNT 20 /* * function prototypes */ static void CALLBACK waveOutProc(HWAVEOUT, UINT, DWORD, DWORD, DWORD); static WAVEHDR* allocateBlocks(int size, int count); static void freeBlocks(WAVEHDR* blockArray); static void writeAudio(HWAVEOUT hWaveOut, LPSTR data, int size); /* * module level variables */ static CRITICAL_SECTION waveCriticalSection; static WAVEHDR* waveBlocks; static volatile int waveFreeBlockCount; static int waveCurrentBlock; int main(int argc, char* argv[]) { HWAVEOUT hWaveOut; /* device handle */ HANDLEhFile;/* file handle */ WAVEFORMATEX wfx; /* look this up in your documentation */ char buffer[1024]; /* intermediate buffer for reading */ int i; /* * quick argument check */ if(argc != 2) { fprintf(stderr, "usage: %s <filename>\n", argv[0]); ExitProcess(1); } /* * initialise the module variables */ waveBlocks = allocateBlocks(BLOCK_SIZE, BLOCK_COUNT); waveFreeBlockCount = BLOCK_COUNT; waveCurrentBlock= 0; InitializeCriticalSection(&waveCriticalSection); /* * try and open the file */ if((hFile = CreateFile( argv[1], GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL )) == INVALID_HANDLE_VALUE) { fprintf(stderr, "%s: unable to open file '%s'\n", argv[0], argv[1]); ExitProcess(1); } /* * set up the WAVEFORMATEX structure. */ wfx.nSamplesPerSec = 44100; /* sample rate */ wfx.wBitsPerSample = 16; /* sample size */ wfx.nChannels= 2; /* channels*/ wfx.cbSize = 0; /* size of _extra_ info */ wfx.wFormatTag = WAVE_FORMAT_PCM; wfx.nBlockAlign = (wfx.wBitsPerSample * wfx.nChannels) >> 3; wfx.nAvgBytesPerSec = wfx.nBlockAlign * wfx.nSamplesPerSec; /* * try to open the default wave device. WAVE_MAPPER is * a constant defined in mmsystem.h, it always points to the * default wave device on the system (some people have 2 or * more sound cards). */ if(waveOutOpen( &hWaveOut, WAVE_MAPPER, &wfx, (DWORD_PTR)waveOutProc, (DWORD_PTR)&waveFreeBlockCount, CALLBACK_FUNCTION ) != MMSYSERR_NOERROR) { fprintf(stderr, "%s: unable to open wave mapper device\n", argv[0]); ExitProcess(1); } /* * playback loop */ while(1) { DWORD readBytes; if(!ReadFile(hFile, buffer, sizeof(buffer), &readBytes, NULL)) break; if(readBytes == 0) break; if(readBytes < sizeof(buffer)) { printf("at end of buffer\n"); memset(buffer + readBytes, 0, sizeof(buffer) - readBytes); printf("after memcpy\n"); } writeAudio(hWaveOut, buffer, sizeof(buffer)); } /* * wait for all blocks to complete */ while(waveFreeBlockCount < BLOCK_COUNT) Sleep(10); /* * unprepare any blocks that are still prepared */ for(i = 0; i < waveFreeBlockCount; i++) if(waveBlocks[i].dwFlags & WHDR_PREPARED) waveOutUnprepareHeader(hWaveOut, &waveBlocks[i], sizeof(WAVEHDR)); DeleteCriticalSection(&waveCriticalSection); freeBlocks(waveBlocks); waveOutClose(hWaveOut); CloseHandle(hFile); return 0; } |
What Next?
What you do now is up to you. I have a few possibly entertaining suggestions:
Distilled last source code:
#pragma comment( lib, "winmm.lib" )
#include <windows.h>
#include <mmsystem.h>
#include <stdio.h>
/*
* some good values for block size and count
*/
#define BLOCK_SIZE 8192
#define BLOCK_COUNT 20
/*
* function prototypes
*/
static void CALLBACK waveOutProc(HWAVEOUT, UINT, DWORD, DWORD, DWORD);
static WAVEHDR* allocateBlocks(int size, int count);
static void freeBlocks(WAVEHDR* blockArray);
static void writeAudio(HWAVEOUT hWaveOut, LPSTR data, int size);
/*
* module level variables
*/
static CRITICAL_SECTION waveCriticalSection;
static WAVEHDR* waveBlocks;
static volatile int waveFreeBlockCount;
static int waveCurrentBlock;
static void CALLBACK waveOutProc(
HWAVEOUT hWaveOut,
UINT uMsg,
DWORD dwInstance,
DWORD dwParam1,
DWORD dwParam2
)
{
/*
* pointer to free block counter
*/
int* freeBlockCounter = (int*)dwInstance;
/*
* ignore calls that occur due to openining and closing the
* device.
*/
if(uMsg != WOM_DONE)
return;
EnterCriticalSection(&waveCriticalSection);
(*freeBlockCounter)++;
LeaveCriticalSection(&waveCriticalSection);
}
int main(int argc, char* argv[])
{
HWAVEOUT hWaveOut; /* device handle */
HANDLE hFile;/* file handle */
WAVEFORMATEX wfx; /* look this up in your documentation */
char buffer[1024]; /* intermediate buffer for reading */
int i;
/*
* quick argument check
*/
if(argc != 2) {
fprintf(stderr, "usage: %s <filename>\n", argv[0]);
ExitProcess(1);
}
/*
* initialise the module variables
*/
waveBlocks = allocateBlocks(BLOCK_SIZE, BLOCK_COUNT);
waveFreeBlockCount = BLOCK_COUNT;
waveCurrentBlock= 0;
InitializeCriticalSection(&waveCriticalSection);
/*
* try and open the file
*/
if((hFile = CreateFile(
argv[1],
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
0,
NULL
)) == INVALID_HANDLE_VALUE) {
fprintf(stderr, "%s: unable to open file '%s'\n", argv[0], argv[1]);
ExitProcess(1);
}
/*
* set up the WAVEFORMATEX structure.
*/
wfx.nSamplesPerSec = 44100; /* sample rate */
wfx.wBitsPerSample = 8; /* sample size */
wfx.nChannels= 2; /* channels*/
wfx.cbSize = 0; /* size of _extra_ info */
wfx.wFormatTag = WAVE_FORMAT_PCM;
wfx.nBlockAlign = (wfx.wBitsPerSample * wfx.nChannels) >> 3;
wfx.nAvgBytesPerSec = wfx.nBlockAlign * wfx.nSamplesPerSec;
/*
* try to open the default wave device. WAVE_MAPPER is
* a constant defined in mmsystem.h, it always points to the
* default wave device on the system (some people have 2 or
* more sound cards).
*/
if(waveOutOpen(
&hWaveOut,
WAVE_MAPPER,
&wfx,
(DWORD_PTR)waveOutProc,
(DWORD_PTR)&waveFreeBlockCount,
CALLBACK_FUNCTION
) != MMSYSERR_NOERROR) {
fprintf(stderr, "%s: unable to open wave mapper device\n", argv[0]);
ExitProcess(1);
}
/*
* playback loop
*/
while(1) {
DWORD readBytes;
if(!ReadFile(hFile, buffer, sizeof(buffer), &readBytes, NULL))
break;
if(readBytes == 0)
break;
if(readBytes < sizeof(buffer)) {
printf("at end of buffer\n");
memset(buffer + readBytes, 0, sizeof(buffer) - readBytes);
printf("after memcpy\n");
}
writeAudio(hWaveOut, buffer, sizeof(buffer));
}
/*
* wait for all blocks to complete
*/
while(waveFreeBlockCount < BLOCK_COUNT)
Sleep(10);
/*
* unprepare any blocks that are still prepared
*/
for(i = 0; i < waveFreeBlockCount; i++)
if(waveBlocks[i].dwFlags & WHDR_PREPARED)
waveOutUnprepareHeader(hWaveOut, &waveBlocks[i], sizeof(WAVEHDR));
DeleteCriticalSection(&waveCriticalSection);
freeBlocks(waveBlocks);
waveOutClose(hWaveOut);
CloseHandle(hFile);
return 0;
}
WAVEHDR* allocateBlocks(int size, int count)
{
unsigned char* buffer;
int i;
WAVEHDR* blocks;
DWORD totalBufferSize = (size + sizeof(WAVEHDR)) * count;
/*
* allocate memory for the entire set in one go
*/
if((buffer = (unsigned char*)HeapAlloc(
GetProcessHeap(),
HEAP_ZERO_MEMORY,
totalBufferSize
)) == NULL) {
fprintf(stderr, "Memory allocation error\n");
ExitProcess(1);
}
/*
* and set up the pointers to each bit
*/
blocks = (WAVEHDR*)buffer;
buffer += sizeof(WAVEHDR) * count;
for(i = 0; i < count; i++) {
blocks[i].dwBufferLength = size;
blocks[i].lpData = (LPSTR)buffer;
buffer += size;
}
return blocks;
}
void freeBlocks(WAVEHDR* blockArray)
{
/*
* and this is why allocateBlocks works the way it does
*/
HeapFree(GetProcessHeap(), 0, blockArray);
}
void writeAudio(HWAVEOUT hWaveOut, LPSTR data, int size)
{
WAVEHDR* current;
int remain;
current = &waveBlocks[waveCurrentBlock];
while(size > 0) {
/*
* first make sure the header we're going to use is unprepared
*/
if(current->dwFlags & WHDR_PREPARED)
waveOutUnprepareHeader(hWaveOut, current, sizeof(WAVEHDR));
if(size < (int)(BLOCK_SIZE - current->dwUser)) {
memcpy(current->lpData + current->dwUser, data, size);
current->dwUser += size;
break;
}
remain = BLOCK_SIZE - current->dwUser;
memcpy(current->lpData + current->dwUser, data, remain);
size -= remain;
data += remain;
current->dwBufferLength = BLOCK_SIZE;
waveOutPrepareHeader(hWaveOut, current, sizeof(WAVEHDR));
waveOutWrite(hWaveOut, current, sizeof(WAVEHDR));
EnterCriticalSection(&waveCriticalSection);
waveFreeBlockCount--;
LeaveCriticalSection(&waveCriticalSection);
/*
* wait for a block to become free
*/
while(!waveFreeBlockCount)
Sleep(10);
/*
* point to the next block
*/
waveCurrentBlock++;
waveCurrentBlock %= BLOCK_COUNT;
current = &waveBlocks[waveCurrentBlock];
current->dwUser = 0;
}
}