Last modified: Saturday, November 10, 2007
| 11/10/2007 | We've updated the FAQ and hints section. |
| 11/8/2007 | The section about interfaces has been updated with some more information that may prove helpful. |
| 11/8/2007 | Some quick announcements and clarifications:
|
| 11/4/2007 | We've made a couple of minor changes to Track, PatternInterface and Pattern; please download the updated beatbox.jar. We've also corrected a typo in the SimpleSong example. |
| 11/1/2007 | Program released. |
You've almost certainly heard the sound of a drum machine before, since synthetic drum sounds are ubiquitous in commercial music. In fact, just as certain manufacturers of conventional musical instruments have enjoyed great acclaim among musicians, some distinctive drum machine models have become legendary in certain circles. (Notably, the Roland TR-808 and TR-909, the Oberheim DMX, and the Akai MPC series are well-known to many fans of hip-hop and electronic dance music.)
In this project, you will develop an application that implements the sequencer for a simple drum machine. A sequencer is a program that allows users to create simple musical patterns and to arrange several patterns into songs. Your application will enable the user to create and modify patterns (which are sequences of notes for several virtual drums) as well as songs (which are sequences of patterns).
Note that you must use Java-language arrays that are accessed with the [] operator on this assignment. You may not use any of the "collection classes" from the java.util package. You also may not use the .clone() method of arrays or the System.arraycopy() method.
You should create a new project in Eclipse to do this programming assignment. To work on this assignment, you'll need to download beatbox.jar and add it to your project.
While developing this project, you will:
In its most basic form, a drum machine is simply an electronic musical instrument that can trigger several different sounds. The different sounds produced by the drum machine are often intended to resemble the sounds of real drums and percussion instruments. (The sounds of historical models weren't always particularly faithful to those of real drums but many have become well-liked in their own right.) The earliest machines, like the Wurlitzer Sideman, supported only pre-programmed rhythms, but later models allowed a human musician to trigger the sounds.
Beginning in the early 1980s, drum machines began to support pattern sequencers. Such machines were not restricted to pre-programmed rhythms and did not require a human to operate them during a performance; instead, the drum machine user was able to program patterns of rhythms and arrange sequences of patterns into a song, which the drum machine was then able to play back in real-time without human intervention.
You can think of a pattern as a short part of a song. Each pattern is a fixed length and has a certain number of steps, with 16 steps to a measure. (Generally, the number of steps is 16 or 32, but the drum machine we build will allow arbitrary numbers of steps.) At each step, the pattern describes whether or not each sound is to be triggered. You can see an example pattern with 7 sounds (one in each row) and 16 steps (one in each column) below. Filled-in squares correspond to triggered sounds, and empty squares indicate that the sound is not triggered at that step.

When the drum machine plays back the pattern, it reads the pattern one step at a time, playing the sounds that are to be triggered at each step. The rate at which the drum machine reads steps and triggers sounds is a function of the song's tempo, or how many beats the drum machine should play per minute. (In all of our songs, there will be four steps per beat.) In the example above, the drum machine will trigger the sound for the bass drum at steps 0, 7, 10, and 15. (You can hear this pattern here)
We can also make our patterns more interesting by allowing each step to contain different kinds of events: instead of just a regular note, we could tell the drum machine to place an accent on a particular step. An accented step could be louder, higher-pitched, or even a completely different sound from a regular step; we could use these accented steps to draw attention to particular parts of a pattern. Consider the pattern below:

If we play this pattern at 97 beats per minute (a reasonable tempo for hip-hop), it sounds OK. Unfortunately, it's rather static (and a little boring), because all of the notes are accented the same way. We can choose to only accent certain notes. In the pattern below, black notes are accented and gray notes are not accented.

As you can hear, adding accents makes the performed pattern far more dynamic. (We've exaggerated the effect a little in order to make it easier to hear.)
You can get a feel for what different sounds and tempos look like by listening to some example patterns.
This pattern is slightly faster than our hip-hop pattern above. It clocks in at 111 beats per minute and recalls the characteristic sound of reggaeton music:

Listen to the reggaeton pattern.
This pattern is much faster, at 158 beats per minute, and follows some of the conventions of the drum-and-bass genre:

Listen to the drum-and-bass pattern.
Finally, we'll slow things down a lot, to 73 beats per minute, with a pattern that might be at home in an electronic recreation of 1970s roots reggae:

Listen to the reggae pattern.
Of course, a song that just has a short drum pattern repeating over and over again would probably be really boring. To enable musicians to generate and maintain interest, most drum machines allow users to specify a song as a sequence of patterns. For example, a song might play one pattern in the introduction, another during the verse, and still another during the chorus.
You can imagine the actions of a drum machine as it plays back a song as a sort of nested for loop. (Don't worry, you won't have to implement the part of the drum machine that actually makes the sounds. The for loop is just to clarify what the drum machine does with a song.)
for (int songPosition = 0; songPosition < songLength; songPosition++) {
/* Code to get the appropriate pattern for this song position goes here */
for (int curStep = 0; curStep < patternLength; curStep++) {
/* Code to get and play the active sounds for the
current step goes here */
/* Code to wait for a short time (depending on how fast the song
is) before advancing to the next step goes here */
}
}
In the drum machine we'll develop, each Song consists of a collection of Patterns (think of these as a pattern library of sorts) as well as a sequence of Patterns taken from the pattern library, which are played in order when you play the song. Each Pattern consists of zero or more Tracks (which are individual drum instruments), and each Track consists of one or more steps (which are either rests, notes, or accented notes). (Every track in a pattern has the same number of steps.)
In Programming Assignments 1 and 2, you developed classes based on prose descriptions of public interfaces that were specified for you in the assignment text. In this assignment, you will be writing classes that implement other public interfaces we have provided. However, an important difference between the interfaces in this assignment and those of earlier assignments is that we have provided the interfaces for this assignment in a machine-readable format. Because we have done this, you can tell the Java compiler that your class is intended to implement a certain interface. It can then check that you have done so correctly, and it will generate a compile-time error if you have not. (This is a nice feature — it's great when the compiler can check errors for you, and it's better to get compile-time errors than to lose points on an assignment! Note, however, that the interfaces don't contain information about constructors — so you'll have to make sure to get those right yourself.)
To specify that a class implements a certain interface, you will use the implements keyword in the class declaration. As an example, if you wanted to say that the class Track implemented the interface TrackInterface, you'd declare the Track class as follows:
public class Track implements TrackInterface {
// The rest of the class goes here, of course...
}
By using the implements keyword to indicate that a class implements a particular interface, you are committing to implement all of the methods specified in that interface. This means that, if the TrackInterface interface contains a method called getInstrument() that returns an int, the Track class must implement such a method. You must use the implements keyword as above to indicate that your classes implement the interfaces provided with this assignment.
Tip: You can use Eclipse to generate "stub methods" within the classes that implement interfaces in this way. Select "Override/Implement Methods..." from the Source menu to try it out, but be sure to only select methods from the given interface!
There are a couple of other advantages to interfaces like the ones we've given you. The first is pretty simple: since we've declared constants in these interfaces, you can use them in the classes that implement those interfaces. (You should not re-declare the constants.) For example, since your Track class implements TrackInterface, you can use the REST constant within Track. You can even refer to REST from other classes as Track.REST. The second advantage is a lot cooler: As you can see, we have methods that take interface types (like TrackInterface) as parameters. What does this mean? Well, when I have a variable (or parameter) of type TrackInterface, that can get a reference to an instance of any class that implements TrackInterface. For example, this is totally legal:
TrackInterface t = new Track("sub bass");
This works because the type TrackInterface includes references to every class that implements TrackInterface. As a consequence, "reference to TrackInterface" is a wider type than "reference to Track," in the same way that double is a wider type than byte. If you want to assign a reference to a TrackInterface to a Track variable, you'll have to cast, just like you'd have to do if you wanted to assign a double value to a byte variable:
TrackInterface t;
// ... t gets a value here somewhere
Track t2 = (Track)t;
Start by reading the rest of the assignment description. In particular, read the whole section about DrumApp, but read this as if you're reading the manual for a program that you're interested in using. Don't worry about how you're going to implement things until you've read the whole thing and the JavaDocs! This assignment is more complicated than your previous assignments, so you should make sure to read the whole description carefully to avoid getting overwhelmed or confused.
Once you start implementing the project, use incremental and test-driven development practices to ease your path. Start with the classes that don't depend on any other classes (like Track) and then develop the classes that these depend on. In fact, you should develop your tester classes before you develop your instantiable classes, so that you can test your code as you write each method. Finally, if some programming task is difficult, start by simplifying it. For example, when you develop your DrumApp class, start by assuming that all user input is valid. Once that works, you can gradually add error checking and additional functionality.
You won't be able to use DrumPlayer until your Song, Pattern, and Track classes are functional, but you don't need to finish DrumApp to make a song to start testing the big picture. In fact, you can create little Songs and Patterns programmatically to test whether or not your classes work with DrumPlayer. Here's an example song you can try once your Song, Pattern, and Track classes are in working order. (The Java code for that song is here.)
We provide the DrumPlayer class and the public interfaces TrackInterface, SongInterface, and PatternInterface. The DrumPlayer class provides two methods that you will use: getInstrumentNames(), which returns an array of Strings corresponding to the different drum instruments it supports, and playSong(SongInterface), which actually plays your song.
You will be responsible for developing the following classes:
You will also develop tester classes for your Song, Pattern, and Track classes, but you will only be required to hand in the tester for Track.
DrumApp is your main class; it consists of a menu-driven user interface to composing and playing songs. You will write code to print out a list of options, to get input from the user using a Scanner object, and then to act on that input.
You may assume that the user will enter the appropriate type of input (e.g., if you ask for an integer, you can assume that she will not enter a double or String), but you will still have to check to make sure that numbers are in the appropriate range or that Strings have the appropriate number of characters. If a user enters inappropriate input, you must prompt for that input again. (Hint: use a do-while loop!)
When a user first starts DrumApp, it will create a new Song with a tempo of 120 and a name of "untitled." This new song is the "active song" — the user will be operating on this song until she creates a new one. DrumApp will then display the following menu, which we will call the main menu:
Main menu 1. Create a new song. 2. Add a new pattern to this song. 3. Display the patterns in this song. 4. Display the sequence of this song. 5. Change this song's sequence. 6. Change this song's tempo (currently 120.0). 7. Change this song's name (currently "untitled"). 8. Edit a pattern. 9. Play the song 0. Quit.
Note that some of these menu choices display information about your song, which will change as the user edits your program. We'll talk about what each of these does in order:
The final menu item will quit the program. You can quit a Java program at any time by returning from main or by invoking System.exit() with 0 as the parameter.
This menu concerns editing the song's pattern sequence. It consists of the following options:
Sequence editing menu 1. Change the sequence length (currently 0) 2. Change the pattern at a given position 3. Silence the sequence at a given position 4. Play the song 5. Return to the main menu
We shall discuss what each of these causes your program to do in order:
If the user selects an option other than 5 ("Return to the main menu"), DrumApp will display the sequence editing menu again.
Recall that, on the way into the pattern editing menu, DrumApp prompted the user for a valid pattern number. The pattern editing menu operates on only one pattern at a time, and it will operate on the pattern the user selected. This menu begins by displaying just this pattern (in the same way we displayed a pattern in the main menu), followed by some options for the user. This is a nice feature because the user can see the changes to the pattern after each command. (Note that the pattern we've displayed here is an example; if you were visiting this menu for the first time in a real song, you'd be starting out with an empty pattern.)
Editing Pattern 0 ---------- kickd(0): X X X X snare(1): x X hihat(2): xXx xXx xXx xXx 1. Add a track to this pattern 2. Change all of the steps for a track 3. Change one of the steps for a track 4. Rotate a track to the left 5. Rotate a track to the right 6. Insert a step into a track 7. Remove a step from a track 8. Play the song 9. Return to the main menu
Unlike the previous two menus, which primarily manipulated a Song instance, the operations in this menu mostly deal with Pattern and Track objects. As before, we'll talk about what each does:
If the user selects an option other than 9 ("Return to the main menu"), DrumApp will display the pattern editing menu again (including the pattern itself at the beginning).
A computer has only one sound card, so how is it possible for multiple programs to generate audio simultaneously? One important job the Operating System plays is to control access to shared resources like hardware. While it seems like multiple programs are all accessing the sound card at the same time, only one is, the Operating System. All the programs, like our Drum Machine, that are generating audio, can simultaneously send this audio to the Operating System. Then a single part of the audio driver combines all the sound inputs and sends a single data stream to the sound card, allowing you to hear all your sounds together, harmonious or not.
Data is another type of resource that is commonly shared between multiple programs or even parts of the same program. This is a very important consideration when writing applications with multiple threads. Since they share data (like objects) in memory, multi-threaded applications have to be carefully designed to prevent multiple threads from fighting over data in memory. Potential problems that arise and ways of solving them are all covered in CS 537, Introduction to Operating Systems.
You must implement all of the public methods in every interface we have given you, and you are not allowed to add any public instance methods to any of your classes. Since the interfaces include documentation, you are not required to document methods declared in public interfaces. However, you are required to comment your code inside methods and to write Javadoc comments for any and all non-public helper methods you write in any class (see the CS 302 standards for style and commenting).
For this assignment you are not required to generate Javadoc documentation.
This section lists several questions that should deepen your understanding of the assignment. Create a text-only file named questions.txt and answer these questions.
Be sure to include the descriptive information listed below as well as the answers to each question:
Assignment Number:
Assignment Name:
Date Completed:
Partner 1 Name:
Partner 1 Login:
Partner 1 Lecturer's Name:
Partner 1 Lab Section:
Partner 2 Name:
Partner 2 Login:
Partner 2 Lecturer's Name:
Partner 2 Lab Section:
Stay tuned, because answers to your questions will be coming soon!
Use the Handin program to hand in your work (see the instructions on how to use the Handin program). Students working in pairs will only submit one copy of all program files, except the README file described below.
Only hand in the files listed below; do not hand in any other files. These files may be handed in any time prior to the due date. Note: you may hand in your files before the deadline as many times as you wish. We suggest you use your handin directory to keep your most recent working copy as you develop your solution.
Required files for this assignment