COMP 310
Spring 2018

Lab07: Playing Music in Java

Home  Info  Canvas   Java Resources  Eclipse Resources  Piazza

Music

Here is a simple piece of music:

Bass Minuet. TBe.017 - staff notation

The following is the corresponding abc file:

X:17
T:Bass Minuet. TBe.017
M:3/4
L:1/4
Q:1/4=100
K:F
C | F/E/F/G/ F | C F2 | GAB | A d/c/ d |\
c F2 | B B,A, | G,GE | F3 ||!
c | f/e/f/g/f/g/ | ea2 | d b/a/ b | g c'2 |\
dbd | ca2 | d g/f/g/e/ f3 |]

You will notice the abc files starts with a bunch of headers (a single capital letter followed by a colon followed by a number or string) and then it has the notes of the song.

Headers

X:17
T:Bass Minuet. TBe.017
M:3/4
L:1/4
Q:1/4=100
K:F

While there are a bunch of abc headers (you can just assume any capital letter names a header), there are only three that are critically important in order to get the computer to play music: L, Q, and K.

The L header indicates the default note length.  In the piece above, this is a quarter note.  This does not have musical meaning, but is rather a convenience when writing the tune. It means that if a note appears without a length, then it is assumed to be a quarter note.  Before you are able to properly parse headers, you may want to just set your default note length to be a quarter note.

The Q header indicates the tempo of the piece.  We will need to know the tempo in terms of quarter notes per minute.  In abc, the tempo can be expressed in "default" notes (those specified by the L header) per minute.  In that case, the value of the Q header will just be a number, i.e., Q:100.  Or the tempo can be expressed explicitly as a certain type of notes per minute.  In the piece above, the tempo is given explicitly as 100 quarter notes per minute.  Some pieces do not specify a tempo, so you will have to always initialize your system to some default tempo.

Finally, the K header indicates the key signature.  The above piece is in the key of F major.  This means that all B notes that appear in the tune should be flat.  You do not need to know all of the rules of musical key signatures.  We have provided you with a KeySignature class that will help you adjust all notes in an appropriate manner for the given key signature.  You just need to understand that the key signature potentially changes the meaning of some notes in the tune, by implicitly modifying them to be sharp (#) or flat (b).  No notes are modified in the key of C major, so you can use that key as a default as you are developing your program.

Notes

C | F/E/F/G/ F | C F2 | GAB | A d/c/ d |\
c F2 | B B,A, | G,GE | F3 ||!
c | f/e/f/g/f/g/ | ea2 | d b/a/ b | g c'2 |\
dbd | ca2 | d g/f/g/e/ f3 |]

The remainder of the abc file specifies notes.  Technically, headers can appear anywhere in the file, but they usually appear first and are followed by the notes of the song.  It is not important that you fully understand the abc notation for specifying notes (we have provided a parser for you), but here are the basics:

A capital letter (A-G) specifies a note in the middle C octave.  A lower case letter (a-g) specifies a note one octave higher.  For simple melodies like this, most notes are in these two octaves.  Lower octaves are specified by one or more following commas (i.e., B,) and higher octaves by one or more following apostrophes (i.e., c').

Notes with no trailing number of / are assumed to be the default length (as specified by the L header).  Trailing numbers indicate the length in terms of multiples of the default length.  In other words, a2 means an A note in the octave above middle C that is 2 default note lengths long.  A trailing / is short hand for 1/2.  An arbitrary fraction can be used: C2/3 is a middle C that is 2/3 of a default note length long.  Dotted notes followed by the appropriately halved note can also be specified as n/m where n>m.

Measures are indicated by a | character.  They can be modified in several ways that do not affect how we play the music, but do affect how the score is displayed (|\, ||! |], etc.).  You can also specify repeated regions by bracketing them in |:, :| measure markers.  You do not need to worry about measures or repeats at all, as the provided parser deals with them appropriately.

Playing Music in Java

We have provided a SequencePlayer class for you that enables you to play music in Java relatively simply.  You should be able to cut and paste the following code to play a single note.  After you have let Eclipse fix the warnings for you, it should play a note.

SequencePlayer sp = new SequencePlayer(16, 0);  // 16 = ticks per QUARTER note
sp.setTicksPerDefaultNote(16);  // 16 = ticks per DEFAULT note
sp.setTempo(140);   // 140 quarter notes (beats) per minute
Note g = new Note('G', 0, 0, 1.0, false);
sp.addNote(g, 10);   // 10 = start tick of note.  Tick of next note is returned.
sp.play(ISequencePlayerStatus.NULL);   // No-op end-of-song cmd supplied.

The sequence player constructor takes two arguments, the first is the number of ticks per quarter note and the second is a number corresponding to an instrument (which we will ignore for now).  The Java MIDI sequencer operates based on a notion of ticks.  All events happen at some tick.  These ticks occur at a regular interval based on the tempo.  So, in the above code, there are 16 ticks per quarter note and the tempo is 140 quarter notes per minute (note the implicit conversion from default notes per minute as specified in the L header to the quarter notes per minute as required by the sequence player!).  So, there are 140 * 16 = 2240 ticks per minute.

Ticks are numbered starting at 0 at the beginning of the song and go until the song ends.  Whenever you "add a note" to the sequence player, you must tell it what tick you want the note to be played at.  Usually, this will just be the tick at which the previous note ends.  To make it easy for you, the addNote method returns the tick when the added note will end.

You will notice that the addNote method unsurprisingly takes an object of type Note.  A Note is specified by a letter (A-G), the octave (0 is middle C), a number representing whether the note is flat (-1), sharp (+1), or natural (0), the duration (as a multiplicative factor of the default note length), and a flag indicating whether the note is specified in the score as a natural note.  The last argument has to do with the key signature and can usually be set to false and ignored.  So, the code above should play a G in the middle C octave for 1 default note length.  Notice that we therefore must tell the sequence player how long a default note should be.  Here we specify it as the duration of a quarter note.

Parsing ABC Files

In order to actually use the sequence player to play music, we first need to get the information out of the abc files and into some format that you can more readily use.  The abc format is actually very difficult to interpret properly.  We have provided a parser, ABCParser, that you can use to read an abc file and convert it to an IPhrase object.  The UML diagram for the IPhrase class hierarchy is shown on the assignment page.  You will be responsible for writing visitors to print and play the music that is returned from the parser as an IPhrase object.

Using the ABCParser is relatively straightforward. When you construct a new parser, you simply give it a filename as an argument.  That filename should be relative to the "src" folder (technically, relative to the bin folder, but it ends up being the same thing).    Start the name of the file with a forward, slash, as this indicates the filename is relative to the root of the classpath (i.e. the bin folder). So, if you have the songs checked out from subversion, you might use "/songs/twinkle.abc".  This will build a new parser, but it does not actually parse the file.  To do that, you simply call the parse method which returns the parsed IPhrase object.

You will also find the ABCUtil class helpful.  You can use the method ABCUtil.Singleton.getFileContents method to get the contents of the specified file (which you could then print, if you wanted).  The ABCUtil class also has other methods to help you deal with the abc file headers as well as retrieve the contents of a text file as a String.

Exercises

Today, we are going get things set up to play some simple music:

  1. Follow the instructions for setting up your Eclipse project for HW06.
  2. Create a new package and put a new class with a main method in it.
  3. Cut and paste the above code to play one G note.
  4. Add a sequence of notes and get them to play one after the other.
  5. The provided "songs" folder should already have a file called "minuet.abc" which the same as the above abc code.   If you are missing that file, simply cut and paste the above abc code into a file called "minuet.abc".
  6. Use getFileContents to print the file to the console.
  7. Parse the abc file to make sure that you are able to read the file correctly.    You will not be able to print out the resultant IPhrase object because you have not yet installed one of the commands used internally to implement the toString() method.   See HW06 for instructions on how to complete the printing capability of IPhrase.

Command-based Visitors

A visitor is conceptually a collection of commands that implement an algorithm on a data structure.  The data structure itself does not know (or care) what the algorithm is, it blindly executes the commands in the visitor to accomplish it.

In the case of our music structure, you will be writing a visitor to visit IPhrase objects in order to play the music.  Therefore, you will need to write commands that can process all of the different concrete types of IPhrase objects.  This will require the construction of commands of the following type:

public interface IPhraseVisitorCmd {
   public Object apply(String id, IPhrase host, Object... params);
}

You will have to install an IPhraseVisitorCmd for each type of IPhrase host you want to process.  When you build the visitor, you have access to the entire context at the time you are building it.  This means that you are closing over that entire context.  Therefore, you can build commands at that time that have access to things such as the visitor itself!

We have provided you with an IPhraseVisitor interface and an APhraseVisitor abstract class that implements the IPhraseVisitor interface. You should use these to construct your visitor.  All implementations of IPhrase expect an IPhraseVisitor.   So, you might build your visitor as follows:

IPhraseVisitor v = new APhraseVisitor(defaultCmd) {
    {
     // Do what you want in the constructor
    }
};

Inside the constructor you could create and add commands (using the addCmd method of APhraseVisitor).  Or you can do so after the visitor is created.  It is up to you and depends on what you are trying to accomplish.  Notice the odd syntax for the constructor in the anonymous inner class that extends APhraseVisitor.  This is because the class has no name, so there is no name to use for the constructor.  Read more about this here.

Note that you can also add commands later!  Maybe you want to add a command because someone pushed a button in a GUI.  Or maybe you want to add a command in response to something that happens inside another command.  Anywhere that you have access to the visitor itself, you may add another command.

These visitor commands actually run when you invoke the execute method of an IPhrase object with a visitor object.  All IPhrase objects must implement this method:

public interface IPhrase {
    public Object execute(IPhraseVisitor algo, Object... params);
}

All concrete classes that implement IPhrase will then implement the execute method as follows:

public Object execute(IPhraseVisitor algo, Object... params) {
    return algo.caseAt(Note.ID, this, params);
}

This is the execute method of the Note class.  You will notice it uses a static final field called ID for its ID value.  Note.ID is simply set to its class name ("Note").   The reason for using this field instead of the string is to emphasize the fact that the Note class doesn't actually care what the ID value is, just that it is the invariant value associated with the Note class.  While this is not necessary, it makes things easier to understand.  However, there are situations in which this is not what you want to do, because you want to invoke different visitor commands.  The caseAt method of the APhraseVisitor will then invoke the apply method of the command that was installed for the Note.ID (or the default command if no such command was ever installed).  (See the IPhrase system documentation to find out what case each host calls on its visitors.)

When processing a recursive data structure, it is likely that the commands themselves will need to invoke the execute method of it's components in order to properly implement whatever algorithm the visitor is designed to implement.

Exercise

Let's create a very simple visitor that only knows how to process a Note object:

  1. Instantiate a SequencePlayer and set some defaults.
  2. Create an IPhrase object that is a single note.  (i.e., IPhrase note = new Note(...);)
  3. Create an IPhraseVisitor with a simple command to process notes.
  4. Execute the visitor on your note.
  5. Play the resulting "music".

WARNING: you will not be able to use this visitor in your homework. Actually processing a note requires adjusting the note based on the key signature. So your command will have to be slightly more complex.  Also, when can you install a command for processing notes that knows about the key signature of the piece?


© 2018 by Stephen Wong