WAVE Files

Although a live performance synthesizer will generate samples every time we want to hear sound, a typical software synthesizer will store the samples in a file for later playback. Several standard sound file formats are widely used. The MP3 format is often used for sound files on the Internet as well as video and audio players. However, MP3 is a compressed format that requires additional processing, cannot be sent directly to many sound devices, and also loses some fidelity due to the compression. The WMF format is similar to MP3. A better choice is the WAVE file format. This format can be used for direct playback, production of CDs, and can also be converted to MP3, WMF and other formats using a stand-alone utility program. Any off-the-shelf software should be able to read WAVE files as well. Most importantly, the samples are in raw form without any compression or equalization required.

A WAVE file is a type of RIFF file with the structure shown in the following diagram. Each gray box is a chunk header and contains the chunk ID along with the size of the chunk. The white boxes represent the chunk data, which includes any nested chunks.

WAVE File Structure

A WAVE file can contain other chunk types in addition to the two shown here. A WAVE file could contain multiple sets of samples, potentially with different sample rates and sample sizes, along with cue information, description, etc. In addition, the chunks are not required to be in the order shown. However, most WAVE files contain only two sub-chunks, one containing format information and the second containing sample data, and those chunks are located first in the file. This allows us to create a simplified representation of a WAVE file and skip the complexities of searching through chunks. Doing so will make the code specific to reading one kind of WAVE file, but is sufficient for the synthesizer we will develop, is very easy to program, and very fast to save or load. The output is readable by other sound processing systems without any problem, and our basic synthesizer code can read most WAVE files produced by other programs as well.

Taking this approach, we can consider the WAVE file as a file with a fixed length header and variable length block of samples. To create a WAVE file, we fill in a header, write it to the file, then write the sample data, converting to the file sample representation if needed. Each sample is one sixteen bit word per channel. For stereo, the sample for the left channel is written followed immediately by the sample for the right channel. In other words, the two channels are interlaced in the file. The following code shows a general purpose WAVE file write function in C++. 

typedef uint8 unsigned char;
typedef uint16 unsigned short;
typedef uint32 unsigned long;
 
struct waveHDR
{
   uint8  riffId[4];   // 'RIFF' chunk
   uint32 riffSize;    // filesize - 8
   uint8  waveType[4]; // 'WAVE' type of file
   uint8  fmtId[4];    // 'fmt ' format chunk
   uint32 fmtSize;     // format chunk size (16)
   uint16 fmtCode;     // 1 = PCM
   uint16 channels;    // 1 = mono, 2 = stereo
   uint32 sampleRate;  // 44100
   uint32 avgbps;      // samplerate * align
   uint16 align;       // (channels*bits)/8;
   uint16 bits;        // bits per sample (16)
   uint8  waveId[4];   // ‘data’ chunk
   uint32 waveSize;    // size of sample data
};
 
 
void writeWaveFile(int16 *sampleBuffer, 
   uint32 sampleTotal, 
   int channels)
{
// two bytes per value and one value per channel
   uint32 byteTotal = sampleTotal * 2 * channels;
 
   waveHDR wh;
   wh.riffId[0] = 'R';
   wh.riffId[1] = 'I';
   wh.riffId[2] = 'F';
   wh.riffId[3] = 'F';
   wh.riffSize = byteTotal + sizeof(wh) – 8;
   wh.waveType[0] = 'W';
   wh.waveType[1] = 'A';
   wh.waveType[2] = 'V';
   wh.waveType[3] = 'E';
   wh.fmtId[0] = 'f';
   wh.fmtId[1] = 'm';
   wh.fmtId[2] = 't';
   wh.fmtId[3] = ' ';
   wh.fmtSize = 16;
   wh.fmtCode = 1;
   wh.channels = (uint16) channels;
   wh.sampleRate = 44100;
   wh.bits = 16;
   wh.align = (wh.channels * wh.bits) / 8;
   wh.avgbps = (wh.sampleRate * wh.align);
   wh.waveId[0] = 'd';
   wh.waveId[1] = 'a';
   wh.waveId[2] = 't';
   wh.waveId[3] = 'a';
   wh.waveSize = byteTotal;
 
   FILE *f = fopen(“out.wav”, “wb”);
   fwrite(&wh, 1, sizeof(wh), f);
   fwrite(sampleBuffer,2*channels,sampleTotal,f);
   fclose(f);
}

Links:

Dan Mitchell's Personal Website