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.
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:
For higher frequencies the resolution is of course much worse, but for audible signals there are still at least two readings per period. This is no coincidence; upcoming courses in signal processing explain why you want to have at least that number.
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:
> java MainYou hear a pure sinusoidal signal with frequency of 440 Hz for two seconds , followed by an equally long tone an octave higher.
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:
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?
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.
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:
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.
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.
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.
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.
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.