Catching a WAVE
Volume Number: 13
Issue Number: 11
Column Tag: develop
by Tim Monroe, Apple Computer, Inc.
Playing WAVE Files on the MacOS
By far the most common type of sound file on Windows computers (and hence on
personal computers in general) is the .WAV format file, also called a WAVE file or a
waveform file. Collections of WAVE files are easily accessible on the Internet and
elsewhere, and the number of WAVE files available for downloading far outstrips the
number of Macintosh sound files. So what's a loyal Macintosh programmer to do when
confronted with audio content in WAVE format? The Sound Manager's SndStartFilePlay
function won't play WAVE files (not yet, at least), but it's easy to use other Sound
Manager capabilities to play the data contained in a WAVE file.
In this article, I'll show how to open, parse, and play a WAVE file. In fact, this is a
surprisingly simple thing to do, largely because the digitized sound data in a WAVE file
is stored in pretty much the same format as digitized sound data on the Mac. So, all we
need to do is extract the sound data from the WAVE file and pass it to standard Sound
Manager routines. If you've never used the low-level Sound Manager interfaces, this
will be a good introduction. Along the way, you'll also learn how to deal with the WAVE
file's chunk architecture and the little-endian byte ordering used on Windows.
If you're not inclined to work with sound files at this low level, don't despair. At the
end of this article, I'll show an alternate strategy for playing WAVE files that uses the
QuickTime APIs instead of the Sound Manager. Indeed, if you're really averse to
low-level coding, you should skip straight to the section "Surfing with QuickTime" and
read about the high-level method. Otherwise, strap on your wet suit, and let's go!
The Chunk Architecture
A WAVE file contains digitized (that is, sampled) sound data, just like most sound files
and resources on the Macintosh. A WAVE file also contains information about the
format of the sound data, such as the number of bits per sample and the number of
channels of audio data (mono vs. stereo). The various kinds of data in a WAVE file are
isolated from one another using an architecture based on chunks. A chunk is simply a
block of data together with a chunk header, which specifies both the type of the chunk
and the size of the chunk's data. Figure 1 illustrates the basic structure of a chunk.
Figure 1. The basic structure of a chunk.
A WAVE file always contains at least two chunks, a data chunk that contains the sampled
sound data, and a format chunk that contains information about the format of the sound
data. These two chunks (and any others that might occur in the file) are packaged
together inside of another chunk, called a container chunk or a RIFF chunk. Like any
chunk, the RIFF chunk has a header (whose chunk type is 'RIFF') and some chunk data.
For RIFF chunks, the chunk data begins with a data format specifier followed by all the
other chunks in the file. The format specifier for a WAVE file is 'WAVE'. Figure 2
shows the general structure of any WAVE file.
Figure 2. The basic structure of a WAVE file.
The first thing we'll do is define some data structures that will help us extract the
information from a chunk header or from the chunk data. In C, we can represent a
chunk header like this:
typedef struct ChunkHeader {
FOURCC fChunkType; // the type of this chunk
DWORD fChunkSize; // the size (in bytes) of the chunk data
} ChunkHeader, *ChunkHeaderPtr;
Here, we're using standard Windows data types; on the Macintosh, these types are
#define'd to more familiar types:
#define WORD UInt16
#define DWORD UInt32
#define FOURCC OSType
We can represent a format chunk like this:
typedef struct FormatChunk {
ChunkHeader fChunkHeader; // the chunk header; fChunkType ==
'fmt '
WORD fFormatType; // format type
WORD fNumChannels; // number of channels
DWORD fSamplesPerSec; // sample rate
DWORD fAvgBytesPerSec; // byte rate (for buffer estimation)
WORD fBlockAlign; // data block size
WORD fAdditionalData; // additional data
} FormatChunk, *FormatChunkPtr;
And, finally, we can represent the relevant parts of a RIFF chunk like this:
typedef struct RIFFChunk {
ChunkHeader fChunkHeader; // the chunk header; fChunkType ==
'RIFF'
FOURCC fFormType; // the data format; 'WAVE' for waveform
files
// additional chunk data follows here
} RIFFChunk, *RIFFChunkPtr;
It's very easy to parse a file that is structured into chunks. You simply begin at the
start of the file data, which is guaranteed to be the beginning of the container chunk.
You can find the first subchunk by skipping over the container chunk header and any
additional container chunk data. And you can find any succeeding subchunks by skipping
over the subchunk header and the number of bytes occupied by the subchunk data.
Listing 1 shows how to find the beginning of a chunk of a specific type.
Listing 1: Finding a chunk of a specific type
OSErr GetChunkType (short theFile, OSType theType,
long theStartPos, long *theChunkPos)
OSErr myErr = noErr;
long myLength = sizeof(ChunkHeader);
UInt32 myOffset;
ChunkHeader myHeader;
Boolean isFound = false;
// set file mark relative to start of file
myErr = SetFPos(theFile, fsFromStart, theStartPos);
if (myErr != noErr)
return(myErr);
// search the file for the specified chunk type
while (!isFound && (myErr == noErr)) {
// load the chunk header
myErr = FSRead(theFile, &myLength, &myHeader);
if (myErr == noErr) {
if (myHeader.fChunkType == theType) {
isFound = true; // we found the desired chunk type
myErr = GetFPos(theFile, theChunkPos);
*theChunkPos -= myLength;
if (myHeader.fChunkType == kChunkType_RIFF)
myOffset = sizeof(FOURCC);
else
myOffset = Swap_32(myHeader.fChunkSize);
if (myOffset % 2 == 1) // make sure chunk size is even
myOffset++;
myErr = SetFPos(theFile, fsFromMark, myOffset);
}
}
}
return(myErr);
}
The function GetChunkType starts searching the file data at a location (theStartPos)
passed to it, which is assumed to be the start of a chunk. GetChunkType reads in the
chunk header and looks to see if it has found the chunk of the desired type. If so, it
returns the file position of the first byte of the chunk header. Otherwise,
GetChunkType figures out where in the file the next chunk begins. If the current chunk
is a container chunk, then the next chunk begins after the data format specifier;
otherwise, the next chunk is to be found after the current chunk's data, whose size is
specified in the chunk header. (Notice that in determining the size of the current
chunk, we need to use the macro Swap_32; see "Which End Is Which?" for an
explanation of why this is necessary.)
Once we know how to find the beginning of a particular chunk, it's easy to get the
chunk's data. Listing 2 defines a function GetChunkData that returns a pointer to a
buffer of memory containing both the chunk header and the data in the chunk.
Listing 2: Getting a chunk's data
ChunkHeaderPtr GetChunkData
(short theFile, OSType theType, long theStartPos)
long myFPos;
ChunkHeader myHeader;
Ptr myDataPtr = NULL;
OSErr myErr = noErr;
long myLength;
// get position of desired chunk type in file
myErr = GetChunkType(theFile, theType,
theStartPos, &myFPos);
// set file mark at the start of the chunk
if (myErr == noErr)
myErr = SetFPos(theFile, fsFromStart, myFPos);
myLength = sizeof(myHeader);
// load the chunk header
myErr = FSRead(theFile, &myLength, &myHeader);
if (myErr != noErr)
return(NULL);
// set file mark at the start of the chunk header
myLength += Swap_32(myHeader.fChunkSize);
myErr = SetFPos(theFile, fsFromStart, myFPos);
}
myDataPtr = NewPtrClear(myLen);
myErr = MemError();
if (myDataPtr != NULL)
myErr = FSRead(theFile, &myLength, myDataPtr);
}
DisposePtr(myDataPtr);
myDataPtr = NULL;
}
return((ChunkHeaderPtr)myDataPtr);
}
GetChunkData calls GetChunkType to find the beginning of the chunk of the desired
kind; then it reads the chunk header to find the size of the chunk data. (Once again,
we've used the macro Swap_32 to massage the chunk data length as it's stored in the
file.) Finally, GetChunkData allocates a buffer large enough to hold the entire chunk
(header and data) and returns the pointer to the caller.
Here's a pleasant surprise: the functions GetChunkType and GetChunkData are simply
slightly modified C language translations of the functions MyFindChunk and
MyGetChunkData found in Inside Macintosh: Sound (pages 2-63 and 2-65,
respectively). We could use those functions, suitably modified, because the AIFF
format (defined by Apple and described in Inside Macintosh: Sound) and the RIFF
format (defined by Microsoft and used for WAVE files) are both chunk-based formats,
which descend from a common parent. See the sidebar "A Brief History of IFF" for
details.
Which End Is Which?
Now we know how to find a chunk in a RIFF file and read the data in that chunk into
memory. We've already seen, however, that we sometimes need to play with that data
before we can use it. That's because of a difference in the way multi-byte data is
accessed on Motorola and Intel processors. Motorola processors in the 680x0 family
expect multi-byte data to be stored with the most significant byte lowest in memory.
This is known as "big-endian" byte ordering (because the "big" end of the data value
comes first in memory). Intel processors used for Windows machines expect
multi-byte data to be stored with the least significant byte lowest in memory; this is
known as "little-endian" byte ordering. (See Figure 3, which shows the value
0x12345678 stored in both Motorola 680x0 and Intel formats.) The PowerPC family
of processors supports both big- and little-endian byte orderings, but uses big-endian
when running the MacOS.
Figure 3. Big- and little-endian byte ordering.
The data stored in a WAVE file is little-endian. As a result, to use that data in a
Macintosh application, we need to convert the little-endian data to big-endian data --
but only for data that is larger than 8 bits. For instance, the chunk data size field in a
chunk header is 4 bytes long, so we need to swap the bytes using our macro Swap_32.
Later, we'll also need to swap the two bytes in a 16-bit field, so we can define these
macros:
#define Swap_32(value)
(((((UInt32)value)<<24) & 0xFF000000) |
((((UInt32)value)<< 8) & 0x00FF0000) |
((((UInt32)value)>> 8) & 0x0000FF00) |
((((UInt32)value)>>24) & 0x000000FF))
#define Swap_16(value)
(((((UInt16)value)>> 8) & 0x000000FF) |
((((UInt16)value)<< 8) & 0x0000FF00))
You might be wondering why we didn't need to swap bytes when reading the chunk type
from a file. That's because a chunk type is a sequence of four (8-bit) characters.
When reading individual characters, no byte swapping is necessary. A byte in
little-endian byte ordering is the same as a byte in big-endian byte ordering. For this
same reason, we don't need to do any work when reading 8-bit audio samples from the
WAVE file and (eventually) passing them to the Sound Manager.
For 16-bit audio samples, however, the bytes do need to be swapped before they can be
passed to the Sound Manager. You could do this yourself, by loading all the data into a
buffer and then running through the buffer swapping every pair of bytes. Better yet,
if you're using Sound Manager version 3.1 or later, you can have the Sound Manager do
the byte swapping for you. You do this by invoking the 'sowt' data decompressor on the
(uncompressed) 16-bit audio data. (Notice that 'sowt' is 'twos' with the bytes
swapped; 16-bit data is stored in a two's-complement format.) See Listing 6 for the
exact details of invoking this codec on 16-bit audio data.
It's worth mentioning that RIFF has a counterpart, RIFX, that uses Motorola byte
ordering. A RIFX file is exactly like a RIFF file except that the container chunk has the