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() };
Note the two data members m_pass and m_pasCurrent. These data members hold pointers to an AudioStreamServices and AudioStream object. For simplicity, the STREAMS sample application allows only a single wave file to be opened at a time. The m_pasCurrent member contains a pointer to an AudioStream object created from the currently open wave file.
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; }
Each window using streaming services must create an AudioStreamServices object and initialize it with a window handle. This requirement comes directly from the architecture of DirectSound which apportions services on a per-window basis so that the sounds associated with a window can be muted when the window loses focus.
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); }
Two lines of code are required to create an AudioStream object:
m_pasCurrent = new AudioStream; m_pasCurrent->Create ((LPSTR)(LPCTSTR (cstrPath)), m_pass);
What looks like typecasting to LPCTSTR on the cstrPath parameter is actually a CString operator that extracts a pointer to a read-only C-style null-terminated string from a CString object. You might also be wondering why I didn't just create a constructor for the AudioStream class that accepts a pointer to a filename instead of making a Create member function to take the filename. I didn't do this because it's possible for the operation to fail and in C++ you can't easily return an error code from a constructor.
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-> lay (); } }
And here's the command handler for the Test Stop menu item:
void CMainWindow::OnTestStop() { if (m_pasCurrent) { m_pasCurrent->Stop (); } }
This code is so simple, I don't think it really needs any explanation. The only control methods I implemented for AudioStream objects are Play and Stop. In a real application, you'd probably want to add some more functionality.
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.
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; };
The Timer object uses the multimedia timer services provided through the Win32 timeSetEvent function. These services call a user-supplied callback function at a periodic interval specified in milliseconds. The Create member does all of the work here:
BOOL Create (UINT nDelay, UINT nRes, DWORD dwUser, TIMERCALLBACK pfnCallback);
The nPeriod and nRes parameters specify the timer period and resolution in milliseconds. The dwUser parameter specifies a DWORD that is passed back to you with each timer callback. The pfnCallback parameter specifies the callback function. Here's the source for Create:
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); }
After stuffing the four parameters into data members, Create calls timeSetEvent and passes the this pointer as the user-supplied data to the multimedia timer callback. This data is passed back to the callback to identify which Timer object is associated with the callback.
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); }
TimeProc contains two action-packed lines of code. The first line simply casts the dwUser parameter to a pointer to a Timer object and saves it in a local variable, ptimer. The second line of code dereferences ptimer to call the user-supplied callback and pass back the user-supplied data. I could have done away with the first line of code altogether and just cast dwUser to access the data members of the associated Timer object but I wrote it this way to better illustrate what's going on. Note that when I say "user-supplied" here, I'm talking about the user of the Timer object, which is in this case, an AudioStream object. 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: lay 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);
}
And here's the static member function that serves as a callback for the Timer object:
BOOL AudioStream::TimerCallback (DWORD dwUser)
{
// dwUser contains ptr to AudioStream object
AudioStream * pas = (AudioStream *) dwUser;
return (pas->ServiceBuffer ());
}
All the important work is done in the AudioStream::ServiceBuffer routine. You could move everything into AudioStream::TimerCallback, but because it's static, you'd have to use the this pointer contained in dwUser to access all class members. I think using a separate nonstatic member function results in code that is easier to read. 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
};
|