Wow, it's been over a year since I started the site... I'll admit, it's had the effect I wanted. In particular, to keep pushing myself to finish the project rather than circle endlessly in a design-only cycle.
In this installment, we'll look at how sound effects are created and played in TMS9900 assembly language. Music is also a part of this, but a music engine is not part of the core game.
And yeah, I changed the scheme to white text on a dark background. Easier to read, harder to read? Visit my development blog and let me know!
It's interesting to note that sound is one of the last things to get done with most computer projects. The main reason for this is that it is used for special effects and dramatic presentation. This automatically makes it a lower priority. Some games may make a puzzle or game out of sounds (such as Myst did), but this is a rare occurrence. I think that most people are tone-deaf, so sound-based puzzles are more frustrating than fun.
Sound on the TI is generated from the built-in sound chip. It was created by TI in the mid to late-70's. It has three separate voice channels, each capable of a square-wave tone with a frequency range of 110hz to 55,938hz. It also has a noise generator capable of periodic (buzzing) or white (static) noise. Each generator is capable of attenuation (volume) of 0-28 decibels.
A curious element of the TI sound chip is the relatively high minimum frequency of 110hz. This is a low A on a piano keyboard, but it's still an octave or two above "base" octaves. As a result, most TI music has to be pushed up an octave or more, making it sound "higher" than other computer systems. The reason? The TI engineers had to sync the sound chip to the 3.58Mhz clock signal. Since this is is at least twice what most of the other 8-bit systems of the time were running at, the sound chip is actually being forced to play higher tones than it's capable of.
The TI sound chip is not an even competitor to the SID chip that was featured in the Commodore 64. (A statement that I'm sure will get me a lot of angry protests from my fellow 99'ers. Sorry guys, it's the truth.) The TI sound chip IS capable of a lot of the techniques the SID can do, but to do it, the developer must replicate them in software. This includes elements such as different waveforms, decaying sound, and so forth.
Keep in mind, though, that the SID was developed several years later, and it was created by engineers who were also audiophiles. So they designed a chip specifically to produce better sound and music than had been heard on any other home computer at the time. So, full points to them, I'd say. Also, I'd say the TI still ranks high on sound quality, compared to the Color Computer (100% CPU-driven and rather soft), the Apple II (hair-raising screeches and buzzes that made your teeth grind), and the Atari 8-bit series. (Hm, well, the POKEY chip WAS pretty decent...)
Unlike some other vintage systems, there's a few different ways to produce sound on the TI. Including some exotic ones, which I won't go into here! But I'll cover the two main ones that game developers would be interested in.
This is the easiest and most common method, primarily because it's supported by a ROM-based ISR, which means it doesn't consume any extra code space to use. It's also what TI BASIC and Extended BASIC use to produce sound through the SOUND subprogram.
You construct byte-length data tables in either VDP or GROM memory, which then have the starting address fed into a memory-mapped port. Each entry begins with a length-byte, which indicates how many bytes are going to be entered into the sound chip. It ends with a duration byte, which indicates how long to play the current "set" until moving on to the next entry. A duration of 0 indicates an end of the sound.
The first nybble (4-bits) of each byte indicate what operation to do. The values >8,>A,>C, and >E indicate a frequency setting of voices 1-4, the fourth being the noise generator. Each of the generators requires two bytes to change or set a frequency, except the noise generator, which only takes one. The noise generator can be set to emit a low, medium, or high tone, or derive its frequency from the third voice's settings.
The values >9,>B,>D, and >F indicate an attenuation setting of voices 1-4. Only one byte is needed to change or set the attenuation of a single voice.
Here's a short example of a sound table entry:
BYTE 7,>9F,>BF,>DF,>FF,>80,>05,>99,10This says there are seven bytes to feed into the sound chip. The first four mute generators 1-4. The fifth and sixth set the first generator's frequency to >050. And the last byte sets the first generator's attenuation to 9. (12 decibels) The last byte, not fed to the sound chip, indicates that 10/60, or 1/6, of a second should pass before processing the next line in the sound table.
This method is one level lower than the ISR method. In fact, the ISR method is essentially doing this for you. Instead of letting the ISR read a sound table automatically, you directly feed the sound commands into the generator yourself, at CPU address >8400. You don't need the length byte at the start or the duration byte at the end. Unless you want to use them, since you'll be writing your own management system.
This method has one real strength, which is it is no longer tied to the interrupts. This means you're no longer limited to a minimum of 1/60 of a second for sound-length. This means your timing becomes dependent on your code cycles, which are MUCH smaller. You can create sound lengths so small the human ear won't pick up the changes. You can make much smoother sound decays and possibly create sound effects that are completely unique and impossible to implement in BASIC.
The drawback is that unlike ISR, you're now involving the CPU in sound processing. This means it's a better method for stand-alone sound applications, such as music playing, as opposed to games, which usually need sound to be a "fire and forget" system. You could always set it up as a user-driven interrupt, but since there's already a ROM-based system in place, it's rather a waste unless you had a very special system in mind.
Well, there's no easy way to do this. If you're not particularly creative about it, you would be best served by studying how other games and programs on the TI have created sound effects. This is how I started myself.
BASIC and Extended BASIC games are always a good place to begin. For one thing, you can easily list programs and find the sound effects, so long as the program isn't protected. (And there's ways around that...)
The one thing to remember is that the SOUND subprogram in both languages, while superficially the same as assembly, has one real drawback: timing. Because the commands are run through the interpreter, you have a lot of overhead lag that makes piecing sound together a tricky business.
Voice control is also lacking. Unlike assembly, if you want to use more than one voice, you have to use them all in a single SOUND command. This wouldn't be a big deal except that in order to generate noise of varied frequency you have to use the third voice. So you end up with SOUND statements with muted first and second voices just to do that, a real waste of code space.
You'll also find that translating sound effects into assembly isn't straight-forward because there's a lot of approximation going on with the floating-point values used in BASIC. In fact, there's only 1024 different individual 'tones' stored in a 10-bit value, >000 to >3FF. And the higher the value, the lower the tone, because this value is used as a divider. So >000 is out of human hearing range, and >3FF is the lowest possible tone you can get.
In a similar fashion, the durations are not, in fact, in milliseconds. They're rounded to the nearest sixtieth of a second. And finally, the volume only ranges from 0-14 (each level is 2 decibels, 0 is 30, 1 is 28, etc.), with 15 being the command to turn off the sound generator. So low is louder than high.
On a side note based on personal observations, I would avoid using the built-in high/middle/low set frequencies for the noise generators. While they take only a single byte to use as opposed to three, they're also frequently used (and over-used) in BASIC.
Mining your favorite cartridge or assembly game for sounds is a more difficult prospect than a BASIC program, but it can be done. The trick is to use a hex editor to study the raw code dumps or object files and find the distinctive pattern that sound data makes.
A good way to start is to look for instances of >E3 and >E7 bytes. These are the periodic and white noise generators, using the third voice to determine frequency. If these are followed with >C#,>##,>F# in some varied order, you have probably found a bit of sound data. Searching for the length and duration bytes is also a good technique; usually, finding very low byte values (1-9) adjacent to each other is indicative of sound data.
I've used this technique to decode a few games from TI cartridges. For example, I really liked the "smack" sound that is made in Munchman when you eat a Hoonoo. I found the sound to be thus:
BYTE 7,>9F,>BF,>DF,>F2,>CC,>01,>E7,1 BYTE 2,>CC,>03,>1 BYTE 2,>CC,>05,1 BYTE 1,>FF,0First it mutes all the generators except the noise generator, which it sets to 2 (26 decibels). Then it sets the third voice to frequency >01C, and sets the noise generator to draw its frequency from the third voice. It plays for only one sixtieth of a second. Then it changes the frequency to >03C (+32) for another sixtieth of a second, and >05C (+32) for another sixtieth of a second. It then mutes the noise generator and returns a duration of 0, which indicates to the ISR routine that the sound table is at an end.
BYTE 3,>9E,>BE,>DF,1 BYTE 6,>87,>03,>A8,>03,>C0,>05,1 BYTE 2,>C0,>05,10 BYTE 4,>88,>03,>A7,>03,1 BYTE 4,>89,03,>A6,>03,1 BYTE 4,>8A,>03,>A5,>03,1 BYTE 4,>8B,>03,>A4,>03,1 BYTE 4,>8C,>03,>A3,>03,1 BYTE 4,>8D,>03,>A2,>03,1 BYTE 0,>17,>8EThe first line sets up the voice generators for the three voices. The second line sets the three voice frequencies, which are all fairly high. Notice that most of the lines only have 1/60 second duration, except one, which is 1/6 seconds. Why? Well, this creates the "pulse" effect you hear, a continuous long tone followed by a short burst of lowering frequency. Notice that the first and second voices are increased, which would lower the tone VERY slightly each time. The final line is a branch command, which loops back up to the address where the second line is. This means the sound effects will process forever until the sound ISR is halted by the application.
BYTE 5,>9F,>A5,>01,>B0,>DF,2 BYTE 2,>A9,>01,2 BYTE 2,>AE,>01,2 BYTE 2,>A4,>02,2 BYTE 2,>AA,>02,2 BYTE 2,>A2,>03,2 BYTE 2,>AC,>03,2 BYTE 2,>A7,>04,2 BYTE 2,>A5,>05,2 BYTE 2,>A5,>06,2 BYTE 2,>A8,>07,2 BYTE 2,>AF,>08,2 BYTE 2,>AA,>0A,2 BYTE 2,>AA,>0C,2 BYTE 2,>A0,>0F,2 BYTE 2,>A5,>01,1 BYTE 2,>A9,>01,1 BYTE 2,>AE,>01,1 BYTE 2,>A4,>02,1 BYTE 2,>AA,>02,1 BYTE 2,>A2,>03,1 BYTE 2,>AC,>03,1 BYTE 2,>A7,>04,1 BYTE 2,>A5,>05,1 BYTE 2,>AA,>0C,1 BYTE 2,>A0,>0F,1 BYTE 2,>AD,>11,1 BYTE 2,>A3,>15,1 BYTE 2,>A4,>19,1 BYTE 2,>A0,>1E,1 BYTE 2,>AB,>23,1 BYTE 2,>A7,>2A,1 BYTE 2,>A7,>32,1 BYTE 2,>A0,>3C,2 BYTE 3,>A5,>01,>B4,1 BYTE 2,>A9,>01,1 BYTE 2,>AE,>01,1 BYTE 2,>A4,>02,1 BYTE 2,>AA,>02,1 BYTE 2,>A2,>03,1 BYTE 2,>AC,>03,1 BYTE 2,>A7,>04,1 BYTE 2,>A5,>05,1 BYTE 2,>AA,>0C,1 BYTE 2,>A0,>0F,1 BYTE 2,>AD,>11,1 BYTE 2,>A3,>15,1 BYTE 2,>A4,>19,1 BYTE 2,>A0,>1E,1 BYTE 2,>AB,>23,1 BYTE 2,>A7,>2A,1 BYTE 2,>A7,>32,1 BYTE 2,>A0,>3C,2 BYTE 3,>A5,>01,>B8,1 BYTE 2,>A9,>01,1 BYTE 2,>AE,>01,1 BYTE 2,>A4,>02,1 BYTE 2,>AA,>02,1 BYTE 2,>A2,>03,1 BYTE 2,>AC,>03,1 BYTE 2,>A7,>04,1 BYTE 2,>A5,>05,1 BYTE 2,>AA,>0C,1 BYTE 2,>A0,>0F,1 BYTE 2,>AD,>11,1 BYTE 2,>A3,>15,1 BYTE 2,>A4,>19,1 BYTE 2,>A0,>1E,1 BYTE 2,>AB,>23,1 BYTE 2,>A7,>2A,1 BYTE 2,>A7,>32,1 BYTE 2,>A0,>3C,2 BYTE 3,>A5,>01,>BC,1 BYTE 2,>A9,>01,1 BYTE 2,>AE,>01,1 BYTE 2,>A4,>02,1 BYTE 2,>AA,>02,1 BYTE 2,>A2,>03,1 BYTE 2,>AC,>03,1 BYTE 2,>A7,>04,1 BYTE 2,>A5,>05,1 BYTE 2,>AA,>0C,1 BYTE 2,>A0,>0F,1 BYTE 2,>AD,>11,1 BYTE 2,>A3,>15,1 BYTE 2,>A4,>19,1 BYTE 2,>A0,>1E,1 BYTE 2,>AB,>23,1 BYTE 2,>A7,>2A,1 BYTE 2,>A7,>32,1 BYTE 2,>A0,>3C,2 BYTE 1,>BF,0Yeah... that is LONG... it's over 300 bytes of sound data. Which doesn't sound like a lot if you're a modern-day coder, but for a vintage system this is VERY big. It takes up about 1/5 of the space that the game devoted to sound effects. For ONE sound.
So what about music? Well, it can be done with either method. The ISR method has one small disadvantage that any breaks in sound (like the natural pause between notes) must be simulated by muting the generator. However, the ISR system would also allow you to have continuously playing music in the background with very little effort.
I decided early on, however, not to have background music in the game proper. My reasons were:
Unfortunately, I've had some trouble locating a way to record sound live in an emulator in a quick, clean, and easy fashion. (I'm open to suggestions!) So instead I'll provide some emulation-ready images.
In order to test sound effects, I had to write a small program that would store the sound data and also let me play them. I also used these to listen to the sound effects I extracted from some game cartridges.
So, below you can download the V9T9/Mess disk images and Classic99-ready files for the test programs for Tunnels of Doom and Munchman.
Please note that the sound selection system is not dummy-proof. You can easily navigate to a sound that doesn't exist, as I didn't take the time to code in checks against literal values. Munchman has 10 sounds (0-9) and Tunnels of Doom has 30 sounds. (0-29) You've been warned!
And no, I'm not sharing my sound effects. Not yet anyway...
To load, use the Editor/Assembler cartridge option #3 to load and run TEST. It will start automatically.
Munchman Sound FX (Classic99 files in ZIP file)
Munchman Sound FX (90k SSSD V9T9/MESS disk-image)
Tunnels of Doom Sound FX (Classic99 files in ZIP file)
Tunnels of Doom Sound FX (90k SSSD V9T9/MESS disk-image)
Well, that was a bit of fun. And yes, I do plan on integrating sound into my present demo soon, I want to have a listen as much as everyone else! Then I will be focusing on the combat engine. Until next time!