Opry99er

Developer's Journal

Adventures in playing sounds on the TI-99/4A Home Computer

Matthew Hagerty
February 2010

0.0 Introduction

I can't remember how this all got started (this was over a week ago, come on, you can't expect me to remember that far back do you?) It must have started with the conclusion of the first game contest on the TI-99/4A section of the AtariAge forum. I believe two people submitted completed games, Owen Brand did Honeycomb Rapture and InfernalKeith did Herding Cats. Both games were done in XB (Extended BASIC) and since they spent the time to write them, I at least took the time to support their efforts and play the games. I really don't like XB much, it is a love/hate relationship really. TIB (TI BASIC) and XB were my first languages on the first computer I ever owned, but they were SLOW! Probably the slowest BASIC on any home computer of the time (as everyone knows) due to the double interpretation. Anyway, after I got the PEB (Peripheral Expansion Box) and E/A (Editor/Assembler) cartridge, I never used XB again.

Thus, when I ran each game, I was skeptical. Herding Cats was interesting because of the unique game play, but HR (Honeycomb Rapture) really stood out because of the background music that was playing during the game. You normally can't do that in XB and I knew there must be some assembly support going on (which there was), but it really made the game! I still don't like to work in XB, but I was duly impressed to say the least.

I played HR once through and at the end of the game the screen turned into a mosaic of colored squares; think multi-color mode with random color selections. I didn't think anything of it at the time, I thought that was simply the "end game" screen, Owen being all "artsy" or something. A day or so later I found out it was a bug. I thought it must be a problem with the assembly support for the background music, so I looked into the code.


1.0 Resources

I'll try to explain as much as I can, but I have to assume you (the reader) have some idea about the TI-99/4A and some basic terminology. There are a few resources I used to work through this process, and here they are:

1. PDF version of the E/A manual. I have two copies of the real manual as well, but the PDF is very convenient and I have permission from TI to make the PDF E/A manual available to anyone reading my book (of which this will be a chapter), so by reading this you can legally have the PDF E/A manual. :-)

2. PDF version of the TMS9900 Data Manual. This the sugar-daddy data book that I use to get definitive answers to what the 9900 actually does. When you are dealing with emulators and non-TI assemblers, this manual is indispensable for troubleshooting and general knowledge.

3. The TI Intern. This is a very excellent book published back in the day and made available in a very high quality PDF format. The book gives a complete dis-assembly, with comments, of the TI ROMs and GROMs, and it explains a lot about the operation of the console as well as detailing the proprietary GLP language. This is a must have for any assembly language programmer.

4. Thierry Nouspikel's TI Tech Pages (http://www.nouspikel.com/ti99/titechpages.htm). Thierry has made an excellent site with all kinds of technical information, software, and new hardware designs. Very good stuff!

5. Miller Graphics "The Smart Programmer" series of publications. I have physical copies of these publications that I got in a "TI stash" some 5 years ago. Lots of good information in there and tons of console memory mapping and dis-assembly.

6. TI XB manual. Briefly consulted for various stuff. I have the real book and the a poor quality PDF scan.

7. Classic99 Emulator. Tursi has done an excellent job on his emulator and I know he is very meticulous at making it correct. I have had some very detailed discussions with him about some instructions and status-flag operation that we both know simply does not exist in other emulators. The debugger built in to Classic99 is also indispensable (although I have some feature requests! ;-) )

8. Asm994a. I use Cory Burr's assembler for convenience, but it has some bugs. I conjunction with Classic99, it makes the assembly language code-test-debug cycle about as quick as it can be without an "all encompassing" IDE.

9. TIdir. Excellent tool for moving files from all the various formats.

All of these documents and software can be found in various places online, and I suspect everyone reading this probably already has them. If not, Google.


2.0 The Console Interrupt Service Routine

Most CPUs have what are called "interrupts", which are usually caused by a signal on an external pin (or pins) of the CPU. Some CPUs also support "software interrupts", but in this case we are talking about external hardware interrupts only. Interrupts are used to get the CPU's attention when an external event needs to be handled *right now*. When an interrupt comes in, the CPU will suspend the currently executing code, determine the interrupt "level", and jump to an ISR (interrupt service routine) via a vector table (which is just a fancy name for a look-up table, which we will get to in a minute.)

The TMS9900 CPU has nice support for 16-levels of hardware interrupts, however as configured in the TI-99/4A the designers in all their infinate wisdom cut that down to 2 interrupts (by wiring all the external interrupt pins to be seen as interrupt 1.) So we have interrupt 0 and 1, and interrupt 0 is the same as a reset, so really there is only 1 usable hardware interrupt.

Side Note. There are also "maskable" and "unmaskable" interrupts. We are talking about maskable interrupts here, which means the CPU can elect to ignore interrupts above a certain level based on an interrupt "mask" that can be set with the LIMI (load interrupt mask immediate) assembly language instruction. A "LIMI 0" instruction will cause the TI-99/4A to ignore its only interrupt, and "LIMI 2" will re-enable the interrupt.

When the 9900 receives an interrupt it needs to know where in the CPU address space (>0000 to >FFFF) the ISR is located that is associated with the received interrupt "level". This is where the "vector table" comes into play. The CPU takes the interrupt level, which will be a number between >0 and >F (0 and 15), and looks in CPU memory starting at address >0000. The vector table contains two 16-bit words (4 bytes) for each entry, a WP (Workspace Pointer) address and a PC (Program Counter) address. So the interrupt level, multiplied by 4, is an index into the first >40 (64) bytes of the CPU address space, and the CPU will issue a BLWP (branch and load workspace pointer) to the address at the interrupt level * 4. So interrupt 0 would BLWP @>0000 (0 * 4 = 0), interrupt 1 would BLWP @>0004 (1 * 4 == 4), interrupt 2 would BLWP @>0008 (2 * 4 == 8), etc. The real vector table looks like this:

>0000 >83E0 Reset vector, Interrupt level 0
>0002 >0024
>0004 >83C0 Interrupt level 1
>0006 >0900
>0008 >83C0 Interrupt level 2
>000A >0A92
.
.
.
>001E >wpwp Interrupt level 15
>001F >pcpc

Remember, the 9900 is a 16-bit CPU and accesses RAM two bytes at a time, and all address pointers require two bytes. When the only meaningful interrupt happens, level 1, the WP will be set to >83C0, the PC set to >0900, and the CPU will start executing the code a >0900. Now, here is the clincher, the TI-99/4A has a *ROM chip* located in the CPU address space from >0000 to >1FFF (the first 2K of CPU address space.) So, the vector table, and the ISR that interrupt 1 executes (the only usable interrupt in the console) is all fixed in ROM. We can't change it.

The console ISR does a few things, first of which is to try and determine which device caused the interrupt. This is really stupid since the designers could have just used the CPU's interrupt levels to do this. Instead the 9901 generates the level 1 interrupt, so you have to consult the 9901 to see which device in the system triggered it to trigger the interrupt. It is all really messy and I'm not going to sort it out here, since Thierry does an excellent job on his Tech Pages.

A few devices cause an interrupt to the 9901 and subsequently to the CPU, one of which is the VDP. The VDP, if configured to do so, will generate an interrupt every time it does a vertical refresh, which happens every 1/50th or 1/60th of a second depending on whether you have a PAL or NTSC console. This is the main driving interrupt for the console, the auto-sprite movement, and the auto-sound playing, among a few other things. In an NTSC system, this also makes for a convenient way to run a game timer based on seconds. The ISR needs to be small though, since it did interrupt the main line of running code, so it should be quick to get its job done and return.

The console ISR does things in about this order:

0. disable interrupts (the ISR is not re-entrant!)
1. check if the interrupt was the cassette
2. check if the interrupt was the VDP
3. check for auto-sprite movement, and if enabled, do all the updating of sprite positions
4. check for auto-sound processing, and if set, process sound list data
5. check for the QUIT key, and BLWP @>0000 is so (software reset, same as interrupt level 0)
6. update screen timeout counter
7. check for user defined ISR
8. return

That is a *lot* going on every 1/60th of a second! Too much in my opinion. At least a lot of it can be skipped quickly by disabling the auto-sprite movement, and not triggering the auto-sound playing. From assembly language you can do everything the ISR does, which is why most of us set LIMI 0 in our programs and leave it that way. The only down side is that we have to poll for the VDP interrupt if we want that nice 1/60th timer ability (which does come in handy!) The other problem with the console ISR is that it accesses the VDP, so any time you need to write to the VDP yourself you must disable the console ISR or your code could be interrupted and the address in VDP RAM that you are reading or writing could be changed; and that usually results in a wonderfully colorful display on the screen followed by cursing and hair pulling.

I knew most of the console ISR stuff before, but I always have to go review it to remember the details. This is where the TI-Intern is indispensable!


3.0 Crash Course in the TMS9919 Sound Generator

I never got into doing much sound on the TI-99/4A, and never via assembly. Heck, I didn't even know what the chip number was for the sound generator. A quick Google search educated me that the 9919 was used in the TI-99/4A only, and was also produced under the number SN94624. TI also produced a version of the chip for general release under the number SN76489, and it was used in an astonishingly large number of systems including the PCjr (I had a PCjr after my TI and I didn't know they had the same sound chip!), Colecovision, Sega Master System, Acorn BBC Micro, and many others.

The chip's technical specifications are:

* Channels: 4
* Channel 1-3: square wave synthesis
* Channel 4: white or periodic noise synthesis
* Stereo: no
* DAC: built-in
* Volume: 16 levels (+/- 1 dB)

When I got the datasheet for the chip, the first thing I noticed was how small it was. It is only a 16-pin DIP! I was expecting some big complicated CPU like the 9918A or the 9901 or something. Nope, not this time. The device is very straight forward and basically just keeps playing a tone via one of its 3 generators (or noise channel) until you set the volume of a channel to "off". Even then, the chip is still generating the tones internally, you just can't hear it. Get the datasheet and read Thierry's Tech Pages for the grueling details, but I'll do the basics here.

First, the chip is wired up to respond to CPU address >8400 (actually the address lines are not fully decoded, like the scratch-pad RAM, so it will respond to any even address in the range >8400 through >85FE. Thanks to Thierry for this info.)

Since the chip only has 8 data pins, everything is based on a "byte", with the longest commands being two bytes. The chip understands 8 "commands" which are determined from the first 4-bits of any byte sent to the chip:

1. 1000xxxx - set tone 1 frequency (2-byte command)
2. 1001xxxx - set tone 1 attenuation (xxxx = >0 through >F)
3. 1010xxxx - set tone 2 frequency (2-byte command)
4. 1011xxxx - set tone 2 attenuation
5. 1100xxxx - set tone 3 frequency (2-byte command)
6. 1101xxxx - set tone 3 attenuation
7. 1110xxxx - set noise control
8. 1111xxxx - set noise attenuation

Notice that the first bit is always "1", this is critical and how the chip distinguishes between a command byte and a "data" byte for the two-byte commands. In two-byte commands, the 2nd byte always has the first bit set to "0".

Internally the tone generators use a 10-bit divider to control the frequency, so to set the frequency requires two bytes. All the other commands are a single byte. The format of the tone commands are this:

>8z >xy
>Az >xy
>Cz >xy

So basically the 2nd nybble of the command byte (the 1st byte) is added to the end of the 2nd "data" byte, and the top bit of the 2nd byte must be 0, and the 2nd bit is ignored. So, in the command byte you get 4-bits, and in the 2nd byte you get 6-bits, for a total of 10-bits to set the frequency:

1st byte: 1000ghij
2nd byte: 0*abcdef

This would set the frequency of generator 1 to the binary number represented by the bits abcdefghij. This splitting of the frequency value, and having to set the top bit of the 2nd byte to 0 (and not caring about the 2nd bit) is what makes coming up with the sound data so much of a pain in the ass. There are plenty of programs out there (I think) for constructing the sound frequency bytes, so by all means find one you like and use it! See the E/A manual or Thierry's Tech Pages for an explanation on how the 10-bit frequency numbers are derived and related to the tone produced.

So, once you send the two frequency bytes, a 3rd command byte is required to set the attenuation level, at which point you hear the tone. The generator will continue to produce the tone until you set the attenuation for that channel to "off" (>F) or change the tone. This is where the console ISR comes in to play. It is convenient to use the 1/60th of a second VDP interrupt to control the sound duration, which means you get a tone from anywhere between 1/60th of a second to about 4.25 seconds when using a single byte to represent a "duration" (255 / 60 = 4.25). This is what the console ISR does and it works out pretty nicely.

Something else to keep in mind, the 9919 is a slow device! It takes about 32-clock cycles to write a single byte. That is on par with some of the 9900's slowest instructions like XOP, MPY, and the shift instructions (DIV is still the slowest coming in at 92 to 124 clocks!) Pumping a lot of data to the 9919 during an ISR is not a good idea. Programming each generator's frequency and attenuation (including the noise channel) takes 352 clocks! So programming a single generator would be 3 bytes or 96 clocks best case. Just something to keep in mind. I can't see much use in sending more than all 11 possible bytes per interrupt though, so it should all be okay.


4.0 Assembly Support for XB, Round 1

I'm pretty sure everyone is familiar with the SOUND subroutine in XB. I think you can chain together the parameters to get all three generators of the 9919 going at the same time, plus a noise, but I think that is it (meaning I don't think you can construct a sound list with the SOUND command.) The call to SOUND also pauses your XB program while the sounds play, which sucks for games. I think you can get around this by issuing a negative duration, then the call to SOUND returns immediately while the sound continues to play. Knowing what we do now about the console ISR and the auto-sound playing it has, we can conclude that this is what XB is doing with the negative duration. But, in Owen's case he wanted an entire tune to play during game play, and all the CALL SOUND statements scattered all over the code was simply not going to work or be smooth-sounding to say the least.

At some point in Owen's development Tursi wrote a sound player in assembly that did the following:

1. Allocated room in the VDP for the sound data
2. Copied the sound data to the VDP
3. Set the address of the sound data at CPU address >83CC
4. Set >01 at address >83CE

The first step was done by modifying the XB string allocation table. XB maintains pointers that represent the low and high memory addresses (in VDP RAM) for allocated strings, so Tursi modified these address to make room. That is kind of the right way to do this, but there was a problem (as we will see in a moment.)

Copying the sound data is easy, but it does have the side effect that it doubles the memory used by the sound data. See, the sound data is part of the assembly language program and is loaded by XB via CALL LOAD into the low 8K of the 32K RAM expansion, which starts at >2000. Then the sound data is copied to the VDP RAM. It has to be copied to VDP RAM because the console ISR auto-sound routine only looks in two places for sound data, VDP RAM and GROM. This can not be changed since the ISR is in ROM.

All these fixed addresses are some of the reasons I don't like using XB or the console ISR, both use the heck out of the scratch-pad RAM and there are specific memory locations that mean something, and you have to be very aware of all of them. In this case, the console ISR looks at address >83CE as a flag that sound data is ready to be played. The ISR then looks at CPU address >83CC for an address of the sound data in VDP RAM. It will then go to that VDP address and start reading the data as a "sound list" as per the format described in the E/A manual.

The sound list format is actually pretty nice. The first byte indicates how many bytes are to be sent to the 9919. Then it sends that many bytes to the 9919. Then a byte is read and used as a duration byte, which will be decremented on every subsequent call of the ISR until it is zero. Once the duration count is zero, the reading of the sound list data repeats. A sound list might look like this:

BYTE >09,>85,>2A,>90,>A5,>2A,>BF,>C5,>2A,>DF,>19
BYTE >03,>9F,>BF,>DF,>00

So 9 bytes of sound data follow: >85,>2A,>90 programs the 1st generator (the >8x of the first byte indicates the 1st generator), >A5,>2A,>BF programs the 2nd generator, and >C5,>2A,>DF programs the 3rd generator. The 3rd byte in each of the 3 sets is the attenuation command >9x, >Bx, Dx for each respective generator. The final byte >19 (25) is the duration and will cause the tones to play for about 0.4166 seconds.

The processing stops when a duration of zero is found, at which point >00 is put in address >83CE, and this is how you know the sound is done playing (and how the ISR prevents itself from processing more bytes on subsequent call of the ISR.) From XB you can use a CALL PEEK(-31794,S) to get access to this byte and if it is 1, a sound is being played by the console ISR.

So the second "line" of the sound list above indicates 3 bytes of data. The >9x, >Bx, and Dx bytes are commands 2, 4, and 6 which are the attenuator commands, and the value of >F means "off". The final >00 byte for the duration will stop the processing of this sound list. The length of a sound list processed by the console ISR is only limited by the VDP RAM, however XB does not provide a means to create more than a few bytes of sound data via the SOUND subroutine.

Tursi's method worked good until XB did "garbage collection". In high-level languages that deal with memory allocation on behalf of the programmer, there is usually a process that goes through the "heap" (free memory) and looks for variables that are no longer in use and reclaims that memory. Garbage collection is a big deal in modern languages like Java and Python, and can really affect the performance of any language, so it is activated only at various times determined by the run-time interpreter.

What happened in this situation, we (Tursi and I) suspect, is that the garbage collector wiped out the sound data. It did this because strings in XB are not just "data" in VDP RAM. They have a format and are associated with an entry in XB's symbol table. A symbol table is where a language maintains information and references on all the variables being used by the program. Part of a string's data in VDP RAM is a pointer back to its symbol table entry, and since the sound data was just random data in the string data space, the bytes that were supposed to point to a symbol table entry did not (of course they did not, no real strings were set up.) So, the garbage collector reclaimed the RAM and trashed the sound list, which who knows what kind of trouble that caused, but obviously ended quickly in a console crash.


5.0 Assembly Support for XB, Round 2

This is where I entered the picture. I can't remember why I started looking, or if Owen asked me if I would check it out. Tursi had already done a lot of work and was being hammered at work, so I told Owen I would see what I could come up with.

The first thing I had to do was understand how Tursi was allocating that string space. After figuring what probably happened to Tursi's code, the first thing that came to mind was to make *real* strings in XB's string space and stuff the sound data into that memory.

Side Note: Keep in mind that we were concentrating on XB's string space because XB keeps strings in VDP RAM, and the console ISR auto-sound code only looks in VDP RAM or GROM for sound list data.

I was going to allocate a string in XB, something like SND$=RTP$(" ",1024), then put the sound data in there. As long as that string is never used in the program, it would not be touched by the XB garbage collector. Then I read in the XB manual that strings can only be 255 characters... Hmm. I probably knew that, 20 years ago. Scratch that idea.

Idea number 2 was to use the PAB space at the top of the VDP RAM. The PAB buffers are huge, like 512 bytes each and if there was no file activity going on during the game (which there was not), then the sound data should be safe up there. I set off to do this and actually got it to work, once. Something was writing into that RAM though, so XB must have been using it for something but I could find anything documented that was definitive. There is a location used by TI-BASIC with the E/A cartridge, address >8370, that is used to indicate the highest available address in VDP RAM, and CALL FILES was supposed to adjust this. Well, CALL FILES did not appear to do anything in XB with regards to the address stored in >8370. I set the value below the sound list data anyway, and got things to work once, but I could not keep it reliable.


6.0 Sound Player, A Solution

At this point this was totally a challenge for me. Owen's tune was very catchy and really made the game, and I was impressed that it was possible at all. I started thinking about the console ISR and that the auto-sound code can't be too complicated since it runs in the ISR. So I pulled the TI-Intern up again and started looking into the code. I was thinking that I could just make my own "sound player". Off to the 9919 datasheet to understand how it was programmed, which is when I really paid any attention to the sound lists described in the E/A manual. Since Owen's sound data was already in that format, and since I liked the format, I decided to make my sound player read the same sound lists. So here was a solution, the only problem was how to run it? Well, for all their faults, the TI-99/4A designers did do one thing for us, they added a hook to the console ISR!

The last thing the console ISR does is look at the word stored at CPU address >83C4, and if the data is not >0000, then the ISR will branch to the CPU address designated by that data! Note that the E/A manual says this is only available on the 4A. So, we can use CALL LOAD from XB to get our program into RAM that XB won't mess with, then hook to the console ISR to have our sound player run every 1/60th of a second just like the console's sound auto-play! This was kind of cool actually and I started getting into it. ;-)

The first thing I needed was a set of calls to provide an interface via XB. This went through a few iterations, but ultimately my specification wound up being this:

* Play sound lists as per the format specified in the E/A manual
* Play sounds from CPU RAM instead of VDP RAM (no copying, no duplication or waste)
* Allow sounds to be started and stopped
* Provide a means to determine if a sound is still playing
* Be compatible with XB's SOUND subroutine (play nice)

The sound player has these routines accessed via CALL LINK:

SPINIT - initialize the sound player and set up the ISR hook
SPSTOP - stops any playing sounds
SPDONE,<numeric var> - returns 0 or 1 to an XB variable to indicate if a sound is playing or not
SPPLAY,<numeric var> | <numeric expression> - plays a sound associated with its index

Sound lists are part of the assembled program and are referenced by an index starting at 1. So, use would be like this:

CALL INIT
CALL LOAD("DSK1.SP")
CALL LINK("SPINIT")

At this point the program is loaded and the sound player ISR hook is being called for every VDP interrupt. To play sound 1, you would use:

CALL LINK("SPPLAY",1)

Or you can use a variable:

10 THEME=1 :: HIVE=2
.
.
.
60 CALL LINK("SPPLAY",THEME)
.
.
.
200 CALL LINK("SPDONE",S) :: IF S=0 THEN CALL LINK("SPPLAY",HIVE)

Or whatever else you need. If you issue any CALL SOUND commands, it will stop any sound being played by the sound player. Also, if any console ISR sound is playing, the sound player will not start any of its sounds. This is "play nice" part of the player.


7.0 The Details

Code to be loaded by XB or the E/A CALL LINK command has to be compiled as "relocatable", which is pretty much the typical way of doing assembly programs (unless you are writing code for something like a cartridge or a specific use.)

The first thing I started with was the ISR hook to make sure it worked as expected.
       DEF SPINIT

ISRHOK EQU  
>83C4            * ISR hook address

ACTIVE BYTE
>00              * Set to >01 when a sound list is playing
ZERO   BYTE
>00              * Zero value byte

SPINIT
       MOVB
@ZERO,@ACTIVE    * Disable playing of any sound list
       LI   R0
,ISR           * Address of the player, i.e. our ISR code
       MOV  R0
,@ISRHOK       * Set up the ISR hook
       B    
*R11

* Empty ISR routine
ISR
       CB  
@ZERO,@ACTIVE    * Check if a sound list is active
       JEQ  ISREND          
* Nothing to do

* Play sound data here...

ISREND B    
*R11

       
END


Anything in the DEF list will be available to XB via CALL LINK. This code will assemble as-is and when you CALL LINK("SPINIT") in XB, whatever code is located at "ISR" (currently is just returns to the console ISR) will be executed by the console ISR once every 1/60th (or 50th) of a second. I use the ACTIVE byte as an indicator that a sound is playing or not. The ZERO byte is just the number >00 that comes in handy, and in a moment that will be joined by an ONE and TWO. :-)

Cool. This worked really well, so I started expanding from there.

First I needed some sound data. I had Owen's complete sound lists, but for this example I'll use only a few notes:

SOUND1
       BYTE
>09,>85,>2A,>90,>A6,>08,>B0,>CC,>1F,>DF,>12
       BYTE
>09,>85,>2A,>90,>A4,>1C,>B0,>C9,>0A,>D0,>12
       BYTE
>03,>9F,>BF,>DF,>00


We also need to fill out the main reading of the sound lists and pumping the data to the 9919, so here is the basic code to do that:

TI9919 EQU  >8400            * 9919 write data
XBWS   EQU  
>83E0            * Workspace when called from XB

SADDR  DATA
>0000            * Address of next sound set or 0 if no sound
DUR    DATA
>00              * Duration of current sound set

ONE    BYTE
>01              * One value byte
       EVEN

ISR
       CB  
@ZERO,@ACTIVE    * Check if a sound list is active
       JEQ  ISREND          
* Nothing to do

* Count down the duration. Since the duration can only be 1 byte, the 16-bit
* value being used can never be negative unless decremented below zero here.
* So a DEC then JGT works to cover all cases.
       DEC  
@DUR             * Count down the duration
       JGT  ISREND

       MOV  
@SADDR,R0        * Put the last sound address in R0 to use auto increment
       CLR  R1
       MOVB
*R0+,@XBWS+3     * Put the number of bytes to send to the 9919 into R1

LP10
       MOVB
*R0+,@TI9919     * Send the sound data to the 9919
       DEC  R1
       JNE  LP10

       CLR  
@DUR
       MOVB
*R0+,@DUR+1      * Set the duration for this sound set
       JNE  JP11            
* Check if the sound list is done (>00 duration)
       MOVB
@ZERO,@ACTIVE    * Set the sound list inactive

JP11   MOV  R0
,@SADDR        * Save the current location in the sound list

ISREND B    
*R11


Many times when working with bytes, it is convenient to use the WP plus some value to store a byte in the low-byte of a register. That's what is happening here:

MOVB *R0+,@XBWS+3

XBWS is equated to >83E0 which is where the XB workspace is, and the workspace set in the console ISR just prior to calling the user hook routine (our code). R0 will be at >83E0 and >83E1, R1 will be at >83E2 and >83E3, with >83E3 being the low-byte of R1. This is done to put the count in R1 without having to use two instructions.

So the ISR code will play a sound, now all we have to do is set SADDR to the CPU address of the sound data we want to play, and set the ACTIVE indicator to >01. For that we need the SPPLAY code that is called from XB. The most basic SPPLAY would be:

SPPLAY
       LI   R0
,SOUND1
       MOV  R0
,@SADDR
       MOVB
@ONE,@ACTIVE
       B    
*R11


That's it. The address of SOUND1 is loaded into R0, then moved to the SADDR (save address) location we reserved for our use. Then >01 is written to ACTIVE which is our own indicator that a sound is playing. Then we return to XB. Note that the address of the sound is set *prior to* setting ACTIVE to >01. If we reversed the instructions and set ACTIVE to >01 prior to the address, and an interrupt occurred after setting ACTIVE but prior to setting the address, then our sound player code would see the sound ACTIVE indicator but no address would have been set. Tursi noted that XB disables interrupts during a CALL LINK, but I like to avoid any possible problems.

So this is what we have so far:

       DEF SPINIT,SPPLAY

TI9919 EQU  
>8400            * 9919 write data
XBWS   EQU  
>83E0            * Workspace when called from XB
ISRHOK EQU  
>83C4            * ISR hook address

SADDR  DATA
>0000            * Address of next sound set or 0 if no sound
DUR    DATA
>00              * Duration of current sound set

ACTIVE BYTE
>00              * Set to >01 when a sound list is playing
ZERO   BYTE
>00              * Zero value byte
ONE    BYTE
>01              * One value byte

SPINIT
       MOVB
@ZERO,@ACTIVE    * Disable playing of any sound list
       LI   R0
,ISR           * Address of the player
       MOV  R0
,@ISRHOK       * User ISR hook
       B    
*R11

SPPLAY
       LI   R0
,SOUND1
       MOV  R0
,@SADDR
       MOVB
@ONE,@ACTIVE
       B    
*R11

* ISR routine
ISR
       CB  
@ZERO,@ACTIVE    * Check if a sound list is active
       JEQ  ISREND          
* Nothing to do

* Count down the duration. Since the duration can only be 1 byte, the 16-bit
* value being used can never be negative unless decremented below zero here.
* So a DEC then JGT works to cover all cases.
       DEC  
@DUR             * Count down the duration
       JGT  ISREND

       MOV  
@SADDR,R0        * Put the last sound address in R0 to use auto increment
       CLR  R1
       MOVB
*R0+,@XBWS+3     * Put the number of bytes to send to the 9919 into R1

LP10
       MOVB
*R0+,@TI9919     * Send the sound data to the 9919
       DEC  R1
       JNE  LP10

       CLR  
@DUR
       MOVB
*R0+,@DUR+1      * Set the duration for this sound set
       JNE  JP11            
* Check if the sound list is done
       MOVB
@ZERO,@ACTIVE    * Set the sound list inactive

JP11   MOV  R0
,@SADDR        * Save the current location in the sound list

ISREND B    
*R11

SOUND1
       BYTE
>09,>85,>2A,>90,>A6,>08,>B0,>CC,>1F,>DF,>12
       BYTE
>09,>85,>2A,>90,>A4,>1C,>B0,>C9,>0A,>D0,>12
       BYTE
>03,>9F,>BF,>DF,>00

       
END


You can assemble and run this code, it does work, I tested it. :-) Once you have the complied object code from the assembler, from XB you set it up like this (assuming you called the object file "SP"):

CALL INIT
CALL LOAD
("DSK1.SP")
CALL LINK
("SPINIT")
CALL LINK
("SPPLAY")


After the call to SPINIT the sound player is hooked in to the console ISR and calling SPPLAY will cause the sound list to be played. In this code the sound list is small, but it can be as long as you want up to about 6K of data. XB loads assembly routines into the lower 8K of the 32K RAM expansion at >2000. However, CALL INIT loads up about 1,536 bytes of support routines, and the REF/DEF table is at the top of that address space (working down from >3FFF.) The sound player code will take up some room too. So assume 2K of the 8K is going to be used by code, which leaves 6K for sound data; but that is a lot of sound!

The final code has all the features mentioned in the specifications, so I'll leave it up to you to check it out and ask questions about things that might be unclear. I will say this, adding the parameter passing to/from XB increased the code side by about a factor of two!

Matthew

**
* Sound Player
* Matthew Hagerty
* February 2010
*
* This code can be used for any purpose other than claiming it is yours
* and trying to copywrite it.
*
* This code is used to set up an interrupt service routine sound player
* that works with sound lists as described in the E/A manual.  It is
* designed to be used via XB.  See comments for details on use, but
* basically you do this:
*
* CALL INIT
* CALL LOAD("DSK1.SP")
* CALL LINK("SPINIT")
*
* CALL LINK("SPPLAY",<sound list number>)
* CALL LINK("SPDONE",<numeric var>)
* CALL LINK("SPSTOP")

       DEF  SPINIT
,SPSTOP,SPDONE,SPPLAY

TI9919 EQU  
>8400             * 9919 write data
VDPADR EQU  
>8C02             * VDP set read/write address
VDPRD  EQU  
>8800             * VDP read data
XBWS   EQU  
>83E0             * Workspace when called from XB
CONSND EQU  
>83CE             * Console sound playing indicator
ISRHOK EQU  
>83C4             * ISR hook address
ARGTYP EQU  
>8300             * Argument types passed by CALL LINK
ARGC   EQU  
>8312             * Number of arguments passed by CALL LINK
VSTOP  EQU  
>8310             * Top of the value stack in VDP RAM
VSBTM  EQU  
>836E             * Bottom of value stack in VDP RAM
STATUS EQU  
>837C             * Return status for CALL LINK
ERR    EQU  
>2034             * XB ERR utility (E/A manual pg.416)

SAVR11 DATA
>0000             * Save R11 so BL can be used locally
SADDR  DATA
>0000             * Address of next sound set or 0 if no sound
DUR    DATA
>00               * Duration of current sound set
ACTIVE BYTE
>00               * Set to >01 when a sound list is playing
ZERO   BYTE
>00               * Zero value byte
ONE    BYTE
>01               * One value byte
TWO    BYTE
>02               * Two value byte
ERRBA  BYTE
>1C               * Error Bad Argument
       EVEN


**
* Gets the address to the value of a single numeric variable passed in via
* CALL LINK(function,numvar)
*
* XB passes variables on a "value stack" in VDP RAM.  The address of the
* last value on the stack is stored in CPU RAM at >836E and the number of
* values passed is stored in CPU RAM >8312.  Using XB there is also a list
* of bytes that define the type of each value on the stack, which can be:
*
* 0 - numeric expression
* 1 - string expression
* 2 - numeric variable
* 3 - string variable
* 4 - numeric array
* 5 - string array
*
* The argument type list is found at >8300 and can be up to 16 values for a
* max of 16 parameters.  If the first parameter is not a numeric variable,
* return an error.
GETNUM
       CB  
@ARGC,@ONE        * Make sure only 1 argument was passed
       JNE  JP1
       CB  
@ARGTYP,@TWO      * Make sure the argument is a numeric variable
       JEQ  JP2
JP1
       MOVB
@ERRBA,R0         * Return Bad Argument
       BLWP
@ERR              * XB ERR utility
       MOVB
@ZERO,@STATUS     * Should be cleared to return to XB
       MOV  
@SAVR11,R11
       B    
*R11              * Return the XB

* Get the 4th and 5th bytes from the value stack which are the
* address of the 8-byte value of the numeric variable.
JP2
GETVSN
       MOV  
@VSBTM,R0         * Get the address of the value stack
       AI   R0
,4              * Adjust to the 4th byte
       MOVB
@XBWS+1,@VDPADR   * Send low byte of VDP RAM read address
       MOVB
@XBWS,@VDPADR     * Send high byte of VDP RAM read address
       MOVB
@VDPRD,@XBWS      * Read high byte from VDP RAM
       MOVB
@VDPRD,@XBWS+1    * Read low byte from VDP RAM

* R0 now holds the CPU address of the numeric variable's actual data value,
* which is a radix 100 number.

       B    *R11


**
* Hook the user interrupt routine which is called by the console ISR every
* 60 (or 50 in Europe) time a second.
SPINIT
       MOVB @ZERO,@ACTIVE     * Disable playing of any sound list
       LI   R0,ISR            * Address of the player
       MOV  R0,@ISRHOK        * User ISR hook
       B    *R11


**
* Stop any playing sound list.
SPSTOP
       MOVB @ZERO,@ACTIVE     * Disable playing of any sound list
       LI   R0,>9F00          * Start with generator 1
LP1    MOVB R0,@TI9919        * Turn it off
       AI   R0,>2000          * Next generator
       JNC  LP1               * Carry set when >EF becomes >00
       B    *R11


**
* Checks if the current sound is done processing.
*
* CALL LINK("SPDONE",S)
*
* The parameter S must be a numeric variable and it will be set to 0 or 1
* depending on if a sound is playing or not.
SPDONE
       MOV  R11,@SAVR11       * Save the return address to XB
       BL   @GETNUM           * Get the numeric argument into R0

* R0 now holds the CPU address of the numeric variable'
s actual data value,
* which is a radix 100 number.

* Write the result into the pass in numeric variable XB numeric variables
* are all in radix 100 format which consists of 8 bytes.  This function will
* return 0 or 1 dependind on if a sound is playing:
*
* 0 in radix 100: >00 >00 >xx >xx >xx >xx >xx >xx
* 1 in radix 100: >40 >01 >00 >00 >00 >00 >00 >00
*
* The >xx is "don't care".  But since the value 1 needs the trailing >00
* values, the whole number will be filled out with >00.

       LI   R2
,8              * Write 8 bytes to the radix 100 value
       CB  
@ZERO,@ACTIVE     * Check if a sound list is active
       JEQ  JP3
       LI   R1
,>4001          * Radix 100 for the value 1
* Writing >4001 with MOVB instead of MOV R1,*R0 because the 8-byte value
* might not be at an even address.
       MOVB R1
,*R0+           * Write the >40
       SWPB R1
       MOVB R1
,*R0+           * Write the >01
       DECT R2                
* Reduce the number of >00 values to write

JP3    MOVB
@ZERO,*R0+        * Write the remaining >00 values
       DEC  R2
       JNE  JP3

       MOVB
@ZERO,@STATUS     * Return everything OK
       MOV  
@SAVR11,R11
       B    
*R11              * Return to XB


**
* Plays the specified sound list
*
* CALL LINK("SPPLAY",numeric value)
* CALL LINK("SPPLAY",numeric variable)
*
* Calling SPPLAY will stop any current sound list and start the
* specified sound list.
SPPLAY
       MOV  R11
,@SAVR11       * Save the return address to XB
* Check the arguments here to allow for a numeric expression or a
* numeric variable.
       CB  
@ARGC,@ONE        * Make sure only 1 argument was passed
       JNE  JP5

       CB  
@ARGTYP,@ZERO     * Check if argument is a numeric expression
       JNE  JP4
       CLR  R1
       MOV  
@VSBTM,R0         * Get the address of the value stack
       INC  R0                
* Adjust to the 2nd byte
       MOVB
@XBWS+1,@VDPADR   * Send low byte of VDP RAM read address
       MOVB
@XBWS,@VDPADR     * Send high byte of VDP RAM read address
       MOVB
@VDPRD,@XBWS+3    * Put the requested sound list in R1
       JMP  JP6

JP4    CB  
@ARGTYP,@TWO      * Check if argument is a numeric variable
       JNE  JP5
* Skip the type checks of GETNUM since they are already done here.
       BL  
@GETVSN
       CLR  R1
       INC  R0                
* Grab the 2nd byte of the radix-100 number
       MOVB
*R0,@XBWS+3       * Put the requested sound list in R1
       JMP  JP6

JP5
       MOVB
@ERRBA,R0         * Return Bad Argument
       BLWP
@ERR              * XB ERR utility
       MOVB
@ZERO,@STATUS     * Should be cleared to return to XB
       MOV  
@SAVR11,R11
       B    
*R11              * Return to XB

JP6
       C    R1
,@STOTAL        * Check if the requested sound list exists
       JH   JP5
       DEC  R1
       JLT  JP5              
* Check if the requested value is < 0

       SLA  R1
,1              * Adjust R1 to address a 16-bit vector
       MOV  
@SVEC1(R1),@SADDR * Get the address of the requested sound list
       MOVB
@ONE,@ACTIVE      * Indicate that a sound list is playing

       MOVB
@ZERO,@STATUS     * Return everything OK
       MOV  
@SAVR11,R11
       B    
*R11              * Return to XB


**
* This is the ISR hook location and will be called every 1/60th of a second
* via the main console interrupt service routine which is triggered by the
* VDP vertical refresh.  This code should be as brief as possible.
ISR
       CB  
@ZERO,@CONSND     * Make sure an XB sound is not being played
       JEQ  JP10
       MOVB
@ZERO,@ACTIVE     * Disable any sound list
       JMP  JP20

JP10
       CB  
@ZERO,@ACTIVE     * Check if a sound list is active
       JEQ  JP20              
* Nothing to do

* Count down the duration. Since the duration can only be 1 byte, the 16-bit
* value being used can never be negative unless decremented below zero here.
* So a DEC then JGT works to cover all cases.
       DEC  
@DUR              * Count down the duration
       JGT  JP20

       MOV  
@SADDR,R0
       CLR  R1
       MOVB
*R0+,@XBWS+3      * Set R1 to number of bytes to send to the 9919

LP10
       MOVB
*R0+,@TI9919      * Send the sound data to the 9919
       DEC  R1
       JNE  LP10

       CLR  
@DUR
       MOVB
*R0+,@DUR+1       * Set the duration for this sound set
       JNE  JP11              
* Check if the sound list is done (>00 duration)
       MOVB
@ZERO,@ACTIVE     * Set the sound list inactive

JP11   MOV  R0
,@SADDR         * Save the current location in the sound list

JP20   B    
*R11


**
* Sound vector table
*
       EVEN
STOTAL DATA
2                 * Change to the total number of sound lists
SVEC1  DATA SOUND1
SVEC2  DATA SOUND2
*SVEC3  DATA SOUND3
*SVEC4  DATA SOUND4
*SVEC5  DATA SOUND5
*SVEC6  DATA SOUND6

* To add more sound lists, add SVECx and SOUNDx DATA statements, and add
* the SOUNDx data lists below.  Sound data lists are in the same format as
* described in the E/A manual on page 313.

* The limit on the number of sounds is about 7K because XB will only use
* the RAM at >2000 to >3FFF, and some of that is the REF/DEF table,
* utility support loaded with CALL INIT, etc.

**
* Sound data
*

SOUND1
       BYTE
9,133,42,144,166,8,176,204,31,223,18
       BYTE
9,133,42,144,167,9,176,204,31,223,9
       BYTE
9,133,42,144,166,8,176,204,31,223,18
       BYTE
9,133,42,144,167,9,176,204,31,223,9
       BYTE
9,133,42,144,166,8,176,204,31,223,18
       BYTE
9,133,42,144,167,9,176,204,31,223,9
       BYTE
9,133,42,144,164,28,176,201,10,208,18
       BYTE
9,133,42,144,172,31,191,204,31,223,9
       BYTE
9,140,31,144,175,7,176,204,31,223,18
       BYTE
9,140,31,144,166,8,176,204,31,223,9
       BYTE
9,140,31,144,175,7,176,204,31,223,18
       BYTE
9,140,31,144,166,8,176,204,31,223,9
       BYTE
9,140,31,144,175,7,176,204,31,223,18
       BYTE
9,140,31,144,166,8,176,204,31,223,9
       BYTE
9,140,31,144,162,21,176,199,9,208,18
       BYTE
9,140,31,144,172,31,191,204,31,223,9
       BYTE
9,133,42,144,166,8,176,204,31,223,18
       BYTE
9,133,42,144,167,9,176,204,31,223,9
       BYTE
9,133,42,144,166,8,176,204,31,223,18
       BYTE
9,133,42,144,167,9,176,204,31,223,9
       BYTE
9,133,42,144,166,8,176,204,31,223,18
       BYTE
9,133,42,144,167,9,176,204,31,223,9
       BYTE
9,133,42,144,164,28,176,201,10,208,18
       BYTE
9,140,31,159,172,31,191,204,31,223,9
       BYTE
9,141,56,144,164,28,176,199,9,208,28
       BYTE
9,141,56,144,164,28,176,201,10,208,28
       BYTE
9,141,56,144,164,28,176,195,11,208,28
       BYTE
9,141,56,144,164,28,176,201,10,208,28
       BYTE
9,133,42,144,166,8,176,204,31,223,18
       BYTE
9,133,42,144,167,9,176,204,31,223,9
       BYTE
9,133,42,144,166,8,176,204,31,223,18
       BYTE
9,133,42,144,167,9,176,204,31,223,9
       BYTE
9,133,42,144,166,8,176,204,31,223,18
       BYTE
9,133,42,144,167,9,176,204,31,223,9
       BYTE
9,133,42,144,164,28,176,201,10,208,18
       BYTE
9,133,42,144,172,31,191,204,31,223,9
       BYTE
9,140,31,144,175,7,176,204,31,223,18
       BYTE
9,140,31,144,166,8,176,204,31,223,9
       BYTE
9,140,31,144,175,7,176,204,31,223,18
       BYTE
9,140,31,144,166,8,176,204,31,223,9
       BYTE
9,140,31,144,175,7,176,204,31,223,18
       BYTE
9,140,31,144,166,8,176,204,31,223,9
       BYTE
9,140,31,144,162,21,176,199,9,208,18
       BYTE
9,140,31,159,172,31,191,204,31,223,9
       BYTE
9,141,56,144,174,18,176,193,7,208,18
       BYTE
9,141,56,144,174,18,176,199,9,208,9
       BYTE
9,141,56,144,174,18,176,193,7,208,18
       BYTE
9,141,56,144,174,18,176,199,9,208,9
       BYTE
9,141,56,144,174,18,176,193,7,208,18
       BYTE
9,141,56,144,174,18,176,199,9,208,9
       BYTE
9,141,56,144,172,37,176,193,7,208,9
       BYTE
9,141,56,144,172,37,176,196,6,208,9
       BYTE
9,141,56,144,172,37,176,201,5,208,9
       BYTE
9,133,42,144,164,28,176,196,5,208,18
       BYTE
9,140,31,159,172,31,191,204,31,223,9
       BYTE
9,141,56,144,172,31,191,204,31,223,9
       BYTE
9,141,56,144,172,37,176,204,31,223,9
       BYTE
9,141,56,144,172,37,176,196,28,208,9
       BYTE
9,133,42,144,173,56,176,204,31,223,18
       BYTE
9,140,31,159,172,31,191,204,31,223,43
       BYTE
>03,>9F,>BF,>DF,>00

SOUND2
       BYTE
>09,>85,>2A,>90,>A5,>2A,>BF,>C5,>2A,>DF,>19
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C5,>0D,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C2,>0E,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C5,>0D,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>CE,>0B,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>A2,>15,>B1,>C9,>0A,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>CE,>0B,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C5,>0D,>D0,>0C
       BYTE
>09,>85,>2A,>91,>AE,>0F,>B1,>C2,>0E,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C5,>0D,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C2,>0E,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C5,>0D,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>CE,>0B,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>A2,>15,>B1,>C9,>0A,>D0,>19
       BYTE
>09,>85,>2A,>90,>A5,>2A,>BF,>C5,>2A,>DF,>19
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C5,>0D,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C2,>0E,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C5,>0D,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>CE,>0B,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>A2,>15,>B1,>C9,>0A,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>CE,>0B,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C5,>0D,>D0,>0C
       BYTE
>09,>85,>2A,>91,>AE,>0F,>B1,>C2,>0E,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C5,>0D,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C2,>0E,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C5,>0D,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>CE,>0B,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>A2,>15,>B1,>C9,>0A,>D0,>19
       BYTE
>09,>85,>2A,>90,>A5,>2A,>BF,>C5,>2A,>DF,>19
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C5,>0D,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C2,>0E,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C5,>0D,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>CE,>0B,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>A2,>15,>B1,>C9,>0A,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>CE,>0B,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C5,>0D,>D0,>0C
       BYTE
>09,>85,>2A,>91,>AE,>0F,>B1,>C2,>0E,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C5,>0D,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C2,>0E,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C5,>0D,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>CE,>0B,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>A2,>15,>B1,>C9,>0A,>D0,>19
       BYTE
>09,>85,>2A,>90,>A5,>2A,>BF,>C5,>2A,>DF,>19
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C5,>0D,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C2,>0E,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C5,>0D,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>CE,>0B,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>A2,>15,>B1,>C9,>0A,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>CE,>0B,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C5,>0D,>D0,>0C
       BYTE
>09,>85,>2A,>91,>AE,>0F,>B1,>C2,>0E,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C5,>0D,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C2,>0E,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>C5,>0D,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>AE,>0F,>B1,>CE,>0B,>D0,>0C
       BYTE
>09,>8C,>1F,>91,>A2,>15,>B1,>C9,>0A,>D0,>19
       BYTE
>03,>9F,>BF,>DF,>00

**
* Add more sound lists here as necessary
*

*SOUND3
*SOUND4
*SOUND5
*SOUND6

       
END


 

Make a Free Website with Yola.