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!:
Mysterious | Obvious |
---|---|
|
|
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:
- The
main
function has a loop that asks the user for a one-character command (like in lab 2).- The
d
command plays a short demonstration song automatically. - The
k
command goes into an interactive keyboard mode. - The
q
command exits the program. - Any other letter will just print an error message.
- The
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:
- Don’t forget you have
print_str
available to you for printing out strings.- You can end a string with
\n
before the closing"
to print a newline after it.
- You can end a string with
- You’ll using the same syscall to get character input that you did on lab 2.
- You know how to properly exit a program for the
q
case by now. - The “sorry, I don’t understand.” message in the output above is the “default” case for the switch.
- This can all be done in maybe 30 or so lines of code (depends on comments and blank lines).
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:
- Make two empty functions after
main
nameddemo
andkeyboard
.- You start a function with a label and
push ra
. It ends withpop ra
andjr 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
andpop ra
are like the{ }
around a function’s code. You never write anything beforepush ra
or afterpop ra
. All the code goes between them.
- You start a function with a label and
- In the demo and keyboard cases in
main
, call the appropriate functions.- You call a function with
jal
likejal demo
. - Move those temporary prints (the
print_str "DEMO NOT IMPLEMENTED\n"
etc) frommain
into those functions, too.
- You call a function with
- 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:
- Function labels should only ever be used with
jal
. - Control flow labels (which start with
_underscores
) should only ever be used withj
or branches (beq
,blt
etc.)
So these instructions are fine:
j _endif
beq t0, 0, _else
jal update
And these instructions are mistakes:
j update
(never jump directly to a function)beq t0, 0, update
(never branch directly to a function)jal _endif
(never call a piece of control flow)
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:
- The two arrays are “in parallel.” So:
- Both arrays are the same length.
demo_notes[0]
anddemo_times[0]
are for the first note.demo_notes[1]
anddemo_times[1]
are for the second note.demo_notes[2]
anddemo_times[2]
are for the third note… etc.
notes
is an array of bytes, one for each note.- If the byte is
-1
, the song is over. - Else, it’s the MIDI note number.
- If the byte is
times
is an array of words, one for each note.- If the corresponding note is
-1
, this means nothing. - Else, it’s the number of milliseconds to wait before playing the next note.
- If the corresponding note is
Playing this music format is not too complicated:
- While the current note is not -1:
- Play the note
- Sleep for that note’s time
- Go to the next note
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:
- In
demo
, add apush s0
after thepush ra
, and apop s0
before thepop 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.
- At the top of
demo
(after thepush
es), sets0
to 0.- This is the
int i = 0
part of thefor
loop.s0
is acting asi
.
- This is the
- 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.
- Put the loop label, and at the end of the loop, increment
- Inside the loop:
- Get
demo_notes[i]
intoa0
.- 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…
- 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
ifa0
is -1.
- You’ll have to make a
- 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, andprint_str "\n"
after the_break:
to put a newline.
- Get
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.
- Remove the
print_str
lines and the syscall to print outa0
. - 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. UseNOTE_DURATION_MS
for that.- Remember, just write the name of the constant in the
li
instruction.
- Remember, just write the name of the constant in the
a2
is the instrument to use. It will be the value of theinstrument
variable.- Go look at its declaration in the
.data
segment. What kind of load should you use?
- Go look at its declaration in the
a3
is the volume/velocity of the note. UseNOTE_VOLUME
for that.- Then set
v0
to 31 andsyscall
.
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:
- Get
demo_times[i]
intoa0
.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?
- Set
v0
to 32 andsyscall
. 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.
- Make a new function called
play_note
afterdemo
.- This one doesn’t need to push/pop
s0
but all functions should push/popra
, and all functions need to end with… what instruction?
- This one doesn’t need to push/pop
- Cut the lines from
demo
that seta1, a2, a3
, and theli v0, 31
andsyscall
lines. Paste them intoplay_note
. - Replace those lines in
demo
withjal 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:
- Asks the user for a character
- If the character is
'\n'
(they hit enter), it exits the loop and goes back to the main menu. - If the character is
'`'
(backtick), it lets them type in a new instrument number. - Otherwise, it tries to translate the character into a note, and if successful, plays the note.
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:
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:
- 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"
- 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
ifinstrument
== 0.
- That is, print whatever value is in the
- 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
indemo
and_loop
inkeyboard
. They are different labels, and the assembler will not confuse them. You don’t have to name them_loop1
,_loop2
etc. it’s unnecessary.
- Because of how function-local labels work, you are allowed to reuse the names of local labels in multiple functions! So you can have
- Inside the loop:
- Get a character with syscall 12.
- 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.
- Important: break out of the loop, not the switch. Maybe name the loop’s break label
- In the default case:
move
the return value intoa0
. (watch the order of the operands…)- call
play_note
.
- If the character is
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:
- Remove the
move
andjal
that you have now. - If the return value is less than 0, break out of the switch (not the loop).
- If the return value is greater than 127, break out of the switch.
- 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 intoa0
.- This is a byte array.
- 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.
- Call
play_note
.
Now it should work properly. Things to test:
qwertyui
should sound like a nice ascending scale.zxcvbnm,
should sound like an ascending scale, but lower.- Holding shift and pressing any of those should make higher or lower sounds.
- Pressing
1
should not make a sound.- If it does, you didn’t check if the value that you got out of the table is -1 properly.
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:
- In
keyboard
in the “change” case, call a function namedchange_instrument
. - Make that
change_instrument
function. - Inside it, you should prompt the user for an instrument number in the range 1 to 128.
- If you’re wondering what these numbers mean, here is the official specification.
- Sadly the default Java MIDI soundfont makes many of them sound identical, but still!!
- If they type a number outside that range, repeat asking them until they type in a valid number.
- DO NOT implement this by jumping/branching to
change_instrument
or having it call itself. Just make a loop. A simple little loop. - The loop will have a label at the top, the prompt/input inside, and two branches at the bottom to check that the return value is in the right range. (Think about when you want to ask them again.)
- DO NOT implement this by jumping/branching to
- After the loop, subtract 1 from the return value and store it into
instrument
.
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:
- On Canvas, go to “Assignments” and click on this lab.
- Click “Start Assignment.”
- Under “File Upload,” click the “Browse” button and choose your
.asm
file. - 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!