Lab 2 in Object Oriented Programming

Digital Signal Processing and Music

In this lab, you should define a small library to generate music that can be listened to with regular music program. Time does not permit an extensive library, so the music that we can provide is limited, here is an example.

Important about the audio for your computer. The speakers of the computers in our labs are for obvious reasons shut. To hear something from these computers you must have a pair of headphones with 3.5mm jack of the type used for mp3 players and the like. It is our hope you all have access to such. If you do the labs on your own computer you probably would not have this problem, but you can enjoy the music from the speakers. If you are sitting in the lab room, you should plug in your headphones, double-click on the link above and make sure you can hear the example.

The end result of your work in this lab will be a library class with five functions all together a few dozen lines, so there is not much code to write. All functions are working with arrays, which are introduced in the lectures. The labs should be reported to a supervisor in the computer room.

Preparation 1. Quick course in digital audio

Sound arises when a shock wave propagates through the air: we have thus an analogous phenomenon. For treatment on computers, we want a digital representation of the analogous signal. We consider the following Figure (from Wikipedia's article Digital audio):

The gray analog signal is sampled (scanned) at discrete points in time (the vertical dashed lines). The signal values ​​(amplitudes) at these time points is rounded to the nearest integers represented by the horizontal dashed lines. The sampled digital signal is thus in this case

[0, 4, 5, 4, 3, 4, 6, 7, 5, 3, 3, 4, 4, 3].

A common standard for digital music is the one used for CDs, where the sampling rate (i.e. number of readings of signal per second) is 44100Hz. The accuracy of the amplitude is typically 16 bits, i.e. 216 = 65536 different amplitudes can be specified. In our library, we will resample signals with this frequency, but represent amplitudes with values ​​of type double, scaled so that the amplitude is always between -1 and 1. Only at the last step, in a class that you get given, we make the 16-bit values ​​for the amplitude. An appropriate data type for music is thus an array of floating point numbers, i.e. double[].

We can make two observations:

  

Preparations 2. Provided classes

  

  Download lab2.zip to your directory for this course (the directory that also contains the subdirectory lab1). Open a terminal window and go in this directory. Now do the following:

> unzip lab2.zip
  inflating : lab2.html
... <More prints of the content of the archive> ...
> ls lab2
Main.java Song.java SoundDevice.java doc elise.txt
> cd lab2
> javac *.java
  

The first command unpacks the archive lab2.zip containing three .java files. Then we move to the directory and compile the java classes.

Next, you should familiarize yourself with the given program:

  1. Run the program (with the headphones on!)
    > java Main
    
    You hear a pure sinusoidal signal with frequency of 440 Hz for two seconds , followed by an equally long tone an octave higher.
  2. See Main.java:
    import java.io.File;
    
    public class Main {
    
      // Create the tone with frequency freq that
      // lasts for duration seconds.
    
      public static double[] sine (double freq, double duration) {
          int n = (int) (duration * SoundDevice.SAMPLING_RATE);
          double[] a = new double[n];
          double dx = 2 * Math.PI * freq / SoundDevice.SAMPLING_RATE;
          for (int i = 0; i < n; i++) {
              a[i] = Math.sin(i * dx);
          }
          return a;
      }
    
      public static void main (String[] args) {
          SoundDevice device = new SoundDevice();
          Song song = new Song(5);
          song.add(sine(440,2));
          song.add(sine(880,2));
          song.play(device);
          song.save(device.getFormat(), new File("twotones.wav"));
          System.exit(0);
      }
    }
    

    We begin by looking at main. There are several things that we have not talked about and will be introduced in Lecture 3. We can still read and try to understand.

    It first creates two objects, a SoundDevice and a Song (new means that you create objects). We do not really know what it means in Java, but the words should still give some associations:

        
    • What is a SoundDevice? My dictionary says about the word device that it's "a thing made ​​or adapted for a particular purpose, esp. a mechanical or electronic contrivance". So one SoundDevice is an electronic device to play sounds. This class makes it possible to access the computer's sound card.
    • Song is more familiar. What it is in Java, we don't know but the key is to see how a song is used, i.e. what we do with it: We can add notes (with add), we can play the song (with play) and we can save it to a file (with save ).
    We read further in main. Twice we add (song.add) something to the song. What we add is the result of the function sine as defined above; from the commentary we see that the function produces a sinusoidal signal with given frequency and length. Then we play the song (that we heard) and finally we save it to a file named "twotones.wav". Do ls again and check that there is now a file with this name. The file can be opened with RealPlayer (the menu Applications / Sound and Video). Do this and check that you hear the two tones.

    We are now in a common situation. We have received an application, tested it, looked at it and understood some of what is going on. However, something is still strange. What does, for example 5 in new Song(5) mean?

  3. In the subdirectory doc is the documentation of the two classes Song and SoundDevice. Open doc/index.html. Reading the documentation about Song, you should be able to understand what the number five does.

    For this lab, we do not need to understand how classes SoundDevice and Song are constructed. It suffices to see that the constant SAMPLING_RATE = 44100 is defined in this class.

  4. It remains to understand the function sine that creates and fills in an array with the readings of a pure sine function. The number of values ​​in the array are the number of readings per second (SAMPLING_RATE) times the length of a note in seconds (duration). The function that we resample is s(t) = sin(2π f · t), where f is a frequency (called freq in the program; in programming we usually use longer variable names than in mathematics).

Task 1 : The sound of a string

So it's time for your first programming task: to create a musically more interesting sound than a pure sine wave. Your task is to define a function pluck with the signature

public static double[] pluck(double freq, double duration)

generating a tone at the given frequency and duration and similar to the sound when you strum a string in a stringed instruments. For this, there is a famous method: Karplus - Strong algorithm, invented about 30 years ago. Remember pluck has the same parameters as sine, i.e. the tone frequency and the duration. Even so, it will sound different.

Start by creating a new class MusicUtils (in a new file MusicUtils.java). Move there sine from Main; your new class will soon contain two functions to create two different sounds. When you moved away sine from the class Main you must change the use in the method main. You must now write MusicUtils.sine for the Java compiler to find this method. Do it, recompile and check that you still can run Main and hear the two sine waves.

You will now add pluck to the new class. Karplus - Strong's algorithm works as follows:

First you must declare and create an array of floating point numbers of suitable size, in just the same way as in the function sine.

Then, you shall fill in the elements with values. This is done in two steps:

      
  1. Let p be the number of readings per period (i.e. sampling frequency 44100) divided by the frequency freq for the desired tone. Fill the first p elements with random numbers in the interval [-1,1] by using a for-loop. To obtain random numbers use the function random in the class Math. See the API's, which are linked at the bottom of the menu on the course home page and search for the class Math. This function gives a result between 0 and 1. How do you make it into a number between -1 and 1?
  2. The other elements of the array, after the first p, you fill in a new for-loop as follows: the element with index i is the sum of the elements with indices i-p and i-(p-1), multiplied by a constant k. A suitable value for k is 0.498, but feel free to experiment with different values ​​ (which give different sounds). Interesting sounds appear only for k values ​​slightly less than 0.5. (Alternatively, we can say that we take the average of the elements with indices i-p and i-(p-1), multiplied by a constant k which is slightly smaller than 1, for example 0.996. Think about it so that you understand that this gives the same result.)
Finally, just like in sine, you return the array that you filled in.

Test your function by modifying the main so that you call pluck instead of sine (they have exactly the same parameters, so you can replace one with the other). You should still hear two tones but now they sound like when you strum on a string.

We can now see that the code in sine and pluck has the same structure: we declare and create an array, fill it in with content and we return it. The content will be different in the two cases and we get therefore different sounds when we play the file.

Task 2: A better way to insert notes

So far, we entered tones by providing frequency and duration. We now want to approach how notes are usually indicated in music, i.e. with notes in a scale. We can not go through music theory here, but confine ourselves to note that, for example, a piano keyboard has twelve keys per octave, seven white and five black:

We're entering notes not with the usual letters CDEFGAH because it requires the sign of the black tones and additionally some way to specify the octave; instead we number the tones with integers on the keyboard above, here note A4 is given number 0 (we choose A4 as a starting point, because, the frequency 440Hz is usually a reference point for musical scales). Note C4 is thus -9 , C5 is 3, C6 is 15, and so on. This also has the advantage that we can easily get a direct formula for the frequency of a given tone: tone number k has a frequency 440 * 2k/12 Hz . C4 is then with frequency 440 * 2-9/12 = 261.63 Hz .

Your task now is to add to MusicUtils an additional function:

public static double[] note(int pitch, double duration)

that generates a tone with a given number and duration. Note: the only thing that needs to do be done is to figure out the frequency of the tone number, call pluck and return the array. note should not create or fill an array: it uses pluck for this. There is another function in Math that is useful to calculate "2 to the power x."

Once you have done this you can test the result by modifying the main so that the program plays Gubben Noak:

song.add(MusicUtils.note(-9,0.4));
song.add(MusicUtils.note(-9,0.4));
song.add(MusicUtils.note(-9,0.4));
song.add(MusicUtils.note(-5,0.4));
song.add(MusicUtils.note(-7,0.4));
song.add(MusicUtils.note(-7,0.4));
song.add(MusicUtils.note(-7,0.4));
song.add(MusicUtils.note(-4,0.4));
song.add(MusicUtils.note(-5,0.4));
song.add(MusicUtils.note(-5,0.4));
song.add(MusicUtils.note(-7,0.4));
song.add(MusicUtils.note(-7,0.4));
song.add(MusicUtils.note(-9,1));
song.play(device);

Note: You should increase the duration of the whole song from 5 to 6 since the duration of all notes together sums up to 5.8.

The choice of 0.4 seconds per tonne is a matter of taste: you can select another tempo if you wish. We also see that it gets very tedious to describe music in this way. But before we (optionaly) fix this, we will improve the sound.

Task 3 : Mixing audio

We get a much more interesting sound, if we simultaneously play multiple tones. As a first step, you should write a function:

public static double[] average(double[] t1, double[] t2)

generating an average of tones t1 and t2. We also assume that the two notes have the same duration so that the fields are of equal length. Function average will simply create a new array where for each index the element is the average of the corresponding values ​​in t1 and t2. Define the function.

As the last mandatory task will you define a function

public static double[] harmonic(int pitch, double duration)

that creates a tone with a given number and duration by mixing three tones created with note, namely tones with numbers pitch, pitch-12 and pitch+12 (i.e. also the two tones an octave below and above the target). Suggestion first mix the last two tones with average and then mix the result with the first note, again with average.

Finally, you can test the result by replacing in main all uses of note with harmonic.

When you come this far, you have defined a library class MusicUtils that contains five features: sine that you had given and pluck, note, average and harmonic which you defined yourselves. You are therefore done with the lab unless if you want to continue with the next, optional task.

Optional: separate the songs from the program

The description of Gubben Noak above was part of main. Much better is to save the description of the song in a file noak.txt:

-9 0.4
-9 0.4
-9 0.4
-5 0.4
-7 0.4
-7 0.4
-7 0.4
-4 0.4
-5 0.4
-5 0.4
-7 0.4
-7 0.4
-9 0.4

and then allow the program to read that file and using the information in each line create a sound that is added to the song. Even better is perhaps to modify the duration of the tones in the file to 0.25 which corresponds to a quarter-tone, and then separately set the tempo.

Now change Main so that it reads a song description from the standard input, builds up a song and plays it. If one has the description in a file, you can still use the program by redirecting the standard input:

java Main <noak.txt

In your directory there is also a file elise.txt that you can use for testing, if you do not want to write your own file for the music that you prefer. This file utilizes the suggested improvement: duration of 0.125 means one eighth tone. If you allow this to be synonymous with the duration of the tone in seconds, then it will be too fast-paced. If one eighth lasts about 0.125 seconds of time then there will be 240 quarter notes in one minute. 148 quarters per minute is more moderate. One possibility is to set the pace on the command line so that it runs as follows:

java Main 148 <elise.txt

This task is easy to do once you have learned how to read numbers from standard input. This is done using the standard class Scanner, which will be discussed at the lecture on Tuesday in week 2. Those who have come here before that can read in Section 11.1.5 of Eck's book, search the net or ask the tutors.

Endnotes

Hopefully you now have learned some programming with arrays and a bit of digital audio. We just want to conclude by pointing out that in a sense we made it easy for us by wasting memory. We create many long arrays for short sound sequences, and we could have done what we have done and more with much less memory consumption. A serious music program needs to be more economical, but for our purposes, a more liberal memory consumption is enough.