Subject: Assembly sound player for XB

From: "Matthew" <[email-masked]>
Date: Tue, 23 Feb 2010 05:20:19 -0000

I posted this over on AtariAge too where I included the entire source code.  I like the forum format better for stuff like this since the "code tags" make it easier to include code examples (even if their syntax highlighting does not support 9900 asm...)

http://www.atariage.com/forums/topic/158605-assembly-sound-player-for-xb/

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

* Empty 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

---

From: "Tursi" <[email-masked]>
Date: Mon, 22 Feb 2010 23:33:07 -0800

Nice writeup, Matthew! I do have one note for you... I should post at AA but
I can't get there right now.

> 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!)

I had to go back and re-run my numbers, so hopefully this is correct. This
did let me find an error in my own notes, though!

The 9919 /does/ take 32 clock cycles, but the 9919's clock in the 4A is not
the CPU clock. As far as I know it runs on the colorburst clock, so 3.58MHz.
This works out to about 8.9uS. (The later version of the chip ran a 500kHz
clock, but only needed 4 cycles to process data, which is 8uS).

As per our work with the VDP, the fastest that the 9900 can write to the
sound chip (via MOVB Rn,*Rm) is 26 cycles at 3.0MHz, or 8.65uS.

Regardless, there is no delay writing to the sound chip, like the VDP, so
the CPU is never slowed by accessing it. It takes the same time as a write
anywhere else in memory. The multiplexer should trigger on it.

> 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.

I'm sure someone beat me to this, but only the /next/ CALL SOUND pauses the
program unless you specify a negative duration.

What is going on is that XB always creates a sound list for a CALL SOUND,
but if you specify a positive duration, it does not do so until the previous
list is complete. So even though it's only a single note, XB does use the
console sound interrupt routine. :)

> What happened in this situation, we (Tursi and I) suspect, is that the
> garbage collector wiped out the sound data.

I actually think the garbage collector happened upon the sound data, and
thought it contained pointers to data it needed to work with, screwing
itself up horribly... but this explanation is close enough, especially since
the rest of the explanation is sensible. hehe :) Of course, your theory has
potential, too, if it wrote large byte counts the console ISR would read
memory into the sound chip till it hit the hardware registers, then go nuts.

It's really cool how you figured out exactly what I was trying to do,
though. :)

> 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.

Remember that CALL FILES is a function of the disk DSR, and not a console
function at all. The console, XB, BASIC, GPL, these all have no concept at
all of the disk system. In Classic99, the disk system doesn't need VDP
buffers so I don't allocate any. :)

> 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.

This is nice! Are you just checking the same scratchpad RAM locations to
track the playlist address?

>Tursi noted that XB disables interrupts during a CALL LINK, but I like to
>avoid any possible problems.

I have to be a little picky here... the GPL interpreter /always/ has
interrupts disabled, except for one very brief window where it does LIMI
2/LIMI 0, right before reading the next GPL instruction. This is not a
problem, it will never miss a pending interrupt due to the short window
because as soon as the mask is set, if an interrupt is pending, it will be
processed. Of course, only one interrupt can be pending as a time, so if you
take too long to get back to that point, then you will see slowdown. So they
ARE disabled during CALL LINK, but not specifically so, they are also
disabled during PRINT, CALL SPRITE, etc. :) If you write a program that does
a lot of scrolling PRINTs, you should hear the sound slow down as a result
of the slow processing time.

Nothing wrong with being sure, though, that's good compatibility. :)

A nice document, and getting it in email too lets me save a copy of it.
Thanks! :)

---

From: ti99_forever <[email-masked]>
Date: Tue, 23 Feb 2010 00:04:36 -0800 (PST)

I can't get through this in one sitting, but after reading Tursi's comment...

My sound player had the same issue with garbage collection. The trick? move the VDP Stack (for GOSUBs and such) higher in memory, and place your sound lists underneath it. The Garbage Collector resets upon hitting this stack.

I guess since you published yours, I will need to publish mine.

--- On Tue, 2/23/10, Matthew <[email-masked]> wrote:

> From: Matthew <[email-masked]>
> Subject: [TI994A] Assembly sound player for XB
> To: [email-masked]
> Date: Tuesday, February 23, 2010, 12:20 AM
> I posted this over on AtariAge too
> where I included the entire source code.  I like the
> forum format better for stuff like this since the "code
> tags" make it easier to include code examples (even if their
> syntax highlighting does not support 9900 asm...)
> 
> 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.
> 
> 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.
> 
> 
> 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:
> 
> 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.
> 
> 5. check for the QUIT key, and BLWP @>0000 is so
> (software reset, same as interrupt level 0)
> 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!
> 
> 
> 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.
> 
> 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:
> 
> 2. 1001xxxx - set tone 1 attenuation (xxxx = >0 through
> >F)
> 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.
> 
> 
> 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:
> 
> 3. Set the address of the sound data at CPU address
> >83CC
> 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.
> 
> 
> 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.
> 
> 
> 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:
> 
> SPDONE,<numeric var> - returns 0 or 1 to an XB
> variable to indicate if a sound is playing or not
> 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 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.)
> 
> ACTIVE  BYTE >00         
>      * Set to >01 when a sound list
> is playing
> ZERO     BYTE >00   
>            * Zero value
> byte
> 
> 
> SPINIT
>        MOVB @ZERO,@ACTIVE  *
>        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 
>        JEQ  ISREND 
>    * Nothing to do
> 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. :-)
> 
> 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 
>        JEQ  ISREND 
>    * Nothing to do
>        DEC  @DUR   
>               * Count
> down the duration
>        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 
>        JNE 
>        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.
> 
> 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
> 
> 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  *
>        LI     
> R0,ISR             
>    * Address of the player
>         LI   
>    R0,SOUND1
>         B   
>    *R11
>        JEQ  ISREND 
>    * Nothing to do
>        DEC  @DUR   
>               * Count
> down the duration
>        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 
>        JNE 
>        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
> 
> 
> 
> 
> ------------------------------------
> 
> Yahoo! Groups Links
> 
> 
>     [email-masked]
> 
> 
>

---

From: Owen Brand <[email-masked]>
Date: Tue, 23 Feb 2010 08:57:29 -0500

Hey, I just wanted to give a big giant thanks to Tursi and Matthew.   
This game just isn't the same without the music-- I knew this was  
possible in theory, but wouldn't have been able to figure it out  
myself... At least not this year.  :). I'll be addressing this further  
at Atariage.  Our community is awesome, guys.... Just wanted to  
express my gratitude here.  Thanks Tursi and Matthew.

Owen Brand
Opry99er.com

---

From: "Matthew" <[email-masked]>
Date: Thu, 25 Feb 2010 16:53:34 -0000

> anywhere else in memory. The multiplexer should trigger on it.

Yup, I was thinking in the wrong terms (clocks vs. uS) and also that the 9919's clock is faster than the system clock.  Thanks for pointing that out.

> memory into the sound chip till it hit the hardware registers, then go nuts.

I was thinking something along those lines too, I just could not come up with a short and concise way to say it. ;-)

> buffers so I don't allocate any. :)

Yeah, you told me that, but sometimes it takes a while for things to sink in. :-)

> track the playlist address?

Exactly.  I check if >83CE is other that zero, and if so, I set my internal "playing" flag to zero and just return.  The only way to start playing again is to start another sound, which won't either if until >83CE is zero.

> A nice document, and getting it in email too lets me save a copy of it.

I hope people find it useful.  I have some code updates which I'll be posting to the AtariAge thread (I don't have anyplace better to put the code right now.)

Matthew