From now on, code style will be graded.

Your programs are getting bigger and the grader(s) need to be able to read them without clawing their eyeballs out. See the style guide for examples of good code style.

You are not required to use “Java-style” indentation, and comments are not required but are encouraged. At the very least, you code should be indented as shown in the “Indentation” section. Remember, tab key, not space bar.

Failure to use proper style can cause you to lose points from now on.

In this lab, you’ll be using arrays, functions, and for loops. You’ll also get a taste of that “top-down design” I talked about in the lecture on functions.

What will this program do? Well it’s a little different than things you may have written before - it will behave like a toy keyboard. Like one of those things on the right, you know? Maybe? I don’t know, is this a thing they have anymore or is it all just apps on tablets now??? aaaaaa

Anyway it will let you play a little demo song automatically, and also use your computer’s keyboard like a musical keyboard. It will do all this using keyboard input, similar to lab 2 (the calculator).


0. Getting started

Here’s the starting code, including the print_str macro you got in lab 2. It’s okay to copy and paste this, to avoid making mistakes.

# YOUR NAME HERE
# YOUR USERNAME HERE

# preserves a0, v0
.macro print_str %str
	# DON'T PUT ANYTHING BETWEEN .macro AND .end_macro!!
	.data
	print_str_message: .asciiz %str
	.text
	push a0
	push v0
	la a0, print_str_message
	li v0, 4
	syscall
	pop v0
	pop a0
.end_macro

# named constants
.eqv NOTE_DURATION_MS 400
.eqv NOTE_VOLUME      100

.data
	instrument: .word 0

	demo_notes: .byte
		67 67 64 67 69 67 64 64 62 64 62
		67 67 64 67 69 67 64 62 62 64 62 60
		#60 60 64 67 72 69 69 72 69 67
		#67 67 64 67 69 67 64 62 64 65 64 62 60
		-1 # ends the song!

	demo_times: .word
		250 250 250 250 250 250 500 250 750 250 750
		250 250 250 250 250 250 500 375 125 250 250 1000
		#375 125 250 250 1000 375 125 250 250 1000
		#250 250 250 250 250 250 500 250 125 125 250 250 1000
		0

	# maps from ASCII to MIDI note numbers, or -1 if invalid.
	key_to_note_table: .byte
		-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 # control characters
		-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
		-1 -1 -1 75 -1 78 82 -1 85 87 -1 -1 60 -1 -1 -1 # symbols and numbers
		75 -1 61 63 -1 66 68 70 -1 73 -1 -1 48 -1 -1 -1
		73 -1 43 40 39 76 -1 42 44 84 46 -1 -1 47 45 86 # uppercase
		88 72 77 37 79 83 41 74 38 81 36 -1 -1 -1 80 -1
		-1 -1 55 52 51 64 -1 54 56 72 58 -1 -1 59 57 74 # lowercase
		76 60 65 49 67 71 53 62 50 69 48 -1 -1 -1 -1 -1
.text

# -------------------------------------------------

.globl main
main:

# -------------------------------------------------

Small tangent: named constants

Look above the .data line in the starting code. You see:

# named constants
.eqv NOTE_DURATION_MS 400
.eqv NOTE_VOLUME      100

The Java equivalent would be something like:

public static final int NOTE_DURATION_MS = 400;
public static final int NOTE_VOLUME      = 100;

These are named constants. Named constants are wonderful. Names are wonderful. Whenever you have some “magic value” that has specific importance, don’t write it inside your code. Make a named constant for it.

These are not variables. You do not use lw with them. Instead, you can use them anywhere a constant is normally written (like in li, or as the last operand to an add, or as the second operand in a beq, etc.).

Also, don’t just write the value of the named constant when you need it. Use the name!:

MysteriousObvious
# 100? why 100? what is 100?
li a3, 100
# ohh, it's the volume argument
li a3, NOTE_VOLUME

Why do we do this? Not only is it more readable, it means we can change the value of the constant in the future, and none of the code that depends on it has to be changed. Every use of that value will be changed across the whole program, all at once.


1. main

Here’s how the program will work:

So the shape of main will be very similar to what you had in lab 2: a loop that asks the user for a letter, then switches on that letter and does different things based on that.

Unlike lab 2, we won’t be putting all the code inside main. Go look back at your lab 2 code. A lot of it is pretty repetitive, right? Like asking for the value for the equals, add, subtract, multiply, and divide cases… code repetition is a sign that you should be using a function to perform the repeated code.

Lab 2’s main is also really big and the control flow is deeply nested. It’s hard to follow what it’s doing. This is another thing functions can help us with: we can split large pieces of code into much smaller ones, which makes control flow much easier for us to read and follow.

So, let’s get started. Here is some example program output for this lab:

command? [d]emo, [k]eyboard, [q]uit: d
DEMO NOT IMPLEMENTED
command? [d]emo, [k]eyboard, [q]uit: k
KEYBOARD NOT IMPLEMENTED
command? [d]emo, [k]eyboard, [q]uit: x
sorry, I don't understand.
command? [d]emo, [k]eyboard, [q]uit: q
bye!

-- program is finished running --

Write code in main to do this. Notes:

Once you have your program working like the above demonstration, you can move on.


2. Stubbing out the other functions

Right now the demo and keyboard cases just print some messages. We want these to be real pieces of code, but the main function is long enough, so we’ll have those cases call some other functions instead.

We are going to make two stub functions (empty functions) for these modes like so:

  1. Make two empty functions after main named demo and keyboard.
    • You start a function with a label and push ra. It ends with pop ra and jr ra, like:
        # ---------------------------------------------------
        demo:
        push ra
      
        pop ra
        jr ra
      
    • You should use comments to visually separate those functions, like the --- above.
    • Remember that the push ra and pop ra are like the { } around a function’s code. You never write anything before push ra or after pop ra. All the code goes between them.
  2. In the demo and keyboard cases in main, call the appropriate functions.
    • You call a function with jal like jal demo.
    • Move those temporary prints (the print_str "DEMO NOT IMPLEMENTED\n" etc) from main into those functions, too.
  3. Assemble and run.
    • Assuming you moved the temporary prints into the functions, it should behave exactly the same as it did before this step.

Now look at main. Isn’t it so nice and short and readable? And the names of the functions you call tell you what the code does, without needing any comments. Nice.


Tangent: “why don’t we just beq to the functions?”

Right now in main, you probably have something like beq v0, 'd', _demo, and then in the _demo case you have jal demo. You might be reasonably thinking, “why don’t we just do beq v0, 'd', demo instead and skip the case?”

This would be very very bad. Functions need a return address - where to come back to when they finish running. The only instruction that sets the return address register ra is jal. beq doesn’t do it.

If you did write beq v0, 'd', demo, it would get into the function just fine. But at the end of the function, when it does jr ra, it would crash because ra was never set to a valid return address. Oops!

This is one of the big reasons I stress that you use _underscore labels for control flow: it visually separates “function” labels from “non-function” labels. This leads to some simple rules:

So these instructions are fine:

And these instructions are mistakes:


3. The demo function

Some of the lines in those arrays are commented out. If you uncomment them, the song will be longer, but it will also take you longer to test your program :)

In the code I gave you are two big arrays: demo_notes and demo_times. These make up a demo song in a very simple format.

This music format works like this:

Playing this music format is not too complicated:

This is a kind of for loop, except instead of looping until the counter reaches a value, it has a different condition.

There is one more complication: this function will itself be calling another function. Because of that, the loop counter cannot be a t register. It will have to be an s register instead. Your first use of an s register, omg!

So here’s what to do:

  1. In demo, add a push s0 after the push ra, and a pop s0 before the pop ra.
    • Remember that anything you push on the stack comes off in reverse order when you pop it off. That’s why the pops are in reverse order from the pushes.
  2. At the top of demo (after the pushes), set s0 to 0.
    • This is the int i = 0 part of the for loop. s0 is acting as i.
  3. Then make a loop.
    • Put the loop label, and at the end of the loop, increment s0 and jump to the loop label.
    • There’s no need to check the value of s0 here because we’ll check the condition inside.
  4. Inside the loop:
    1. Get demo_notes[i] into a0.
      • This isn’t the syntax you will use.
      • demo_notes is a byte array, so which kind of load will you use?
      • Which register represents i? Do you need to multiply it by anything?
      • What is the syntax for getting a value out of an array? It’s the special form we learned in the arrays lecture…
    2. If a0 is -1, exit the loop.
      • You’ll have to make a _break: label after the jump at the end of the loop, and branch to that _break if a0 is -1.
    3. Print out a0 for now.
      • You know how to print an integer by now.
      • You can also print_str " " after that to put a space after the number, and print_str "\n" after the _break: to put a newline.

Wait, print the integer? Sure. No one writes a whole program, or even a whole function, in one shot. It’s a very good habit to build up code piece by piece, and test it as you write it. This is especially true for control flow in asm!

If you test this demo function now, you should see something like this:

command? [d]emo, [k]eyboard, [q]uit: d
67 67 64 67 69 67 64 64 62 64 62 67 67 64 67 69 67 64 62 62 64 62 60
command? [d]emo, [k]eyboard, [q]uit: q
bye!

-- program is finished running --

If you see this, you’re on the right track. Importantly, it should ask for another command after printing out the numbers. It shouldn’t loop forever! If your program doesn’t work right, don’t waste time, get help.

Now that you have confirmed that your for loop is working correctly, you can replace the test prints with real code.

  1. Remove the print_str lines and the syscall to print out a0.
  2. Instead of printing out a0, we’re going to use syscall 31 to play a musical note. It takes 4 arguments:
    • a0 is the note to play, and you already have that.
    • a1 is the length/duration of the note in milliseconds. Use NOTE_DURATION_MS for that.
      • Remember, just write the name of the constant in the li instruction.
    • a2 is the instrument to use. It will be the value of the instrument variable.
      • Go look at its declaration in the .data segment. What kind of load should you use?
    • a3 is the volume/velocity of the note. Use NOTE_VOLUME for that.
    • Then set v0 to 31 and syscall.

a cat laying on a piano keyboard. Maybe this is who’s playing?

If you test the program now, it… makes sound! It’s not a great sound though. It sounds like someone is laying on a piano. That’s because the program is running so fast that it played all the notes in only a few milliseconds.

So the last part is to make the program wait or sleep after playing each note. After doing syscall 31:

  1. Get demo_times[i] into a0.
    • demo_times is a word array, so which kind of load will you use?
    • Which register represents i? Do you need to multiply it by anything?
  2. Set v0 to 32 and syscall. This is the “wait” or “sleep” syscall.

Done correctly, the demo mode should now play Camptown Races, yaaaaaay!

If you get an alignment error, you probably didn’t multiply the index into demo_times correctly. Don’t overwrite s0! Multiply by the byte size of one item.

If the song only plays a few notes and then stops forever, you’re probably multiplying the index by the bit size of one value. That’s wrong. Sadly the only way to stop this is to close MARS and restart it.

If you hear nothing at first, and then a bunch of notes all at once, and then it starts playing normally, that’s just a bug in MARS. Run the program again and it should sound fine.


4. Factoring out play_note

Right now the demo function does syscall 31 directly. But that note-playing functionality will be needed for the keyboard function too. Instead of copying and pasting it, we should move it out into its own function. This is called “factoring out” code, kind of like in algebra.

  1. Make a new function called play_note after demo.
    • This one doesn’t need to push/pop s0 but all functions should push/pop ra, and all functions need to end with… what instruction?
  2. Cut the lines from demo that set a1, a2, a3, and the li v0, 31 and syscall lines. Paste them into play_note.
  3. Replace those lines in demo with jal play_note instead.

We actually just made a function that takes an argument! Notice that in demo, we load a value into a0 before jal play_note, so a0 is the argument to it. Inside play_note, we “use” that argument by doing the syscall that takes a0 as its argument.

Done correctly, it should work and sound exactly the same as it did before. Not too exciting, but it will make the keyboard function easier. (And it made demo shorter, too.)


5. The keyboard function

Now to make it interactive. This mode is really very simple. It does a loop which:

The last array I gave you, key_to_note_table, is what will help us do that last step. Observe my beautiful three-minute diagram of how the computer keys are mapped to piano keys:

a computer keyboard, but QWERTYUIOPZXCVBNM and comma are the white keys, and 2356790SDGHJ are the black keys.

The bottom row is a lower octave than the top row. In addition, you can also hold shift and hit a key to get a higher (or lower) octave.

Here’s what you have to do in the keyboard function:

  1. Start off with these messages (just copy and paste, it’s fine):
         print_str "play notes with letters and top row of numbers.\n"
         print_str "change instrument with ` and then type the number.\n"
         print_str "exit with enter.\n"
    
  2. Print out the current instrument plus one.
    • That is, print whatever value is in the instrument variable, but with 1 added to it.
    • Don’t increment the instrument variable. We’re not changing it, just printing its value + 1.
    • The output should look something like current instrument: 1 if instrument == 0.
  3. Go into an infinite loop.
    • Because of how function-local labels work, you are allowed to reuse the names of local labels in multiple functions! So you can have _loop in demo and _loop in keyboard. They are different labels, and the assembler will not confuse them. You don’t have to name them _loop1, _loop2 etc. it’s unnecessary.
  4. Inside the loop:
    1. Get a character with syscall 12.
    2. Switch on it:
      • If the character is '`', go to a “change” case.
        • You can just stub that out with a message for now.
        • Don’t forget to break out of the switch at the end of the case.
      • If the character is '\n', break out of the loop.
        • Important: break out of the loop, not the switch. Maybe name the loop’s break label _break_loop or something so it’s obvious where you’re going.
      • In the default case:
        • move the return value into a0. (watch the order of the operands…)
        • call play_note.

Test it out. In this example interaction, I hit k to go into the keyboard, ` to make sure the “change” case works, hit some letters (which made really funny sounds), enter/return to leave keyboard mode, and q to quit.

command? [d]emo, [k]eyboard, [q]uit: k
play notes with letters and top row of numbers.
change instrument with ` and then type the number.
exit with enter.
current instrument: 1
`change
qwerioqiOQIWRO
command? [d]emo, [k]eyboard, [q]uit: q
bye!

-- program is finished running --

Do you hear the funny sounds?

Okay, things aren’t quite right yet. It definitely makes sounds when you hit keys, but it sounds like bells?? Capital letters sound lower. And digits and punctuation sound the lowest of all. What on earth is going on?

Syscall 12 returns the character the user typed in. A character is just an integer that is interpreted a different way. For example, the character A (uppercase A) is represented by the integer with the decimal value of 65.

This mapping of numbers to characters is mostly done with Unicode these days, but the first 128 characters of Unicode are adapted from ASCII, an older American standard which is universal in the English-writing world.

Here is a table of all the characters in ASCII.. Notice the values of the characters (the Dec columns): punctuation and digits are the lowest values, then come uppercase letters, and finally lowercase letters. Hey, that matches what’s happening with our keyboard!

What we’re doing right now is taking an ASCII character, and handing it off to the MIDI output syscall which misinterprets that number in a different way. In ASCII, 60 means the < symbol. But to MIDI, 60 means “middle C”!

Implementing note translation

Now we’ll make use of the key_to_note_table array to fix this problem. This is a lookup table, a way of making a simple mathematical function. It’s an array of constants where the input is the index into the array, and the output is the value at that index. Since ASCII only has 128 characters in the range 0 to 127, we just need an array of 128 values, one for each character.

To use this properly, we need to be a bit careful: we don’t want to index the array out of bounds, because unlike in Java, there is no ArrayIndexOutOfBoundsException to save us if we mess up.

So here’s what we do in the default case:

  1. Remove the move and jal that you have now.
  2. If the return value is less than 0, break out of the switch (not the loop).
  3. If the return value is greater than 127, break out of the switch.
  4. Now we know the return value is in the right range. Use it as the index into the key_to_note_table array, and get the value into a0.
    • This is a byte array.
  5. If a0 is -1, break out of the switch.
    • This is because not all ASCII characters are valid notes. The table has -1s for invalid notes.
  6. Call play_note.

Now it should work properly. Things to test:

One last piece.


6. Changing instruments

The instrument variable holds which instrument we are playing. It defaults to 0, which is the piano. But there are lots of other instruments we could play!

So here’s what you need to do:

That’s it. Now you should be able to change the instrument in keyboard mode. Be sure to test the range-checking too (notice I enter numbers that are too small and too big in the below example):

command? [d]emo, [k]eyboard, [q]uit: k
play notes with letters and top row of numbers.
change instrument with ` and then type the number.
exit with enter.
current instrument: 1
`
Enter instrument number (1..128): 0
Enter instrument number (1..128): 999
Enter instrument number (1..128): -7
Enter instrument number (1..128): 64
qwerty
command? [d]emo, [k]eyboard, [q]uit: d
command? [d]emo, [k]eyboard, [q]uit: q
bye!

-- program is finished running --

There is also a little… bonus feature? easter egg? slight bug? demonstrated above: if you go into the keyboard mode, change the instrument, then exit keyboard mode by hitting enter, when you play the demo song it will use the new instrument :)


Last tangent: “why make a function that is only called once?”

The play_note function is used in two places, once in demo and once in keyboard. So it makes sense that it is a function, so that it can be reused. But the change_instrument function is only used in one place. Why not just put its code inside keyboard?

Again, it has to do with readability. Putting all the code from change_instrument inside the case in keyboard would make keyboard harder to read and understand. You would have a loop inside a switch-case inside a loop inside a function. It’s too much. keyboard is also a long function already, and that would make it even longer. (Maybe you could factor keyboard’s default case out into its own function!)

Another potential benefit is future proofing: if I want to extend this program and add more features, maybe I want to be able to ask the user for a new instrument in a different way, like by adding an i command to the main menu. I have a function ready to go when I need it.


Submitting

Once you’re sure everything works (or if things don’t work, they are stubbed out with “UNIMPLEMENTED” messages or something), you can submit:

  1. On Canvas, go to “Assignments” and click on this lab.
  2. Click “Start Assignment.”
  3. Under “File Upload,” click the “Browse” button and choose your .asm file.
  4. Click “Submit Assignment.”

If you need to resubmit, that’s fine, just click “New Attempt” on the assignment page and upload it again.

Try This: Knowing what you know about functions now, go back to your lab 2 code. See if you can factor out some repeated code (like the parts that ask for a value in 5 different cases!) into their own functions and call them. The program should still behave exactly the same way, but the code should be shorter and more readable!