Platform SDK: DirectX |
For short sounds, the easiest way to play a wave file is to load it into a static buffer by using the DirectSound.CreateSoundBufferFromFile method. For larger files that do not conveniently fit in memory, you need to create a streaming buffer and read the file in pieces.
Wave files are in the Resource Interchange File Format (RIFF), which consists of a file header followed by a variable number of "chunks," each made up of a header and data. The chunk header consists of a four-character tag identifying the type of data and a Long giving the length of the data.
The wave file header is organized as follows:
The first chunk is always the format chunk, which has the following format.
The PCM sample data in a wave file is contained in a chunk beginning with the string "data", which can also be read as the Long &H61746164. Most often this chunk immediately follows the format chunk, but because RIFF is an extensible format, there is no guarantee that another type of chunk will not precede it. Your file parser must be capable of ignoring chunks it cannot handle.
To parse a wave file, it is helpful to have three user-defined types. The first will receive all the information in the file header and in the header of the format chunk.
Private Type FileHeader lRiff As Long lFileSize As Long lWave As Long lFormat As Long lFormatLength As Long End Type
The second type will receive the format data. You can't use the WAVEFORMATEX type for this, because the members are in a different order. This type needs to retrieve only 14 of the 16 bytes in the chunk, because the last Integer value is equivalent to WAVEFORMATEX.lExtra, which is not used in PCM files.
Private Type WaveFormat wFormatTag As Integer nChannels As Integer nSamplesPerSec As Long nAvgBytesPerSec As Long nBlockAlign As Integer wBitsPerSample As Integer End Type
The third type can be used to retrieve the header of any chunk, including the data chunk.
Private Type ChunkHeader lType As Long lLen As Long End Type
The following sample function verifies that a file is a RIFF wave file, seeks the beginning of the sample data, and returns a WAVEFORMATEX type containing information about the wave format.
Dim FileFree As Long ' Global file handle Dim lDataLength As Long ' Global data length Private Function FillFormat(FileName As String) As WAVEFORMATEX Dim Header As FileHeader Dim HdrFormat As WaveFormat Dim chunk As ChunkHeader Dim by As Byte Dim i As Long ' Open the file and read the header. Close #FileFree FileFree = FreeFile Open FileName For Binary Access Read As #FileFree Get #FileFree, , Header ' Check for "RIFF" tag and exit if not found. If Header.lRiff <> &H46464952 Then Exit Function End If ' Check for "WAVE" tag and exit if not found. If Header.lWave <> &H45564157 Then Exit Function End If ' Check format chunk length; if less than 16, ' it's not PCM data so we can't use it. If Header.lFormatLength < 16 Then Exit Function End If ' Retrieve format. Get #FileFree, , HdrFormat ' Seek next chunk by discarding any format bytes. For i = 1 To Header.lFormatLength - 16 Get #FileFree, , by Next ' Ignore chunks until we get to the "data" chunk. Get #FileFree, , chunk Do While chunk.lType <> &H61746164 For i = 1 To chunk.lLen Get #FileFree, , by Next Get #FileFree, , chunk Loop ' Retrieve the size of the data. lDataLength = chunk.lLen ' Fill the returned type with the format information. With FillFormat .lAvgBytesPerSec = HdrFormat.nAvgBytesPerSec .lExtra = 0 .lSamplesPerSec = HdrFormat.nSamplesPerSec .nBitsPerSample = HdrFormat.wBitsPerSample .nBlockAlign = HdrFormat.nBlockAlign .nChannels = HdrFormat.nChannels .nFormatTag = HdrFormat.wFormatTag End With End Function
The application can now begin reading data from the file and streaming that data into the secondary sound buffer. There is no way to read data directly from the file into a DirectSoundBuffer, so you must first read the data into a private buffer and then write it by using DirectSoundBuffer.WriteBuffer. For more information, see Using Streaming Buffers.
In order to use the wrapper functions in Wavread.cpp, you must declare the following four variables:
WAVEFORMATEX *pwfx; // Wave format info HMMIO hmmio; // File handle MMCKINFO mmckinfoData; // Chunk info MMCKINFO mmckinfoParent; // Parent chunk info
The first step in reading a wave file is to call the WaveOpenFile function. This gets a handle to the file, verifies that it is in RIFF format, and gets information about the wave format. The parameters are the filename and the addresses of three of the variables you have declared:
if (WaveOpenFile(lpzFileName, &hmmio, &pwfx, &mmckinfoParent) != 0) { // Failure }
Note that the wrapper functions all return zero if successful.
The next step is to call the WaveStartDataRead function, causing the file pointer to descend to the data chunk. This function also fills in the MMCKINFO structure for the data chunk, so that you know how much data is available:
if (WaveStartDataRead(&hmmio, &mmckinfoData, &mmckinfoParent) != 0) { // Failure }
The application can now begin copying data from the file to a secondary sound buffer. Normally you don't create the sound buffer until you have obtained the size of the data chunk and the format of the wave. The following code creates a static buffer just large enough to hold all the data in the file.
/* It is assumed that lpds in a valid pointer to the DirectSound object. */ LPDIRECTSOUNDBUFFER lpdsbStatic; DSBUFFERDESC dsbdesc; memset(&dsbdesc, 0, sizeof(DSBUFFERDESC)); dsbdesc.dwSize = sizeof(DSBUFFERDESC); dsbdesc.dwFlags = DSBCAPS_STATIC; dsbdesc.dwBufferBytes = mmckinfoData.cksize; dsbdesc.lpwfxFormat = pwfx; if FAILED(lpds->CreateSoundBuffer(&dsbdesc, &lpdsbStatic, NULL)) { WaveCloseReadFile(&hmmio, &pwfx); return FALSE; }
Because in this case the application is not streaming the data but simply filling a static buffer, the entire buffer is locked from the beginning. There is no wrap around, so only a single pointer and byte count are required.
LPVOID lpvAudio1; DWORD dwBytes1; if FAILED(lpdsbStatic->Lock( 0, // Offset of lock start 0, // Size of lock; ignored in this case &lpvAudio1, // Address of lock start &dwBytes1, // Number of bytes locked NULL, // Wrap around start; not used NULL, // Wrap around size; not used DSBLOCK_ENTIREBUFFER)) // Flag { // Error handling Close(); . . . }
The WaveReadFile function copies the data from the file to the buffer pointer and returns zero if successful.
UINT cbBytesRead; if (WaveReadFile( hmmio, // file handle dwBytes1, // no. of bytes to read (BYTE *) lpvAudio1, // destination &mmckinfoData, // file chunk info &cbBytesRead)) // actual no. of bytes read { // Handle failure on non-zero return WaveCloseReadFile(&hmmio, &pwfx); . . . }
Finally, the application unlocks the buffer and closes the wave file:
lpdsbStatic->Unlock(lpvAudio1, dwBytes1, NULL, 0); Close();
For a streaming buffer, you would typically call WaveReadFile at regular intervals determined by the current play position. (See Play Buffer Notification.) If the locked portion of the buffer wrapped around, of course, you would call WaveReadFile once for each segment of the lock.