an image of a 1980s era Casio SK-1 keyboard.

For this project, you’ll be making a very simple keyboard, in the style of those “kids keyboards” that I’m sure at least some of you had. With it, you will be able to:


Grading rubric

Note: if you submit on the late due date, 10 points will be deducted after all other grading has been done.

Also note: if you submit the wrong thing (submit a file that isn’t your project, or an outdated version of your project) and it isn’t found until the grader looks at it, you will be allowed to resubmit, but you will lose 20 points. Be careful about what you are submitting. (But if you make a mistake in what you submit before the deadline, it’s no problem. Just resubmit with the correct file. You have infinite resubmissions before the deadline.)

Code style: although there is no point category for code style, the grader may take off up to 10 points if your code is very poorly-written. Poor indentation may lose you a couple points, but mostly it’s about using the calling convention we learned; using the right registers for the right purposes; and writing/using functions correctly. Remember: keep your code neat and tidy while you write it, not at the end.


Getting started

Download this file and extract it somewhere sensible.

Then rename the abc123_proj1.asm file. Your username is not abc123.

Then you can open it in MARS. By the way, did you know you can drag files from a folder onto the MARS window and it will open them without having to use the File > Open dialog?

Overview:

and of course, _proj1.asm is the program itself.

What’s all this in _proj1.asm?

First are all the constants you might need.

Then come all the variables and arrays. You’ll probably need most or all of these, but you won’t need to add any more than I already gave you.

main looks similar to what you did on lab 6, no? You won’t need to change it.

load_graphics is filled in for you, you don’t need to change it either.

Most of the rest of the functions are stubbed out, some of them print a message.

Finally the last pre-written function is play_note, though you’ll be adding to it later. It uses syscall 31 to output a MIDI piano note.


Drawing the Keyboard

If you assemble and run the project right now, it should run just fine, and display a green screen. If it doesn’t assemble, please get help.

The first thing you need to do is draw the keyboard onto the screen so the user has something to interact with. You’ll do that in draw_keyboard which is already stubbed out for you.

Your goal is pictured to the right, and I’m trusting you to be able to figure out how to draw this. Here is what you need to know:

Basically, I don’t care how you solve this, just that you get the keyboard drawn in the right way in the right place on the screen. I mean, I guess the only thing I care about is that you use a loop to do it, because damn, doing it without a loop would be torture.

Remember: plan it out, think about how you’d do it in Java, then translate that to assembly. Nothing about this is particularly complicated. It only feels complicated if you try to do it entirely in assembly first.


Clicking on the keyboard keys

First, let’s change check_input: add a jal check_keyboard_clicks. Then, stub out a check_keyboard_clicks function under it. (We’re doing this because we’ll be adding more input checking to check_input later.)

In check_keyboard_clicks you can put a _return: label like this:

check_keyboard_clicks:
    push ra

_return: # <------ put this right before the pops!
    pop ra
    jr ra

Now, any time you want to return from this function, you can branch/jump to _return.

What we need to do

This function needs to:

  1. check if the mouse is onscreen, and return if not.
  2. check if the mouse is over the keyboard (within its rectangle of pixels), and return if not.
  3. check if the mouse button is being pressed, and return if not.
  4. convert the X coordinate into a key number
  5. get the note for that key number, and play it

Put some comments in your check_keyboard_clicks function to “sketch out” this sequence of steps. Remember, a lot of writing assembly is just discipline. Keeping your thoughts straight will make it much easier.

The display lets you get mouse input in a very simple way:

So, here’s what you need to do:

  1. checking if the mouse is offscreen
    • Basically, if display_mouse_x is < 0, you can return.
  2. checking if the mouse is over the keyboard
    • the constants (KEYBOARD_PX, KEYBOARD_PY) define the top-left pixel coordinate of the keyboard. if the mouse is to the left of, or above, these coordinates, return.
    • the constants KEYBOARD_PW and KEYBOARD_PH define the pixel width and height of the keyboard. That means the bottom-right corner is at (KEYBOARD_PX + KEYBOARD_PW, KEYBOARD_PY + KEYBOARD_PH). if the mouse is to the right of, or below, these coordinates, return.
    • (so yeah, there will be 4 branches, one for each side of the rectangle.)
  3. checking if the left mouse button is pressed
    • load display_mouse_pressed as a word.
    • then and it with MOUSE_LBUTTON: e.g. and t0, t0, MOUSE_LBUTTON.
    • if the result of the and was 0, return.
      • just trust me on this.
  4. convert the mouse X coordinate into a key number
    • at this point, we know the user is clicking on a key! we just have to figure out which one
    • it’s easy: it’s display_mouse_x / KEYBOARD_KEY_PW
    • you now have a number in the range [0, 13]
  5. get the note for that key number, and play it
    • the white_keys array contains the note numbers for the white keys.
    • use the key number to index this array (careful! look what type of array it is)
    • and pass the value you load from the array to play_note
    • i.e. steps 4 and 5 together are play_note(white_keys[display_mouse_x / KEYBOARD_KEY_PW])

Done correctly, you should now be able to click the white keys and hear notes. But there might be some issues with the sound output, so read below as well.

If it’s not working correctly, try adding some debugging prints to see how far into the function you are getting. If you click inside the keyboard, it should absolutely get to “step 4” above, but it should never get there if you click outside. A big part of debugging is about narrowing down where something goes wrong, so don’t be afraid to use the print_str and println_str macros to print out debug messages to help you narrow it down!


A note about the MIDI output in MARS

The first MIDI note played after MARS starts can be delayed by a few seconds. There is nothing that can be done about this. But after that, MIDI notes should start playing immediately.

If MARS has been running for a while, all MIDI notes will be delayed by several seconds. I have no idea why. Just restart MARS. (You don’t need to restart your computer. You never need to do that if a program is misbehaving.)


Handling the black keys

To handle the black keys, we are going to treat the keyboard as two rectangles:

This isn’t the 100% most perfect and accurate way to do this, but it’s simple and that’s all that matters.

To do this, you’ll have to modify the code you wrote on the last step:

Right after you’ve checked for the mouse click, you need an if-else:

To play a black key:

Now you can test out the black keys - they should make notes that are “in between” the white key notes.


Drawing the buttons

Next you need to draw three buttons, shown to the right.

The data for these buttons is in the button_ arrays:

Drawing the buttons in draw_buttons is pretty straightforward.

Once you get the button icons showing up, you need to draw the text (you can do that inside the same loop):

Done correctly, your display should match the image above.


Clicking the buttons

Make a new check_button_clicks function, and call it from check_input.

Here’s how this function will work:

  1. return if the mouse is offscreen (just like before)
  2. return if the left button is not pressed (just like before)
  3. for each button,
    • check if the mouse is over the button
      • its top-left is button_xs[i], button_ys[i]
      • its bottom-right is that plus BUTTON_W and BUTTON_H
      • if the mouse is not over the button, DO NOT RETURN. if you do, you will exit the loop early, and only check 1 button instead of 3. Instead, you need to structure it as an if within the for loop:
        for(int i = 0; i < N_BUTTONS; i++) {
            if(mouse is over the button)
            {
                // do the next step!
            } // <------- come HERE if the mouse is NOT over the button
            // this is the line immediately before the increment of the loop.
        }
      
    • if the mouse IS over the button:
      • load the function from button_funcs[i] and……….. call it???
      • yes. jalr reg lets you call a function whose address is in reg.
      • so you’ll do something like:

          lw   t1, button_funcs(t0) # assuming t0 contains multiplied index
          jalr t1                   # calls the function!
        

If this seems weird, well guess what: this is exactly how method calls in object-oriented languages like Java work. Index a function out of an array and call it. This way, the same call instruction can call many different functions instead of being hard-coded to call just one. Think about it: without this ability, we’d have to have like a big ugly switch-case to decide which function to call based on which button we clicked. Gross!

Done correctly, clicking the buttons should print some messages (since I put some code in press_demo, press_record, and press_play to do that)!


The demo song, and the “song format”

Every toy keyboard needs a cheesy song built in that kids can use to annoy their parents and siblings. I’ve provided one for you. But first, some theory.

The demo song arrays

In the .data segment you’ll find:

    # the demo song as a pair of arrays
    demo_notes: .byte
        64 64 64 64 64 64 64 67 60 62 64
        65 65 65 65 65 64 64 64 64 62 62 64 62 67
        -1

    demo_times: .word
        12 12 24 12 12 24 12 12 18 6 48
        12 12 18 6 12 12 18 6 12 12 12 12 24 24
        0

The song “format” which you will be playing works like this:

A frame is about 1/60th of a second, and is 1 time through the loop in main. (This is actually set up by display_init by passing 15 as the “milliseconds per frame” argument. Don’t change it.)

Doing multiple things at the same time

We want to be able to play the keyboard at the same time as the demo plays. This sounds complicated but it’s really not.

You’ve been “doing multiple things at the same time” for the whole project already. On each frame, you do one check for clicking on the keyboard, one check for clicking on each button, etc. The demo playback isn’t going to be any different. On each frame, we check if we need to “move to the next step.” That’s it.

Walking pointers

We are going to be working with arrays whose addresses are held in variables. This is different from accessing them directly, and MIPS has some limitations that makes accessing such arrays a bit clunky. So, we’ll be using a slightly different technique for accessing these arrays, something I like to call a “walking pointer.”

A pointer is variable which contains the memory address of another variable. In our case, the play_note_ptr and play_time_ptr variables will hold the memory addresses of the note and time arrays discussed above… or any other note and time arrays (such as the ones used for recording).

We could do the old A + i * S thing with these pointers, but MIPS makes it awkward, so instead, we will increment their values to move through the arrays. Here is an animation of how “walking pointer” works:

This way, we can iterate over the items of the array without even needing to remember what index we’re on.


Demo playback

In press_demo (already stubbed out for you), you’ll do the following:

  1. return if is_recording is != 0. (this avoids weirdness in the future.)
  2. Load the address of demo_notes into a0 using la.
  3. Load the address of demo_times into a1 using la.
  4. Call start_playback, which we’re about to write

In start_playback (already stubbed out):

  1. Store a0 into play_note_ptr and a1 into play_time_ptr
  2. Load the first note (as a byte) from (a0) and play it with play_note
  3. Do play_last_frame = display_frame_counter
    • these are both .word variables
    • display_frame_counter is a “magical” variable which increments by 1 every time you call display_finish_frame - it’s how we will keep track of time

Finally, in update_playback (already stubbed out) we will write our song-playing code:

  1. if play_note_ptr is 0 (null!), return.
  2. if “not enough time has elapsed,” return. How you check that:
    • first, compute display_frame_counter - play_last_frame
    • then, load play_time_ptr into a register, let’s say t0
    • then lw t1, (t0) to load the time out of the array item that t0 is pointing to.
    • finally, if display_frame_counter - play_last_frame is less than the time you just loaded, return.
  3. enough time has elapsed, move to the next note:
    • play_time_ptr += 4 (to “walk” to the next item in the array of words)
    • play_note_ptr += 1 (to do the same, but it’s an array of bytes)
    • load the next note as a byte from the address held in play_note_ptr.
    • if it’s negative,
      • set both play_note_ptr and play_time_ptr to 0 (null). (this will stop playback because of the very first thing we checked in this function)
    • else,
      • play that note,
      • and set play_last_frame = display_frame_counter

This is gonna be tricky for sure. But if you get it working correctly, clicking the play button marked “demo” should play a short song for you.

In addition, you should be able to play notes on the keyboard while the demo song is playing!

If everything is working correctly so far, you have an 80%.


Recording songs (and playing back the song you recorded)

Recording uses the following variables

Here’s the general flow:

What you need to do follows.

press_record

press_play

play_note needs some code added to it before the lines that do the syscall:

(there is no “else”, the if just ends right before the li a1, NOTE_DURATION line)

start_recording

stop_recording

There are many things which could go wrong during this. Remember to try to narrow down where problems are, instead of throwing up your hands in despair.

Done correctly, you should be able to record songs and play them back, as explained before.


Submitting

Upload only your _proj1.asm file to Gradescope once it’s open. There is no way to autograde this, but it will try to assemble it, and remember, never turn in a program that doesn’t assemble/compile.