The Three Streams.exe has a nearly identical UI to the Circular Streams sample, (see Figure 6.2), but uses the notifications rather than the polling model of managing the streaming.
Behind the scenes, what is happening is that each of the three slots in the UI matches a three-second buffer. Each time that you press Play, the first three seconds of a track are loaded and begin playing. This buffer is refilled from the file on the hard disk every fifth of a second; if the three tracks are playing together, this constant refilling of each buffer goes on in a different thread for each track.
Using notifications, the streaming buffers must be set up in software, so there is a high cost of applying special effects or 3-D processing in those cases. The reason for this oddity is that Windows notifications require the refilling of each stream to be handled by a separate thread, and audio hardware does not work well with a program that is multithreaded and sending notifications to more than one of these threads. For this reason, we are suggesting the use of notification-based streaming buffers only in a few specific cases:
Playing one or more music tracks.
For human dialog, consider setting up one stream per speaker.
The Three Streams.cpp file is the only source file unique to this sample, and is relatively short, only about eight pages long. The basis of the sample, however, is the CStreamingSound class declared in extended_dsutil.cpp . We have not amended this class from its original form in the dsutil .cpp file (but note that it inherits the CSound class, which has been extensively amended).
The key data and definitions for the sample follow.
#define nStreams 3 #define AllStreams (streamNumber=0;streamNumber<nStreams;streamNumber++) #define NUM_PLAY_NOTIFICATIONS 15 #define NUM_SECONDS 3 CSoundManager* g_pSoundManager = NULL; CStreamingSound* g_pStreamingSound[nStreams] = { NULL, NULL, NULL }; HANDLE g_hNotificationEvent[nStreams] = { NULL, NULL, NULL }; DWORD g_dwNotifyThreadID[nStreams] = { 0, 0, 0 }; HANDLE g_hNotifyThread[nStreams] = { NULL, NULL, NULL };
The number of streams was arbitrarily chosen to be three (held by the nStreams define) and the AllStreams definition simply provides us with a macro to step through each of the streams (it assumes that streamNumber has been declared as a variable within the method that the macro is being used in).
The definition NUM_SECONDS sets the length of the streaming buffers, obviously to 3 seconds in this case. Note that the actual size of the three buffers in bytes will only be equal if their sampling rates are also identical.
The NUM_PLAY_NOTIFICATIONS define sets the number of times that a Windows notification event is sent for the length of the streaming buffer. For example, with this define set at 15 and the length of the buffer set at three seconds, an event is sent off to refill the buffer every fifth of a second. Setting NUM_PLAY_NOTIFICATIONS to be too big or too small might affect the performance of the audio if you have a large number of streaming buffers. Trial and error seems to be the method of choice in determining what combinations of buffer size and notification frequency work best.
Next you will see that we need one sound manager object, with one streaming sound object per stream. Note that an object created from the CStreamingSound class handles a single streaming buffer. This differs from objects created from the CSound class, which can handle multiple buffers per wave file.
The next three declarations handle the multithreaded nature of this sample. Each stream, and therefore each thread, requires its own handle, as well as a notification event handle and ID. All these variables are initialized to zero or NULL for the sake of code safety, although in some cases, this is unnecessary.
If you have not attempted multithreaded programming before, then this sample is a good place to start. It is not as intimidating as you might think; most of the work, as is often the case, is in initializing everything correctly. Starting with the essential WinMain function:
INT APIENTRY WinMain( HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR pCmdLine, INT nCmdShow ) { int streamNumber; g_hInst = hInst; GetCurrentDirectory(MAX_PATH, g_soundDir); g_soundDir[2] = 0;// Delete all but the drive. strcat(g_soundDir,"\AVBook\Audio\Music"); for AllStreams g_hNotificationEvent[streamNumber] = CreateEvent( NULL, FALSE, FALSE, NULL ); DialogBox( hInst, MAKEINTRESOURCE(IDD_MAIN), NULL, MainDlgProc ); for AllStreams CloseHandle( g_hNotificationEvent[streamNumber] ); return TRUE; }
After storing the music sound directory in a global string variable, note the three calls to the Windows CreateEvent function. The four parameters are for event attributes, whether a manual reset is enabled, the initial state, and an optional event name . We do not need any variations on the default for this function, so the four parameters are all set to NULL or FALSE appropriately.
After you close the Three Streams dialog box, there are three calls to the Windows CloseHandle function to shut down the three events that we have created.
The next function in the code is one of the core functions for multithreaded event-driven programming, NotificationProc. This is the function, which runs in its own thread.
DWORD WINAPI NotificationProc( LPVOID lpParameter ) { HRESULT hr; MSG msg; DWORD dwResult = 0; BOOL bDone = FALSE; BOOL bLooped = TRUE; int streamNumber = (int) lpParameter; while( !bDone ) { dwResult = MsgWaitForMultipleObjects( 1, &g_hNotificationEvent[streamNumber], FALSE, INFINITE, QS_POSTMESSAGE ); switch( dwResult ) { case WAIT_OBJECT_0 + 0: if (g_pStreamingSound[streamNumber] != NULL) { if( FAILED( hr = g_pStreamingSound[streamNumber] -> HandleWaveStreamNotification( bLooped ) ) ) bDone = TRUE; } else bDone = TRUE; break; case WAIT_OBJECT_0 + 1: while( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) ) { if( msg.message == WM_QUIT ) bDone = TRUE; } break; } } return 0; }
With three threads, we will have three different versions of this procedure running simultaneously . Also, any functions called by NotificationProc will run separately and independently in each of the three threads.
The lpParameter parameter provided in this code is used to identify which stream is being serviced (note the conversion to an int and its assignment to streamNumber). Typically, lpParameter is used to point to an array or a structure, if more information beyond just a single value needs to be sent to the thread. In this case, our use of global data keeps things simple.
Once initiated, the NotificationProc function will run continuously in its thread until it receives a WM_QUIT message, which only occurs when the Three Streams sample is closed. The NotificationProc function basically loops , picking up posted WAIT_OBJECT events after they are sent. The MsgWaitForMultipleObjects function is used to pick up the events, and takes a number of parameters. The 1 indicates that we are waiting for one object, the handle indicates which notification event (and therefore which thread and stream) we are waiting for, and the next parameter is only relevant if we are waiting for multiple objects, so we set it to FALSE . The INFINITE setting indicates that we do not want the waiting to be timed out.
Note that although we are sending events every fifth of a second when a stream is playing, we are not sending events if the stream has been stopped ; however, we want to keep the thread open and running in case Play is pressed again. The final parameter ( QS_POSTMESSAGE ) simply indicates which kind of event we are waiting for, which in this case is posted messages, rather than say, timer events, mouse movements and so on.
The only two messages that will be received are WAIT_OBJECT_0 and WAIT_OBJECT_0 + 1 . There is a convention in notification coding to add the redundant + 0 to the first message to highlight the fact that no offset is being added. If the first of these messages is received, we check that the streaming sound pointer is not erroneously set to NULL (which should not happen if we have coded things correctly). Then, we call the HandleWaveStreamNotification method of the streaming sound object. For the Three Streams sample, the tracks are always looped, which is the only parameter to this method.
The next few functions of the Three Streams.cpp file mostly concern the UI, which we won t discuss here in any depth. However, we do need to point out a few issues.
In MainDlgProc , notice that when one of the Stop buttons is pressed, a call is made to the Stop and then Reset methods of the streaming sound object. In this case, the Stop method is inherited from the CSound class, but the Reset method is specific to the CStreamingSound class.
Also in MailDlgProc , when the final WM_DESTROY message is received, note the sending of the WM_QUIT message to all three threads. When the word is received that the thread has closed down, the handles are closed too.
In the OnInitDialog function, the last few lines of code create the three threads that we need, one for each stream.
for AllStreams { g_hNotifyThread[streamNumber] = CreateThread( NULL, 0, NotificationProc, (LPVOID) streamNumber, 0,&g_dwNotifyThreadID[streamNumber] ); }
The first two parameters of the CreateThread function refer to attributes and stack size, which we won t go into here except to point out that they should be NULL and 0, respectively. Then, we send the all-important address of the NotificationProc function, followed by the stream number cast into an LPVOID, even though it is not a pointer. Remember that we stated that it is often convenient to send the address of an array or structure if more than one value needs to be sent to the notification procedure, which is why an LPVOID is used here. As we are sending just one value to the notification function, simply casting it as an LPVOID works just fine. The zero that follows the streamNumber parameter indicates that we are setting no specific creation flags for the thread. Finally, we pass in the address of the array holding the thread IDs. These IDs are filled in by this call.
Moving on to the OnOpenSoundFile function, note that it first stops and resets any stream that is currently playing in the buffer, and then finally calls LoadWaveAndCreateBuffer to load in a new wave file. This function is another important one for streaming buffers.
VOID LoadWaveAndCreateBuffer( HWND hDlg, TCHAR* strFileName, int streamNumber ) { HRESULT hr; CWaveFile waveFile; DWORD dwNotifySize; if( FAILED( hr = waveFile.Open( strFileName, NULL, WAVEFILE_READ ) ) ) { waveFile.Close(); SetDlgItemText( hDlg, IDC_FILENAME1 + streamNumber, TEXT("Bad wave file.") ); return; } if( waveFile.GetSize() == 0 ) { waveFile.Close(); SetDlgItemText( hDlg, IDC_FILENAME1 + streamNumber, TEXT("Wave file blank.") ); return; } waveFile.Close(); DWORD nBlockAlign = (DWORD)waveFile.m_pwfx->nBlockAlign; INT nSamplesPerSec = waveFile.m_pwfx->nSamplesPerSec; dwNotifySize = nSamplesPerSec * NUM_SECONDS * nBlockAlign / NUM_PLAY_NOTIFICATIONS; dwNotifySize -= dwNotifySize % nBlockAlign; SAFE_DELETE( g_pStreamingSound[streamNumber] ); if( FAILED( hr = g_pSoundManager->CreateStreaming( &g_pStreamingSound[streamNumber], strFileName, DSBCAPS_LOCSOFTWARE, GUID_NULL, NUM_PLAY_NOTIFICATIONS, dwNotifySize, g_hNotificationEvent[streamNumber] ) ) ) { SetDlgItemText( hDlg, IDC_FILENAME1 + streamNumber, TEXT("Could not support the file's audio format.") ); return; } EnablePlayUI( hDlg, TRUE, streamNumber ); SetDlgItemText( hDlg, IDC_FILENAME1 + streamNumber, strFileName ); }
The LoadWaveAndCreateBuffer function first checks that the wave file is valid, and is not of zero length. The next statements calculate dwNotifySize , which is basically the number of samples per second multiplied by the number of seconds multiplied by the block alignment (usually one or two bytes), then divided by the number of notifications. The final subtraction is just to make sure that the notification size is an integer multiple of the block alignment (so if the block alignment is two bytes, the notification size is a multiple of two).
Following the calculation of dwNotifySize , any existing sound is deleted, and a new streaming sound object is created with a call to CreateStreaming (a method on the sound manager object). The parameters to CreateStreaming are as follows:
A pointer to the streaming sound object to be created.
The full path of the wave file.
Any creation flags in addition to those enforced by the CreateStreaming method (so, as we have three streams, we set the DSBCAPS_LOC-SOFTWARE flag).
A 3-D GUID algorithm which we have set to GUID_NULL as we do not want 3-D effects to be applied.
The number of notifications.
The notification size.
The notification event handle.
If all of this works, the function successfully updates the Three Streams UI. The PlayStreamingBuffer function is called when a Play button is pressed for that stream.
HRESULT PlayStreamingBuffer( BOOL bLooped, int streamNumber ) { HRESULT hr; if( NULL == g_pStreamingSound[streamNumber] ) return E_FAIL; // Sanity check.
if( FAILED( hr = g_pStreamingSound[streamNumber]->Reset() ) ) return E_FAIL; LPDIRECTSOUNDBUFFER pDSB = g_pStreamingSound[streamNumber]->GetBuffer( 0 ); if( FAILED( hr = g_pStreamingSound[streamNumber]-> FillBufferWithSound( pDSB, bLooped ) ) ) return E_FAIL; if( FAILED( hr = g_pStreamingSound[streamNumber]-> PlayBuffer(0, 0, DSBPLAY_LOOPING, DSBVOLUME_MAX, DSBPAN_CENTER, NO_FREQUENCY_CHANGE ) ) ) return E_FAIL; return S_OK; }
The last call in PlayStreamingBuffer , to the PlayBuffer method of the streaming sound object, can be used to set volume, panning and frequency changes. However, these will not work unless you also set the appropriate creation flags in the call to CreateStreaming mentioned previously. In this case, one or more of the following flags should be set in this call:
DSBCAPS_CTRLFREQUENCY
DSBCAPS_CTRLPAN
DSBCAPS_CTRLVOLUME
We will show an example of how to do this, enabling a change of volume, in Chapter 8.
You will notice that there is a small latency problem in the Three Streams sample if you try to play all three tracks exactly in sync, as the PlayStreamingBuffer function first fills a buffer and then starts the sound playing. Better synchronization is achieved by filling all the buffers first, and then setting the tracks to play together. This is also addressed in the coding for the Concertina framework in Chapter 8.
The final function of Three Streams.cpp, EnablePlayUI , just sets the Play and Stop buttons appropriately.
There are only four methods in the CStreamingSound class: the creation function, the destructor (which is empty), and the two methods mentioned previously, HandleWaveStreamNotification and Reset. All the other methods needed to set and get information on an object created from the CStreamingSound class are identical to those in the CSound class, and therefore are available to the streaming sound object as CStreamingSound inherits CSound.
However, first we should look at the CreateStreaming method of the CSoundManager object.
HRESULT CSoundManager::CreateStreaming( CStreamingSound** ppStreamingSound, LPTSTR strWaveFileName, DWORD dwCreationFlags, GUID guid3DAlgorithm, DWORD dwNotifyCount, DWORD dwNotifySize, HANDLE hNotifyEvent ) { HRESULT hr; if( m_pDS == NULL ) return CO_E_NOTINITIALIZED; if( strWaveFileName == NULL ppStreamingSound == NULL hNotifyEvent == NULL ) return E_INVALIDARG; LPDIRECTSOUNDBUFFER pDSBuffer = NULL; DWORD dwDSBufferSize = NULL; CWaveFile* pWaveFile = NULL; DSBPOSITIONNOTIFY* aPosNotify = NULL; LPDIRECTSOUNDNOTIFY pDSNotify = NULL; pWaveFile = new CWaveFile(); if( pWaveFile == NULL ) return E_OUTOFMEMORY; pWaveFile->Open( strWaveFileName, NULL, WAVEFILE_READ ); dwDSBufferSize = dwNotifySize * dwNotifyCount; DSBUFFERDESC dsbd; ZeroMemory( &dsbd, sizeof(DSBUFFERDESC) ); dsbd.dwSize = sizeof(DSBUFFERDESC); dsbd.dwFlags = dwCreationFlags DSBCAPS_CTRLPOSITIONNOTIFY DSBCAPS_GETCURRENTPOSITION2; dsbd.dwBufferBytes = dwDSBufferSize; dsbd.guid3DAlgorithm = guid3DAlgorithm; dsbd.lpwfxFormat = pWaveFile->m_pwfx; if( FAILED( hr = m_pDS->CreateSoundBuffer( &dsbd, &pDSBuffer, NULL ) ) ) { if( hr == DSERR_BADFORMAT hr == E_INVALIDARG ) return DXTRACE_ERR( TEXT("CreateSoundBuffer"), hr ); return DXTRACE_ERR( TEXT("CreateSoundBuffer"), hr ); } if( FAILED( hr = pDSBuffer->QueryInterface( IID_IDirectSoundNotify, (VOID**)&pDSNotify ) ) ) { SAFE_DELETE_ARRAY( aPosNotify ); return DXTRACE_ERR( TEXT("QueryInterface"), hr ); } aPosNotify = new DSBPOSITIONNOTIFY[ dwNotifyCount ]; if( aPosNotify == NULL ) return E_OUTOFMEMORY; for( DWORD i = 0; i < dwNotifyCount; i++ ) { aPosNotify[i].dwOffset = (dwNotifySize * i) + dwNotifySize - 1; aPosNotify[i].hEventNotify = hNotifyEvent; } if( FAILED( hr = pDSNotify->SetNotificationPositions( dwNotifyCount, aPosNotify))) { SAFE_RELEASE( pDSNotify ); SAFE_DELETE_ARRAY( aPosNotify ); return DXTRACE_ERR( TEXT("SetNotificationPositions"), hr ); } SAFE_RELEASE( pDSNotify ); SAFE_DELETE_ARRAY( aPosNotify ); *ppStreamingSound = new CStreamingSound( pDSBuffer, dwDSBufferSize, pWaveFile, dwNotifySize ); return S_OK; }
The CreateStreaming method first checks the sense of its input parameters, and then initializes most of its data members to NULL. Then it opens the appropriate wave file. Note the calculation of the buffer size with the following statement.
dwDSBufferSize = dwNotifySize * dwNotifyCount;
Following this, a buffer description is prepared, and then a call is made to create a single sound buffer. Then note the call to QueryInterface to get a notification interface pointer and store it in the pDSNotify data member.
hr = pDSBuffer->QueryInterface( IID_IDirectSoundNotify, (VOID**)&pDSNotify )
The next statement creates new DSBPOSITIONNOTIFY structures for each of the required notifications. This structure is declared in dsound .h and is defined as follows.
typedef struct _DSBPOSITIONNOTIFY { DWORD dwOffset; HANDLE hEventNotify; } DSBPOSITIONNOTIFY, *LPDSBPOSITIONNOTIFY;
The loop that follows the creation of these DSBPOSITIONNOTIFY structures populates the two data members of each structure. These structures are used as input parameters to the call to SetNotificationPositions , using the pDSNotify pointer to the IDirectSoundNotify interface. A call to the creation function for a streaming sound ends the CreateStreaming method (of the CSoundManager class).
CStreamingSound::CStreamingSound( LPDIRECTSOUNDBUFFER pDSBuffer, DWORD dwDSBufferSize, CWaveFile* pWaveFile, DWORD dwNotifySize ) : CSound( &pDSBuffer, dwDSBufferSize, 1, pWaveFile, 0 ) { m_dwLastPlayPos = 0; m_dwPlayProgress = 0; m_dwNotifySize = dwNotifySize; m_dwNextWriteOffset = 0; m_bFillNextNotificationWithSilence = FALSE; }
In the CStreamingSound class, the HandleWaveStreamNotification method is easily the most complex method.
HRESULT CStreamingSound::HandleWaveStreamNotification( BOOL bLoopedPlay ) { HRESULT hr; DWORD dwCurrentPlayPos; DWORD dwPlayDelta; DWORD dwBytesWrittenToBuffer; VOID* pDSLockedBuffer = NULL; VOID* pDSLockedBuffer2 = NULL; DWORD dwDSLockedBufferSize; DWORD dwDSLockedBufferSize2; if( m_apDSBuffer == NULL m_pWaveFile == NULL ) return CO_E_NOTINITIALIZED; // Restore the buffer if it was lost. BOOL bRestored; if( FAILED( hr = RestoreBuffer( m_apDSBuffer[0], &bRestored ) ) ) return DXTRACE_ERR( TEXT("RestoreBuffer"), hr ); if( bRestored ) { // The buffer was restored, so we need to fill it with new data. if( FAILED( hr = FillBufferWithSound( m_apDSBuffer[0], FALSE ) ) ) return DXTRACE_ERR( TEXT("FillBufferWithSound"), hr ); return S_OK; } // Lock the DirectSound buffer. if( FAILED( hr = m_apDSBuffer[0]-> Lock( m_dwNextWriteOffset, m_dwNotifySize, &pDSLockedBuffer, &dwDSLockedBufferSize, &pDSLockedBuffer2, &dwDSLockedBufferSize2, 0L ) ) ) return DXTRACE_ERR( TEXT("Lock"), hr ); if( pDSLockedBuffer2 != NULL ) return E_UNEXPECTED; if( !m_bFillNextNotificationWithSilence ) { // Fill the DirectSound buffer with wave data. if( FAILED( hr = m_pWaveFile->Read( (BYTE*) pDSLockedBuffer, dwDSLockedBufferSize, &dwBytesWrittenToBuffer ) ) ) return DXTRACE_ERR( TEXT("Read"), hr ); } else { // Fill the DirectSound buffer with silence. FillMemory( pDSLockedBuffer, dwDSLockedBufferSize, (BYTE)( m_pWaveFile->m_pwfx-> wBitsPerSample == 8 ? 128 : 0 ) ); dwBytesWrittenToBuffer = dwDSLockedBufferSize; } // If the number of bytes written is less than the // amount we requested, we have a short file. if( dwBytesWrittenToBuffer < dwDSLockedBufferSize ) { if( !bLoopedPlay ) { // Fill in silence for the rest of the buffer. FillMemory( (BYTE*) pDSLockedBuffer + dwBytesWrittenToBuffer, dwDSLockedBufferSize - dwBytesWrittenToBuffer, (BYTE)(m_pWaveFile->m_pwfx-> wBitsPerSample == 8 ? 128 : 0 ) ) ; // Any future notifications should just fill the // buffer with silence. m_bFillNextNotificationWithSilence = TRUE; } else { // We are looping, so reset the file and fill the // buffer with wave data. DWORD dwReadSoFar = dwBytesWrittenToBuffer; // From previous call above. while( dwReadSoFar < dwDSLockedBufferSize ) { // This will keep reading in until the buffer is full // (for very short files). if( FAILED( hr = m_pWaveFile->ResetFile() ) ) return DXTRACE_ERR( TEXT("ResetFile"), hr ); if( FAILED( hr = m_pWaveFile ->Read( (BYTE*)pDSLockedBuffer + dwReadSoFar, dwDSLockedBufferSize - dwReadSoFar, &dwBytesWrittenToBuffer ) ) ) return DXTRACE_ERR( TEXT("Read"), hr ); dwReadSoFar += dwBytesWrittenToBuffer; } } } // Unlock the DirectSound buffer. m_apDSBuffer[0]->Unlock( pDSLockedBuffer, dwDSLockedBufferSize, NULL, 0 ); // Figure out how much data has been played so far. When we have played // past the end of the file, we will either need to start filling the // buffer with silence or starting reading from the beginning of the file, // depending on whether the user wants to loop the sound. if( FAILED( hr = m_apDSBuffer[0]-> GetCurrentPosition( &dwCurrentPlayPos, NULL ) ) ) return DXTRACE_ERR( TEXT("GetCurrentPosition"), hr ); // Check to see if the position counter looped. if( dwCurrentPlayPos < m_dwLastPlayPos ) dwPlayDelta = ( m_dwDSBufferSize - m_dwLastPlayPos ) + dwCurrentPlayPos; else dwPlayDelta = dwCurrentPlayPos - m_dwLastPlayPos; m_dwPlayProgress += dwPlayDelta; m_dwLastPlayPos = dwCurrentPlayPos; // If we are now filling the buffer with silence, we have found the end, // so check to see if the entire sound has played. // If it has, then stop the buffer. if( m_bFillNextNotificationWithSilence ) { // We don't want to cut off the sound before it's done playing. if( m_dwPlayProgress >= m_pWaveFile->GetSize() ) { m_apDSBuffer[0]->Stop(); } } // Update where the buffer will lock (for next time). m_dwNextWriteOffset += dwDSLockedBufferSize; m_dwNextWriteOffset %= m_dwDSBufferSize; // Circular buffer. return S_OK; }
The first thing that the HandleWaveStreamNotification method does is restore the contents of the buffer if it was lost to another process. After this, the main thing to note is that the buffer is locked while work is being done on it, preventing any other process from gaining access to it. The buffer is filled from the file, taking into account whether the file should be looped, or any extra space filled with silence, before it is unlocked. Finally, a number of pointers and counts are updated.
The Reset method for this class differs from the much simpler method in the CSound class, largely because of the need to reset various counts and also reset the file.
HRESULT CStreamingSound::Reset() { HRESULT hr; if( m_apDSBuffer[0] == NULL m_pWaveFile == NULL ) return CO_E_NOTINITIALIZED; m_dwLastPlayPos = 0; m_dwPlayProgress = 0; m_dwNextWriteOffset = 0; m_bFillNextNotificationWithSilence = FALSE; // Restore the buffer if it was lost. BOOL bRestored; if( FAILED( hr = RestoreBuffer( m_apDSBuffer[0], &bRestored ) ) ) return DXTRACE_ERR( TEXT("RestoreBuffer"), hr ); if( bRestored ) { // The buffer was restored, so we need to fill it with new data. if( FAILED( hr = FillBufferWithSound( m_apDSBuffer[0], FALSE ) ) ) return DXTRACE_ERR( TEXT("FillBufferWithSound"), hr ); } m_pWaveFile->ResetFile(); return m_apDSBuffer[0]->SetCurrentPosition( 0L ); }
You will probably not need to amend any methods in the CStreamingSound class, but it is useful to know the process that each of them is working through.