# Initialize Otter
import otter
grader = otter.Notebook("lab8-audio.ipynb")

Lab 8: Audio Synthesis with Numpy#

This lab explores using loops to synthesize different types of audio waveform in python.

Entering Your Information for Credit#

To receive credit for assignments it is important we can identify your work from others. To do this we will ask you to enter your information in the following code block.

Before you begin#

Run the block of code at the top of the notebook that imports and sets up the autograder. This will allow you to check your work.

# Please provide your first name, last name, Drexel ID, and Drexel email. Make sure these are provided as strings. "STRINGS ARE TEXT ENCLOSED IN QUOTATION MARKS."

# In the assignments you will see sections of code that you need to fill in that are marked with ... (three dots). Replace the ... with your code.
first_name = ...
last_name = ...
drexel_id = ...
drexel_email = ...
grader.check("q0-Checking-Your-Name")
import matplotlib.pyplot as plt
import numpy as np
import IPython.display as ipd

Generating a Sine Wave

A sine wave \(s(t)\) with amplitude \(A\) and frequency \(f\) can be computed using the following equation:

\[ s(t) = A sin(2 \pi f t)\]

In python, we can compute an array of values of the sine function by making \(t\) be an array of time values.

For instance, the following code sets t to be an array of \(16000\) values lienaarly spaced from 0 to 1. (A few of these values are shown when we print t below.)

# creates an array of 16000 time values from 0 to 1
t = np.linspace(0, 1, num=16000, endpoint=False) 

print(t)

Now we can plug our t array in into our equation to get a sine wave s:

# amplitude of 1.0
A = 1.0 
# frequency of 440.0
f = 440.0

# compute sine wave from equation
s = A * np.sin(2 * np.pi * f * t) 

Now if we print s, it might be difficult to tell that it’s a sine wave…

print(s)

So let’s plot it!

# plot sinusoid s
plt.plot(s)
# show just the first 100 values
plt.xlim(0,100)

We can listen to the sine wave audio with the code below. (The sampling rate 16000 needs to be provided to specify how quickly we want to play the audio samples.)

# we must provide 
ipd.Audio(s, rate=16000)
ipd.Audio(s, rate=16000)

Task 1: Approximating a Square Wave with Sinusoids

A square wave is a waveform that alternates steadily between two fixed values. We can compute a square wave \(q(t)\) with fundamental frequency \(f\) using the following infinite sum of sinusoids:

\[ q(t) = \sum_{k=0}^{\infty} \frac{sin(2 \pi (2k+1) f t)}{2k+1}\]

Usually it is a good option to use a for loop when implementing a mathematical sum in python. However, this sum goes to inifinity: we can’t compute infinite loops. We can get an approximation of a square wave by summing just the first \(K\) terms:

\[ q(t) \approx \sum_{k=0}^{K-1} \frac{sin(2 \pi (2k+1) f t)}{2k+1}\]

Write python code to do the following:

  • Complete the function called square_wave which returns an approximate square wave based on input arguments (a fundamental frequency f and a number of terms K).

  • Loop over the desired number of terms K.

  • For each term, calculate the appropriate sinusoid and add it to the provided starting array q.

  • For the input time value \(t\) in the equation above, use the provided array of tiem values t.

  • The function returns the approximate square wave q.

Your code replaces the prompt: ...

def square_wave(f,K):

    # initialize our square wave q to be all zeros
    q = np.zeros((16000,))

    # create an array of linearly spaced time values
    t = np.linspace(0, 1, num=16000, endpoint=False) 


    # loop over all desired values of k
    # add each sinusoid to q
    ...

    # return square wave q
    return q

# plot a square wave
plt.plot(square_wave(440,200))
# show just the first 100 values
plt.xlim(0,100)
grader.check("task1-square")

Listen to a \(200\)-term square wave approximation with fundamental frequency \(440\). In electronic music, square wave synthesizers are often used to provide a hollow, distorted sound.

ipd.Audio(square_wave(440,200), rate=16000)

Task 2: Sum of Harmonic Sinusoids

The square wave is just one waveform the can be constructed using a sum of sinusoids. In general, any periodic waveform \(w(t)\) can be approximated using the following equation:

\[ w(t) \approx \sum_{k=0}^{K-1} A_k sin(2 \pi (k+1) f t)\]

Where \(A_k\) is an amplitude value that is different for each term. Your task is to make a function that computes \(w(t)\) given a fundamental frequency \(f\) and a list of amplitude values \(A_k\). The number of terms \(K\) will be inferred by the length of the list of amplitudes.

Write python code to do the following:

  • Define a function called waveform which accepts as input a fundamental frequency f, and a list of amplitudes A.

  • First, initialize an empty array w of \(16000\) zeros, as in previous tasks.

  • Then create an array t of \(16000\) linearly-spaced time values, as in previous tasks.

  • Loop through all values of \(k\), adding on the appropriate sinusoid.

  • Return the final waveform w

Your code replaces the prompt: ...

...

# plot a square wave with the first 3 nonzero terms
plt.plot(waveform(440,[1,0,(1/3),0,(1/5)]))
# show just the first 100 values
plt.xlim(0,100);
grader.check("task2-sinesum")

Task 3: Musical Sequence

Your final task involves using the waveform function you made to generate sequences of musical notes.

Music software often uses a protocol called MIDI (Musical Instrument Digital Interface) to format messages related to music. MIDI uses a numbering scheme for the notes on a piano, so that every note has a unique pitch value. Each pitch \(p\) is an integer in \([0,127]\). The following equation shows how to compute the fundamental frequency \(f\) for a note with MIDI pitch value \(p\):

\[ f = 440 * 2^{(p - 69) / 12} \]

In this problem, you will complete a function called music_sequence which creates a waveform containing a sequence of musical notes. The input to the function will be a list of MIDI pitch values.

Write python code to do the following:

  • Complete the function called music_sequence which accepts as input a list of MIDI pitch values P.

  • Loop through all the pitch values in list P

  • For each pitch \(p\), compute its frequency \(f\) using the equation above

  • Using the computed frequency \(f\) and the provided amplitude list A, generate a waveform using the waveform function from the previous task

  • Append each waveform to the sequence list, which has been initialized for you

  • The provided code concatenates the sequence of notes and returns the final waveform

Your code replaces the prompt: ...

def music_sequence(P):

    # initialize a empty list for our sequence of notes
    sequence = []
    # use this list of amplitudes A when generating waveforms
    # it gives the notes an organ-like sound
    A = [1, 0, 0, (1/4), 0, 0, 0, (1/8), 0, 0, 0, (1/12)]

    # loop over all pitches in P and compute each frequency
    # using the frequency and list A, generate a waveform
    # append the waveform to the sequence list
    ...

    return np.concatenate(sequence, axis=0)

# listen to the music sequence
ipd.Audio(music_sequence([60,62,64,65,67,69,71,72]), rate=16000)
grader.check("task3-musical-scale")

Submitting Your Assignment#

To submit your assignment please use the following link the assignment on GitHub classroom.

Use this link to navigate to the assignment on GitHub classroom.

If you need further instructions on submitting your assignment please look at Lab 1.

Viewing your score#

It is your responsibility to ensure that your grade report shows correctly. We can only provide corrections to grades if a grading error is determined. If you do not receive a grade report your grade has not been recorded. It is your responsibility either resubmit the assignment correctly or contact the instructors before the assignment due date.

Each .ipynb file you have uploaded will have a file with the name of your file + Grade_Report.md. You can view this file by clicking on the file name. This will show you the results of the autograder.

We have both public and hidden tests. You will be able to see the score of both tests, but not the specific details of why the test passed or failed.

Note

In python and particularly jupyter notebooks it is common that during testing you run cells in a different order, or run cells and modify them. This can cause there to be local variables needed for your solution that would not be recreated on running your code again from scratch. Your assignment will be graded based on running your code from scratch. This means before you submit your assignment you should restart the kernel and run all cells. You can do this by clicking Kernel and selecting Restart and Run All. If you code does not run as expected after restarting the kernel and running all cells it means you have an error in your code.

Fin#