CS 302 Project 3: A different kind of "loops"

Due date: Tuesday, November 20 at 9:00 AM (CSL time)

Announcements | Overview | Goals | Description | FAQ and hints | Handin

Last modified: Saturday, November 10, 2007


Announcements

Includes:  Additions, Revisions, and FAQs (Frequently Asked Questions).
Please check here frequently for the latest announcements!

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:
  1. The documentation for the rotateLeft method of Track might be a little confusing because of the font. In the example, there is a space (that is, a rest) at the beginning of the sequence before invoking rotateLeft; this rest is at the end of the sequence after invoking rotateLeft. Another example would be the sequence with the String representation "Xx X x", which would become "x X xX" after rotating left.
  2. When the user chooses the "Create a new song" option in DrumApp, you should create a new, empty Song object with operator new. This means that the pattern sequence and pattern collection of the old song will go away (that is, they will be garbage collected at some point in the future); they will not be a part of the new song.
  3. Be sure that you understand the difference between two Song methods with similar names: getSeqLength returns the number of pattern references in a song's sequence, while getSeqCapacity returns the largest pattern sequence that a song could support without resizing an array. (Recall that you are implementing Song in terms of a partially-filled Java language array -- that is, one in which the number of elements you care about at any given time may be less than the array's length.)
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.


Overview

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.


Goals

While developing this project, you will:


Table of contents

Background and description

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.

Making songs from patterns

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.

image of drum machine

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:

image of 97 bpm hip-hop pattern

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.

image of 97 bpm hip-hop pattern, with accents

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:

image of 111 bpm reggaeton pattern, with accents

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:

image of 158 bpm DnB pattern, with accents

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:

image of 73 bpm reggae pattern, with accents

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

Using interfaces

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;

The big picture: how to do this assignment

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

Classes we provide

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.

Classes you will write

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.

About DrumApp

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

The main menu

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:

  1. This item prompts the user for a song name (a nonempty String) and a tempo (a double). It then uses these to create a new song and makes the new song into the active song; it will cause the formerly-active song to be garbage-collected.
  2. This item will prompt the user for an integer, which must be one or greater, and create a new pattern of that length. It will then add this pattern to the active song and display the new pattern's pattern number to the user.
  3. This item will display each pattern in the song in the following format: the word "Pattern", followed by a space and the pattern number, a newline, ten hyphens, a newline, and then the String representations of each track in the pattern, each on a line by itself, followed by a blank line. (An example is here; the line numbers are for clarity and should not appear in your output.)
  4. This item will display the sentence "This song is x seconds long." on a line by itself (substituting the length of the song in seconds for x, of course). It will then display the sequence of pattern numbers that make up the song, on a single line, with a space after each.
  5. This item will take the user to the sequence editing menu (see below).
  6. This item will prompt the user for a floating-point number between 60.0 and 240.0 and set the song's tempo to this number.
  7. This item will prompt the user for a nonempty String and set the song's title to this String.
  8. This item will prompt the user for a valid pattern number to edit. It will then take the user to the pattern editing menu (see below).
  9. This item will use a DrumPlayer instance to play the active song.

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.

The sequence editing menu

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:

  1. This item prompts the user for a valid (i.e., non-negative) sequence length and changes the length of the active song.
  2. This item prompts the user for a valid sequence position — that is, between 0 (inclusive) and the sequence length (exclusive). It then displays the pattern that is active at that position and prompts the user for a new valid pattern (i.e., "That position is playing pattern 4. Which pattern would you like it to play?"). Once the user has entered a valid pattern number, this method should update the sequence.
  3. This item works like the previous item, except it doesn't prompt for a pattern — it simply sets that sequence element to EMPTY_PATTERN
  4. This item works like option 9 in the main menu; we have included it here to make the program easier to use.
  5. This item returns the user to the main menu.

If the user selects an option other than 5 ("Return to the main menu"), DrumApp will display the sequence editing menu again.

The pattern editing menu

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:

  1. This item prompts the user for a track name, displays the list of instruments (found in the getInstrumentNames() method of DrumPlayer), prompts the user for a valid instrument number, and creates a new Track of the same length as the current pattern. It then adds the newly-created Track to the current pattern.
  2. This item prompts the user for valid track number. It then prompts the user for a String representation of the notes played by that track. This string must have the same number of characters as the length of the track, and every character must be either Track.REST, Track.REGULAR, or Track.ACCENTED. Once the user has entered a valid track string, DrumApp will use the fromString() method of Track to change all of the steps at once.
  3. This item will prompt the user for a valid track number and a valid step number (steps start with 0). It will then display the character representing the step at that position in the given track and prompt the user for a new step value, which must be either Track.REST, Track.REGULAR, or Track.ACCENTED. Finally, it will update the track to reflect the changed step.
  4. This item will prompt the user for a valid track number and then invoke that track's rotateLeft() method.
  5. This item will prompt the user for a valid track number and then invoke that track's rotateRight() method.
  6. This item will prompt the user for a valid track number and a valid step number. It will then prompt the user for a valid step value (again, either Track.REST, Track.REGULAR, or Track.ACCENTED), and use the insertStep() method of Track to insert that value before the given step number, discarding the last step in the sequence.
  7. This item will prompt the user for a valid track number and a valid step number. It will then use the removeStep() method of Track to remove the given step from the given track, padding the end of the sequence with one step of silence.
  8. This item works like option 9 in the main menu; we have included it here to make the program easier to use.
  9. This item returns the user to the main menu.

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

Look Ahead: Managing Shared Resources

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.

About documentation

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.

Questions

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:
  1. We haven't provided a mechanism to delete patterns from a song. What would we have to do to ensure it was possible to do this safely?
  2. How would we have to change the design of the program to allow playing pitched instruments (that is, instruments that could be either on or off, and — if they were on, at a given pitch?)
  3. How would we have to change the design of the program in order to allow sounds that last longer than one step?
  4. You probably got sick of writing code to handle array resizing, copying, and insertion and deletion. One way to make this easier would be to package up these operations in a class. Discuss how you'd design such a class, and identify any possible pitfalls. (Hint: what type of things would you store in the array?)
  5. In the Song class, we represented a sequence as a collection of ints that corresponded to Patterns. Alternatively, we could have represented a sequence as a collection of references to Patterns. How might this have changed your code? What would have been simpler to implement? What would have been harder to implement?
  6. Refer to the design tradeoff we posed in the previous question. Which approach would you have taken if you had designed this program? Why?
  7. Note that the pattern display only lines up Tracks if their names and instrument numbers are the same length. It would be easier to edit patterns if we could see individual steps in Tracks lined up vertically with corresponding steps in other Tracks. Propose a change to the program that would make this possible. (Hint: consider methods in the String class.)
  8. Most of the programs you use every day use graphical interfaces. Which parts of this program would you have to change in order to support a graphical interface?
  9. Compose a short song in a musical style you enjoy and submit it here. (Copy it from the console window and paste it as your answer to this question.)

FAQ and hints

FAQ

In the Javadoc for Song, what does it mean to "round up to the nearest int?"
The getSongLength() method should return the smallest int that is larger than or equal to the song length. So, if the song length were 30.0, it would return 30. If the song length were 30.1, it would return 31.
A lot of these methods specify preconditions, but they also specify what we should do if the precondition is violated. In the past, we've simply allowed a method invocation that violates a precondition to have undefined results. What's the deal?
Think of preconditions as providing part of a contract between the code implementing a method and the code that invokes that method. In our programs, just as in real life, there can be different penalties for breaching contracts. In the past, we've simply declared the contract to be voided when breached; if a method's client fails to meet its precondition, all bets are off and anything can happen. This makes it easier to write library code, but it's not totally desirable in all cases. (In terms of real-life contracts, this treatment of preconditions is analogous to a cell-phone company unleashing a terrifying horde of robot zombies on an unsuspecting world -- simply because someone cancelled his or her service.) Sometimes, we'd like to limit the potential damage caused by a precondition violation, so we specify invariants that a method must maintain no matter what, as we've done in this project. When we use them this way, invariants are analogous to real-world penalties for breach of contract, like an early termination fee on a service contract. By writing code to maintain invariants (like the invariant that a Track's pattern can't contain bogus steps), you'll get practice programming defensively and you'll also ensure that your Track, Pattern, and Song objects will always be safe to use with the DrumPlayer code.
I'm confused about the copy constructors. What should they do?
A copy constructor takes a reference to an object as a parameter and initializes the new object so that it is a copy of the object referenced by the parameter. Changes to the new, copied object should not affect the original object. (In particular, note that you are required to allocate a new step array in the Track copy constructor and initialize each step to the corresponding step in the referenced Track object.)
I'm a little confused about interfaces. What kind of a type is TrackInterface? When do I need to cast? My code doesn't compile! Help!
The section about interfaces covers these questions (and some others). If you're still confused, do let us know.

Stay tuned, because answers to your questions will be coming soon!

Hints


Handin

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


Feedback, questions, or accessibility issues: contact Rebecca Hasti (hasti@cs.wisc.edu)
©2007 Will Benton, Asim Kadav, Eric Aderhold, Leah Kluegel, Baris Aydinlioglu, Khai Tran, and Rebecca Hasti, all rights reserved