CS173: Intro to Computer Science - MIDI Audio (100 Points)

Assignment Goals

The goals of this assignment are:
  1. To implement functions to facilitate code re-use
  2. To invoke functions that may accept parameters

Background Reading and References

Please refer to the following readings and examples offering templates to help get you started:

The Assignment

The Musical Instrument Digital Interface (MIDI) standard enables your computer to play synthesizer music that you can control programmatically.

Adding the MIDI library to your project

We can download libraries of new functions that we can use in our projects; these are packaged conveniently in a single file called a jar file. jar files added to the libs directory of your project will be available for use in your code. Download the MIDILib.jar file into a location you’ll remember (your Downloads directory is fine, or you can use a subdirectory of your project called libs).

To use this jar, after creating a Java project in NetBeans as usual, right-click on the project in your left project navigation pane (you can click the Window menu and select Projects if you don’t see this), and click Properties, as shown:

Click the Libraries category on the left side of the window that appears. Then click, the + sign next to the word Classpath, and click Add JAR/Folder, as shown:

Finally, navigate to the jar file you downloaded earlier, and double click on it to add it to your project. Click OK to close the window, and you’re done!

To learn what functionality our new jar file has provided us, see the Javadoc for more information about the library [1, 2, 3, 4, 5, 6].

Instantiating a MIDIPlayer object

The MIDI library you imported includes a MIDIPlayer class that you can instantiate to help you play sounds through your computer. You can create one like you would any object variable, with the new keyword:

MIDIPlayer player = new MIDIPlayer();

Be sure to add the import for MIDIPlayer to your program (we’ll go ahead and add the imports for the other library classes that we’ll need later as well):

// These lines go above and outside of your public class line, right below the package line
// They give you access to our library functions in your code

import com.MIDI.MIDIPlayer;
import com.MIDI.Instruments;
import com.MIDI.Notes;

Open the Javadoc to find out about the method interface this library provides. There are a number of methods you can call inside the MIDIPlayer class. What are a few of them, and, in your own words, what do they do?

In addition, there are two classes Instruments and Notes which contain constants that represent some of the instruments, notes, and note durations that you can play. These include Notes.NOTE_C4 for a C4 note, Instruments.GUITAR for a guitar, and Notes.NOTE_QUARTER which specifies a quarter note duration. Click on the Instruments and Notes javadoc classes and make a note of some of the instruments and notes that are available to you. Because it’s a synthesizer, some of the “instruments” are actually creative sound effects - feel free to have fun with this!

Javadoc documentation is really helpful because it enables you to observe functionality made available by a library “at a glance.” You can see how methods are named, information about how they are invoked, and what parameters they require. These are generated programmatically through comments that you write throughout your code, and this is prescribed by the Style Guide.

Part 1: Playing a Note

Let’s test the interface and the computer sound setup by playing a note using a synthesized instrument. The MIDI interface works by defining a number of “channels” on which you can play notes, chords, and sounds. You can think of a channel as a particular instrument. In fact, you can assign an instrument to a channel. The default channel is “channel 0,” but you can have several more of them, referenced by number. In this assignment, we will stick with the default channel, but feel free to see me if you are interested in extending this to multiple channels and instruments.

To do this, you can first set an instrument on the default channel by calling the following method of the MIDIPlayer library:

player.setInstrument(Instruments.HARP);

Here, Instruments.HARP is the instrument that you want to use. This will assign the harp to the default channel 0. To play a note, you can call the following function:

player.playNote(Notes.NOTE_C4, Notes.NOTE_QUARTER, Notes.DEFAULT_INTENSITY);

This will play a C4 quarter note, using a default intensity value.

Try it and verify that you get sound output. Try changing this to play an A4 whole note at half intensity (you can divide Notes.DEFAULT_INTENSITY by 2). The A4 note is a higher pitched note, and this is because A4 is a higher frequency vibration which your ear perceives as a higher pitch. In fact, A4 is defined here as 440 Hz, and, consequently, C4 (known as “middle C”) is ~262 Hz. Every note is defined by its own fundamental frequency. Incidentally, there is repitition here: if A3 is 220 Hz, what frequency do you think C3 is? How about A2? You can try listening to some notes and frequencies using this online tone generator.

Part 2: Playing a Song

Using the playNote function, try playing the first part of the “alphabet song,” (actually, it’s basically Twinkle Twinkle Little Star) which plays the following quarter notes: C4 C4 G4 G4 A4 A4 G4.

Now, try adding the second part: F4 F4 E4 E4 D4 D4 D4 D4 C4. Note that there are more notes in this part than the first: to align the time, the D4 notes should be eighth notes instead of quarter notes.

There should have been a brief pause between the first part (C4 C4 G4 G4 A4 A4 G4) and the second part (F4 F4 E4 E4 D4 D4 D4 D4 C4). The following function call will introduce a “rest” or pause (in this case, equivalent to an eighth note duration) in between the parts:

MIDIPlayer.rest(Notes.NOTE_EIGHTH);

Try inserting this in between the parts.

You might notice that we are calling MIDIPlayer.rest() while we call player.playNote() elsewhere. Both come from the same code library, so what’s the difference? Take a look at the Javadoc and notice that the rest() method is a static function. This means that the rest() method is not specific to a particular MIDIPlayer, and so it belongs to the class rather than to the object. A “rest” does not actually play a sound, so you are not using the variables and methods of the MIDIPlayer object variable to accomplish this. It’s generic to any MIDIPlayer, so it’s not necessary to call the function on any particular variable. Notice that the constants in Instruments and Notes are also static - they aren’t unique to any one MIDIPlayer - every student is using the same ones regardless of the computer they’re using or the program they’re writing. As a rule of thumb: if you don’t need to call new to get a variable first in order to use a method or variable from that class, it could probably be declared static.

Finish the alphabet song. Here are the notes, which are quarter notes unless otherwise specified (I suggest adding an eighth note duration rest in between each part):

C4 C4 G4 G4 A4 A4 G4(1/2 note)
F4 F4 E4 E4 D4(1/8th note) D4(1/8th note) D4(1/8th note) D4(1/8th note) C4(1/2 note)
G4 G4 F4 (rest 1/16th note) E4 E4 D4(1/2 note)
G4(1/8th note) G4(1/8th note) G4(1/8th note) F4 (rest 1/16th note) E4 E4 D4(1/2 note)
C4 C4 G4 G4 A4 A4 G4(1/2 note)
F4 F4 E4 E4 D4 D4 C4(1/2 note)

For fun, try changing the instrument to a guitar before playing the song:

player.setInstrument(Instruments.GUITAR);

Compare the result to Twinkle Twinkle Little Star (there is a MIDI rendition on the Wikipedia page that you can play!). There are a few small differences to make the alphabet fit into the song - can you spot them? What would you change in your program to match Twinkle Twinkle Little Star?

Part 3: Using Functions to Enable Code Re-Use and Repitition

Did you notice that part of the song repeats? The first part is exactly the same as the fifth part! You can avoid having to do this extra work of copying and pasting your code using functions. In addition to saving you some typing, functions a standard good practice in case it becomes necessary to change that part of the song in a way that affects every use of the part. With a function, you can change the song once, and have it play correctly both times that you use it.

Define a function playMainPart() that contains the code to play the C4 C4 G4 G4 A4 A4 G4 part, and modify your program so that you call this function each of the two times you want to play that part in the song.

This function should accept one parameter of type MIDIPlayer, and when you call it from main, pass the player variable to it. This way, your function will have access to the player variable!

Part 4: Creating Your Own Function to Generate Note Values

The Notes and Instruments classes provide a number of sample notes and instruments that you can play, but you may need one that is not provided there. These notes and instruments (for example, NOTE_C4 and GUITAR) are just variable names that correspond to int variables, so you can provide your own numeric values here if you’d like to specify other notes or instruments! For example:

player.setInstrument(25); // steel acoustic guitar
player.playNote(101, Notes.NOTE_QUARTER, Notes.DEFAULT_INTENSITY); // F7 note

You will find a complete list of notes (the numeric value is the first column, and the note is the fourth column), and instruments (see the instrumentNames variable halfway through the code listing), in case you would like to customize. Note, however, that this is completely optional: it is perfectly OK to use only the notes and instruments I’ve provided in the library. I provide these references only for your entertainment in case you would like to get creative here!

Write your own function called generateNote that accepts a double parameter for the frequency (for example 880.0), and returns the MIDI note number (in this example, 81) from the first column of this list of notes. Call this function from main and store the result in a variable.

The formula you will implement in your generateNote function to convert a frequency f to a MIDI note number n is as follows:

\(n = 12 log2(\frac{f}{440.0}) + 69\)

Java’s Math.log() function uses base 10 instead of base 2, so you can do a simple conversion so that you can use the Math.log() function in your implementation:

\(n = 12 \frac{log(\frac{f}{440.0})}{log(2)} + 69\)

Then, pass that variable to the first parameter of playNote like this:

player.setInstrument(25); // steel acoustic guitar
int note = generateNote(880.0); // you'll write this function!
player.playNote(note, Notes.NOTE_QUARTER, Notes.DEFAULT_INTENSITY); // F7 note

Part 5: Make Up Your Own Song

Every answer is correct - be creative and play some notes. Explore the Notes class and the Instruments class for a few examples that you can play. Let me know if you come up with something fun that you’d be willing to share - we can take a few minutes in class sometime for a few “code demonstrations.”

If you’d like to use your custom notes, you can look up the MIDI note numbers under the first column in this list of notes, and pass those to the playNote function, or call generateNote to obtain them given a frequency, if you are musically inclined!

Part 6 (Optional): Just for Fun…

When we explore arrays and loops, we will see how we can represent a song like this using a single variable (an “array”) that represents the entire collection of notes, and this can be played using a single call to the function to play all the notes. “Loops” can iterate over these collections, one by one, and automatically play each note, so that you don’t have to copy your code like you did here.

If you’d like to try out chords using arrays, try running the following code in your program:

player.setInstrument(Instruments.GUITAR);
       
int chordNotes[] = {Notes.NOTE_C4, Notes.NOTE_E4, Notes.NOTE_G4};
player.playChord(chordNotes, Notes.NOTE_WHOLE, Notes.DEFAULT_INTENSITY);

You may not be familiar with the notation yet, but we are asking the synthesizer to play three notes instead of just one. This is called a chord (in this case, the chord is comprised of the C4, E4, and G4 notes). chordNotes is called an array: it is a variable that contains a collection of multiple values, rather than just a single value.

If you are familiar with older versions of Microsoft Windows, you might be familiar with these sounds (OK, this rendition is far from faithful, but I am admittedly not musically inclined! You may need to use your imagination here…):

player.setInstrument(Instruments.BRIGHT_PIANO);
        
player.playNote(Notes.NOTE_D4, Notes.NOTE_QUARTER, Notes.DEFAULT_INTENSITY);
player.playNote(Notes.NOTE_D3, Notes.NOTE_SIXTEENTH, Notes.DEFAULT_INTENSITY);
player.playNote(Notes.NOTE_A3, Notes.NOTE_SIXTEENTH, Notes.DEFAULT_INTENSITY);
player.playNote(Notes.NOTE_G3, Notes.NOTE_EIGHTH, Notes.DEFAULT_INTENSITY);
player.playNote(Notes.NOTE_D3, Notes.NOTE_EIGHTH, Notes.DEFAULT_INTENSITY);
player.playNote(Notes.NOTE_D4, Notes.NOTE_EIGHTH, Notes.DEFAULT_INTENSITY);
int[] notes = {Notes.NOTE_A4, Notes.NOTE_A3};
player.playChord(notes, Notes.NOTE_HALF, Notes.DEFAULT_INTENSITY);
        
MIDIPlayer.rest(Notes.NOTE_WHOLE);
        
player.playNote(Notes.NOTE_G4, Notes.NOTE_EIGHTH, Notes.DEFAULT_INTENSITY);
player.playNote(Notes.NOTE_D4, Notes.NOTE_EIGHTH, Notes.DEFAULT_INTENSITY);
player.playNote(Notes.NOTE_G3, Notes.NOTE_EIGHTH, Notes.DEFAULT_INTENSITY);
player.playNote(Notes.NOTE_A3, Notes.NOTE_HALF, Notes.DEFAULT_INTENSITY);
        
MIDIPlayer.rest(Notes.NOTE_WHOLE);

Take a look at the playChord function call. What is different about it from the calls to playNote? We know that it is playing multiple notes together (in this case, A4 and A3). How do we know this?

Finishing Touches and Writeup

Don’t forget to test your program with several different inputs to help verify that things work the way you expect! Think in terms of trying to break your program; if it’s really hard to “trick” your program into getting the wrong answer, you’ve probably done a good job making your code robust.

Also, check the Style Guide to make sure that you’ve written high quality code; make sure your code is “readable,” well indented, uses good variable names, and includes good comments throughout the program.

When you’re done, write a README for your project, and save all your files, before exporting your project to ZIP. In your README, answer any bolded questions presented on this page. In addition, write a few paragraphs describing what you did, how you did it, and how to use your program. If your program requires the user to type something in, describe that here. If you wrote functions to help solve your problem, what are they, and what do they do? Imagine that you are giving your program to another student in the class, and you want to explain to them how to use it. What would you tell them? Imagine also that another student had given you the functions that you wrote for your program: what would you have wished that you knew about how to call those functions?

Exporting your Project for Submission

Here is a video tutorial describing how to write a README for your project, and how to export it. Be sure to save your README file before exporting the project, so that your work is included in the submission!

Submission

In your submission, please include answers to any questions asked on the assignment page in your README file. If you wrote code as part of this assignment, please describe your design, approach, and implementation in your README file as well. Finally, include answers to the following questions:
  • Describe what you did, how you did it, what challenges you encountered, and how you solved them.
  • Please answer any questions found throughout the narrative of this assignment.
  • If collaboration with a buddy was permitted, did you work with a buddy on this assignment? If so, who? If not, do you certify that this submission represents your own original work?
  • Please identify any and all portions of your submission that were not originally written by you (for example, code originally written by your buddy, or anything taken or adapted from a non-classroom resource). It is always OK to use your textbook and instructor notes; however, you are certifying that any portions not designated as coming from an outside person or source are your own original work.
  • Approximately how many hours it took you to finish this assignment (I will not judge you for this at all...I am simply using it to gauge if the assignments are too easy or hard)?
  • Your overall impression of the assignment. Did you love it, hate it, or were you neutral? One word answers are fine, but if you have any suggestions for the future let me know.
  • Any other concerns that you have. For instance, if you have a bug that you were unable to solve but you made progress, write that here. The more you articulate the problem the more partial credit you will receive (it is fine to leave this blank).

Assignment Rubric

Description Pre-Emerging (< 50%) Beginning (50%) Progressing (85%) Proficient (100%)
Algorithm Implementation (60%) The algorithm fails on the test inputs due to major issues, or the program fails to compile and/or run The algorithm fails on the test inputs due to one or more minor issues The algorithm is implemented to solve the problem correctly according to given test inputs, but would fail if executed in a general case due to a minor issue or omission in the algorithm design or implementation A reasonable algorithm is implemented to solve the problem which correctly solves the problem according to the given test inputs, and would be reasonably expected to solve the problem in the general case
Code Indentation and Spacing (10%) Code indentation and spacing are generally inappropriate or inconsistent Code indentation or spacing are generally appropriate but inconsistent in a few isolated instances Code indentation or spacing are appropriate or consistent, with minor adjustments needed Code indentation and spacing are appropriate and consistent
Code Quality (10%) Prior code quality feedback and style guide standards are not reflected in the submitted code to a great extent Code quality conforms to several standards in the course Style Guide, and progress is demonstrated in improving code quality from prior feedback Code quality conforms to the standards in the course Style Guide to a great extent, with a few identified areas of improvement Code quality substantially conforms to the standards in the course Style Guide
Code Documentation (10%) Code commenting and structure are absent, or code structure departs significantly from best practice Code commenting and structure is limited in ways that reduce the readability of the program; specifically, javadoc style comments are present for some functions Code documentation is present that re-states the explicit code definitions Code is documented at non-trivial points in a manner that enhances the readability of the program; specifically, javadoc style comments are present for all functions
Writeup and Submission (10%) An incomplete submission is provided, or the README file submitted is blank The program is submitted, but not according to the directions in one or more ways (for example, because it is lacking a readme writeup or missing answers to written questions) The program is submitted according to the directions with a minor omission or correction needed, including a readme writeup describing the solution and answering nearly all questions posed in the instructions The program is submitted according to the directions, including a readme writeup describing the solution and answering all questions posed in the instructions

Please refer to the Style Guide for code quality examples and guidelines.