CS 124, Fall 2017
Lab 9: Classes

In this lab, you will write two fairly simple classes from scratch. You will also write a class to hold the main program, and you will use several of Java's built-in classes for the first time: Scanner, File, and ArrayList. The program that you will write will be able to play simple tunes using Midi. The program will be able to read a tune from a file and play it. It will also be able to make up simple random tunes. The program will work on the command line. There is no GUI.

You will need a copy of the file SimpleSynth.java, in the package named "midi," from the previous lab. (You will not need KeyboardPanel.java from that lab, but it won't hurt to have it in your project. In fact, if you want, you could put your work for this lab in the same project as Lab 8.)

The two class files that you write will be called Note.java and Tune.java, which I believe should be in package midi. The main program will be MidiPlayer.java, which can be in the default package or any other package. Note that you should not modify SimpleSynth.java; your program must work with the original version. The program is due by the beginning of lab next Thursday. You should submit the three files using the submit website, as usual.

Some Style Rules for Methods and Classes

https://xkcd.org/1513

The Program

You should create a class named MidiPlayer to hold the main() routine of the program, plus any static methods that it needs. The program will run in a loop where it presents a text menu to the user, reads the user's response, and carries out the user's command. Here is the menu that you should use:

What do you want to do?

    1.  Read a tune from a file.
    2.  Make up a random tune.
    3.  Select an instrument for the tune.
    4.  Select a volume for the tune.
    5.  Play the tune.
    6.  EXIT  
    
Enter the number for your choice:

The user should respond with a number from 1 to 6. The program should carry out the requested action before returning to the start of the loop and printing the menu again. If you would like to see a working program, you can run MidiPlayerComplete.jar, which you can find in /classes/cs124. To run that program on the commmand line, use:  java -jar MidiPlayerComplete.jar

Note that you can write the main program first, and code the non-trivial actions as subroutine "stubs." A subroutine stub is an empty subroutine that you can call but that doesn't yet do anything useful. You would then go on to complete the program by filling in the subroutine stubs. (This is top-down design.) Alternatively, you could write the Note and Tune classes first, and then write the main program that uses them. (That would be more of a bottom-up design process.)

The program will use input from the user, but you should not use TextIO to do that input. Instead, you should use an object of type Scanner. As discussed in class and in Section 2.4.6, you can create the Scanner that you need using the statement

in = new Scanner( System.in );

Since you will probably need to use the variable in in other subroutines, you can declare it as a static global variable

private static Scanner in;

The Scanner methods that you might want to use in your program include:

You can get full credit fo this assignment as long as your program can handle valid inputs. However, you might want to make your program more robust by handling some possible errors. Note that there are two ways to deal with an error in input: You can let the exception occur, and catch it. Or you can use a test, such as calling in.hasNextInt() before trying to read an int, to avoid any possibility of an exception. In either case, before continuing with the program, you might need to call in.nextLine() to clear out the line of input that contains the error.

Note and Tune

You need to create two new classes to use in the program. In Eclipse, you can use the "New" / "Class" command to create a class. You will have to type in the name of the class (without the .java at the end). You should also make sure that the class is going into the right package. (If you right-click the package name in the Package Explorer, and select "New" / "Class" from the pop-up menu, then the correct package should already be filled in in the dialog box.)

Create a class named Note in package midi. A Note object should have a Midi note number and a duration. The duration should be specified in milliseconds. The values for these instance variables should be specified as parameters to the constructor subroutine for the Note class.

The class should also include an instance method for playing the note. That method needs a parameter of type SimpleSynth, so that it can use the SimpleSynth to turn the note on and off. Between turning the note on and turning it off, you need to insert a delay of duration milliseconds. There is a method Thread.sleep for doing that, but that method can throw a funny exception that must be caught. So, the code for delaying for duration milliseconds looks like this:

try {
    Thread.sleep( duration );
} 
catch (InterruptedException e) {
}

You will probably need to work on the design of the Note class as you continue writing the program.

You also need to create a class named Tune. A Tune represents a list of Notes that can be played in sequence. To store the list of notes, you should use an instance variable of type ArrayList<Note>. It will be easy to add notes to the list and to traverse the list and play the notes that it contains. The class should have an instance method named add for adding a note to the list, and an instance method named play for playing the tune. The play method will need a parameter of type SimpleSynth for playing the individual notes in the tune.

The Tune class should also have two instance variables to store the Midi instrument number that will be used to play the tune and the volume at which it will be played. (It will not be possible to change the instrument or volume in the middle of the tune.) The class should have (at least) setter methods for these two instance variables. The variables should be given reasonable initial values, so that the user can play the tune without explicitly setting the instrument and volume first.

If you want a simple test of your classes, they should work with the following main routine:

import midi.*;  // (if this is in another package)
public static void main(String[] args) {
    Tune tune = new Tune();
    tune.setVolume(50);      // not too loud
    tune.setInstrument(20);  // Reed Organ
    tune.add( new Note(1, 1000) );  // inaudible
    tune.add( new Note(48, 500) );
    tune.add( new Note(48, 500) );
    tune.add( new Note(48, 500) );
    tune.add( new Note(44, 2000) );
    SimpleSynth midi = new SimpleSynth();
    tune.play(midi);
}

About Creating a Random Tune

One part of the assignment is to create random tunes. Now, you could make a random tune simply by adding a bunch of notes with randomly selected note numbers to a Tune, but that would probably sound bad. My idea for making a random tune was that the change from one note to the next should ordinarily be very small, but that sometimes there should be larger jumps. That is, I select a random change in the note number from one note to the next. So, the notes do a kind of random walk along the keyboard. The results sound better to me when the probability of a large jump is much smaller than the probability of a small jump. (If you think about it, this is kind of like the problem of selecting a random number of runs scored in an inning in our baseball simulation.) You could also play with the idea of random durations for the notes.

When selecting random values or doing a random walk, you should make sure that the note numbers lie in a reasonable range. Although there are 128 possible note numbers, many of the notes are actually inaudible to human hearing. My program makes sure that the note numbers remain in the range from 36 to 84.

Note that when the user selects the command "Make up a random tune," the program should just create the Tune object, and save it in a variable. It shouldn't play the tune. When the user wants to play the tune, they will select the command "Play the tune."

About Reading from a File

Your program will also have to read a tune from a file. You can use a scanner to read from a file, using the same commands that are used to read data from user input. The only problem is creating the Scanner object. You will need the name of the file, which you can ask the user to input. If the user specifies a file that can't be read, there will be an exception, and the exception has to be caught. Here is some code that you might use. It uses the Scanner named in to read the file name from the user.

System.out.print("Enter the path to the file: ");
String filepath = in.next();
Scanner filein;  // A scanner for reading from the file
try {
    filein = new Scanner( new File(filepath) );
}
catch (Exception e) {
    System.out.println("Can't read a file named " + filepath);
    return null;  // Give up on reading the file!
}
// Now, filein can be use for reading from the file.

Your goal is to read the specified file and construct the Tune that it represents. (You shouldn't play the tune until the user chooses the "Play the tune" command.) Of course, you need to know the file format...

Each line of a tune file starts with a word (which can be read with filein.next()). The word is followed by one or two integers, depending on the word. Here are the legal words and their meanings:

Note that instrument, volume, and beat are optional in a file.

Your program should be able to handle any legal tune file. I have made three short examples, which you can find in the following files:

There is no need to copy these files anywhere. When the program asks for a path to a file, you can enter one of these three strings exactly as shown.

The same directory also contains three invalid tune files which contain various errors. You can get full credit for the assignment as long as your program can correctly handle valid files, but if you want to make a more robust program, you can test it on these files:

A really robust program would detect the errors in these files and report them to the user, without crashing. But testing for all possible errors can be difficult.