In this CS2 assignment, students create a musical instrument with an simple and intuitive graphical interface. In doing so, they learn how to implement and be a client of a class and how to work with 1D arrays. The assignment covers many of the topics traditionally covered by a dynamic arrays data structure assignment, but has broad appeal to both majors and nonmajors.
This assignment asks students to implement a version of the Tonematrix developed by André Michelle. The Tone Matrix visually appears as a 2D grid of lights. Each light in the grid represents a musical note at a specific point in time. Lights further to the left are played before lights further to the right. The row of a light determines which note is played: lights toward the bottom of the Tone Matrix have a lower pitch than lights toward the top. All lights on the same row play the same note and correspond to playing that note at different points in time. If multiple lights are turned on in the same column, they will be played simultaneously when their column is played.
Here's a video demonstrating what students create:
The notes in the Tone Matrix are drawn from a major pentatonic scale, so pretty much any combination of lights will sound good. You do not need to be a musician to make beautiful music with the result. You can literally drag the mouse around randomly on the grid and get a nice melody.
This assignment is designed to serve as a student's first introduction to class implementation and raw arrays. It's therefore broken down into a series of smaller steps, each of which reinforces some key concept in classes and arrays.
Students begin by implementing a StringInstrument type that uses the Karplus-Strong algorithm to simulate a vibrating string. (This portion of the assignment is adapted from the Guitar Heroine Nifty Assignment developed by Kevin Wayne in 2012.) This involves allocating an array inside a constructor, maintaining a cursor inside that array, and modifying the array while handling wraparound. The intent is to give students a chance to warm up to class implementation and arrays without the code being too complex. The trickiest bit is in the wraparound, which can be handled either using the mod operator or with simple if statements.
Next, students implement a ToneMatrix type. This type maintains the state of the lights inside the grid, handles mouse interactions, generates sound samples, and handles changes to the size of the light grid.
The first step in implementing ToneMatrix is to maintain a grid of lights. Although the light grid is a 2D object, we have students represent the lights as a 1D boolean[] array in row-major order. This decision is partially for technical reasons (the original version of this assignment was given in C++, where dynamically-allocating 2D arrays with new requires introducing students to double pointers) and partially to give students more practice with array copying (described later).
In addition to the grid of lights, students maintain an array of StringInstruments. They thus get experience acting as a client of a type they just implemented.
Next, students implement a pair of methods (mousePressed and mouseDragged) that handle mouse interaction. These methods change the states of the lights in the grid. The purpose of this exercise is both to get students comfortable with row-major order calculations and to expose students to the idea of using fields to remember information from one method call (mousePressed) so that it can be retrieved in another (mouseDragged).
After that, students implement the logic to generate sound samples from the Tone Matrix. This works by summing up the generated samples from each row's StringInstrument and periodically plucking those strings based on which lights are enabled. This again requires students to determine what extra information they need to store in the type (a cursor to track which column is active and a timer to determine when it's time to move to the next column) and is where students first make calls to the StringInstrument type they implemented earlier.
Finally, students implement a resize() method that resizes the underlying grid. Conceptually, this involves expanding or contracting the grid of lights, preserving the contents of the grid, and updating the array of instruments by either shrinking or expanding it. Because the grid is represented as a 1D array in row-major order, the copy logic here is slightly nontrivial; students can't simply copy from index i of the old array to index i of the new array.
We provide students with a test-driver framework that includes 20+ prewritten test cases. Students are then asked to contribute their own test cases for specific parts of the assignment. You can share all the tests with students as we do when teaching the course, or you could hold some of them back for use in an autograder.
| Summary | Students create a musical instrument with a simple and intuitive graphical interface. In doing so, they learn how to implement their own classes, serve as clients of a class, and perform various algorithms on 1D arrays. |
| Topics | Arrays, class design, graphics, sound. |
| Audience | CS2, though could easily be adapted for late in a CS1 course. |
| Difficulty | Low to medium. Students report this assignment to be very manageable. |
| Length | We ask students to complete this assignment in one week. |
| Strengths | This assignment provides practice with class design and 1D arrays in a context that has appeal beyond pure computer science. The finished product is just plain fun to play with and serves as a motivator for students to get the assignment completed. |
| Weaknesses |
This assignment does not explore big-O notation as would be expected in a traditional data structures assignment
(e.g. implementing a stack, queue, or list). While it does involve working with arrays, the array manipulations
are not as complex as, say, removing the middle element of a list.
If you hold office hours for students and a lot of people attend, be prepared to hear a lot of music playing at the same time. Some people may find this a bit distracting or annoying. Others will find it charming. Before students can start coding, they need to learn a bit about sound processing and internalize the details of the various algorithms. There's therefore a good amount of reading required. |
| Dependencies | The assignment requires a sound and graphics package. The Java version of this assignment uses the packages that natively ship with the language. A version in C or C++ would require a third-party sound and graphics library (such as Qt). |
| Variants |
There are many, many ways this assignment could be adapted. Here's a sampler:
|
| Teaching Notes |
This assignment was designed to be easily unit-tested. Unlike the "true" Karplus-Strong algorithm, which involves the
use of white noise, we ask students to use square waves. This makes it a lot easier to unit-test the string simulation,
as the values stored inside the relevant arrays can be determined exactly. We also specify the exact names and types
of some key fields in each class so that we can use invasive (white-box) tests.
The version you're seeing here is a Java port of the original assignment, which was done in C++. That version of the assignment covers these same points but also serves as an introduction to working with low-level memory management and the sorts of errors that can arise there (uninitialized values, memory leaks, dangling pointers, etc.). That increases the difficulty of the assignment compared to this one. Due to small rounding errors in sizing the arrays, the notes for each row are ever so slightly out of tune. Students with a good ear for music may pick up on this. It's a great launching-off point for having students improve on the base assignment. |