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".


Part 8: Grade your code on your own machine before submitting it!

If you reached this point in the notebook, then probably your code is working well, but before you run the autograder on the server, you should first run it on your own machine.

You can do that by going to a terminal, and running the following command line:

python grade.py

If everything worked perfectly, you'll get a message that looks like this:

..... ---------------------------------------------------------------------- Ran 5 tests in 0.275s OK

But suppose that something didn't work well. For example, suppose you run python grade.py, and you get the following:

....F ====================================================================== FAIL: test_synthesize_song (test_visible.TestStep) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/jhasegaw/Dropbox/mark/teaching/ece401/ece401labs/21fall/mp1/tests/test_visible.py", line 71, in test_synthesize_song self.assertAlmostEqual( AssertionError: 0.16870767993167918 != 0.08435383996583959 within 7 places (0.08435383996583959 difference) : *** synthesize_song([ 220. 329.63 17.32 698.46 20.6 41.2 116.54 55. 46.25 41.2 3520. 1396.91 16.35 466.16 329.63 4186.01 82.41 246.94 311.13 311.13],[0.1981401 0.1537068 1.28657637 1.72814063 1.34908962 1.77778565 0.20913126 0.71278756 1.08220118 1.05674697 0.81888192 1.27969944 0.76927221 1.33997218 1.38888085 0.72640863 0.70723419 1.63271861 0.95165971 1.62999141],89,18586,[ 0.94385485-0.21474033j 1.05689668-0.33092966j 0.335067 +0.86765753j 0.93081609-0.15036639j 0.5054813 +0.63242576j -0.38066782+0.87194557j 0.89222872+0.6459836j -0.88226954+0.83670909j 0.10012949-0.94724238j 1.02861232-0.4692548j ]), sample 4130, should be 0.16870767993167918 ---------------------------------------------------------------------- Ran 5 tests in 0.226s FAILED (failures=1)

This error message means that the function test_synthesize_song, in the file tests/test_visible.py, failed (all of the other tests succeeded -- only that one failed).

The error message specifies that synthesize_song was called with the following parameters: [ 220. 329.63 17.32 698.46 20.6 41.2 116.54 55. 46.25 41.2 3520. 1396.91 16.35 466.16 329.63 4186.01 82.41 246.94 311.13 311.13], [0.1981401 0.1537068 1.28657637 1.72814063 1.34908962 1.77778565 0.20913126 0.71278756 1.08220118 1.05674697 0.81888192 1.27969944 0.76927221 1.33997218 1.38888085 0.72640863 0.70723419 1.63271861 0.95165971 1.62999141], 89, 18586, [ 0.94385485-0.21474033j 1.05689668-0.33092966j 0.335067 +0.86765753j 0.93081609-0.15036639j 0.5054813 +0.63242576j -0.38066782+0.87194557j 0.89222872+0.6459836j -0.88226954+0.83670909j 0.10012949-0.94724238j 1.02861232-0.4692548j ]

With those inputs, it created a signal whose 4130'th sample should have been 0.16870767993167918. What value was produced instead? If you look at the line starting AssertionError, you can see that the function produced a sample with the incorrect value of 0.08435383996583959.

How can you debug this?

The first thing to do is to run your solution here, in the notebook, to see what answer you're getting.

Now let's compare that to the solution that the autograder was expecting.

The solutions the autograder was expecting are in the file solutions.hdf5. You are strongly encouraged to browse that file, in case you need to debug any problems with the autograder. Here's how you browse it:

As you can see, this file contains a lot of objects, created during a sample run of the solution code with random parameters. Let's plot the song that it was expecting:

Right away, we notice that the expected solution (above) is twice the amplitude of the solution being generated by my file! Upon inspection, I discover that I accidentally multiplied all of my outputs by 0.5. Fixing that line in submitted.py fixes all of the results.

When gradescope grades your code, it will run grade.py. It will test your code using the solutions in solutions.hdf5, and using the test code tests/test_visible.py. It will also test your code using some hidden tests. The hidden tests are actually exactly the same as those in tests/test_visible.py, but with different input parameters.


Extra Credit

You can earn up to 10% extra credit on this MP by finishing the file called extra.py, and submitting it to the autograder.

This file has just one function, called extra.fourier_series_coefficients, with the following signature:

Remember that the Fourier series coefficients are normally defined as

$$X_k = \frac{1}{T_0} \int_0^{T_0} x(t) e^{-j2\pi ktF_0}dt$$

In order to summarize information over the entire length of the input signal, though, let's average over the entire input signal length, thus

$$X_k = \frac{1}{L} \sum_{n=0}^{L-1} x[n] e^{-j2\pi knF_0/F_s}$$

where $L$ is the length of the signal, and $F_s$ is the sampling frequency (samples/second).

When you think you have it working, you can test it by running:

python grade.py

in your terminal. Yes, indeed, this is the same code you ran for the regular MP! The only difference is that, when you unzipped mp1_extra.zip, it gave you the test file tests/text_extra.py. So now, when you run grade.py, it will grade both your regular MP, and the extra credit part.

Incidentally, what is this test doing? It is testing to see if your analysis of the violin results in the right phasors. The violin is playing a middle C, which has an $F_0=261.63$. Its sampling frequency is $F_s=44100$. The results look something like this:

In order to find something a little closer to what we're used to, let's compute the levels of these eight notes. Since these are computed from the whole file, we'll add 120dB to the levels, in order to account for the effect of averaging over the entire file:

Well! These levels, averaged over the whole file, are pretty different from what we measured right at the peak using Praat. There are some similarities, though: for example, the 1st, 3rd, 6th and 8th harmonics have high amplitude, while the 2nd, 4th, 5th, and 7th harmonics are smaller.

Congratulations! That's the end of MP1. Good luck!