You now have a working string instrument simulation. Your next task is to implement the ToneMatrix type, which will maintain a grid of lights, respond to user input, determine when to pluck each string, and decide what data gets sent to the computer speakers.
Here’s how ToneMatrix is defined:
class ToneMatrix {
public:
/* Creates a Tone Matrix whose grid is gridSize x gridSize and
* where each light in the matrix has size lightSize x lightSize.
* The lightSize parameter just determines the size, in pixels,
* of each light in the grid.
*/
ToneMatrix(int gridSize, int lightSize);
/* Frees all memory used by this Tone Matrix. */
~ToneMatrix();
/* Reacts to the mouse being pressed at a given position on
* the screen, toggling the state of the light the mouse
* hovers over.
*/
void mousePressed(int mouseX, int mouseY);
/* Reacts to the mouse being dragged from its previous
* position to position (mouseX, mouseY). The light at the
* new position has its state updated appropriately. Details
* are given later.
*/
void mouseDragged(int mouseX, int mouseY);
/* Draws each light in the Tone Matrix. Details are
* given later.
*/
void draw() const;
/* Advances time forward one step, returning the next sound
* sample to play. Details are given later.
*/
Sample nextSample();
/* Described later. */
void resize(int newGridSize);
private:
/* Number of lights in one side of the grid (e.g. in a 16 x 16
* grid, this will be 16).
*/
int _gridSize;
/* How big each light in the tone matrix is. */
int _lightSize;
/* The 2D grid of lights in the matrix; details are given below. */
bool* _grid;
/* The instruments corresponding to each row. */
StringInstrument* _instruments;
/* Additional data members and member functions of your own choosing;
* you're free to add extra information here if you'd like. Details are
* given below. Friendly reminder to follow the convention of adding an
* underscore in front of any private member variables you declare!
*/
};
We’ve broken the task of implementing this type down into four smaller milestones to be completed in sequence.
Milestone 1: Implement Constructor, Destructor, and mousePressed()
Your first task is to implement the constructor ToneMatrix::ToneMatrix, the destructor ToneMatrix::~ToneMatrix, and the function ToneMatrix::mousePressed.
As a reminder, the Tone Matrix is a square grid of lights, all initially off. The gridSize parameter to the constructor determines how big the grid of lights should be; specifically, the grid will have dimensions gridSize × gridSize. You can assume gridSize > 0 and don’t need to call error() if this is not the case. You’ll need to store the value of gridSize for later in the _gridSize data member; note that you should store gridSize (the length of one side of the grid) here rather than the total number of lights.
When drawn to the screen, each of those lights renders as a square of some size. The dimensions of each light is specified as an argument to the ToneMatrix constructor. Store this information in the _lightSize data member. You’ll need this later to react to the mouse and draw the Tone Matrix on screen. You can assume lightSize > 0 and don’t need to call error() if this is not the case.
Your constructor also needs to store the state of each of the gridSize × gridSize lights. In the past, you’d do this with a Grid<bool>, where true indicates a light is on and false indicates it’s off. However, on this assignment, you are doing all your own memory management, so Grid and other container types are off-limits. Instead, you will store the state of the lights as a standard array of bools that you will dynamically allocate and deallocate. The _grid data member of ToneMatrix will store the pointer to this array.
You might be wondering how you would represent a two-dimensional grid using a one-dimensional array. This can be done by as follows: the first gridSize elements of the array correspond to the first row of the matrix, the next gridSize elements correspond to the second row of the matrix, etc. [1]. Here’s an example of this idea applied to a 3 × 3 grid:
There is a nice formula that maps from a row/column position in the 2D grid to an index in the 1D array. Given an n × n grid, the entry in row r and column c is at index nr + c. One way to think about this: increasing or decreasing the column index simply shifts forward or backward one position in the array, while moving up or down to a different row requires jumping over n consecutive elements.
You will also need to initialize an array of StringInstruments in the ToneMatrix constructor. The Tone Matrix plays music by associating each row with a StringInstrument. The _instruments data member has type StringInstrument*, perfect for pointing at an array of instruments. To determine the frequency of the instrument of a given row, call the frequencyForRow() function defined in ToneMatrix.cpp.
Now, on to the destructor. As usual, this should simply clean up all memory allocated by your ToneMatrix type.
Finally, let’s talk about mousePressed(). This function is called by the graphics system whenever the mouse button is pressed inside the Tone Matrix. The function’s arguments, mouseX and mouseY, give the position where the mouse is pressed, relative to the upper-left corner of the Tone Matrix. For example, if the mouse is pressed at the top-left corner of the Tone Matrix, mouseX and mouseY will be 0. If mouseX is 137 and mouseY is 106, it means that the mouse was pressed 137 pixels to the right of the top-left corner of the matrix and 106 pixels down.
Your mousePressed() function should do the following. First, you should figure out, based on mouseX and mouseY, which light in the grid the mouse was pressed on. As a hint, you know how big each cell is (it’s a square whose size is _lightSize × _lightSize), and you should not need to do any complicated math. Next, you should toggle the state of that light: turn the light on if it’s off, and turn the light off if it’s on.
We have provided you with a battery of test cases you can use to validate your implementation. Because this is the first milestone in working with the ToneMatrix type, you don’t need to write any of your own test cases here, though we think you may find it useful to do so. We suggest you read over our test cases to see how we call the functions of ToneMatrix to make changes to the grid of lights.
To summarize, here’s what you need to do:
-
Implement the
ToneMatrixconstructor, destructor, andmousePressed()functions inToneMatrix.cpp. -
Use our provided test cases to validate that your code works correctly. You should pass all the tests for Milestone 1 and fail all the other tests.
Some notes on this problem:
-
You must not use any container types (e.g.
Grid,Vector,string, etc.) when coding up these functions. (More generally, these types are not permitted in this assignment.) -
Remember that arrays of
bools in C++ do not have thosebools default tofalse. Rather, their values are unpredictable and based on whatever happened to be in memory at the spot where the array was created. -
Be careful working with row/column coordinates and (x, y) coordinates. In row/column coordinates, the row corresponds to the
ycoordinate and the column corresponds to thexcoordinate. It’s easy to get these backwards. -
You can assume that the mouse is indeed hovering over a light; you don’t need to worry about negative
xorycoordinates, about the mouse being off the right or below the bottom of the Tone Matrix, etc. -
You are welcome to add extra helper functions to the
ToneMatrixtype if you would like. However, for this milestone, do not add any new data members. -
Our SimpleTest framework is not capable of detecting memory errors with primitive types. Therefore, double- and triple-check your code to make sure that your destructor frees the array of
bools you allocate in the constructor, that you aren’t allocating more arrays than you need, etc. Learning to check your code for memory leaks is an essential skill as a C++ programmer.
Milestone 2: Implement mouseDragged()
Your next task is to implement ToneMatrix::mouseDragged(). This function is called by the graphics system whenever the mouse is moved over the Tone Matrix with the mouse button down. The mouseDragged() function is only called after an initial mousePressed() is called, and it’s assumed that each mouseDragged() is part of a sequence of mouse movements that started with the most recent call to mousePressed().
Like mousePressed(), mouseDragged() takes in two integers, mouseX and mouseY, indicating where the mouse was dragged within the Tone Matrix, relative to the Tone Matrix’s upper-left corner. Like mousePressed(), mouseDragged() updates the state of the light directly under the mouse. However, the way mouseDragged() does this is different. Specifically:
-
If the last time
mousePressed()was called a light was turned on, then the light under the mouse inmouseDragged()turns on. -
If the last time
mousePressed()was called a light was turned off, then the light under the mouse inmouseDragged()turns off.
This has a nice, intuitive feel to it: if the user clicks on a light to turn it on and then drags the mouse around, every light they move the mouse over will turn on. If they click on a light to turn it off and drags the mouse around, then every light they move the mouse over will turn off.
In order to do this, mousePressed() will need to communicate information into mouseDragged() so mouseDragged() knows what to do. The proper way to do this is to add one or more additional data members (fields) to the ToneMatrix type. The mousePressed() function can then write information to those data members that is then read later by mouseDragged(). We will leave it up to you to decide what information you want to remember this way; there are many good choices.
As part of completing this milestone, you will need to edit functions you have already written in the last milestone. You will need to update mousePressed(), and, depending on your implementation, you might need to update the constructor or destructor. (The two simplest strategies we know of do not require updates to the constructor or destructor, though.)
Because you are changing code you have already written, you may accidentally introduce bugs into previously working code. Such bugs are called regressions. Fortunately, we’ve provided a lot of test cases for Milestone 1, so if your code changes break anything, there is a good chance that those test cases will flag the error. If you start failing tests for Milestone 1, investigate and see if you can identify what change you made that caused the regression.
We have provided a few test cases for this milestone, but they’re not as extensive as what we gave you for the first milestone. You will therefore need to write some test cases of your own to help check that your code works as intended. Specifically, you should write at least two new test cases: one that ensures mousePressed() updates the internal state of ToneMatrix appropriately, and one that ensures that mouseDragged() performs as intended. You may want to refer to our provided tests for guidance on how to design such test cases.
To summarize, here’s what you need to do:
-
Implement
mouseDragged()inToneMatrix.cpp. In doing so, you will need to add one or more data members in theprivatesection ofToneMatrix.h, and you will need to update your implementation ofmousePressed()from the previous milestone. -
Write at least one custom test case for
mousePressed()and at least one custom test case formouseDragged(), and ideally more. Use your tests, plus our provided test cases, to validate that your code works correctly. You should pass all the tests for the first two milestones and fail all tests for the later milestones.
Some notes on this problem:
-
You must not use any container types (e.g.
Grid,Vector,string, etc.) when coding up these functions. -
Be aware of variable shadowing. If you declare a data member inside a
classand then have a local variable with the same name inside a member function implementation, then the name of that variable will refer to the local variable, not the data member. For example, if you have a data member namednumSmilesin theprivatesection ofToneMatrixand declare a local variablenumSmilesinsidemousePressed, then any code using the namenumSmilesinsidemousePressedwill refer to the local variablenumSmilesrather than thenumSmilesin theprivatesection of the class. This is a key reason why we require you to use an underscore in front of all data member names: it ensures that it’s always clear whether we’re referring to a local variable or a data member from our class, and makes variable shadowing far less likely to occur, since none of your local variable names should start with underscores. -
You do not need to handle the case where
mouseDragged()is called beforemousePressed()is. However, your code formouseDragged()should work correctly if there were several different calls tomousePressed()that precede it. In that case, you should always focus on just the last call tomousePressed(). -
You should not need to write that much code inside
mousePressed(). If you find yourself having to significantly overhaulmousePressed(), it may indicate that you are overcomplicating the design. -
Feel free to add additional helper member functions to the
ToneMatrixtype. You may find that there is a good deal of code overlap betweenmousePressed()andmouseDragged(). -
The computer only periodically reads the state of the mouse and calls
mouseDragged(). This means that if the user moves the mouse quickly from one point to another, the mouse may appear to “jump” from the initial point to the destination. This can cause the mouse to skip over lights in the Tone Matrix. You do not need to worry about this; just havemouseDragged()focus on the light directly under the mouse when the function is called.
Milestone 3: Implement draw()
Your next task is to implement ToneMatrix::draw(), which draws the lights of the Tone Matrix. We have provided you with a function
void drawRectangle(const Rectangle& rect, Color color);
that draws the specified rectangle on-screen with the given color. The Rectangle type is a struct that holds the top-left (x, y) coordinate of a rectangle, as well as its width and height:
struct Rectangle {
int x, y, width, height;
};
ToneMatrix::draw() computes the rectangular bounding boxes for all the lights in the grid, then calls drawRectangle(), passing in those rectangles and the appropriate colors. (We have provided you two color constants, kLightOnColor and kLightOffColor, for this purpose.) You should assume the upper-left corner of the Tone Matrix is at position (0, 0) and that each light is a perfect square whose size is _lightSize × _lightSize.
Because it’s a bit tricky to test what rectangles were drawn on screen, we have provided you with test cases for this milestone. You do not need to write your own test cases, but you are welcome to do so if you’d like.
To summarize, here’s what you need to do:
-
Implement
draw()inToneMatrix.cpp. -
Use our provided test cases to validate that your code works correctly.
Some notes on this problem:
-
You must not use any container types (e.g.
Grid,Vector,string, etc.) when coding up this function. -
You should not need to update any functions you’ve written previously, and you should not need to add any new data members.
-
You are welcome to add new member functions if it would be helpful.
Milestone 4: Implement nextSample()
Your next task is to get the Tone Matrix to actually send sound to the computer speakers.
The hardware inside computer speakers periodically asks the computer for samples to play. In this assignment, the speakers will call the function ToneMatrix::nextSample() every time they need another sound sample to play. Whatever you return from this function will thus be played on your speakers. [2]
Like StringInstrument::nextSample(), this function needs to both determine what sound sample gets sent to the speakers and make appropriate updates to the internal state of the Tone Matrix. At a high level, nextSample() should do the following in the following order:
-
Determine whether it’s time to pluck more strings and, if so, pluck the appropriate strings.
-
Add up the samples returned by all the strings and send that to the speakers.
We’ll begin by discussing when to pluck the strings. As you saw from the constructor, the Tone Matrix maintains an array of StringInstruments, one per row of the matrix. Imagine there’s an arrow at the bottom of the Tone Matrix, initially pointing at the leftmost column. Most of the time nextSample() is called, that arrow remains in place. However, on the very first call, and every 8,192nd call after that [3], two things happen.
-
The Tone Matrix plucks strings. Specifically, it looks at the column pointed at by the arrow, finds all rows in that column that are lit up, and plucks the string corresponding to each of those rows.
-
The arrow moves one column to the right, wrapping around back to the start if necessary.
Next, let’s talk about how nextSample() determines its sound sample. After deciding whether to pluck any strings, the Tone Matrix calls nextSample() on each of its StringInstrument, adds the values together, and returns the sum as the sample to play. [4] Because calling nextSample() on an unplucked StringInstrument always returns 0, strings that haven’t been plucked will not contribute to the generated sound. Similarly, since vibrating strings quiet over time, strings that haven’t been plucked in a long time will contribute negligibly to the overall sound.
To make this work, you will need to add at least one (and possibly more) data members to the ToneMatrix class. We’re going to leave the design up to you; there are many options and you’re free to choose whichever one you’d like.
Because you will be adding new data members to ToneMatrix, you will need to add at least one test case to make sure that the new data members are properly initialized. You will also need to write at least one test case to make sure that nextSample() keeps track of the progress of time correctly and remembers what the current column is. We have provided some test cases that check whether the correct strings are plucked; the logic to check for this is much more involved and so we’ll take care of it for you.
To summarize, here’s what you need to do:
-
Implement
nextSample()inToneMatrix.cpp. This will require adding data members to theprivatesection ofToneMatrix.h, and may require you to update your constructor. -
Write at least one custom test case that checks whether your new data members are initialized correctly when a new
ToneMatrixis constructed. -
Write at least one custom test case that checks whether your implementation correctly keeps track of the current column and how many more calls to
nextSample()need to elapse before the column advances. -
Use our provided test cases, plus your own custom tests, to validate that your code works correctly.
Some notes on this problem:
-
You must not use any container types (e.g.
Grid,Vector,string, etc.) when coding up this function. -
You do not need to write much code here. If things are starting to seem complicated, pause, back up, and see if you can find an easier approach.
-
Feel free to write as many additional helper functions as you’d like.
Milestone 5: Implement resize()
Your final task is to implement the ToneMatrix::resize() function. This function takes as input an int called newGridSize and changes the dimensions of the grid of lights to that new size. (If newGridSize is zero or negative, you should call error() because that’s not a valid grid size.) For example, you can use this function to change the grid size from 7 × 7 to 4 × 4, or 16 × 16, etc. There are multiple steps involved here, each of which is described below.
First, because each row of the Tone Matrix has a StringInstrument associated with it, a call to resize() will entail changing the total number of StringInstruments. You should create a new, dynamically-allocated array of StringInstruments of the new size. (In lecture, when discussing how to implement the stack, we used separate size and capacity variables, leaving some buffer room for the stack to grow or shrink. Don’t do that here. Instead, just make a totally new array of StringInstruments of the indicated size.) Copy over as many of the existing StringInstruments as you can, and fill in the remaining rows with new StringInstruments whose frequencies are determined by the frequencyForRow function.
Here’s an illustration. Suppose the Tone Matrix was originally of size 4 × 4, meaning there are four StringInstruments. If we were to resize the Tone Matrix to size 8 × 8, then we would copy over the existing four StringInstruments, filling in the remaining rows with newly-created StringInstruments: [5]
On the other hand, if we were to resize the grid from 8 × 8 to 4 × 4, we would keep the first four StringInstruments and discard the remaining four.
Next, you will need to resize the underlying light grid. You should do as follows: any lights present in both the new and the old grid should retain their previous values, while any new lights will initially be turned off. For example, here’s what it would look like if we resized a 4 × 4 grid to a 6 × 6 grid:
On the other hand, if you were to resize a 6 × 6 grid to a 4 × 4 grid, you would discard the two rightmost columns and the two bottommost rows, essentially reversing the picture above.
As with resizing the list of instruments, we do not recommend you approach this by using separate size and capacity variables to manage a larger-than-necessary array. Instead, just allocate a totally-new array of bools and initialize it with the appropriate values.
Be careful how you copy old grid values to the new grid. Each grid is represented as a 1D array, and the formula that converts between 1D indices and 2D indices depends on the grid size. This means that when copying lights over, the source and destination indices will not necessarily be the same.
The last step in resizing the grid is to reset the left-to-right column scan done by nextSample(). In particular, whenever resize() is called, you should move the arrow sweeping across the columns so that it’s back at the leftmost column, and the very next call to nextSample() should be treated as one of those special, every-8,192th calls that plucks the strings. So for example, if nextSample() has been called 5,000 total times and then resize() is called, then the 5,001st call to nextSample() should pluck the strings of the first column, and every 8,192nd call after that will pluck the strings from the subsequent columns.
To recap, here’s what you need to do:
-
Implement
resize()inToneMatrix.cpp. -
(Optional, but recommended.) Add your own
STUDENT_TESTtest cases to ensure your implementation works correctly. -
Use our provided test cases, plus you own custom tests if you chose to write them, to validate that your code works correctly.
Some notes on this problem:
-
You must not use any container types (e.g.
Grid,Vector,string, etc.) when coding up this function. -
Don’t forget to call
error()if the new size is zero or negative. -
Feel free to write as many additional helper functions as you’d like. Each of the three steps here - resize the instruments array, change the grid size, and reset the sweep - are independent of one another.
-
This is the most likely spot in the assignment for memory errors to show up. It’s very easy to walk off the end of an array here, or to index into a nonexistent position, or to read/write deallocated or unallocated memory. As you saw in the debugger warmup, these errors can manifest in many different ways - tests that fail sometimes but not always, unusual garbage values showing up in places they shouldn’t, outright program crashes, etc. If you see these errors, pull out the debugger and trace through the code to see what’s going on. By inspecting the variables panel, you will eventually find something that’s not what it’s supposed to be.
-
As you work through this function, draw lots of memory diagrams! Draw boxes labeled
_gridSize,_lightSize,_grid, and_instruments, and give them sample values. Draw sample grid and instrument arrays, give them memory addresses, and put those addresses in the_gridand_instrumentsboxes as appropriate. Then, draw new, larger versions of those arrays, as well as boxes for any other variables you create in this function, and trace through on paper how various values (especially the memory addresses!) move around as you copy the contents of one array to another and allocate and deallocate chunks of memory.
(Optional) Milestone 6: Enjoy Your Creation!
Congratulations! You have just created a digital musical instrument from scratch. Everything you see on the screen, every way in which the program responds to the mouse, and all the sound emitting from your speakers is fully your own. You didn’t rely on any ADTs in the course of coding this up, and aside from a little bit of glue code to connect ToneMatrix to C++’s windowing and audio systems, we didn’t do any of the heavy lifting for you. This really is your own handiwork!
So take some time to play around with what you made. Can you make any cool tunes or rhythms? Are there any patterns that sound particularly aesthetically pleasing?
(Optional) Part Four: Extensions!
We have barely scratched the surface of what you can do with audio processing. There are entire courses (e.g. Music 220a) dedicated to making music with computers. If you want to go above and beyond what’s required here, go for it! Here are some suggestions to help get your creative juices flowing.
-
There are tons of different ways you can synthesize sound. FM synthesis, invented here at Stanford, is a remarkably powerful tool for creating interesting sounds, and is not that tricky to code up. Subtractive synthesis has been used extensively in the music industry since its invention. Envelope generation lets you make sounds that ease in and out naturally.
-
The visualization of the Tone Matrix that we asked you to code up just shows which lights are on and which are off. There’s no indicator of which column is the active column, so it’s tough to see what sounds are playing. Consider updating how you draw the squares of the Tone Matrix so that it’s clearer what’s happening. You could do that by highlighting the current column, or you could do something fancy like have each light emit a little wave pulse when it’s played.
-
The Tone Matrix consists of an interface (how you play it and what draws on the screen) combined with sound sources. What other interfaces can you come up with? Could you invent new ones that include things like, say, the ability to control the speed / volume, change the shapes of the waves to make different types of sounds, etc.?
-
If you have some music theory training, you might recognize the sounds played by the Tone Matrix as a series of major pentatonic scales. We picked that because these tend to produce pleasing music regardless of which combination they’re played in. Could you change the scales so that you get back different melodies with the same patterns of lights? Or could you make it so that each note controls a chord rather than a single note? Or something more creative than this?
-
Are you musically inclined? Record a video of a performance with your Tone Matrix that you think is artistically pleasing and share it with your SL.
-
Can you make the Tone Matrix automatically change the patterns of lights as time progresses? If so, how would you choose which lights turn on and off, at what times do they change, and what is the resulting sound pattern?
As usual, if you want to submit extensions, please submit two separate sets of code: one that meets all the baseline assignment requirements, and one that contains your freeform extensions.
Footnotes
| [1] | This method of laying out a 2D grid inside a 1D array is called [row-major order](https://en.wikipedia.org/wiki/Row-_and_column-major_order) and is how our `Grid` type is implemented. |
| [2] | Here's the full technical lowdown of how this works. Computer speakers internally have a memory buffer, which is essentially just an array like the ones you get with `new[]` and `delete[]`, holding the next sound sample to play. As the speakers play, they consume what's in this array and generate sound. When the buffer is running out of samples, they call the operating system to request that a new buffer be filled up with samples. The OS in turn notifies your program that it needs to fill up the buffer. We have set up some glue code that fills the buffer by repeatedly calling `ToneMatrix::nextSample()`. You can see how this works in `AudioDevice.cpp` and `ToneMatrixGUI.cpp` if you're curious. |
| [3] | The number 8,192 is purely arbitrary here. We picked it because it's a nice round number 8,192 = 213 and because with a sampling rate of 44.1kHz, it's approximately four beats per second. |
| [4] | When you add together multiple sound samples, the resulting value can be bigger than +1 or lower than -1. Since ±1 are the upper limits of the waveforms the speaker can produce, values above +1 get "clipped" to +1 and values below -1 get "clipped" to -1. This is called [clipping](https://en.wikipedia.org/wiki/Clipping_(signal_processing)) and distorts the sound wave. Fortunately, you don't need to worry about that in this assignment: there are at most 20 total strings, and the amplitude of each waveform is 0.05, so the range of possible samples generated this was is -1 to +1. |
| [5] | "Hey!," you might be saying, "the instruments in your schematic aren't all string instruments!" Yes, you're right. Unfortunately, there are only so many string instruments for which we have emojis. Sorry. |

