CSCI 150: Lab 9

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

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

    % 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 adjust the sound by typing

    % 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, individual.

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 a class attribute called length.


    • Store amp in a class attribute called maxvol.


    • Store samplerate in a class attribute with the same name.


    • Create a class 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 length*samplerate (rounded down to an integer), where entry t in ths 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 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 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. 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

      
            samples = samples + [v]
                            
      Indeed, this does what we want. Unfortunately, it's actually quite slow. Why? Let's think about what the plus operator is doing. samples + [v] is building a new list whose first part is samples and whose last part is the singleton list [v]. But this means the operation has to copy every element of samples into this new list. This is useful if we want to maintain a copy of the original list for future use. But we're immediately assigning that list to the variable samples again! If we're halfway through creating a 1-second soundwave, then samples contains 22050 values, so we're doing 22050 copies just to add one more value to the list. Our computers can do 22050 copies pretty fast, but that's just adding one value -- we still have another 22050 values to add! Doing 22050 x 22050 copies is just too slow.

      Fortunately there's an easy fix to this. Instead of using list concatenation, we'll use the list function append:

      
            samples.append(v)
                              
      This function modifies samples by adding a new entry (v) to the end, and takes roughly a single operation to do so. This isn't what we'd want if we needed to keep the old version of samples around for some reason, but since we don't, we'll use the much-faster append function.

      An alternative solution to the above problem is as follows. Since we know in advance how long our sample list should be, we can first create a list of the correct length initialized to zeroes (using [0]*n for the required value of n), and then assign the appropriate value at each index of this list.


    • Notice that you aren't asked to store halftones as a class attribute. 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, 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 (middle 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, and update both the length and maxvolume attributes 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 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 (superposition) 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. The other attributes of 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 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, maxvol, and length 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. 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.

    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 monkeyed 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 functions 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, individual.

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


  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

mozart.py: 8 points, individual.

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 original 16 minuet measures, and then play the resulting Soundwave.
You may want to refer back to lab02 if you don't remember how to generate random integers, lab05 if you need a refresher on reading from a text file, and 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 to organize related instructions into logical groups.


  3. Comment any non-obvious block of code.

Wrap Up

README: 2 points, individual.

As with every lab, your last job prior to submission is to complete a brief write-up in a README file. If you haven't already done so, please create a new README file in your lab09 folder.

In this file, write a sentence or two about what you learned in this lab. Also give an estimate of the amount of time you spent on the lab. If you have further thoughts about the lab (e.g. parts that were confusing, helpful, annoying, fun, or challenging), please let us know.

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. Also, if you followed the Honor Code in this assignment, insert a paragraph attesting to this fact in your README file.

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 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)
   README