Part 39: Hex Wars: An autopsy (Part 3)
Hex War: An Autopsy: Part 3Welcome back! Last time, we mostly did prep work, making a joystick-reading subroutine, getting ready to redefine the font and to define a sprite, and setting some ease-of-use variables. Let's see where we go from here.
code:
26 FOR A = 54272 TO 54295: POKE A, 0: NEXT: POKE A, 15: POKE 54273, 40: POKE 54277, 25
One of the Commodore 64's most memorable features is its Sound Interface Device, or SID. This chip is what produces the C64's signature sound, a sound that was used to create tons of memorable music and is still used to this day in the retro music scene. Here's an example from back in the day:
https://www.youtube.com/watch?v=1UzJUCPkCjo
And here's a more recent one, using some very fancy tricks to play actual samples on a chip never intended to do so:
https://www.youtube.com/watch?v=p6LYrQk5I7s
The SID chip, as you can see from the video, provides three channels of mono audio. It can make four basic waveforms - triangle, sawtooth, square, and noise - and offers oscillator synchronization, ring modulation, and low-, band-, and high-pass filters. You can control the attack speed, decay speed, sustain volume, and release length of each note. (The following graphic explains visually what those mean.)
You can also set the frequency, or pitch, of each note, and the pulse width (see the beginning of the Driller video to see the effects of adjusting that on the fly). Even if you don't understand any of that terminology, you should still get the idea that the SID chip was a very powerful sound-making tool for the time.
Hex War, Line 26
We aren't going to be doing a whole lot with the SID chip in this game, but we will have some sound. To that end, the FOR-NEXT loop in this program writes zeroes into the registers for all three voices (as they're called), turning them off completely, then writes 15 into address 54296 -
- wait, what?
FOR-NEXT Loops and Scope
Commodore BASIC provides a control structure to repeatedly run part of a program. The syntax is as follows (curly brackets indicate optional parts):
FOR variable = start TO end {STEP step}
{code to run repeatedly}
NEXT {variable}
At the start of the structure, variable is given the value start. The code, if any, between the FOR and the NEXT is run, and then variable has step added to it. (step can be negative. If the definition of step is omitted, it defaults to 1.) If variable is now greater than end (or less than end, if step is negative), the loop ends; otherwise, the loop repeats. (There's one exception to this, which I'll discuss a little later.)
A few instructive examples:
- The loop FOR A = 1 TO 10 STEP 2 will run five times, with A equalling 1, 3, 5, 7, and 9.
- The loop FOR A = 1 TO 10 STEP -1 will run once, with A equal to 1.
- The loop FOR A = 1 to 10 STEP 0 will run forever, with A equal to 1 each time.
Scope is the concept that things in a program only exist for a certain part of a program. For instance, the variables inside a subroutine only exist inside of that subroutine, so you can have a 'video' variable inside the subroutine and a 'video' variable in the program calling the subroutine and the two are completely different variables.
Scope in Commodore BASIC is extremely simple: it doesn't exist. All variables exist everywhere.
What does this mean? Well, for one thing, it means that any variable A we have before we start the FOR loops above is erased and given the value 1. It also means the variable A still exists after the loop, containing whatever value it had at the end of the loop. The third loop goes forever, but the first two end with A equal to 11 and 0, respectively. (Note well that this is not the value of end. It's the first value of A that satisfies the ending-loop condition.)
In addition, we can reassign the value of A inside the loop. If we wanted the third loop to end, all we would have to do is, at some point inside the loop, set A to a number greater than or equal to 10. (This is the exception I mentioned earlier. If step is 0, then variable being greater than or equal to end ends the loop.) It would then stop looping at the end of the current loop.
Why might this be useful? Well, here's one possibility.
FOR ZZ = 1 TO 500: GET K$: IF K$ <> "" THEN ZZ = 500: NEXT
This loop runs 500 times. Each time, it checks the keyboard for a single keystroke. If it gets one, then it sets ZZ to 500, which will cause the loop to end; otherwise, it keeps going until it's out of loops. Congratulations: you now have a way to give the player only a limited amount of time to press X to not die.
Hex War, Line 26, continued
So after the end of the loop, A is 54296. (It was 54295 during the last loop; we then added one, saw it was greater than the end value of 54295, and exited the loop.) Putting 15 into 54296 sets the SID chip's volume to maximum, turns off any band pass filters, and makes sure voice 3 is on. (You'd turn off voice 3 here if you wanted to use ring modulation, which multiplies - not adds - the waveforms of voice 1 and voice 3. Obviously, if you're doing this, you probably don't want to also hear voice 3.)
The next thing we do is put 40 into memory address 54273. Addresses 54272-54273 are the frequency of voice 1.
Big-Endian Versus Little-Endian
When you want to store a number that's larger than you can store in a single byte (0 - 255), you have to use multiple bytes to store it. Memory addresses for the C64, for instance, use two bytes. One of the two bytes is multiplied by 256, then added to the other, to give a number between 0 and 65535. In a Windows 32-bit system, four bytes are used; you multiply one by 256, add it to the second, multiply that by 256, and so on until you've added all four together and have a number between 0 and 4,294,967,295.
But what do we mean by 'the first byte'? Turns out there are two different approaches computers use to decide that sort of thing. In a big-endian system, the first byte is the one with the lowest memory address. If you wanted to store 31173 in two bytes starting at 42, you'd divide 31173 by 256, which gives 121 with a remainder of 197, and store 121 in memory address 42 and 197 in memory address 43.
The Commodore 64, however, is little-endian. That means that, if we wanted to store the same number in the same place, we'd store 121 in memory address 43 and 197 in memory address 42. The reasons some systems are big-endian and some are little-endian are complicated; if you really care, Wikipedia's article on endianness is instructive.
Hex War, Line 26, continued again
By storing 40 in memory address 54273, and having previously stored 0 in memory address 54272, we've put the number (40 * 256 + 0 =) 10240 in voice 1's frequency register. Note that this is not the frequency of the resulting note in cycles per second! (And a good thing, too; 10240 Hz is on the 'annoying dogs' end of the audio spectrum.) The exact equivalence of SID frequency to real-world frequency depends on whether you're using a PAL (European) Commodore 64 or an NTSC (US) one for Reasons, but the difference is only 3%. For an NTSC Commodore 64 (note that VICE emulates a PAL C64 by default, but you can change that), you divide the SID frequency by 16.4043934 to get the real-world frequency; in this case, it's about 624 Hz, which is almost exactly the D# above high C. (On a PAL C64, the note would be about halfway between D and D#.)
The last POKE on line 26 stores 25 in memory address 54277, which controls the attack and decay length of voice 1. 25, or binary 00011001, selects an attack of 8 milliseconds (the first half, 0001) and a decay of 750 milliseconds (the second half, 1001). We've already put 0 into 54278, which controls sustain volume and release length; since sustain volume is 0, that means that any note we play with this voice will hit full volume in 8 milliseconds, then fade to nothing in 750 milliseconds without us having to explicitly tell the SID chip that the note is over.
We aren't playing the note yet, though. We're just setting it up. To play it, we have to put a number into 54276, which controls a lot of aspects about voice 1. We'll talk more about that when we actually get to playing the note.
Whew! I was expecting to get a bit farther this time! Still, we've covered a lot of ground topic-wise, so this is a good place to stop. Until next time!