Lab 08

The Sound of Music
Due by 6pm on Tuesday, April 7

The purpose of this lab is to:

As requested in the prelab, please use headphones while working on this lab.

Getting Started

Download the file mozart.tar into your cs150 folder and unpack it. Do NOT save it in your lab08 folder!

    # save the file mozart.tar into ~/cs150
    % In a terminal window navigate to your cs150 folder and tye tar xvf mozart.tar

    % ls

You should now have the directories Mfiles and Tfiles, each containing a slew of .wav files.

Now create a folder called lab08 inside your cs150 folder. This is where you'll put all the files you create for this lab. You should save the following files into your lab08 folder. If you use the chromium browser you can right-click on each of the links and select "Save link as ..." from the popup box that appears. Other browsers have the same capability, perhaps with different terminology.

audio.py
middlec.py
furelise.py
vol.py
surprise.py
fromfile.py
mTable.txt
tTable.txt

Do NOT move the Tfiles or Mfiles folders, or their contents. We do not want you submitting a bajillion .wav files when you run handin. The lab machines will break. More than usual.

Checking the Sound

Now let's check that you have what it takes to play sound files. Plug in your headphones, and use the file browser to navigate your way to the lab08 directory. If you've untarred the files properly, you should have some music files (with the .wav extension) in the Mfiles and Tfiles directory within your cs150 directory. Right-click on one of these files and open it with Audacity.

If you don't hear anything, then your sound may be either muted or turned down. In a terminal window type

    % alsamixer

This will start a program that adjusts volume. With the cursor in the mixer window, use the F6 key (at the top of the keyboard) to bring up the soundcard information. With the arrow keys move the gray line down to

      1 HDA Intel PCH

Select this line wtih the Enter key. This will bring up a graphical display of 7 volume controls, labeled "Master", "Headphones", "Speaker", "PCM", and so forth. If the control is turned on it should have "OO" at the bottom (meaning left and right chanels are both On); if it is off it will have "MM" at the bottom (meaning both chanels are Muted). You can use the left and right arrow keys to move to each control; the "M" key will toggle between "OO" and "MM" for this control. You need to be sure that the Master, Headphone, and PCM controls are all in the "OO" position. When the controls are on you can change their levels with the up and down arrow keys, though they are probably already set to the maximum volvue and that is okay. You can use the ESC key to exit from the alsamixer application.

Part 1 - Sound Wave Basics

First, let's talk about music representation in .wav files. Each music note is just a sine wave. This wave is defined by its frequency (pitch), its amplitude (volume), and its length (duration). Middle C (the center key on a piano) has frequency around 523, and a safe volume is an amplitude under 1. We can define all notes relative to this one: to go up a semitone, you just multiply your frequency by the 12th root of two (1.05946).

For example, C# (C sharp) has the value 523*(2**(1/12))=554, and B has the value 523/(2**(1/12))=493. There are 12 semitones in an octave (count 'em up on a piano if you like), so to go up an octave, you double your frequency (e.g. high C has the value 1046).

You can refer back to the prelab for all the details. For now all you really need to know is that at second t, the y-value of the sound wave is amplitude*sin( 2*PI*frequency*t ).

Here is a table listing the frequencies of the middle octave from A to A.

A A# B C C# D D# E F F# G G# A
440.0 466.2 493.9 523.3 554.4 587.3 622.3 659.3 698.5 740.0 784.0 830.6 880.0

Since we're going to be creating music using sound waves, it seems like a good idea to have a Soundwave class that will represent a sampled sound wave. You should create this class in the file soundwave.py (note that the file name is lowercase, the class name is capitalized) using the following guidelines.

  1. Include the import statement import audio. The file audio.py contains useful functions for converting between a .wav file and a sample represented as a list of floats in Python. It also includes a function allowing you to play a sample using an appropriate application. You shouldn't need to change any code in this file. Note that this will not work on Windows, so you will need to work either in the lab, or on a Mac.

  2. As usual, the constructor for your Soundwave class will be specified by a function called __init__. While we'd like a Soundwave to potentially hold arbitrary sounds, for a number of our applications we'll want to create simple musical notes. Therefore, we'll start by designing our constructor to take in four parameters specifying a single note: halftones (the number of halftones above or below middle C of the note), duration (the length of the note in seconds), amp (the amplitude of the note), and samplerate (the sampling rate).

    This constructor should:

    • Create an attribute called samples, and define samples to be a list of the values in the waveform. You should fill in this list such that it has a length of duration*samplerate (rounded down to an integer), where entry t in this list is given by
          amp*math.sin(2*math.pi*freq*t/samplerate)
      
      where the value of freq is
          440*(2**((halftones+3)/12))
      

      Observe that the note A440 is 3 semitones below C, and indeed if halftones has a value of -3, freq evaluates to 440. Once we've computed the frequency of the note, we're just applying our earlier formula for the sound wave at time t, except that we divide t by samplerate because we're taking samplerate samples for each second.

    • Since even a single second of sound is using 44100 floats, we need to be a bit more careful than usual about efficiency here. It is possible to add item x onto the end of list L by saying:
              L = L+[x]
      but Python implements this by creating a new list, copying all of the elements of L into it, copying the one element of [x] into it, and then assigning this list to L. This is fine if the list only contains a few elements, but if the list has many thousands of elements this is ridiculously inefficient. A much more efficient way to add x into the end of L is with the append method for lists. So each time your constructor computes another element v for the self.samples list you should say
               self.samples.append(v)

    • Notice that we aren't storing any of constructor parameters. That is because all of the information we need for functions we'll write is contained within the samples list, and once we've created the list, we no longer care about any of the individual parameters.

    • For the purposes of this lab, we'll always be using a sample rate of 44100. We're just adding that part for completeness. 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 samplerate should be 0, 0.0, 1.0, and 44100 respectively. 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 probably write a small program that uses your Soundwave class and check that your samples are being initialized properly. You can't play it (yet), but you could use slices to print the first 10 or so samples, and see if they seem to be changing in a sensical way as you adjust the parameters passed to the constructor. You should also check that the number of samples you're generating is what you intended.

  3. Add a function called play to your soundwave class. This function should take no parameters (except for self). The function play should simply pass samples to the audio.play() function.

    You should now be able to run the provided file middlec.py. When you run this program, you should hear a single note (C) for approximately 2 seconds. If it works, great, continue onward! Otherwise you'll need to track down some bugs.

  4. Add a function called concat to your soundwave class. This should take a single parameter s2 (in addition to self), namely a second Soundwave object that will be concatenated to the invoking Soundwave. To do this, you'll want to append the samples of s2 to the samples of the invoking Soundwave. Since this is intended to actually change the invoking Soundwave object, we can save some time by using the extend function on it's samples. This function is similar to append, except it takes in a list to be added rather than a single element.

    You should have be able to run the provided files furelise.py and vol.py. The first of these checks that the concat function is working properly, while the second tests volume.

  5. Add a function called plus to your soundwave class. This function will allow us to create a new soundwave by superimposing existing soundwaves and thus let us play multiple notes at once. Like concat, plus should take in another Soundwave object s2. Unlike concat, however, this function should create and return a new soundwave object, and leave the original two soundwaves unchanged. The samples of this new soundwave should be the sum of the samples of s2 to the samples of the invoking object. That is, the nth sample in the new soundwave should have a value equal to the sum of the nth samples of s2 and self. Make sure your program works even when the two Soundwaves have different lengths. In creating plus, you may find it useful to create a copy function that lets you duplicate a soundwave.

    Having done this, the provided program surprise.py should now work.

  6. Finally, we'd like to be able to initialize a Soundwave object from a .wav file. To do this, we're going to modify our Soundwave constructor. We'd like to be able to call the constructor like
        snippet = soundwave.Soundwave("imonaboat.wav")
    

    and have this set the samples appropriately. To suppose this, we'll need to do some type checking on the parameters. In particular, if the type of the first parameter passed to the constructor is a string (str), we want to use audio.read_file function on that string to generate our samples list. Otherwise, we want to do what we've been doing.

    To check whether halftones is a string representing a .wav file rather than an integer representing halftones from middle C, you can use the function isinstance(halftones,str). As you might expect, this returns True if halftones has type str and returns False otherwise. To put this all together, one part of your constructor will say
           if isinstance(halftones, str):
                 self.samples = audio.read_file(halftones)



    To test whether this is working, try running the provided file fromfile.py. You might want to also double-check that the previous programs still work now that you've monkeyed with Soundwave's constructor. If all is good, congratulations, you're now ready to move on to the next part!


Part 2 - Playing Scales

A scale is a sequence of notes, defined by the intervals between them. For example, the major scale is defined by the 7 intervals (and hence 8 notes) (2,2,1,2,2,2,1), that is, there are 2 semitones between the first and second notes, and 2 between the second and third notes, but a single semitone between the third and fourth notes, and so on. The C major scale is the major scale starting at C and is thus the sequence of notes starting at around frequency 523 and ending around 1046, that is, the notes (C, D, E, F, G, A, B, C). The D major scale is the major scale starting at D: the sequence of notes (D, E, F#, G, A, B, C#, D). The A major scale is the sequence of notes of (A, B, C#, D, E, F#, G#, A).

There are many other interesting scales, such as the minor scale, defined by the intervals (2,1,2,2,1,2,2), and the blues scale, defined by the intervals (3,2,1,1,3,2) (the scale only contains 7 notes).

For this part of the lab, you should write a program scale.py that will play a scale specified by two inputs from the user: the tonic note as a semitone offset from middle C, and a choice between major, minor, and blues scales. An A scale will have offset 3, a C scale will have offset 0, and so forth.

For example, if variable s holds the initial offset, the major scale consists of semitones

             s, s+2, s+4, s+5, s+7, s+9, s+11, and s+12

You should be able to see how those tones come from the major scale intervals [2, 2, 1, 2, 2, 2, 1].

Requirements for your program:

  1. To make use of your Soundwave object, include import soundwave at the top of your file.

  2. Declare the three lists of intervals:
         majorIntervals = [2, 2, 1, 2, 2, 2, 1}
         minorIntervals = [2, 1, 2, 2, 1, 2, 2]
         bluesIntervals = [3, 2, 1, 1, 3, 2]

    Once the user indicates a choice of scales, set variable intervals equal to the appropriate interval list, and use that to construct and play a soundwave object wtih the appropriate notes.

  3. Your program should have a loop that first asks the user for the mode of a scale, which can be "major", "minor", "blues", or "quit". Naturally, if the user enters "quit" you should exit the program. For the other three modes you should ask for the tonic note of the scale as an offset from middle C (0 for C, -3 for A, and so forth). Your program should then create and play the indicated scale. You should gracefully handle invalid input from the user.

  4. Do not play the notes individually, as you will hear the gap between successive notes. Instead, create a single soundwave using your concat function.


Part 3 - Minuet and Trio

Now let's talk about this Minuet and Trio business. What is a Minuet and Trio? It is musical piece that is often the third movement of a sonata in Classical (i.e, Mozart and Haydn) style.. Both the Minuet and Trio follow a specific rhythm and form, and they are usually combined by first playing the Minuet, then playing the Trio, then the Minuet once more. You can listen to a very nice Minuet and Trio here.

You'll be generating a Minuet and Trio based on a random algorithm developed by Wolfgang Amadeus Mozart himself. Your Minuet will contain 16 measures (musical snippets), as will your Trio. For each of the 16 measures in the Minuet, you will randomly generate a number between 0 and 10 (inclusive); use each such number to pick a specific music snippet from the following table (there are 176 total minuet snippets). For example, if I generate the 16 random numbers (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0, 1, 2, 3, 4, 5) for the Minuet, then I will select the snippets (32, 95, 113, 45, 154, 133, 169, 123, 102, 20, 26, 56, 73, 160, 1, 151). Here is such a randomly generated Minuet and Trio. For the Trio, you do the same thing except your random number is between 0 and 5, inclusive.


Minuet Measures

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
0 96 22 141 41 105 122 11 30 70 121 26 9 112 49 109 14
1 32 6 128 63 146 46 134 81 117 39 126 56 174 18 116 83
2 69 95 158 13 153 55 110 24 66 139 15 132 73 58 145 79
3 40 17 113 85 161 2 159 100 90 176 7 34 67 160 52 170
4 148 74 163 45 80 97 36 107 25 143 64 125 76 136 1 93
5 104 157 27 167 154 68 118 91 138 71 150 29 101 162 23 151
6 152 60 171 53 99 133 21 127 16 155 57 175 43 168 89 172
7 119 84 114 50 140 86 169 94 120 88 48 166 51 115 72 111
8 98 142 42 156 75 129 62 123 65 77 19 82 137 38 149 8
9 3 87 165 61 135 47 147 33 102 4 31 164 144 59 173 78
10 54 130 10 103 28 37 106 5 35 20 108 92 12 124 44 131


Trio Measures

16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
0 72 6 59 25 81 41 89 13 36 5 46 79 30 95 19 66
1 56 82 42 74 14 7 26 71 76 20 64 84 8 35 47 88
2 75 39 54 1 65 43 15 80 9 34 93 48 69 58 90 21
3 40 73 16 68 29 55 2 61 22 67 49 77 57 87 33 10
4 83 3 28 53 37 17 44 70 63 85 32 96 12 23 50 91
5 18 45 62 38 4 27 52 94 11 92 24 86 51 60 78 31

In this part of the lab, you will write a program mozart.py that generates such a random Minuet and Trio (that is, it generates a Minuet followed by a Trio followed by your Minuet a second time). There are three main steps to this program, which will be discussed in more detail below.

  1. The first step is to read in the above tables from the files "mTable.txt" and tTable.txt" you copied into your lab08 folder at the start. Remember that you can parse a string into a list of strings as indicated by whitespace with the split( ) method for strings. For example, if s = "I'm a little teapot", the function s.split() returns the list ["I'm", "a", "little", "teapot"]. Both of these files are just sequences of numbers. You should iterate through those numbers and group them into rows of 16, appending each such row onto a list for the appropriate table.
  2. The second step is to generate the 32 random numbers and select the appropriate measures from the tables you read in in step 1.
  3. The final step is to construct the music from the individual measures; you will concatenate the 16 minuet measures followed by the 16 trio measures followed by the original 16 minuet measures, and then play the resulting Soundwave. Minuet measure d in in file "../Mfiles/Md.wav". For example, Minuet measure 41 is in file "../Mfiles/M41.wav". Similarly, Trio file names all start with T, as in "../Tfiles/T89.wav". Recall that we modified our Soundwave constructor to accept such a string and use to to create a Soundwave object.
You may want to refer back to lab03 if you don't remember how to generate random integers, lab05 if you need a refresher on reading from a text file.

Requirements and suggestions for your program:

  1. Gracefully handle exceptions (as might arise if the requested files aren't found) by reporting the error and quitting the program.

  2. Use functions to organize related instructions into logical groups.

  3. Comment any non-obvious block of code.

Handin

PLEASE READ: When you do your handin, double check that you haven't somehow moved the Mfiles or Tfiles into your lab08 directory.

Check through your files and make sure you have your name at the top in comments. Also, if you followed the Honor Code in this assignment, insert a paragraph attesting to this fact in the top comment block of one of these files.

I affirm that I have adhered to the Honor Code in this assignment.

You now just need to electronically handin all your files.

    % cd                # changes to your home directory
    % cd cs150          # goes to your cs150 folder
    % handin            # starts the handin program
                            # class is 150

			    # assignment is 8
			    # file/directory is lab08

    % lshand            # should show that you've handed in something

You can also specify the options to handin from the command line

    % cd ~/cs150                 # goes to your cs150 folder
    % handin -c 150 -a 8 lab08

File Checklist


You should have submitted the following files:
   soundwave.py
   scale.py
   mozart.py