Rumble, eject.
Credit to Dr. Fred DePiero who wrote the WAV_IN/WAV_OUT classes, Dr. Chris Taylor whose webpage I got them from, and Mr. Daniel Zingaro who suggested DSP as a nifty programming assignment.
Do you know how a speaker works? All it does is push and pull air. This creates waves of pressure in the air that our ears and brains interpret as sound. Think about that. All your speaker can do is move in and out different amounts at different times. That means that sound can be stored as a sequence of numbers indicating how far it should push out (the samples), combined with a speed indicating how quickly to move from one number to the next (the sampling rate). Here's a sequence of numbers:
100 90 120 50 40 40 45 100 101 104 ...
Now let's say that the speaker should move to each of those positions 44,100 Hertz (times per second). That's the whole deal. Beethoven's greatest symphony, recorded on a CD, is just one long sequence of numbers. Believe it? Check out the starter code and we'll try it out.
https://cssvn.utrgv.edu/svn_etomai/201720_2380/lab07_soundwave/<your username>
(You will get warning about the unsafeness of sprintf and fopen when you first compile. That's because this is using some older code I haven't updated. You can ignore those, just make sure you're not ignoring other warnings or errors about your actual code. This is a good example of why leaving warnings in is a bad pactice.)
WAVE is a common, uncompressed audio file format. Beyond some fancy organization, it's just an array of samples combined with a sampling rate. The starter project contains code to read, write and play WAVE files, and a class that abstracts that level of functionality. Of all the files in the project, you only need to worry about:
What does this all have to do with pointers? At 44,100 samples per second, that sequence of numbers is quite large. An application that manipulates audio files can't go allocating oversized arrays to hold the sound data. It needs to use dynamic memory allocation with pointers and the new operator.
Take a look at the Wav class definition that I wrote in wav.h. This class reads in a WAVE file, stores the samples in an array of doubles, and can also write it to disk and play it for you.
The sound samples are kept in a dynamic array pointed at by the private data member samples. As usual with an array, you also need a counter to keep track of how many samples are being stored in it. This is called sampleCount.
Next, take a look at the constructors. There's the default one, and one that takes a filename (the WAVE file to open). Switch over to wav.cpp to see how those constructors work.
The default constructor sets all the data members to 0, and the data pointer to NULL. Straightforward, I hope.
The constructor that takes a string is more interesting. It uses the WAV_IN class to read in the specified WAVE file. The resulting object (infile) is then used to get important information about the WAVE data. It then iterates from 0 to sample count and reads each sample into the samples array.
There might be a problem there, but we'll get back to that.
Go over to main, and try this class out.
Run the program with debugging and you should get a hard crash with one of those "access violation" errors. That means you tried to use a pointer that was not pointing at allocated memory. Go into the debugger and notice where the problem happened. What's the fix?
Oops! My samples pointer isn't pointing at anything. Please fix it by dynamically allocating an array of doubles to hold the samples. Where do you want to put the line that does that? How big should the array be?
All fixed up? Try run again and see if you can hear those samples. (Don't forget to turn on your sound.)
Now remember, whenever you allocate memory, you are responsible to deallocate it as well. Add a destructor that deletes the array, but only if it's not NULL.
Now that we have our samples in a nice, convenient array, let's mess with it. Check out the Wav::Reverse method and see how it alters the data array. Add calls in main after you play the sound to reverse the door sound and then play it again. You should hear it play forward, then backward.
In the previous section, you reversed your sound by altering the order of the samples. Unfortunately, the only way to get the original sound back is to re-reverse it. Instead of doing that, let's make a copy of the Wav object, and reverse that, leaving the original sound intact.
In main, after you declare the door variable, declare another variable called backdoor, but have it copy from door. In C++, we do this using the copy constructor, which is just a constructor that takes another object of the same class to copy.
Wav door( "sounds\\door.wav" ); Wav backdoor( door );
As we discussed in class, the compiler will automatically create a copy constructor for you that copies the data members from the passed in object (door) to the object being created (backdoor). And this will be a problem, because it's a shallow copy. Test it out (you'll get an error at the end):
Oops! We changed the original sound! The shallow copy only copied the address of the samples, leaving both objects pointing at the same array. And we altered it by calling Wav::Reverse on backdoor. Then at the end of main, it deleted both door and backdoor and...Why did that cause an error to come up?
Alright, fix it! You need to overload the copy constructor. To overload, you just add a method to your class with the exact same method signature (same name, parameters, return type). The copy constructor signature is just a constructor that takes an object (by reference) of the same class with the const keyword to prevent the method from making any changes to it:
Wav::Wav( const Wav& other );
Inside that constructor you need to set the variables for this object (the one being constructed) with a deep copy:
Once it's working, backdoor should have it's own copy of the samples to reverse, and door should still play the original sound.
The assignment operator is just another way to copy an object, and it also needs to be overloaded for deep copy whenever you have a class that allocates dynamic memory. Change you door/backdoor declarations to look like this:
Wav door( "sounds\\door.wav" ); Wav backdoor; backdoor = door;
This is doing the same thing, only now we're constructing backdoor with the default constructor (which does very little), then using an assignment statement to copy door into it. Run the code and you'll see that the same-array shallow copy problem is back. Now you need to overload the assignment operator that gets called whenever you assign an object of this class to another object of this class. The signature looks like:
Wav& Wav::operator=( const Wav& other )
The code for this operator is about identical to the copy constructor code, but there are two extra things you need to do:
Get the assignment operator right, and you should be back to having two separate sounds.
As we discussed in class, whenever you create a class that does dynamic
memory allocation, you have to do some extra work to make sure that it works
correctly. Namely, you need to define a destructor to clean up, and a copy
constructor and an assignment operator that do deep copy. You'll be using this
class again in your homework, so make sure it works correctly!
The fancy term for what you just did is Digital Signal Processing (DSP). There's a ton more, involving real complex math, but the foundation of samples and sampling rate is just the same.