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