Jump to content
matthew180

Assembly sound player for XB

Recommended Posts

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.
[code]
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
[/code]

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:

[code]
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
[/code]

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:

[code]
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
[/code]

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:

[code]
SPPLAY
LI R0,SOUND1
MOV R0,@SADDR
MOVB @ONE,@ACTIVE
B *R11
[/code]

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:

[code]
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
[/code]

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"):

[code]
CALL INIT
CALL LOAD("DSK1.SP")
CALL LINK("SPINIT")
CALL LINK("SPPLAY")
[/code]

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

[code]
**
* 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
[/code]

Share this post


Link to post
Share on other sites
Excellent, excellent stuff Matthew! Very well written as well.

We should add this to the development resources sticky thread (tutorial section) and ninerpedia.
This will assure it doesn't get overseen too easily when new threads appear.

I'm hoping this kind of information (<plug on>including my commented Pitfall source code & SPECTRA game library</plug off>)
will get more people to program assembly language on the TI-99/4A :)

Hoping to see more quality tutorial material from you ;)

Share this post


Link to post
Share on other sites
On a sidenote: most of the resources mentioned in the turorial can be found in the [url="http://www.atariage.com/forums/topic/153704-ti-994a-development-resources/page__view__findpost__p__1881598"]development resources sticky thread[/url]
(except for the TMS9900 Data Manual PDF and miller graphics "The Smart Programmer" publications) ;)

Share this post


Link to post
Share on other sites
Matthew, I can't thank you enough for your hard work here. This player adds a new level of fun to all my programming projects, and will serve to (I'm sure) make music more available in XB. I know I'll be using this player in all my future XB projects... Music is very important to me and it makes the gaming experience richer. Thank you so much, you and Tursi both.... My utility for converting CALL SOUNDs to BYTEs is taking shape too... Willsy is helping me automate the process a bit, and I am working on creating a synth function for it. :). That plus this player gives us all a bit more control over our XB music. With contributors like Tursi and Matthew, our development community is sure to continue to grow. Much obliged guys!!!

Share this post


Link to post
Share on other sites
Thanks everyone for the kind responses; that is what makes doing stuff like this worth while. I'll continue to provide stuff like this as long as there is someone who needs it.

Owen, I was looking at your sound data for the first time last night (meaning I was paying attention to what the data numbers actually were), and I noticed that in the main theme music the tone for the first generator repeats a lot:

[code]
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
[/code]

Since the 9919 will continue to play a tone until you change it or set the volume to "off", you do not need all those 133,42,144 bytes for every bar, just when the note for that generator needs to change. You could cut your sound data down quite a bit I think. I'll test it out later, unless you beat me to it. You might want to consider that in your "sound byte generator" too, having it remove the unnecessary repeating notes from on bar to the next.

By the way, does the new code fix the slight slow-playing problem you mentioned?

Matthew

Share this post


Link to post
Share on other sites
Matthew,
I was not aware that was possible. The reason I did that was to maintain a consistent template and also to make sure that any channel that needed to be turned "OFF" was done immediately and easily. Mark Wills has started working on the BYTE converter to automate it. I don't know if you have tried it out yet, but DL the most recent version of it from the thread here on Atariage. There are 2 or 3 versions done, but the most recent is called V1(BETA.2) I think. It's pretty rudimentary at the moment, but getting better. =) Thanks again.

As far as the slow down of sound, I determined that if I closed other applications on my computer that it didn't do that as much... Funny--- Classic99 taking up that much CPU!!! I have 4 GB of RAM!!!

=)


Opry99er

Share this post


Link to post
Share on other sites
By the way, I added this process to the "Assembly Tips and Tricks" page on Opry99er.com under the header "Sound Player." You should check it out. =) There are 3 documents accessible on that page, all by Matthew Hagerty. =) Might as well just call it the Matthew Hagerty page

Opry99er

Share this post


Link to post
Share on other sites
Here's the rudimentary converter in the early stages of development. In XB.. You'll get the idea

To test, try using the following CALL SOUND

CALL SOUND(1000,220,1,440,1,659,0)

=) :thumbsup:

Pretty sure this is the latest BETA version Edited by Opry99er

Share this post


Link to post
Share on other sites
Owen, I strip down the theme music song and it works just fine. The data is about half the size that is was. Also, I tested the sound playing on my real TI and it works just fine. :-)

For the sound generators, just remember, they keep playing a tone until you change their frequency or set the volume to off. You can also set a generator's volume without changing the frequency, the commands are separate.

I also have some sound player changes coming later today, inspired by some stuff I read on Thierry's page.

Matthew

Share this post


Link to post
Share on other sites
Okay, here is a code update to the sound player. The player now supports three "commands" in the sound list, two of which are original to the sound lists supported by the sound auto-play code in the console's ISR (I didn't know about these "commands" until after my initial release.)

The commands are special values in the first byte of a "bar" in a sound list. This byte usually indicates how many bytes of 9919 sound data follow, but if it is any of these "command" values, the data means something else.

>00 - SLJ: Sound List Jump. Use the next two bytes as an address to read sound data from. Effectively a GOTO for sound lists

>FE - SLP: Sound List looP. Same as >00, but an additional two bytes are used as a count-down to the number of times to take the jump, and a reset once the count-down reaches zero (typically these two values would be initialized the same.) THIS COMMAND IS NOT COMPATIBLE WITH THE CONSOLE ISR SOUND PLAYER!!

>FF - SLM: Sound List jump and switch Memory. This is here only for compatibility with the sound list format supported by the console and functions exactly as the >00 command.

The console ISR only plays sound from VDP RAM or GROM, so the >FF command could be used to switch between reading from one or the other. Since the sound player I wrote only uses CPU RAM, the >FF command does not have any meaning, and just works as >00.

The specific data format for each command are thus:

>00, >jump address (2-bytes)

>FF, >jump address (2-bytes)

>FE, >jump address, loop-count reset (1-byte), current loop count (1-byte), >0000 (used internally by the sound player)

See the code for examples. Song #2 has a loop that repeats 4 times, and song #3 (although not pretty sounding) demonstrates nested sound list loops.

I'm not sure how attachments work, but I'm adding sp.zip to this post. Let me know if it does not show up.

Matthew

Share this post


Link to post
Share on other sites
This is interesting stuff, thanks :)

Last year I added a sound player to the latest beta version of my SPECTRA game library (not released yet).
I was in need for one for Pitfall. I had the ISR disabled and needed a sound-player that runs as a thread next to the
other game threads. It also uses the "ISR" sound format and only plays sounds from VDP memory.

I wasn't aware of the sound-list jump, so guess I'll have to add it to the player as well :D

Share this post


Link to post
Share on other sites

This is very well done. I did not know it existed when I was working on GPL Sound. This could have really helped me out.

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...

  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...