MP1: Phasors

In this lab, you'll measure the amplitudes and phases of the harmonics of a musical instrument. Then you'll use those harmonic amplitudes and phases to create phasors, add them together, and synthesize a musical tune.

In order to make sure everything works, you might want to go to the command line, and run

pip install -r requirements.txt

This will install the modules that are used on the autograder, including numpy, h5py, and the gradescope utilities.


Part 1: Measuring harmonic spectral levels (in dB)

First, let's load a violin note. This sample is a violin, playing a middle-C note (C5), extracted from the file Violin.arco.mf.sulG.C5G5.aiff in the University of Iowa musical instrument database (http://theremin.music.uiowa.edu/MISviolin.html).

Let's zoom in on part of it.

So, obviously, it's periodic. That means that it is made up of the sum of many harmonics, each with a different amplitude and phase. But how do you find its amplitudes and phases?

At this point, you should switch to Praat (https://www.fon.hum.uva.nl/praat/).

  1. In the "Praat Objects" window, click the "Open" button, and choose "Read from file." In the file browser that pops up, navigate to the directory containing the file "violin.wav," and choose it. The words "Sound violin" should show up in your "Praat Objects" window.
  2. Click on "Sound violin," then click "View & Edit".
  3. Click your mouse near the peak of the waveform, then click on the "Spectrum" button, and choose "View Spectral Slice". You should see something that looks like the following.

spectralslice

Notice that, in the above spectrum, I've selected the fourth harmonic, so you can see that it is at a frequency of 1025Hz, and has a level of 30dB. Do the same for all of the first eight harmonics, and change the following line so it correctly lists the levels of the first eight harmonics.


Part 2: Converting levels to amplitudes

At this point, we'll load the file submitted.py.

The file submitted.py is the only part of your work that the autograder will see. The only purpose of this notebook is to help you debug submitted.py. Once you have revised submitted.py enough to make this notebook work, then you should go to the command line, and type python run_tests.py. Once that command returns without errors, then you can go ahead and submit your file submitted.py to the autograder. You can submit to the autograder as often as you want, but it will save you trouble if you debug as much as you can on your local machine, before you submit to the autograder.

We will use importlib in order to reload your submitted.py over and over again. That way, every time you make a modification in submitted.py, you can just re-run the corresponding block of this notebook, and it will reload submitted.py with your modified code.

Since the file is called submitted.py, python considers it to contain a module called submitted. As shown, you can read the module's docstring by printing submitted.__doc__. You can also type help(submitted) to get a lot of information about the module, including its docstring, a list of all the functions it defines, and all of their docstrings. For more about docstrings, see, for example, https://www.python.org/dev/peps/pep-0257/.

Now it's time for you to open submitted.py, and start editing it. You can open it in another Jupyter window by choosing "Open from Path" from the "File" menu, and then typing submitted.py.

Once you have it open, try editing the function level_to_amplitude so that its functionality matches its docstring. Here is what it's docstring says:

Remember that the relationship between levels and amplitudes is given by

$$\mbox{level} = 20 \log_{10}(\mbox{amplitudes})$$

Your goal is to invert that equation, so that, given levels, you return amplitudes.

Once you have performed that task correctly, the following code block should print the values

[0.001 0.1 1 10 100 1000]

(it might be printed in scientific notation).


Part 3: Creating phasors

A phasor is just a complex number, $z=ae^{j\theta}$, where $a$ is some amplitude, and $\theta$ is some phase. Open submitted.py, and modify the function create_phasors so that it does what its docstring says it should do. Here's what its docstring says it should do:

If you get it working, the following block should produce the result

[ 1  1+1j  2j -2+2j ]

(it might be written in scientific notation. There might be some very small nonzero real part in the 2j term.)


Part 4: Synthesizing a musical note

Now, let's synthesize a musical note! The input will be a list of phasors, a fundamental frequency (in Hertz), a sampling rate (in samples/second), and a duration (in seconds). The output is a musical note.

Remember how you use phasors:

$$x(t) = \sum_{k=0}^{N-1} \Re\left\{ z[k]e^{j2\pi (k+1) F_0 t}\right\}$$

where $t$ is the time of the sample (in seconds), and $\Re\{...\}$ means "real part of ...", and the frequency of the $(k+1)^{\textrm{st}}$ harmonic is $(k+1)F_0$ (so for example, the first harmonic is at $F_0$, the second is at $2F_0$, and so on). If your code is working, the following should print one period of a sine wave, which will look something like

[ 0.00000000e+00  7.07106781e-01  1.00000000e+00  7.07106781e-01
  1.22464680e-16 -7.07106781e-01 -1.00000000e+00 -7.07106781e-01]

The following should plot two periods of a square wave, approximated by its first nine harmonics (of which the even-numbered harmonics all have zero amplitude, and the odd-numbered harmonics have amplitude of +/-1/n and phase of zero).

If you measured the amplitudes of your violin note accurately, up above, the following should generate an 0.5-second note that looks and sounds kind of like a violin.

Note that we're using 0 for the phases. That's because we

In order to make the whole note sound more like a violin, the following code multiplies the whole note by a Hanning window. That serves to give the note a gradual onset, and a gradual offset; otherwise, it would start and end suddenly.


Part 5: Converting note names into fundamental frequencies

You've been provided with a file named note2f0.py that contains a dict, note2f0.F0. You can use it to look up the fundamental frequency of a note by name, as follows:

Use it to create a function submitted.names_to_F0 with the following signature:

If it works, the following block should produce a result something like

[311.13, 415.3, 349.23, 392.0, 349.23]

Part 6: Synthesizing a Tune

The last part of this MP will synthesize a song. You'll do that synthesizing a sequence of violin notes, then sequencing them to create the song.

If it works, the following blocks should synthesize the first two bars of "Hail to the Orange".