article

Playing Audio in Windows using waveOut Interface

Email
Submitted on: 1/4/2015 1:42:00 AM
By: David Overton (from psc cd)  
Level: Intermediate
User Rating: By 28 Users
Compatibility: C, Microsoft Visual C++
Views: 800
 
     This tutorial will teach you how to use the Windows waveOut multimedia functions. It also explains a little about how audio is stored in the digital form. I hope this tutorial is useful. Full source code is included as is a downloadable version wrapped in an MSVC++ project.

 
				

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!
  • What is Digital Audio?
  • Opening the Sound Device
  • Playing a Sound
  • Streaming Audio to the Device
  • The Buffering Scheme
  • The Driver Program
  • What Next?
  • Contacting Me

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;
}
 

Note that when compiling this program you will need to add winmm.lib to your list of library files or the linker will fail.

So that was the first step. The device was ready and waiting for you to write audio data to it.

Playing a Sound

Opening and closing the device is fun for a while but it doesn't actually do that much. What we want is to hear a sound. We need to do two things before this can happen.

  • Obtain a source of raw audio in the correct format
  • Work out how to write the data
Problem 1 is easy to solve. You can convert any music file into raw audio using a program like Winamp with the Disk Writer plug-in. Start small and convert one of the Windows sounds into a raw file. These files are located in your \Windows\Media directory. Ding.wav seems like a good choice to start with. If you can't convert this to a raw file you can have fun playing the unconverted file back instead. It will sound too fast since these files are mostly sampled at 22 kHz.

Problem 2 is slightly more tricky. Audio is written in blocks, each with its own header. It's easy to write one block but at some point we're going to have to come up with a scheme for queuing and writing many blocks. The reason I said to start with a small file is that the second version of our application will load the entire file as a single block.

We will first tackle Problem 2 by writing a function that will send a block of data to the audio device. The function will be called writeAudioBlock. To write audio data you use up to three functions. These are waveOutPrepareHeader, waveOutWrite, and waveOutUnprepareHeader and are called in the order I have listed them. It would be a good idea to look these up in your documentation now to familiarise yourself with them.

Here is the code for a preliminary version of the function writeAudioBlock

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);
}
 

Now we've got a function for writing a block of data we need a function for getting hold of one in the first place. That is the task of loadAudioBlock. loadAudioBlock will load a file into memory and return a pointer to it. Here is the code for loadAudioBlock.

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;
}
 

Finally for this section, here are the changes that must be made to the beginning of the file and to main.

#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;
}
 

If you've put all the code in the correct place and it compiled it will now play small audio files. We've accomplished something similar to what the PlaySound function does. Try playing with this. Change the playback sample rate (in main) or the sample size (multiple of 8 btw) and see what happens, or even the number of channels. You'll find that changing the sample rate or number of channels speeds up or slows down the audio. Changing the sample size has a somewhat devastating affect!

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:

  • We can't play very large files due to the way they are loaded. The current method buffers the entire file and plays it all back at once. Audio by its very nature is large so we need to find a way of streaming the data to the device block by block.
  • The current version of writeAudioBlock is synchronous so writing multiple blocks bit by bit will cause a gap between each block output (we can't refill the buffer fast enough). Microsoft recommends at least a double buffering scheme so that you fill one block while another is playing and then switch the blocks. This itself is not nearly enough. Even switching the blocks will cause a very small (but annoying) gap in the output.
Fortunately reading in blocks is a very easy exercise so I will defer from writing the code for that right now. Rather, I will concentrate on a buffering scheme for writing audio to the device in a gapless stream.

This problem of block switching is not nearly as serious as it sounds. No you can't switch two blocks without a gap but the interface does something which allows you to get around this. It maintains a queue of blocks. Any block which you have passed through the waveOutPrepareHeader function can be inserted into the queue using waveOutWrite. This means we can write 2 (or more) blocks to the device and fill a third while the first is playing, then perform the switch while the second is playing. This gives us gapless output.

The final problem before I describe a method of doing this is, how do we know when a block has finished playing? I was doing something very bad in the first version of writeAudioBlock and polling the device using waveOutUnprepareHeader until the block had finished. We can't do this any more because we need the time to refill audio blocks, and there are much better ways offered by the waveOut interface.

The waveOut interface offers 4 types of callback mechanism to notify you of when blocks have finished playing. These are:
  • An event - an event is set when a block completes
  • A callback function - a function is called when a block completes
  • A thread - a thread message is sent when a block completes
  • A window - a window message is sent when a block completes
The way you specify which of these is used is in the dwCallback parameter of the waveOutOpen function. In my method we will be using a function as the callback.

So we need a new function: waveOutProc. This (user defined) function is actually documented so you can look that up now. As you can see the function is called for three things: When the device is opened, closed, and when a block finishes. We are only interested in the call for when a block finishes.

The Buffering Scheme

My buffering scheme works on a principle similar to that discussed above. It requires the use of a variable that keeps count of the number of free buffers at any time (yes a semaphore would be ideal here but we can't use one, I'll explain why later). This variable is initialised to the number of blocks, decremented when a block is written and incremented when a block completes. When no blocks are available we wait until the counter is at least 1 and then continue 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 Microsoft Documentation:

 "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 ReleaseSemaphore which you shouldn't use. In practice it is a little more flexible than this - I have seen code that uses semaphores from the callback but what works on one Windows version may not work 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 :)

You'll notice that waveOutOpen provides a method of passing instance data (a user defined pointer) to the callback function. We're going to use this to pass a pointer to our counter variable.

One more thing before we write the waveOutProc function by the way. Since waveOutProc is called from a different thread, two threads will end up writing to the block counter variable. To avoid any conflict we need to use a Critical Section object (which will be a static module variable called waveCriticalSection).

Here is the waveOutProc function:

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);
}
 

The next thing we need is a couple of functions for allocating and freeing the block memory and a new implementation of writeAudioBlock called writeAudio. Here are the functions allocateBlocks and freeBlocks. allocateBlocks allocates a set number of blocks, with headers at a given size, and freeBlocks frees this memory. allocateBlocks will cause the program to exit if it fails. This means we don't need to check its return value in main.

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);
}
 

The new function writeAudio needs to queue as many blocks as necessary to write the data. The basic algorithm is:

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
 
This raises a question: How do I tell when a block is prepared and when it isn't?
This is a fairly easy one actually. Windows makes use of the dwFlags member of the WAVEHDR structure. It is used for a few things but one thing waveOutPrepareHeader does is set the WHDR_PREPARED flag. All we have to do is test for the flag in the dwFlags member.

I will make use of the dwUser member of the WAVEHDR structure to maintain a count of how full a block is. Here is the listing for the writeAudio function:

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;
}
}
 

Now we have this new function for writing the audio you can scrap the writeAudioBlock function since it's not being used any more. You can also scrap the loadAudioBlock function because the next section will start a new implementation of main that doesn't require loadAudioBlock.

The Driver Program

If you've followed this tutorial right though you will now have a C file containing the following functions:

  • main
  • waveOutProc
  • allocateBlocks
  • freeBlocks
  • writeAudio
Note that this file won't compile until we strip off the old version of main and declare the module variables needed.

We're now going to write a completely new version of main that will stream files from disk to the waveOut device. This listing also contains the declarations for the module variables and the prototypes for the functions we've already written.

#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:

  • Try modifying the rawaudio program so that it reads from standard input. This would make an application that you can directly pipe audio into from the command line.
  • Rework the reader so that it reads Wave (*.wav) files as opposed to RAW files. You will find this surprisingly easy, wave files contain a WAVEFORMATEX structure to describe their format which you can use when opening the device. See wotsit's format (http://www.wotsit.org) for information on the wave file format.
  • See if you can come up with any new or better buffering schemes
  • Try attaching this code to an open source decoder such as the Vorbis decoder or an MP3 decoder that you can acquire the source to. You then have your the beginnings of your own media player.
You can see get my example winamp plug-in from http://www.insomniavisions.com/software and try that. It is also open source.

Contacting Me

You can contact me (David Overton) by the usual methods available on Planet Source Code.

You can also go to http://www.insomniavisions.com/feedback to send feedback from my Website.

A complete working example of the code on this page can be downloaded here.


Other 2 submission(s) by this author

 


Report Bad Submission
Use this form to tell us if this entry should be deleted (i.e contains no code, is a virus, etc.).
This submission should be removed because:

Your Vote

What do you think of this article (in the Intermediate category)?
(The article with your highest vote will win this month's coding contest!)
Excellent  Good  Average  Below Average  Poor (See voting log ...)
 

Other User Comments


 There are no comments on this submission.
 

Add Your Feedback
Your feedback will be posted below and an email sent to the author. Please remember that the author was kind enough to share this with you, so any criticisms must be stated politely, or they will be deleted. (For feedback not related to this particular article, please click here instead.)
 

To post feedback, first please login.