CSCI 150: Lab 9

The Sound of Music
Due: 10PM on Tuesday, April 23

The purpose of this lab is to:

  • Practice creating and using your own classes
  • Learn how digital music is created and stored
  • Think about issues of efficiency
  • Review reading text from files
  • Create some music of your own (with a little help from Mozart)

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 lab09 folder!

    # save the file mozart.tar into ~/cs150

    % cd

    % cd cs150

    % 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 lab09 inside your cs150 folder. Switch into this directory. This is where you'll put all the files you create for this lab. You should also save the following files here:

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 lab09 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. First try increasing the volume via the keyboard volume control. If this fails, you can try the adjusting the volume using PulseAudio Volume Control (click on Application on the top left of the screen, mouse over Multimedia, then select PulseAudio Volume Control). If that still isn't working, you can adjust the sound by typing in the Terminal

    % alsamixer
                

This will start a program that adjusts volume. You can adjust the volume up or down with the up/down arrows; toggle mute with the M key, and go from slider to slider with the left/right arrows. Press the escape key to quit the program.

Part 1 - Sound Wave Basics

transformers!

soundwave.py: 22 points, partner allowed.

First, let's talk about music representation in .wav files. Each music note is just a sine wave (for us, anyway). This wave is defined by its frequency (pitch), its amplitude (volume), and its length (duration). Middle C (the centre 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 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.

  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:

    • Store duration in an instance variable called self.length.


    • Store amp in an instance variable called self.maxvol.


    • Store samplerate in an instance variable called self.samplerate.


    • Create a instance variable called self.samples, and define self.samples to be a list of the values in the waveform. You should fill in this list such that it has a length of self.length*self.samplerate (rounded down to an integer), where entry t in ths list is given by

      
              self.maxvol*math.sin(2*math.pi*freq*t/self.samplerate)
                            
      where the value of freq is

      
            440*(2**((halftones+3)/12))
                            

      Observe that the note A440 is 3 semitones below middle 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 self.samplerate because we're taking samplerate samples for each second.

    • Say you've computed a new value v that you plan on appending to your list samples. A natural way to do this is with the instruction

      
            self.samples.append(v)
                              
    • Notice that you aren't asked to store halftones as an instance variable. The reason is that as we combine and overlay sounds, our soundwaves will cease to have a single frequency. They will, however, continue to have a maximum volume, sample rate, and length.


    • 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 (e.g., self.samples[0:10]), 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 (i.e., len(self.samples)) you're generating is what you intended.
  3. Add a method called play to your soundwave class. This method should take no parameters (except for self). The method play should simply pass the self.samples instance variable 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 (middle C) for approximately 2 seconds. If it works, great, continue onward! Otherwise you'll need to track down some bugs.

  4. Add a method called concat to your soundwave class. In addition to self, this method should take a single parameter s2, namely a second Soundwave object that will be concatenated to the Soundwave. To do this, you'll want to append the samples of s2 to the samples of the self, and update both the length and maxvolume instance variables appropriately. 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 method is working properly, while the second tests volume.

  5. Add a method called plus to your soundwave class. This method 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 method 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 (superposition) of the samples of s2 to the samples of the invoking object. That is, the i-th sample in the new soundwave should have a value equal to the sum of the i-th samples of s2 and self. The other instance variables (maxvol and lengthof this new soundswave should be updated as necessary. 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 method 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 self.samples, self.maxvol, and self.length appropriately (explained below). 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. From that, we should be able to determine the length and maxvol. 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.

    After you create self.samples using audio.read_file(halftones), you will want to make sure to (1) set self.maxvol to be the largest value in self.samples and (2) set self.length to be a value based on both the number of samples in self.samples and self.samplerate. This will insure you always have your four instance variables, regardless of whether your Soundwave is created to be a single note or the sounds from a file.

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

Optional

  • Add support for creating sounds that aren't simply sine-waves to generate sounds that are more like real instruments (or are just more interesting).
  • Add additional methods for manipulating or combining Soundwaves.
  • Genereate your own tunes and sound effects.
  • If you want us to see any of your masterpiece, submit it as a program called optional.py.

Part 2 - Playing Scales

scale.py: 8 points, partner allowed.

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, 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 command-line arguments. In particular, we'd like to be able to type

      python3 scale.py -3 M
                
to play an A major scale,

      python3 scale.py 0 N
                
to play a C minor scale, or

      python3 scale.py 4 B
                
to play a blues scale in E.

In particular, you will pass as command-line arguments the tonic note (in its half-tone offset from middle C) and which scale to play (as a character: 'M' for major, 'N' for minor, 'B' for blues). How can your program make use of the arguments you add after the program name? Easy -- if you add import sys at the beginning of your program, you'll get access to the variable sys.argv, which is a list of the arguments passed to Python. The first of these is always the name of the program itself. But if you were to run

      python3 scale.py 4 B
                
and that program included the statement print(sys.args), we'd get as output

      ['scale.py', '4', 'B']
                
Given this (and possibly judicious use of the int function), you should be able to get all the input you need from command-line arguments.

Requirements for your program:

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


  2. Declare a list of lists
    
          intervals = [[2,2,1,2,2,2,1],
                       [2,1,2,2,1,2,2],
                       [3,2,1,1,3,2]]
                        
    These correspond to the number of halftones between successive notes in the Major, Minor, and Blues scales respecively. This should make building scales cleaner.


  3. Gracefully handle invalid input from the user by catching exceptions, reporting the error, and quitting the program. You may want to refer back to lab08 for details on how to handle command line inputs 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 method.

Part 3 - Minuet and Trio

mozart.py: 8 points, partner allowed.

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 the Classical sonata cycle. 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 Mr. 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 supplied text files so that the information is available to your program.


  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 same 16 minuet measures used at the start of the song, and then play the resulting Soundwave.
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, and lab06 or lab08 for hints on parsing a string into a list separated by spaces.

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/methods to organize related instructions into logical groups.


  3. Comment any non-obvious block of code.

Wrap Up

As with every lab, your last job prior to submission is to complete a brief write-up by filling out a Google Form. Please remember that each person must submit a Google Form response to sign the Honor Code, even if you worked with a partner and they submitted your code.

Handin

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

Check through your files and make sure you have your name at the top in comments. 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 9
                        # file/directory is lab09

    % 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 9 lab09
                

File Checklist


You should have submitted the following files:

   soundwave.py
   scale.py
   mozart.py
   optional.py       # optional (obviously)