WAV file code completed!

I know it has been a few days since I wrote about designing C++ classes to read and write .WAV audio files, but stuff came up and I didn’t have time to finish it until today. But I finished it and it does work!

The approach I finally used was to break reading and writing into separate classes.

I defined structures for all the RIFF chunks that I know about.

struct fmtChunk // ID = "fmt "
{
 u16 AudioFormat; //PCM=1
 u16 Channels; //1=Mono, 2=Stereo
 u32 SampleRate; //Sample Rate in Hz
 u32 ByteRate; //SampleRate * Channels *
               //BitsPerSample/8
 u16 BlockAlign; //NumChannels * BitsPerSample/8 
 u16 BitsPerSample; //8,16, etc.
};

Originally I stored the headers as string macros, but since the Chunk IDs are 4 characters with no 0 for termination, it is hard to work with them using many of the normal C functions, so I made hex versions of them also.

#define fmtChunkID "fmt "
#define fmtChunkIDInt (0x20746d66)

For reading, I read the whole file into memory and keep it.

 void * AllData;

I parse every type of header that I know about, and keep pointers to anything that I think I will use.

 fmtChunk * fmt;
 factChunk * fact;
 samplerChunk * smpl;
 sampleLoop * loops;
 instrumentChunk * inst;
 cueChunk * cue;
 cuePoint * cuepoints; 
 void * data;//sample data

Some data I copy out of the header and keep as class members, others I just reference in their structures using the pointers I saved.

size_t size; //whole file size, determine at read time
u32 sampleDataStart; //first byte of sample data
u32 sampleDataLength; //length of sample data in bytes
u32 sampleCount; //sample count in samples
u16 loopCount; //number of loops
u16 cuePointCount; // number of cue points

Then I set up a bunch of methods to access all of the useful data.

void * AllSamples();
void * Sample(u32 SampleNum, u16 Channel=0);
u16 AudioFormat();
u16 Channels(); //1=Mono, 2=Stereo
u32 SampleRate(); //Sample Rate in Hz... 44100 for CD
...

The methods sometimes have to do a little math to find the data, and I might need to clean it up a little someday, but I think it works ok.

//length of loop in samples
u32 WAVfileIn::LoopLengthSamples(u16 index)
{
 if ( AllData && data && loops && loopCount && index < loopCount )
 {
 return( (loops[index].End - loops[index].Start) / (fmt->Channels * fmt->BitsPerSample/8) );
 }
 return( 0 );
}

The “Read” function is pretty large, since it parses every type of header.  Since I am using 32 bit integers instead of strings, I can use a big “switch/case” statement to check for all the types.

switch ( head->ID32 )
{
 case fmtChunkIDInt:
  ...
  fmt = (fmtChunk*)at;
  ...
  break;
 case dataChunkIDInt:
 ...

I even made an “anonymous union” to allow me to reference the data as integers or as characters.  Accessing as characters is more useful when I want to print them for debugging and such.

union ID4
{
 char ID[4]; 
 u32 ID32;
};

I read the header in first, check the type, save pointers or data or whatever I need to do, and then increment the pointer to the end of the data using the size embedded in the pointer.

at+=head->Size;

If I ever need to add the ability to process additional chunks later, they are easy to add to the structures  and “case” statements.

For the .WAV output class, it takes in enough data to build the header in the “Open” function, with the intent of fixing it up later when the “Close” function is called and we know how big the data is.

bool WAVfileOut::Open( _TCHAR * filename, 
 u32 SampleRate, //Sample Rate in Hz... 44100 for CD
 u16 BitsPerSample, //8,16, etc.
 u16 Channels, //1=Mono, 2=Stereo
 u16 AudioFormat) //PCM=1 currently we don't do any compression, so leave this alone

After setting up all of the headers for the basic stuff  (RIFF, fmt, fact, data…) it is ready for “Write” calls to add samples to the file.

bool WAVfileOut::Write(void * SampleData, 
                       u32 SampleCount)

Along the way, it uses “ftell” to store the location of various “size” fields that will need to change when the final data size is known.

 factAt = ftell(f);

Then when “Close” is executed, I can easily find my way back to write the correct value in it.

fseek(f,factAt,SEEK_SET)

So writing a file looks like this:

WAVfileOut wavout;
wavout.Open( _TEXT("test1.wav"),
             wavin.SampleRate(),
             wavin.BitsPerSample(),
             wavin.Channels(),
             wavin.AudioFormat()
);
wavout.Write( wavin.AllSamples(), 
              wavin.SampleCount());
wavout.Close();

Structuring it like this allows me to output as I go.

Yeah, I suppose I could have used a stream class, but not every platform has those. But there is almost always something that resembles fread and fwrite…

Now, you’ll notice there is no way to write loops and such yet, but since there is space between Open and Write, and between Write and Close, adding extra functions to write that data would be very simple.

Now that I have classes to work with .WAV files, I can start working on the audio engine…

 

 

One thought on “WAV file code completed!”

  1. I got my information on the structures from several websites, but mostly from “The Sonic Spot”: http://www.sonicspot.com/guide/wavefiles.html .

    Note that the hex values they give for the Chunk IDs are in the wrong endian-ness for Windows PCs, and are frequently wrong, like they were block copied and never updated… Everything else is spot on, tho.

    I could not find any reference to the “CDif” Chunk type…

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>