soundwave.py: 22 points
Our main goal for this lab is to write a sound generator capable of playing any combination of notes, or tones, we desire. We can think of these tones as objects with their own attributes and methods. For example, a tone can have a duration attribute and a method for combining itself with other tones to create a song!
In this part of the lab, we’ll get started by creating a SoundWave
class that will represent a sampled sound wave. Your SoundWave
class should be written in a separate soundwave.py
file, and it should follow the specifications below. Remember to refer back to the Warmup for details on how to generate and sample the sine waves needed to create your tones.
The constructor for your SoundWave
class will begin with an __init__()
function for instantiating new SoundWave
objects. We’ll start by designing our constructor to accept four parameters specifying a single note:
halftones
- the number of halftones above or below middle C of the noteduration
- the length of the note in secondsamp
- the amplitude of the note (i.e., it’s volume or loudness)sample_rate
- the interval at which the sine wave is sampledThese four parameters define everything we need to generate the sine wave for a given half tone. Recall, however, that the sine wave equation requires a frequency. We can compute the frequency as outlined in the Warmup using:
freq = 220*(2**((halftones+3)/12))
Now that the sine wave parameters have been determined, we need to sample the waveform and store those sampled values. Your constructor should create an instance variable called self.samples
, and define self.samples
to be the list of sampled values. You should populate this list such that it has a length of duration * sample_rate
(truncated to an integer), where entry i
in this list is created as follows:
s = amp * math.sin(2 * math.pi * freq * i/sample_rate)
Say you’ve computed a new sample s
that you plan on appending to your list samples
. A natural way to do this is with the instruction:
self.samples.append(s)
For the purposes of this lab, we’ll always be using a sample rate of 44100
, or 44.1 kHz (this value is dictated by the WAV audio format we’ll be using to save our music). To make our programs cleaner, we’ll want our SoundWave
constructor to assign default values when parameters are left unspecified. The default values for halftones
, duration
, amp
, and sample_rate
should be 0
(middle C), 0.0
(zero seconds), 1.0
(max volume), and 44100
, respectively.
Setting Default Values
We can set the default value of a function parameter by doing an assignment within the function definition. For example,
def my_function(param1, param2=10):
# do something
sets a default value of 10
to param2
and no default value for param1
. By setting the default value of param2
, we can call the function with
my_function(param1)
and Python will automatically set param2
to 10
. Note that default values work the same for methods.
By having these default values (and the parameters in this order), we allow users of this object to invoke the constructor as:
note = soundwave.SoundWave(6,3,.5)
to get an F-sharp with length 3 seconds at volume 0.5,
note = soundwave.SoundWave(2,1)
to generate a D with length 1 second at volume 1.0, or
note = soundwave.SoundWave()
to generate an empty SoundWave
object.
Before moving on, you should test out your SoundWave
class, even though you can’t play the full tone just yet!
Below your class definition, write a function called test_samples()
which, when called, creates a SoundWave
instance s
and prints out the first 10 samples of that instance (e.g., s.samples[0:10]
). You should look at the output and check that the values seem to be changing in a sensical way as you adjust the parameters passed to the constructor (i.e., getting bigger but less than amp
). Your function should also print out the total number of samples in s
to confirm you are generating what you intended (i.e., duration * sample_rate
, truncated to an int).