Friday, November 9, 2012

Do the WAV, Part Deux

File formats are rather dry, so I'm going to try to wrap my overview of WAV up with this post. Fortunately, I'll be providing code snippets and sample WAV files, so the monotony might be somewhat alleviated. As an aside, this post will assume you have basic knowledge of C++ file I/O, as well as some understanding of bit manipulation. If not, just check out the docs online - it's pretty simple stuff.

Okay, so the code listed below is the method I wrote to handle writing the header to the WAV file.

1:  bool WavWriter::writeHeader( std::ofstream & fout, int length ) {  
2:       char * intBuf = new char[4];  
3:       char * shortBuf = new char[2];  
4:    
5:       ByteConverter::intToBytes( 36 + length, intBuf );  
6:         
7:       // write "RIFF"  
8:       fout.write( ckId, 4 );  
9:       // write total size of the chunks, less the 8 bytes for RIFF and WAVE  
10:       fout.write( intBuf, 4 );  
11:       // write "WAVE"  
12:       fout.write( format, 4 );  
13:       // write "fmt "  
14:       fout.write( fmt_, 4 );  
15:    
16:       ByteConverter::intToBytes( 16, intBuf );  
17:       // write chunk one size  
18:       fout.write( intBuf, 4 );  
19:    
20:       // write the compression level  
21:       if( m_format == PCM ) {  
22:            ByteConverter::shortToBytes( 1, shortBuf );  
23:            fout.write( shortBuf, 2 );  
24:       } else {  
25:            // support for other compression formats later  
26:            return false;  
27:       }  
28:    
29:       if( isStereo() ) {  
30:            ByteConverter::shortToBytes( 2, shortBuf );  
31:            fout.write( shortBuf, 2 );  
32:       } else {  
33:            // mono  
34:            ByteConverter::shortToBytes( 1, shortBuf );  
35:            fout.write( shortBuf, 2 );  
36:       }  
37:    
38:       ByteConverter::intToBytes( m_sampleRate, intBuf );  
39:       // write the sample rate  
40:       fout.write( intBuf, 4 );  
41:         
42:       int channels = 1;  
43:       if( isStereo() ) channels = 2;  
44:    
45:       // write byteRate  
46:       int byteRate = m_sampleRate * channels * ( m_bitsPerSample / 8 );  
47:       ByteConverter::intToBytes( byteRate, intBuf );  
48:       fout.write( intBuf, 4 );  
49:    
50:       // write block align  
51:       short blockAlign = channels * ( m_bitsPerSample / 8 );  
52:       ByteConverter::shortToBytes( blockAlign, shortBuf );  
53:       fout.write( shortBuf, 2 );  
54:    
55:       // write bits per sample  
56:       ByteConverter::shortToBytes( (short) m_bitsPerSample, shortBuf );  
57:       fout.write( shortBuf, 2 );  
58:    
59:       // write "data"  
60:       fout.write( data, 4 );  
61:    
62:       delete [] intBuf;  
63:       delete [] shortBuf;  
64:    
65:       return true;  
66:  }  

It's a bit verbose, but fairly straightforward. Each write to the file (fout) is either a 32-bit or 16-bit length byte array, depending upon which part of the chunk is being written. As you can see, the chunk IDs and chunk sizes are 4 bytes in length, as well as the sample and byte rates. The rest of the fields need only be 2 bytes in length. Please refer to the chart I placed in my previous post it get a more visual overview of the tag ordering in the header.

The ByteConverter methods seen in the code above simply convert the 32-bit and 16-bit datatypes to byte arrays, with the MSB (most significant byte, in this case) being last in the array. The code for such a method would look as such:

1:  void ByteConverter::shortToBytes( short value, char * buffer ) {  
2:  /* Writes a short in byte array format, big endian */  
3:       buffer[0] = value & 0xFF;  
4:       buffer[1] = ( value >> 8 ) & 0xFF;  
5:  }  

Now that the header-writing code is taken care of, we can worry about the data being written. This is actually quite simple, and is handled in the method shown below:

1:  bool WavWriter::writeWav( char * wave, int length ) {  
2:       if( isStereo() ) {  
3:            return writeWav( wave, wave, length );  
4:       }  
5:    
6:       std::ofstream fout( m_filename.c_str(), std::ios::out | std::ios::binary );  
7:       if( !fout.is_open() ) {  
8:            return false;  
9:       }  
10:    
11:       if( !writeHeader( fout, length ) ) {  
12:            return false;  
13:       }  
14:    
15:       char * intBuf = new char[4];  
16:       // write chunk two size  
17:       ByteConverter::intToBytes( length, intBuf );  
18:       fout.write( intBuf, 4 );  
19:    
20:       // write the data  
21:       fout.write( wave, length );  
22:         
23:       fout.flush();  
24:       fout.close();  
25:    
26:       delete [] intBuf;  
27:    
28:       return true;  
29:  }  

The check for isStereo() at line 2 simply checks if the user wishes to write the data into two channels. I won't post the code for that here, since it's quite similar to this code, but one must simply interleave the two sets of data for left and right channels, alternating samples in the file. The rest of this code writes the size of the data chunk to the file, followed by the data. The file is then flushed and closed, as is good practice, and voila! a WAV file, hot from the oven.

My main function looks as such below. Don't mind the oscillator objects I have used in there; they simply encapsulate waveform creation. They are basically giving me one sample of the waveform every time I loop, so that I may fill up my data buffer. The WAV is then written, and the program exits.

1:  int main( int argc, char ** argv ) {  
2:       if( argc <= 2 ) {  
3:            std::cout << "please provide valid command line arguments. Syntax is '/wav_writer <filename.wav> <oscillatortype>'" << std::endl;  
4:            return -1;  
5:       }  
6:    
7:       std::string filename;  
8:       filename = "res/";  
9:       filename += argv[1];  
10:    
11:       Oscillator * oscillator;  
12:       Oscillator * oscillatorTwo = 0;  
13:    
14:       if( strcmp( argv[2], "triangle" ) == 0 ) {  
15:            oscillator = new TriangleOscillator();  
16:       } else if( strcmp( argv[2], "rsaw" ) == 0 ) {  
17:            oscillator = new RisingSawtoothOscillator();  
18:       } else if( strcmp( argv[2], "additive" ) == 0 ) {  
19:            oscillator = new SineOscillator();  
20:            oscillatorTwo = new SineOscillator( 523.0f );  
21:       } else {  
22:            oscillator = new SineOscillator();  
23:       }  
24:    
25:       WavWriter wavWriter( filename );  
26:    
27:       wavWriter.setBitsPerSample( 16 );  
28:       wavWriter.setStereo( false );  
29:         
30:       int dataSize = 5 * oscillator->getSampleRate() * 2; // duration in seconds * sample rate * bytes per sample  
31:       char * data = new char[dataSize];  
32:         
33:       if( oscillatorTwo != 0 ) {  
34:            for( int i = 0; i < dataSize - 1; i+=2 ) {  
35:                 ByteConverter::shortToBytes( oscillator->nextSample() / 2 + oscillatorTwo->nextSample() / 2, data, i );  
36:            }  
37:       } else {  
38:            for( int i = 0; i < dataSize - 1; i+=2 ) {  
39:                 ByteConverter::shortToBytes( oscillator->nextSample(), data, i );  
40:            }  
41:       }  
42:    
43:       if( wavWriter.writeWav( data, dataSize ) ) {  
44:            std::cout << "hooray! it worked!" << std::endl;  
45:       } else {  
46:            std::cout << "aww, no worky." << std::endl;  
47:       }  
48:    
49:       delete oscillator;  
50:       if( oscillatorTwo != 0 ) {  
51:            delete oscillatorTwo;  
52:       }  
53:    
54:       return 0;  
55:  }  

And that's it! Fairly simple, no? Below, I've posted links to WAV files I've produced using the oscillators listed in the code above. If you want to see the shapes of the waveforms produced below, just open them up in your favorite waveform editor. I would suggest Audacity for a lightweight, yet powerful editor.

Sine
Triangle
Rising Sawtooth
Additive

Looking for the complete source code? Just hit me up in a comment! Cheers!

- End Transmission -

No comments:

Post a Comment