Playing small wave files with DirectSound requires little buffer management; you can simply load the entire sound into memory and play it. With larger wave files, though, you should be more efficient in your memory usage, especially if you will be playing multiple sounds simultaneously. Streaming is a technique of using a small buffer to play a large file by filling the buffer with data from the file at the same rate that data is taken from the buffer and played.
In this article I discuss the techniques required to stream wave files from disk and play them using the DirectSound application programming interface (API). I chose to implement my solution in C++, but the techniques presented here apply to a C implementation as well.
[size="5"] Introduction to DirectSound
DirectSound is the 32-bit audio API for Windows(R) 95 and Windows NT(R) that replaces the 16-bit wave API introduced in Windows 3.1. It provides device-independent access to audio accelerator hardware, giving you access to features like real-time mixing of audio streams and control over volume, panning (left/right balance control), and frequency shifting during playback. DirectSound also provides low-latency playback (on the order of 20 milliseconds) so that you can better synchronize sounds with other events. DirectSound is available in the DirectX 2 SDK.
[size="5"] Just the Facts, Ma'am
I'm going to stick to the subject of streaming wave files and not rehash all of the basics of DirectSound.
If you want to experiment with DirectSound or build the STREAMS sample application, you'll need the DirectX 2 SDK. This SDK is available in the July release of Microsoft Developer Network Development Platform. If you don't subscribe to the Development Platform, have we got a deal for you! For a limited time (how limited is still up in the air), you can download the DirectX SDK from this Web site. You'll have to be a real bit hound though--it's over 34MB! Even with a 28.8 kHz modem, you're looking at 4 to 5 hours of download time. Don't forget to disable call waiting!
If you're already familiar with DirectSound and don't want to read this entire article to get the goodies, skip to the Quick Fix section for a summary of what you need to know about streaming wave files with DirectSound.
[size="5"] How Streaming Works
The purpose of streaming is to use a relatively small buffer to play a large file. Specific implementations vary, but visualize streaming by imagining continually pouring water into a barrel with a hole in it. The idea is to keep enough water in the barrel so that the flow out of it is uninterrupted. In our case, the barrel is a sound buffer and the water is wave data. Let's carry this metaphor a bit further and say that to put water in the barrel, we have to fetch it from a lake with a bucket. The challenge of streaming, then, is to get the proper-sized bucket and a helper who can carry the bucket between the lake and the barrel fast enough to keep up with the outflow from the barrel. If the barrel (buffer) runs out of water (wave data), the flow (sound) is interrupted.
[size="5"] Streaming with DirectSound
If you've worked with the low-level wave API in Windows 3.1, you're probably familiar with the waveOutWrite function. This function sends a block of wave data to the driver; and when the driver is finished playing the buffer, it notifies the application and returns the buffer. To keep the drivers satisfied, the application must use at least two buffers and be able to fill a buffer with data in less time than it takes the driver to play a buffer. The following diagram illustrates the streaming mechanism used with the low-level wave API:
Double-buffer streaming with 16-bit wave API
The streaming mechanism used with DirectSound is a different beast altogether. With DirectSound, you create a secondary buffer object (I'll explain the "secondary" part of this jargon in a bit). This buffer is owned by DirectSound, and you must query the buffer to determine how much of the wave data has been played and how much space in the buffer is available to be filled with additional data. Conceptually, this mechanism is identical to a traditional circular buffer with head and tail pointers. The following diagram illustrates the streaming mechanism used with DirectSound:
Single-buffer streaming with DirectSound
With single-buffer streaming, the application is responsible for writing sound data into the buffer before the driver plays the data. The application should keep the buffer as full as possible to prevent any interruptions in sound playback. The DirectSound name for these buffers is secondary buffers. Each of these secondary buffers can have a different format. During playback, DirectSound mixes the data from all of the secondary buffers into a primary buffer. There is only one primary buffer and its format determines the output format. Applications do not write wave data directly to the primary buffer.
[size="5"] Polling vs. Interrupt-Driven Buffer Monitoring
Single-buffer streaming requires that the application monitor the buffer and supply it with sound data when necessary. There are two approaches to implementing buffer monitoring:
- Continuously polling the buffer.
- Periodically monitoring the buffer with an interrupt-driven routine.
[size="5"] A C++ Implementation of Streaming
The STREAMS sample application includes a C++ implementation of streaming with DirectSound. I chose to do a C++ implementation of streaming for several reasons:
- DirectSound's native interface is based on C++
- I have not seen any other C++ implementations of streaming with DirectSound
- I like to program in C++
[size="5"] Design Goals
My primary design goal was to create some reusable objects that implement streaming with DirectSound. I didn't want to introduce the complexities of COM or OLE, so the objects are reusable at the source-code level. I wanted the objects to have high-level interfaces and be easy to use in an application.
The STREAMS sample application uses the Microsoft Foundation Class (MFC) Library , a C++ application framework. I didn't base any of my streaming classes on MFC, so if you're using a different application framework, you should be able to reuse this code easily.
[size="5"] Building the STREAMS Sample Application
The STREAMS sample-application package includes source code for one target executable, STREAMS.EXE. I've included a project file for Visual C++, Version 4.0. The following table summarizes the files required to make STREAMS.EXE. If you're not using Visual C++, you can use this table to easily recreate the project in your favorite IDE.
FileDescriptionASSERT.CSource file containing basic assert services. DEBUG.CSource file containing basic debug services. AUDIOSTREAM.CPPSource file containing implementation of AudioStreamServicesand AudioStream objects. TIMER.CPPSource file containing implementation of Timerobject. WAVEFILE.CPPSource file containing implementation of WaveFileobject. STREAMS.CPPSource file for application. STREAMS.RCResource script file. WINMM.LIBSystem library file. DSOUND.LIBSystem library file.
The key source files are AUDIOSTREAM.CPP, TIMER.CPP, and WAVEFILE.CPP. These files contain the source for all of the objects required to implement wave streaming with DirectSound. The ASSERT.C and DEBUG.C files contain source for some simple debug and assert macros. The remaining source file, STREAMS.CPP, contains the source for a basic MFC-based application.
To build the STREAMS sample application, you'll need the Win32 SDK and the DirectX 2 SDK. To run STREAMS.EXE, you need the DirectX 2 runtime libraries and, of course, a sound card.
[size="5"] A Top-Down View
Before I get into the implementation of the objects that support streaming (the AudioStreamServices, AudioStream, Timer, and WaveFile objects), let's take a look at how these objects are used in the STREAMS sample application.
STREAMS is built on a basic two-object MFC model for frame window applications. The two objects are CMainWindow and CTheApp, based on CFrameWnd, and CWinApp, respectively. The following is the declaration of the CMainWindow class taken from STREAMS.H:
class CMainWindow : public CFrameWnd
{
public:
AudioStreamServices * m_pass; // ptr to AudioStreamServices object
AudioStream *m_pasCurrent; // ptr to current AudioStream object
CMainWindow();
//{{AFX_MSG( CMainWindow )
afx_msg void OnAbout();
afx_msg void OnFileOpen();
afx_msg void OnTestPlay();
afx_msg void OnTestStop();
afx_msg void OnUpdateTestPlay(CCmdUI* pCmdUI);
afx_msg void OnUpdateTestStop(CCmdUI* pCmdUI);
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnDestroy();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
[size="3"] Creating and Initializing the AudioStreamServices Object
Before a window uses streaming services, it must create an AudioStreamServices object. The following code shows how the OnCreate handler for the CMainWindow class creates and initializes an AudioStreamsServices object:
int CMainWindow::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CFrameWnd ::OnCreate(lpCreateStruct) == -1)
return -1;
// Create and initialize AudioStreamServices object.
m_pass = new AudioStreamServices;
if (m_pass)
{
m_pass->Initialize (m_hWnd);
}
// Initialize ptr to current AudioStream object
m_pasCurrent = NULL;
return 0;
}
[size="3"] Creating an AudioStream Object
Once a window has created and initialized an AudioStreamServices object, the window can create one or more AudioStream objects. The following code is the command handler for the File Open menu item:
void CMainWindow::OnFileOpen()
{
CString cstrPath;
// Create standard Open File dialog
CFileDialog * pfd
= new CFileDialog (TRUE, NULL, NULL,
OFN_EXPLORER | OFN_NONETWORKBUTTON | OFN_HIDEREADONLY,
"Wave Files (*.wav) | *.wav||", this);
// Show dialog
if (pfd->DoModal () == IDOK)
{
// Get pathname
cstrPath = pfd->GetPathName();
// Delete current AudioStream object
if (m_pasCurrent)
{
delete (m_pasCurrent);
}
// Create new AudioStream object
m_pasCurrent = new AudioStream;
m_pasCurrent->Create ((LPSTR)(LPCTSTR (cstrPath)), m_pass);
}
delete (pfd);
}
m_pasCurrent = new AudioStream;
m_pasCurrent->Create ((LPSTR)(LPCTSTR (cstrPath)), m_pass);
[size="3"] Controlling an AudioStream Object
Once you've created an AudioStream object, you can begin playback with the Play method. The following is the command handler for the Test Play menu item:
void CMainWindow::OnTestPlay()
{
if (m_pasCurrent)
{
m_pasCurrent->Play ();
}
}
void CMainWindow::OnTestStop()
{
if (m_pasCurrent)
{
m_pasCurrent->Stop ();
}
}
[size="5"] The Timer and WaveFile Objects
Now that I've given you a look at how to use the AudioStreamServices and AudioStream objects in an application, let's dig into their implementation. I'll begin with two helper objects, Timer and WaveFile, that are used by AudioStream objects.
[size="3"] The Timer Object
The Timer object is used to provide timer services that allow AudioStream objects to service the sound buffer periodically. Here's the declaration for the Timer class:
class Timer
{
public:
Timer (void);
~Timer (void);
BOOL Create (UINT nPeriod, UINT nRes, DWORD dwUser,
TIMERCALLBACK pfnCallback);
protected:
static void CALLBACK TimeProc(UINT uID, UINT uMsg, DWORD dwUser,
DWORD dw1, DWORD dw2);
TIMERCALLBACK m_pfnCallback;
DWORD m_dwUser;
UINT m_nPeriod;
UINT m_nRes;
UINT m_nIDTimer;
};
BOOL Create (UINT nDelay, UINT nRes, DWORD dwUser, TIMERCALLBACK pfnCallback);
BOOL Timer::Create (UINT nPeriod, UINT nRes, DWORD dwUser,
TIMERCALLBACK pfnCallback)
{
BOOL bRtn = SUCCESS; // assume success
// Set data members
m_nPeriod = nPeriod;
m_nRes = nRes;
m_dwUser = dwUser;
m_pfnCallback = pfnCallback;
// Create multimedia timer
if ((m_nIDTimer = timeSetEvent (m_nPeriod, m_nRes, TimeProc,
(DWORD) this, TIME_PERIODIC)) == NULL)
{
bRtn = FAILURE;
}
return (bRtn);
}
Before I lose you here, take a look at the declaration of the Timer::TimeProc member function. It must be declared as static so that it can be used as a C-style callback for the multimedia timer set with timeSetEvent. Because TimeProc is a static member function, it's not associated with a Timer object and does not have access to the this pointer. Here's the source for TimeProc:
void CALLBACK Timer::TimeProc(UINT uID, UINT uMsg, DWORD dwUser,
DWORD dw1, DWORD dw2)
{
// dwUser contains ptr to Timer object
Timer * ptimer = (Timer *) dwUser;
// Call user-specified callback and pass back user specified data
(ptimer->m_pfnCallback) (ptimer->m_dwUser);
}
In similar fashion, any object that uses a Timer object must supply a callback that is a static member function and supply its this pointer as the user-supplied data for the callback. For example, here's the code from AudioStream::Play that creates the Timer object:
// Kick off timer to service buffer
m_ptimer = new Timer ();
if (m_ptimer)
{
m_ptimer->Create (m_nBufService, m_nBufService, DWORD (this), TimerCallback);
}
BOOL AudioStream::TimerCallback (DWORD dwUser)
{
// dwUser contains ptr to AudioStream object
AudioStream * pas = (AudioStream *) dwUser;
return (pas->ServiceBuffer ());
}
[size="3"] The WaveFile Object
In addition to an object to encapsulate multimedia timer services, I needed an object to represent a wave file, so I created the WaveFile class. The following is the class declaration for the WaveFile class:
class WaveFile
{
public:
WaveFile (void);
~WaveFile (void);
BOOL Open (LPSTR pszFilename);
BOOL Cue (void);
UINT Read (BYTE * pbDest, UINT cbSize);
UINT GetNumBytesRemaining (void) { return (m_nDataSize - m_nBytesPlayed); }
UINT GetAvgDataRate (void) { return (m_nAvgDataRate); }
UINT GetDataSize (void) { return (m_nDataSize); }
UINT GetNumBytesPlayed (void) { return (m_nBytesPlayed); }
UINT GetDuration (void) { return (m_nDuration); }
BYTE GetSilenceData (void);
WAVEFORMATEX * m_pwfmt;
protected:
HMMIO m_hmmio;
MMRESULT m_mmr;
MMCKINFO m_mmckiRiff;
MMCKINFO m_mmckiFmt;
MMCKINFO m_mmckiData;
UINT m_nDuration; // duration of sound in msec
UINT m_nBlockAlign; // wave data block alignment spec
UINT m_nAvgDataRate; // average wave data rate
UINT m_nDataSize; // size of data chunk
UINT m_nBytesPlayed; // offset into data chunk
};
FunctionDescriptionOpenOpens a wave file. CueCues a wave file for playback. ReadReads a given number of data bytes. GetNumBytesRemainingReturns the number of data bytes remaining to be read. GetAvgDataRateReturns the average data rate in bytes per second. GetDataSizeReturns the total number of wave data bytes. GetNumBytesPlayedReturns the number of data bytes that have been read. GetDurationGets the duration of the wave file in milliseconds. GetSilenceDataReturns a byte of data representing silence.
I chose to use the Win32 Multimedia File I/O services (MMIO) for implementation of WaveFile objects because these services take care of the basics of parsing the chunks in Resource Interchange File Format (RIFF) files. Since the point of this article is to explain streaming with DirectSound, I'm not going to explain the WaveFile code in detail. Take my word for it: the biggest challenge in writing this code was properly handling the myriad of errors that can occur when accessing files.
[size="3"] Silence, Please!
There is one detail I do want to explain. Implementing the AudioStream class required that blocks of data representing silence be written to the sound buffer (if you read the remainder of this article, you'll learn why). Since the data representing silence depends on the format of the wave file, I added a GetSilenceData member function to the WaveFile class. Word size for pulse-code modulation (PCM) formats can range from one byte for 8-bit mono to four bytes for 16-bit stereo, as shown in the following table.
PCM FormatWord SizeSilence Data8-bit mono1 byte0x808-bit stereo2 bytes0x808016-bit mono2 bytes0x000016-bit stereo4 bytes0x00000000
Rather than make the AudioStream code deal with the different word sizes for different wave file formats, I took advantage of the fact that regardless of word size, silence data for PCM formats can be represented by a single byte. Thus, the GetSilenceData functions returns a BYTE. This shortcut saved me from having to write a lot of extra code.
[size="5"] The AudioStreamServices Object
The DirectSound interface consists of two objects, IDirectSound and IDirectSoundBuffer. The IDirectSound object represents the DirectSound services for a single window. Services are apportioned on a per-windows basis to facilitate muting a sound stream when a window loses the input focus. I created the AudioStreamServices class to wrap the IDirectSound object:
class AudioStreamServices
{
public:
AudioStreamServices (void);
~AudioStreamServices (void);
BOOL Initialize (HWND hwnd);
LPDIRECTSOUND GetPDS (void) { return m_pds; }
protected:
HWND m_hwnd;
LPDIRECTSOUND m_pds;
};
// Initialize
BOOL AudioStreamServices::Initialize (HWND hwnd)
{
BOOL fRtn = SUCCESS; // assume success
if (m_pds == NULL)
{
if (hwnd)
{
m_hwnd = hwnd;
// Create IDirectSound object
if (DirectSoundCreate (NULL, &m_pds, NULL) == DS_OK)
{
// Set cooperative level for DirectSound. Normal means our
// sounds will be silenced when our window loses input focus.
if (m_pds->SetCooperativeLevel (m_hwnd, DSSCL_NORMAL) == DS_OK)
{
// Any additional initialization goes here
}
else
{
// Error
DOUT ("ERROR: Unable to set cooperative level\n\r");
fRtn = FAILURE;
}
}
else
{
// Error
DOUT ("ERROR: Unable to create IDirectSound object\n\r");
fRtn = FAILURE;
}
}
else
{
// Error, invalid hwnd
DOUT ("ERROR: Invalid hwnd, unable to initialize services\n\r");
fRtn = FAILURE;
}
}
return (fRtn);
}
After successfully creating an IDirectSound object, the Initialize code calls the SetCooperativeLevel member function specifying the DSSCL_NORMAL flag to set the normal cooperative level. This is the lowest cooperative level--other levels are available if you require more control of DirectSound's buffers. For example, in normal cooperative level, the format of audio output is always 8-bit 22kHz mono. To change to another output format, you have to set the priority cooperative level (DSSCL_PRIORITY) and call the SetFormat function.
[size="5"] The AudioStream Object
Now we're down to the good stuff. I've explained how to use AudioStreamServices and AudioStream objects in an application. I've described the Timer and WaveFile objects that are used to provide periodic timer services and read wave files. Now I'm going to explain the implementation of the AudioStream object, the object that actually streams wave files using DirectSound. Here's the AudioStream class declaration:
class AudioStream
{
public:
AudioStream (void);
~AudioStream (void);
BOOL Create (LPSTR pszFilename, AudioStreamServices * pass);
BOOL Destroy (void);
void Play (void);
void Stop (void);
protected:
void Cue (void);
BOOL WriteWaveData (UINT cbSize);
BOOL WriteSilence (UINT cbSize);
DWORD GetMaxWriteSize (void);
BOOL ServiceBuffer (void);
static BOOL TimerCallback (DWORD dwUser);
AudioStreamServices * m_pass; // ptr to AudioStreamServices object
LPDIRECTSOUNDBUFFER m_pdsb; // sound buffer
WaveFile * m_pwavefile; // ptr to WaveFile object
Timer * m_ptimer; // ptr to Timer object
BOOL m_fCued; // semaphore (stream cued)
BOOL m_fPlaying; // semaphore (stream playing)
DSBUFFERDESC m_dsbd; // sound buffer description
LONG m_lInService; // reentrancy semaphore
UINT m_cbBufOffset; // last write position
UINT m_nBufLength; // length of sound buffer in msec
UINT m_cbBufSize; // size of sound buffer in bytes
UINT m_nBufService; // service interval in msec
UINT m_nDuration; // duration of wave file
UINT m_nTimeStarted; // time (in system time) playback started
UINT m_nTimeElapsed; // elapsed time in msec since playback started
};
The main players here are the Create and Play methods, and a third method, ServiceBuffer, that is not an interface. Here is an explanation of the role each of these methods plays in streaming wave files:
- Create opens a wave file, creates a sound buffer, and cues the stream for playback.
- Play begins DirectSound playback and launches a timer to service the sound buffer.
- ServiceBuffer determines how much of sound buffer is free and fills free space with wave data (or with silence data if all wave data has been sent to buffer). ServiceBuffer also maintains an elapsed time count and stops playback when all of wave file has been played.
Before creating a sound buffer, you must open the wave file to determine its format, average data rate, and duration. Here's the corresponding code from the Create method:
// Create a new WaveFile object
if (m_pwavefile = new WaveFile)
{
// Open given file
if (m_pwavefile->Open (pszFilename))
{
// Calculate sound buffer size in bytes
m_cbBufSize = (m_pwavefile->GetAvgDataRate () * m_nBufLength) / 1000;
m_cbBufSize = (m_cbBufSize > m_pwavefile->GetDataSize ())
? m_pwavefile->GetDataSize ()
: m_cbBufSize;
// Get duration of sound (in milliseconds)
m_nDuration = m_pwavefile->GetDuration ();
. . .
}
}
const UINT DefBufferLength = 2000;
const UINT DefBufferServiceInterval = 250;
// Create sound buffer
HRESULT hr;
memset (&m_dsbd, 0, sizeof (DSBUFFERDESC));
m_dsbd.dwSize = sizeof (DSBUFFERDESC);
m_dsbd.dwBufferBytes = m_cbBufSize;
m_dsbd.lpwfxFormat = m_pwavefile->m_pwfmt;
hr = (m_pass->GetPDS ())->CreateSoundBuffer (&m_dsbd, &m_pdsb, NULL);
[size="3"] Filling the Sound Buffer with Wave Data
After successfully creating the sound buffer, Create calls the AudioStream::Cue method to prepare the stream for playback. Cue resets the buffer pointers and the file pointer and then calls AudioStream:: WriteWaveData to fill the buffer with data from the wave file. The following is the source for WriteWaveData:
BOOL AudioStream::WriteWaveData (UINT size)
{
HRESULT hr;
LPBYTE lpbuf1 = NULL;
LPBYTE lpbuf2 = NULL;
DWORD dwsize1 = 0;
DWORD dwsize2 = 0;
DWORD dwbyteswritten1 = 0;
DWORD dwbyteswritten2 = 0;
BOOL fRtn = SUCCESS;
// Lock the sound buffer
hr = m_pdsb->Lock (m_cbBufOffset, size, &lpbuf1, &dwsize1, &lpbuf2, &dwsize2, 0);
if (hr == DS_OK)
{
// Write data to sound buffer. Because the sound buffer is circular,
// we may have to do two write operations if locked portion of buffer
// wraps around to start of buffer.
ASSERT (lpbuf1);
if ((dwbyteswritten1 = m_pwavefile->Read (lpbuf1, dwsize1)) == dwsize1)
{
// Second write required?
if (lpbuf2)
{
if ((dwbyteswritten2 = m_pwavefile->Read (lpbuf2, dwsize2)) == dwsize2)
{
// Both write operations successful!
}
else
{
// Error, didn't read wave data completely
fRtn = FAILURE;
}
}
}
else
{
// Error, didn't read wave data completely
fRtn = FAILURE;
}
// Update our buffer offset and unlock sound buffer
m_cbBufOffset = (m_cbBufOffset + dwbyteswritten1 + dwbyteswritten2)
% m_cbBufSize;
m_pdsb->Unlock (lpbuf1, dwbyteswritten1, lpbuf2, dwbyteswritten2);
}
else
{
// Error locking sound buffer
fRtn = FAILURE;
}
return (fRtn);
}
[size="3"] Beginning Playback
The AudioStream::Play method begins playback by calling the IDirectSoundBuffer::Play method and creating a timer to service the sound buffer:
// Begin DirectSound playback
HRESULT hr = m_pdsb->Play (0, 0, DSBPLAY_LOOPING);
if (hr == DS_OK)
{
// Save current time (for elapsed time calculation)
m_nTimeStarted = timeGetTime ();
// Kick off timer to service buffer
m_ptimer = new Timer ();
if (m_ptimer)
{
m_ptimer->Create (m_nBufService, m_nBufService, DWORD (this),
TimerCallback);
}
. . .
}
[size="3"] Servicing the Sound Buffer
The Timer object created by AudioStream::Play periodically calls the ServiceBuffer routine to perform the following tasks:
- Maintain an elapsed time count.
- Determine if playback is complete and stop if necessary.
- Fill sound buffer with more wave data or with silence data if all wave data has been sent to buffer.
LONG lInService = FALSE; // reentrancy semaphore
BOOL AudioStream::ServiceBuffer (void)
{
BOOL fRtn = TRUE;
// Check for reentrance
if (InterlockedExchange (&lInService, TRUE) == FALSE)
{ // Not reentered, proceed normally
// Maintain elapsed time count
m_nTimeElapsed = timeGetTime () - m_nTimeStarted;
// Stop if all of sound has played
if (m_nTimeElapsed < m_nDuration)
{
// All of sound not played yet, send more data to buffer
DWORD dwFreeSpace = GetMaxWriteSize ();
// Determine free space in sound buffer
if (dwFreeSpace)
{
// See how much wave data remains to be sent to buffer
DWORD dwDataRemaining = m_pwavefile->GetNumBytesRemaining ();
if (dwDataRemaining == 0)
{ // All wave data has been sent to buffer
// Fill free space with silence
if (WriteSilence (dwFreeSpace) == FAILURE)
{ // Error writing silence data
fRtn = FALSE;
}
}
else if (dwDataRemaining >= dwFreeSpace)
{ // Enough wave data remains to fill free space in buffer
// Fill free space in buffer with wave data
if (WriteWaveData (dwFreeSpace) == FAILURE)
{ // Error writing wave data
fRtn = FALSE;
}
}
else
{ // Some wave data remains, but not enough to fill free space
// Write wave data, fill remainder of free space with silence
if (WriteWaveData (dwDataRemaining) == SUCCESS)
{
if (WriteSilence (dwFreeSpace - dwDataRemaining) == FAILURE)
{ // Error writing silence data
fRtn = FALSE;
}
}
else
{ // Error writing wave data
fRtn = FALSE;
}
}
}
else
{ // No free space in buffer for some reason
fRtn = FALSE;
}
}
else
{ // All of sound has played, stop playback
Stop ();
}
// Reset reentrancy semaphore
InterlockedExchange (&lInService, FALSE);
}
else
{ // Service routine reentered. Do nothing, just return
fRtn = FALSE;
}
return (fRtn);
}
I also want to explain why you need to write silence data to the sound buffer. DirectSound has no concept of when playback of a wave file is complete--it just happily cycles through the sound buffer playing whatever data is there until it's told to stop. The ServiceBuffer routine keeps track of how much time has elapsed since playback was started and stops playback as soon as enough time has elapsed to play the entire wave file. Since you can't stop playback at the exact millisecond that the last wave data byte is played, you have to follow the wave data with data representing silence. If you don't do this, you will get some random blip of sound at the end of a wave file.
[size="3"] Managing the Read-and-Write Cursors
Two offsets are required to manage data in a circular buffer. Traditionally these offsets are called the head and the tail of the buffer. I can never remember which is the head and which is the tail, so I like to call these two offsets the "read cursor" and the "write cursor." In this case, the read cursor identifies the location in the buffer where DirectSound is reading wave data and the write cursor identifies the location where we need to write the next block of wave data.
If you take a look at the IDirectSoundBuffer::GetCurrentPosition method, you'll see that it returns a read cursor and a write cursor. Looks easy enough. At least that's what I thought, but that's not exactly correct. It took me several days of hair-pulling to fi gure out that the write cursor returned by GetCurrentPosition was not the write cursor I needed to manage a sound buffer. Don't you hate it when things don't work like you want them to?
To manage a sound buffer with DirectSound, you must maintain your own write cursor. In the AudioStream class I represent the write cursor with the m_cbBufOffset data member. Each time you write wave data to the sound buffer, you must increment m_cbBufOffset and check to see if it has wrapped around to the beginning of the buffer. It's not difficult code to write, but it certainly took me a while to discover that I couldn't use the write cursor provided by DirectSound! The following code is a helper method called by ServiceBuffer to determine how much of the sound buffer has already been played (in other words, how much data can be written to the sound buffer):
DWORD AudioStream::GetMaxWriteSize (void)
{
DWORD dwWriteCursor, dwPlayCursor, dwMaxSize;
// Get current play position
if (m_pdsb->GetCurrentPosition (&dwPlayCursor, &dwWriteCursor) == DS_OK)
{
if (m_cbBufOffset <= dwPlayCursor)
{
// Our write position trails play cursor
dwMaxSize = dwPlayCursor - m_cbBufOffset;
}
else // (m_cbBufOffset > dwPlayCursor)
{
// Play cursor has wrapped
dwMaxSize = m_cbBufSize - m_cbBufOffset + dwPlayCursor;
}
}
else
{
// GetCurrentPosition call failed
ASSERT (0);
dwMaxSize = 0;
}
return (dwMaxSize);
}
Now I'll bet you're wondering what the deal is with the write cursor maintained by DirectSound. No, it's not broken, that's the way it was designed to operate! DirectSound's write cursor specifies the position in the buffer where it is safe to write data. During playback, DirectSound won't allow you to write to the section of the sound buffer that begins with its play cursor and ends with its write cursor. Typically, this is about 15 milliseconds worth of data. DirectSound does not change its write cursor when you write data to a sound buffer--the write cursor always tracks the play cursor and leads it by about 15 milliseconds during playback.
[size="5"] Quick Fix: A Summary of Streaming with DirectSound
This following list summarizes what you need to know about streaming wave files with DirectSound:
- DirectSound uses a single sound buffer. For streaming, you need to create a looping secondary buffer by calling the IDirectSound::CreateSoundBuffer method without specifying either the DSBCAPS_STATIC or DSBCAPS_PRIMARYBUFFER flags in the DSBUFFERDESC structure.
- The required size of the sound buffer depends on the format of the wave file you are streaming. For example, a 44.1 kHz 16-bit stereo file will require a much larger sound buffer than an 11.025 kHz 8-bit mono file. I recommend using a one- or two-second sound buffer.
- Use the Win32 multimedia timer services to provide a periodic timer interrupt to service the sound buffer. The timer interval depends on the size of the sound buffer and the data rate of the wave file you are streaming. I recommend using a timer interval that is one-fourth the size of your sound buffer. For example, with a two-second sound buffer, use a timer interval of 500 milliseconds.
- There are two pointers used to manage the contents of the sound buffer, a play cursor and a write cursor. DirectSound maintains the play cursor, which you can obtain with the IDirectSoundBuffer::GetCurrentPosition method. You must maintain your own write cursor to determine how much wave data to write into the buffer and where to write the data. Don't use the write cursor maintained by DirectSound for this purpose.
- DirectSound will continue to play the contents of the sound buffer until you tell it to stop. After you've written all of the wave file data into the sound buffer, you must write data representing silence to the buffer until you determine that all of the wave file data has been played. To determine when all of the data has been played, calculate the duration of the wave file and keep track of how much time has elapsed since you began playback.
- DirectSound only plays PCM data formats. Compressed wave formats are not supported. To play compressed wave data, you must first expand the data into PCM format before writing the data to a DirectSound sound buffer.