Lab 6

Published

April 4, 2025

Submit on Gradescope

Objectives

  • Write some simple loops and some that will require some thinking
  • Learn a little about sound manipulation and music theory while we are at it
Python subset

This lab does not allow for the use of the string .split method that was introduced in Challenge 02. If specified, you are expected to use the types of loops mentioned in the assignment.

Part 0: Getting started

If you have not already, before you jump into the lab, you will want to read through the first part of Working with sound. This will walk you through installing middsound.

To begin, create a file called lab06.py inside your labs directory and import middsound at the top of the file.

Part 1: Write a tone generator

We want to write a function called append_tone(snd, freq, duration) that adds duration seconds of a pure tone at frequency freq to sound snd. Don’t worry, We’ll walk you through it…

append_tone
Parameters
snd Sound
freq int/float
duration int/float
Returns None

Create the structure of the function

  • In your Python file, import middsound and math
  • Start your function in the usual way.
  • Next, create a new variable called num_samples and assign it the desired number of samples that the function should add to the sound. You don’t know how many samples you need, but you do have the number of seconds (duration) that the tone should play. To get to how many samples you need, you can start with the sample rate (the number of samples per second) of the new sound. You can get this from the snd parameter that was passed to the function using snd.framerate. Multiply this value (samples per second) by the number of seconds, and you will get the number of samples that you need to create a sound of the correct duration. Make sure to convert this to an integer and store the value in num_samples.
  • Now write a for loop that runs once for every every sample that you want to add to the sound. Use range() and num_samples to get the loop to run the appropriate number of times.
  • Inside the loop use the sound’s .append() method (function) to append the value 0 on each iteration of the loop. This will fill your sound with silence as your first step.
  • Check your function in the shell by typing the following (the freq parameter is not used in your function, so you can use whatever value you like there):
>>> sound = middsound.new()
>>> append_tone(sound, 440, 2)
>>> sound.play()

You will not see much going on. The shell may hang, but probably it will just seem like nothing happened.

You can check to see that samples have been added by using len(sound). If you want, you can also write sound.save('silence.wav'), and it will write the silence out to a file called 'silence.wav' in the same folder where your script is. If you open that with an audio player, you will see there is 2 seconds of silence inside.

Generate the tone

As we saw in class, we can get a pure tone if our samples form a sine wave. So, the final piece is to actually add samples that make this a sine wave at the given frequency so that we get a tone.

Conceptually, we want to start with a sine wave of the right frequency, and then sample it at our sampling rate (the number of samples per second). However, we are going to approach this a little differently. We have math.sin(), which will tell us the value of the sine function at any point, we need to adjust the input a little to get the sine wave to give us samples at the right frequency. Brace yourselves – we are about to get a little mathy in here…

Start by thinking of the x-axis as time in seconds. Our sine function returns results in radians, so the period of the wave (the time for one complete cycle) is \(2\pi\). Let’s say that we wanted the period to be 1 second so that at time \(x = 0.25\) the function is at 1, at \(x = 0.5\), the function is at 0, at \(x = 0.75\) the function is at -1, and at \(x = 1\), the function is 0, completing one complete wave period in one second. In this case, \(x\) is a fraction of the period and, to get the sine function to go from 0 to \(2\pi\) while x (our time) goes from 0 to 1, we need to multiply x by \(2\pi\) (i.e., \(\sin(2\pi \cdot x)\)).

Of course, this would give us a frequency of 1 oscillation (full cycle) per second, which is pretty slow for sound. We would like to have a frequency like 440Hz, so we can get 440 oscillations in a second (this is the note ‘A’ above middle ‘C’ that is often used to tune instruments). We can get this by multiplying in the frequency: \(\sin(440 \cdot 2\pi \cdot x)\), or more generically, \(\sin(freq \cdot 2\pi \cdot x)\). Now we have an expression that, given x in seconds, gives us the correct value that the sine function should have at that instant in order to generate our desired tone.

In reality, though, we cannot store continuous and infinitely dense values of \(x\) in our computers so we need to think about the idea of samples. In your for loop, the index variable tells you which sample you are on. If we divide this number by the framerate (which is in samples per second), we will get a number that is the corresponding fraction of the second (e.g., if the framerate is 44,100 samples per second, and we are on the first sample, we are at \(\frac{1}{44100}\)th of a second). And this is the value that we can use for our \(x\) in the expression we created above. So it will take 44,100 samples (if the framerate is 44,100) to go through one second!

Don’t worry too much if we’ve lost you at this point… sampling takes a little bit to digest.

The important thing is that we have a formula to pass into the sin() function: \(2\pi \cdot freq \cdot index / frame\_rate\). To get the value of \(\pi\), you will need to use math.pi. index is whatever name you gave to your iterator variable and the frame rate is the sound’s framerate (that you got from the snd object that was passed to the function)

  • Create a new variable called sample and store the result of the above function in it
  • The output of sin() will be between 1 and -1, so we want to scale this to make it louder. Multiply sample by middsound.MAXVALUE. This will scale the amplitude of the sine wave to be as loud as possible.
  • Our samples need to be integers, so wrap that whole expression with an int() to convert it to an integer.
  • Go back to your code and replace the 0 you were appending to the sound with sample.
  • Run the function. Pass in a value like 440 (an A), and make sure you get the right tone. 440 is a good starting choice because it is the tuning pitch, and there is a multitude of samples online for you to compare against. If you want to try some other notes, here is a lookup table for the piano keyboard.

Part 2: Write a song generator

We are going to use what we learned to make a function that take strings and converts them to songs!!!

This function should be called make_song(notes). The notes argument will be a string containing the description of the song you want to create. The string will consist of notes and spaces. The notes will be pairs of characters like ‘a5’, which would be an ‘A’ in the fifth octave on the piano keyboard:

  • When you see a note, you should add 0.25 seconds of the note into the sound.
  • When you see a space, you should insert 0.1 seconds of silence.

There are some sample strings (songs) down below.

make_song
Parameters
notes string
Returns Sound

Reading the song

Before we dive into creating the song, let’s just handle parsing (reading and interpreting) the notes string.

  • Create a while loop that iterates over the string. Note that this cannot be a for loop because we are not looking at the string one character at a time. We will “consume” one character if the character is a space, but two if it is a note.
  • Inside of the while loop, write a conditional statement that checks the character at the current location in the string to see if it is a space.
    • If it is a space, print it out on the screen and advance your index by 1.
    • If it is not, print out the first two characters on the screen and advance your index by 2.
  • Run this and make sure it works. It should print the sequence of notes and spaces one after the other row-by-row.

Creating the sound

  • Add a line before the while loop that creates a new sound (i.e., snd = middsound.new()).
  • Add a line after the while loop that returns the new sound you created
  • Instead of printing out the space, change your code to append 0.1 seconds of silence into the sound. Use append_tone() with a frequency of 0.
  • To generate the notes, you need the frequency. You can get this by calling the middsound.get_frequency(note) function. Once you have the frequency, call your append_tone() function to add 0.25 seconds of tone at the appropriate frequency to your sound.

At this point you should have a function that will take in a string description of a song and output a wave file.

Add one of the following variables to your file and call your song generator passing these as notes.

mcdonald = "c5 c5 c5 g4 a4 a4 g4      e5 e5 d5 d5 c5   g4 c5 c5 c5 g4 a4 a4 g4      e5 e5 d5 d5 c5"
smoke = "b4  d5  e5e5  b4  d5  f5e5e5   b4  d5  e5e5  d5  b4"

# thanks to Katie Manduca
twinkle = "c5 c5 g5 g5 a5 a5 g5g5      f5 f5 e5 e5 d5 d5 c5c5    g5 g5 f5 f5 e5 e5 d5d5     g5 g5 f5 f5 e5 e5 d5d5         c5 c5 g5 g5 a5 a5 g5g5      f5 f5 e5 e5 d5 d5 c5c5"

# thanks to Chloe Johnson
hush = "d5 b5 b5 c5 b5 a5 a5 a5 a5    d5 d5 a5 a5 a5 a5 b5 a5 g5 g5    d5 b5 b5 c5 b5 a5 a5    d5 d5 a5 a5 a5 a5 b5 a5 g5 g5"

# thanks to Lea LeGardeur
land = "c4c4 d4d4 e4e4 f4f4f4f4 f4f4f4f4f4f4 f4f4 c4c4 d4d4 e4e4e4e4 e4e4e4e4e4e4 c4c4 c4c4 e4e4 d4d4d4d4 d4d4d4d4d4d4 d4 d4 c4c4 d4d4 e4e4e4e4 e4e4e4e4e4e4 c4 c4 d4d4 e4e4 f4f4f4f4 f4f4f4f4f4f4 f4 f4 c4c4 d4d4 e4e4e4e4 e4e4e4e4e4e4e4e4e4e4e4e4 d4d4 d4d4d4d4 c4c4 b3b3 b3b3 c4c4 d4d4 c4c4c4c4c4c4c4c4c4c4"

# thanks to Annie Beliveau
birthday = "g4     g4a4a4a4g4g4g4c5c5c5b4b4b4               g4     g4a4a4a4g4g4g4d5d5d5c5c5c5               g4     g4g5g5g5e5e5e5c5c5c5b4b4b4a4a4     f5     f5e5e5e5c5c5c5d5d5d5c5c5c5c5c5c5"

# thanks to Julie Erlich
mary = "a5g5f5g5a5 a5 a5 g5 g5 g5 a5 c6 c6 a5g5f5g5a5 a5 a5 a5 g5 g5a5g5f5"

# thanks to Corbin Dameron (she notes they sound better if tone duration is set to 0.15)
jingle = "e5e5 e5e5 e5e5e5e5 e5e5 e5e5 e5e5e5e5 e5e5 g5g5 c5c5c5 d5 e5e5e5e5e5e5e5e5 f5f5 f5f5 f5f5f5 f5 f5f5 e5e5 e5e5 e5 e5 e5e5 d5d5 d5d5 e5e5 d5d5d5d5 g5g5g5g5"

ode = "e5e5 e5e5 f5f5 g5g5 g5g5 f5f5 e5e5 d5d5 c5c5 c5c5 d5d5 e5e5 e5e5e5 d5 d5d5d5d5 e5e5 e5e5 f5f5 g5g5 g5g5 f5f5 e5e5 d5d5 c5c5 c5c5 d5d5 e5e5 d5d5d5 c5 c5c5c5c5 d5d5 d5d5 e5e5 c5c5 d5d5 e5 f5 e5e5 c5c5 d5d5 e5 f5 e5e5 d5d5 c5c5 d5d5 g4g4g4g4 e5e5 e5e5 f5f5 g5g5 g5g5 f5f5 e5e5 d5d5 c5c5 c5c5 d5d5 e5e5 d5d5d5 c5 c5c5c5c5"

# thanks to Lizzie Kenter
theOffice = "d5g4d5      d5g4d5F4d5  d5F4e5g4e5     e5g4c5  c5  c5b4a4b4g4     g5g5         F4g5F4d5e5          c5  c5  c5b4a4b4g4     g5g5         F4g5a5F4e5      d5e5d5b4c5  c5  c5b4a4b4g4     g5g5         F4g5F4d5e5          c5  c5  c5b4a4b4g4     g5g5         F4g5a5F4e5      d5e5d5b4c5  c5  c5b4a4b4g4        g4b4d5b4d5g5d5g5b5g5b5d6b5d6 g6"

# thanks to Eva Ury
harryPotter = "e4e4a4a4a4c5b4b4a4a4a4a4e5e5d5d5d5d5d5d5b4b4b4b4b4b4a4a4a4c5b4b4g4g4g4g4b4b4e4e4e4e4e4e4e4e4e4e4e4e4a4a4a4c5b4b4a4a4a4a4e5e5g5g5g5g5F4F4f5f5f5f5C4C4f5f5f5e5D4D4D3D3D3D3c5c5a4a4a4a4a4a4a4a4a4a4a4"

This is an example of what calling your function would look like:

>>> song = make_song(mcdonald)
>>> song.play()

Do that, and you should get a familiar song.

It will not be the greatest thing you have heard, but it should be recognizable.

Try to create some additional songs. Search for easy piano songs and you should be able to find some written in a similar notation. If you find some good ones or create some, please share them on campuswire!

Part 3: Main function

Write a function main that should have no parameters and make and play a song when called.

main
Parameters
Returns None

Turning in your work

One you are done, submit lab06.py to gradescope and ensure that you have passed all of the tests. Your file should contain three functions: append_tone, make_song, and main. Each function should have a docstring.

Try to make this sound better (do not include them in your submitted file)!

Here are some ideas:

  • The way we append the tones creates discontinuities in the waves, which we hear as clicks. This is particularly noticeable when we repeat notes to make longer notes (try the land song). One way to make this a little bit better would be to add an inner loop that keeps reading notes as long as they remain the same and makes a single call to append_tone() with the right duration.
  • We also get clicks at the start and end of notes. You could write another version of append_tone() that fades in (attack) and fades out (release or decay depending on who you ask) at the start and end of the tone (the sound of real instruments are described in terms of their attack, decay, sustain and release, which together is referred to as the envelope).
  • Currently, we get rhythm (duration) through the use of multiple spaces and notes. You could add notation to your strings to encode the length of the note (this would be an alternative to the first suggestion).
  • Another challenge would be to support flats and/or sharps in the input format (this is already done in middsound.get_frequency() but think how you would do it before looking at how we did it).

Also, if you come up with some alternative songs, send them along and we’ll include them in the collection above.

For inspiration, here are some examples from students who tinkered with the algorithms to make the output sound better: