INSIGHT: Atari
Bill Wilkinson
Optimized Systems Software
Cupertino. CA
This month, I present a session on how to steal a system. Before all you kleptomaniacs rejoice, though, I should explain that I mean to show you how to take control of your Atari's software system when a user pushes the SYSTEM RESET button. This will, I hope, be useful to BASIC and assembly language programmers alike.
There will be more on the inner workings vs. outer appearances of Atari BASIC; and, as space permits, I will have my usual assortment of cute tricks and Did You Knows.
In a departure from the norm, I will review a product here. Since my company, Optimized Systems Software, both solicits and sells software for Atari (and Apple) computers, I do not think it would be fair for me to do software reviews. But, unless you and/or my dear editor object, I may, from time to time, discuss new and wondrous happenings in the world of Atari.
A Short Review
It generally strikes me as unfair for a magazine to carry a review of something it sells; but every other magazine does it, so I prevailed on COMPUTE! to let me review COMPUTE!'s First Book of Atari. I am doing so on the condition that the review must be run as I submit it. (Okay, okay, Richard…you can fix my punctuation.) I have to do a good review or they won't let me do it again (just kidding… think).
First, let me say that I did not start reading COMPUTE! regularly until about December, 1980, so most of the material in the book was new to me. Boy was it new to me! Quite frankly, I had lulled myself into thinking that, until I started writing my column, the poor Atari user had no insight (an insight gag) into the workings of Atari BASIC, DOS, etc. Not so!! There was a lot of good stuff published in COMPUTE! during 1980.
I don't want to make this review sound like a whitewash, so let's get the bad stuff over with first. The first warning that needs to be given is that, in general, this is not a book for software hackers: there is little of interest to the assembly language programmer (but see below for some notable exceptions), and the person who has read and understood (II) the technical manuals and, perhaps, De Re Atari won't find much he or she didn't know. However, for most people there is much useful material here. There are some bloopers in the material presented, things which probably wouldn't get past the current, more Atari-sophisticated, editorial staff. There's a little duplication of material. And there's a lot of stuff that has been updated by better articles which have appeared (usually in COMPUTE!) in 1981 and 1982. Actually, my biggest complaint is in a reprinted article titled "The OUCH in Atari BASIC": the article states, and the editorial lead-in agrees (and the lead-in was written recently – Oh, for shame!), that keywords can't be used in variable names. Yet, the very next article in the book says that all keywords can be used as variable names! (Still not quite true – "NOT" is poison as the first three characters of a name, and a few keywords, such as "TO" and "STEP" can't be used as-is. Oh well, this was 1979 and 1980. And, come to think of it, even Atari's BASIC Reference Manual still says not to use keywords as names. Of course, it also says that substrings are limited to 99 characters, so maybe it's not a good reference point.)
OK. So much for the bad stuff. "Not possible!." you scream? Sorry, but it's true. I really don't have any dirt to sling. Oh, some of the little example programs might now be found in the Atari manuals, etc., but they aren't bad, just not of as much value as the rest of the book. And I wish I had the time and space to correct every little goof I found. (But I gotta tell you one: the order and size of variables and their names has no impact at all on the speed of an Atari BASIC program. Honest.) With those caveats in mind, we examine the value of the book.
And the book is of value. If you had to choose between losing your left pinkie (not quite up to the left thumb, anymore), the First Book, and De Re Atari, you should really think about how useful a little pinkie is. If you must choose between De Re and First Book, let your experience level be your guide: if you almost understand the Atari technical manuals, you are probably ready for De Re. If you are just learning to program, stick with First Book. If you're in the middle, better let the little pinkie go.
My own favorite pair of articles from the book are "Inside Atari BASIC" and "Atari Tape Data Files," both by Larry Isaacs. I am just now getting to the point where I am discussing things in "Insight: Atari" that Larry explored over a year ago (there will be overlap, hopefully to your benefit). Other articles worth mentioning include the following (an asterisk indicates something of interest to assembly language buffs):
"Printing to the Screen from Machine Language"*
(not because of what the presented program does as much as for some of the techniques it introduces)"The Fluid Brush"
(Ditto. And its ideas have been much copied.)"Player/Missile Graphics" *
(by Chris Crawford. What more need I say?)"Adding a Voice Track to Atari Programs"*
(This one was even swiped! There are more sophisticated methods shown in De Re, but this is adequate for many purposes.)"Atari Memory Locations" *
(Just a table. You need to read the Technical Manual and/or De Re first, but this will serve as a handy reminder.)"Input/Output on the Atari"
(I hesitated on this one: you should ignore what it says about XIO! It's misleading. Read my Atari I/O series.)
You'll note that most of that stuff is kind of heavyweight. Well, that's what appeals to me and, I think, to a large portion of COMPUTERS Atari readership. However, there are several little goodies, usable by virtually anyone, which deserve honorable mention. No commentary on these: their names tell it all and you just have to try them to appreciate them:
"Reading the Atari Keyboard on the Fly"
"Al Baker's Programming Hints"
"Atari Sounds Tutorial"
"Using the Atari Console Switches"
"Atari Meets the Real World"
You may have your own favorites, but my criterion for a good article (or good magazine-published program) is that it teaches you something. Thus I rate tvpe-it-in-and-run-it games relatively low. (There are remarkably few of them that appear in the book.)
In final summary, I have to say that, for $ 12.95, you are unlikely to find this much (184 pages, including –can you believe it –a usable index) useful Atari material presented again (well…until the Second Book ?). Real software hackers will find some of the material too elementary, but they are probably the only ones that will be disappointed.
Stealing A System
During my series on Atari I/O (COMPUTE! November, 1981, through March, 1982, issues 18 through 21), I mentioned (more than once) the "proper" way to add device drivers to OS. I summarize it here again:
- Inspect the system MEMLO pointer (at $2E7).
- Load or relocate your routine at/to the current value of MEMLO.
- Add the size of your routine to MEMLO.
- Store the resultant value back in MEMLO.
- If your routine is a device driver, connect it to OS by adding its name and address to HATABS.
- Fool OS so that steps three through five will be re-executed if SYSTEM RESET is hit.
In COMPUTE! (January, 1982, #20) we added the driver for the "M:" device by following steps one through five as above. We discussed step six briefly, but did not show how to implement it. This month, we will show how to fool OS. And, rather than repeating the lesson about adding device drivers, we will take this opportunity to show how to give Atari BASIC some measure of control over what happens on RESET.
In particular, we "steal" the system in a way that the user who hits RESET will cause a TRAPpable error in the running BASIC program. In other words, if you write your BASIC program in a way that TRAP (to a line number) is always active, you will be able to detect when your user hits the RESET key, but your program will not stop running, will not lose its variable values, and will be impacted in the minimum possible way.
Some cautions are in order (it seems like I always have to say that): before vectoring through RAM (and thus allowing our little trick) Atari's OS ROMs perform several actions when SYSTEM RESET is hit. If you need to know exactly what happens, try to get hold of the CIO listings (they are moderately readable); generally, the following lists all that matters except to those who would make their own cartridges:
- The system resets any memory size pointers (MEMLO, MEMTOP, etc.).
- Most hardware registers are reset to zero ($D00()-$D0FF, $D200-$D4FF).
- OS clears its own RAM ($200-3FF, $10-$7F). Note that this zaps all IOCB's for all files.
- All the ROM-based device drivers are initialized (via their own initialize routines).
- CIO's initialization is called, which effectively marks all files as properly closed.
- Screen margins, etc., are reset and the E: device is opened on file channel #0 (which is equivalent to GRAPHICS 0 from BASIC).
- The file manager's initialization routine is called via an indirect call through location DOSINH ($0C).
- If there are no cartridges installed, then DOS is invoked by an indirect jump through location DOSVEC ($0A). If a cartridge is installed and wants control, though, OS goes to the cartridge instead of DOSVEC.
(NOTE: OS/A+ uses a variation on 7., above, so don't bang your head against the wall trying what is written here with OS/A +. I will be glad to tell you of the differences if the manual is not clear enough.)
Program 1 takes into account not only all of the above, but also the requirements of Atari BASIC related to executing a pseudo-warmstart. I will not try to explain why the various JSRs and tests shown are needed; just take my word for it that they are indeed necessary (I found out the hard way). Actually, the part pertaining to stealing the DOSINIT vector is straightforward, as you may note, and changing MEMLO is trivial.
Once again, for space and time reasons, I have cheated with this program: I have assumed that my routine can load and execute at $IF00 and can move MEMLO to $2000. Those of you who want to do the whole thing right can follow the techniques I showed in COMPUTE! February, 1982, #21, for generating relocatable programs. Also, please note that the listing, as is, is designed to produce an AUTORUN.SYS file. You may need to do a little surgery to use it in other ways (e.g., remove the load-and-go vector at the end, JMP directly to the start of the BASIC cartridge, etc. – experiment).
The most important thing to note about this routine's implementation is how the address found in DOSINIT is moved into the JSR instruction (at the label RESET). Obviously, you could go look at the contents of DOSINIT and code the JSR directly, omitting the move of the address. And this will work as long as you use the same version of OS and DOS. But … all too many Atari software developers fell into the trap of thinking that OS and DOS were immutable, only to have Atari announce DOS 2S and OS version B.
To Atari's credit, they have carefully documented which locations, vectors, etc., are guaranteed to remain unchanged. It you write your code properly, you should never have to change it. Incidentally, another example of this same concept appealed in my last article: the vector from VVBLKD was preserved, rather than simply JuMPing to the routines in ROM.
Enough preaching: investigate the listing. But I leave you with one last freehee. If you change three lines of code in the listing (lines 1950 to 1970) to the following two lines, you will cause BASIC to re RUN the program currently in memory, rather than causing an error TRAP.
1950 JSR $B755 1960 JMP $A962
The following short BASIC program illustrates the use of our stolen pointer:
10 TRAP 100 20 PRINT "LOOPING AT LINE 20" 30 GOTO 20 40 STOP : REM can't get here from there … or anywhere 100 IF PEEK(195)<>255 THEN PRINT "HOW? WRONG ERROR CODE!" : STOP 110 PRINT "RESET KEY WAS HIT…I WILL START AGAIN" 120 RUN
Note that you can't get out of this program with the RESET key! Now, if you also trap the BREAK key, the user is truly locked in your program. If you have BASICS A+, of course, you tan trap the BREAK key via SET0. If not, then refer to the listing titled "Idiot-Proofing the Keyhoard" in De Re Atari. (Summary of that listing: since the BREAK key is one of the two IRQ's not vectored through RAM, you must change the system master IRQ vector to point to your own routine. In your routine, you check for and ignore BREAK key IRQ's and pass other IRQ's on unchanged. Not trivial, perhaps, but certainly less complicated than what we have done above.)
Inside Atari BASIC, Part 3: Enhancements
After skipping last month, we return to our discussion of the hows and whys of Atari BASIC. Recall that in COMPUTE! February, 1981, #21, we discussed how BASIC checks your entered line for correct syntax and produces a tokenized result. Let us begin this month with a discussion of how BASIC executes (RUNs) a program.
First, note that if you enter a direct line (one without a line number), BASIC arbitrarily assigns it to line number 32768 and then pretends that it is like any ordinary line. That means that even direct lines must go through the tokenizing and execution process. It also means that BASIC makes little or no distinction between statements (within a program) and commands (given directly); thus you can LIST or RUN or even CONTinue from inside a BASIC program.
Whenever a line is finished executing, BASIC checks to see if the next line exists (it doesn't if a direct line was just executed) or if the next line has a line number greater than 32767 (i.e., if the line executed is the last one prior to the direct line). If either condition prevails, BASIC pretends to itself that it got an "END" statement and, presto, you are back staring at the "READY" prompt.
But let us assume that the direct command given was "RUN." The execution of a RUN statement simply causes all BASIC'S pointers and flags to be reinitialized, including setting BASIC'S "next line" pointer to the beginning of the program. Then RUN returns to what we call "Execution Control" which decides that it needs to start executing the next line…which conveniently is the first line.
So far, so good. But how does BASIC know what to do with the tokens? The answer is that it doesn't, really. Recall that there are two separate kinds of execution (as opposed to variable) tokens: statements and operators. Each of these has a table of two-byte pointers residing in BASIC'S ROM. Execute Control simply picks up the next byte of the program, assumes that it is a statement token (incidentally, in the range of $00-$7F), and uses double its value as an index into the table of statement pointers. It uses the address thus found as an indirect jump and goes to the appropriate statement execution routine.
In a non-syntaxed BASIC (i.e., Microsoft), much of the preceding applies virtually unchanged. But, when the statement execution routine gets control in such a BASIC, it has no idea what the next character or token in the program might be, so it must needs go through a set of checks to determine what is legal and what is not.
In Atari BASIC, though, the statement execution routine knows that the byte(s) that follow constitute legal syntax! So it need not waste time checking for legality. Since the bytes following the statement token may range from the non-existant (as in CONT, which has no following bytes) to the extremely complex (as in PRINT, in all its variations), each statement generally has responsibility for choosing what to do with these bytes.
With the exception of assignment-type operations (LET, READ, INPUT, etc.), file designators (PRINT #), and complex statements (FOR…TO…STEP), what follows the statement byte is generally a series of one or more expressions, separated by commas, equal signs, semicolons, etc. Thus it comes as no surprise that there is a major subroutine in Atari BASIC entitled "Execute Expression," which can evaluate virtually any numerical or string expression.
As a simple example, let us examine the mechanism of POKE. The syntax is properly "POKE <aexp>,<aexp>" (where <aexp> means Arithmetic EXPression). So POKE's statement execution routine simply calls Execute Expression for the first value, saves it away someplace safe, skips the comma (it knows the comma is there…he syntaxer said so!), calls Execute Expression for the second value, and stores the second into the memory location designated by the first. Now, in truth, POKE calls a variation on Execute Expression which is guaranteed to return a l6-bit (or 8-bit, as required) value; but the concept holds for most statements.
It is really beyond the scope of this article to try to explain the intricacies of Execute Expression. It will suffice to point out that it must worry about operator precedence ("*" before " + ", etc.) and parentheses and subscripted variables and functions (SIN, RNI), etc.) and more.
And that's about it. Except to note that when a statement is finished it usually simply returns to Execute Control, which checks for another statement on the same line and/or moves its pointer to the first statement of the next line.
Much of the point of this discussion has been to show why it is hard to fool BASIC into believing that it has a new statement to use. Even with the source, it is no easy task to make sure that the correct syntax for a new statement is entered into the syntax tables (which are actually a miniature language in their own right), the name tables, and the execution tables (to say nothing of writing the code to execute the statement). With Atari BASIC locked in ROM, the task is really impossible since BASIC makes use of no RAM-based pointers or indirect jumps throughout this process.
So how can we add features to Atari BASIC? Several ways:
- Try the USR function as suggested last month. This really is the simplest, most straightforward, most guaranteed-to-work.
- Make your own special device handler (a la "M:" in COMPUTE!, January, 1981, issue #20). Open a channel to it (OPEN #1,…). PRINT something to it. When your driver gets control, it can actually go in and look at the BASIC tokens and decide what to do from there. Cryptic, but it works.
- If you are interested in commands, as opposed to statements, you can intercept BASIC's call to "E:" (for the next input line) and examine the line yourself (presumably as does Monkey Wrench). This implies that you must check syntax, find variables, convert ASCII to floating point, etc., in your routine. Tedious, but obviously feasible and usable.
As you can, no doubt, tell, I am much in favor of method 1. It is by far the easiest to do and requires the least knowledge of BASIC'S internals.
Is there yet another way? A month ago I would have said "no!" But, now, I have discovered a crack in the door. It is complicated, prone to programmer error, fairly inflexible, and of doubtful value for anyone but professional software developers. To explain it would take a couple of more columns, and I'm simply not willing to write that much on a topic of dubious value. If you feel you absolutely must know, write me (care of OSS). If enough people write, I may make up a pamphlet and sell it at an outrageous price. Are you sure you can't live with method I ?
Easy Horizontal Joysticks
If you have an application (a polite way of saying game) that needs a joystick that moves only horizontally (or only vertically, if you are willing to hold your joystick turned 90 degrees from "normal"), then have I got a trick for you! Try this program, with joystick number 0 plugged in:
10 PRINT PTRIG(0)-PTRIG(1), 20 FOR J = 1 TO 50 : NEXT J 30 GOTO 10
Now push the joystick in all directions. Neat? Pushing it left gives you a value of -1 and right gives you + 1. And, of course, you can use A = PTRIG(2)-PTRIG(3) to read joystick number 1, etc.
Why does it work? Because the paddle triggers happen to use the same pins on the connector that the horizontal switches in the joystick use. I discovered that by reading the technical manual; so, you see, there is probably still buried gold in those books.
Unfortunately, no such happy coincidence exists for reading the vertical joystick switches. Incidentally, use of this trick does not affect STRIG in any way.
Dissonances
The algorithm Atari gives for figuring out what actual frequency will result from the divider FREQ in (for example) SOUND 0,FREQ,tone,volume is as follows:
This means that, at values for FREQ around 85 (the middle of the Atari's frequency range), the minimum actual frequency step is about 4 Hz. While adequate for solo parts, this kind of frequency resolution can really grate on your ears when there are three or four voices active. To illustrate the real meaning of this, try the following one-liner:
FOR F = 255 TO 0 STEP -1 : SOUND 0,F,10,15 : NEXT F
The resultant sound is a smooth glide until you get near the top end, when you begin to really hear the steps.
For those of you with a keen ear and/or a strong sense of music, cheer up. Atari, once again, gave us a solution. The entire Atari audio system is controlled by hardware register AUDCTL ($D208). Normally, the audio channels are clocked by an oscillator running at 63921 Hz. But, the user may specify that channels zero and two (which Atari calls one and three in the Technical Manual… oh well) are to be clocked by a 1,789,790 Hz oscillator. If you change 63921 to 1789790 in the formula above and plug in 255 (the highest value) for FREQ, you will see that the lowest note thus playable is around 3000 Hz!
But we have yet another solution available via AUDCTL: instead of an 8-bit counter for a single audio channel, we use a pair of channels to produce a 16 bit counter. (Unfortunately, we then are limited to two sound channels.) The modified formula then becomes:
Since FREQ now has values from 0 to 65535, it's obvious we have many more actual frequencies available to us. I present herewith two sample programs using this technique. Program 2 shows two voices doing very smooth glides. Program 3 shows a 9-octave chromatic scale (C, C#, D, D#, etc.). This compares to the 3-octave scale available via the standard SOUND commands.
Finally, if you simply need lower notes than you can get with SOUND, TRY POKEing AUDCTL to one. This has the effect of lowering all notes by approximately two octaves. Unfortunately, you do not get to have some channels high and others low. Example:
Program 1.
equates and commentary 0000 1000 .PAGE "equates and commentary" 1010 ; 1020 ; STEAL A RESET 1030 ; 1040 ; listing for COMPUTE! magazine, April, 1982 1050 ; 1060 ; 1070 ; there are two parts to this listing: 1080 ; 1. what happens when this is first loaded 1090 ; (which initializes everything) 1100 ; 2. what happens when the User pushes 1110 ; SYSTEM RESET. 1120 ; 1130 ; Most of 1 is understandable. Most of 2 1140 ; is magic. If it works, don't knock it. 1150 ; 02E7 1160 MEMLO = $2E7 ; BOTTOM OF USABLE MEM 1170 ; 00FF 1180 LOW = $FF 0100 1190 HIGH = $100 1200 ; 1210 ; EQUATES INTO BASIC ROM 1220 ; BD72 1230 SETDZ = $BD72 ; ENSURE OUTPUT TO CONSOLE 0092 1240 MEOL = $92 ;FLAG: LINE MODIFIED BD99 1250 FIXEOL = $BD99 ; UNMODIFY 00B9 1260 ERRNUM = $B9 ; AT LEAST BASIC THINKS SO B940 1270 ERROR = $B940 ; HANDLE ERRORS 00BD 1280 TRAPFLAG = $BD DA51 1290 INITBUF = $DA51 ;SAFETY 0011 1300 BRKFLAG = $11 BD41 1310 CLOSEALL = $BD41 ;close IOCBs 1-7 000C 1320 DOSINIT = $0C ;see article 1330 ; SETUP THE RESET VECTOR 0000 1340 .PAGE "SETUP THE RESET VECTOR" 1350 ; 1360 ; We move the OS DOSINIT vector to point to outselves 1370 ; 1380 ; ***** NOTE: change next line to suit!!! ***** 0000 1390 *= $1F00 1400 CHANGEDOSINIT 1F00 A50C 1410 LDA DOSINIT 1F02 8D231F 1420 STA RESET + 1 1F05 A50D 1430 LDA DOSINIT + 1 ; Self modifying code…nasty 1F07 8D241F 1440 STA RESET + 2 1F0A A922 1450 LDA #RESET&LOW 1F0C 850C 1460 STA DOSINIT1F0E A91F 1470 LDA #RESET/HIGH 1F10 850D 1480 STA DOSINIT+1 ; We have chanqed the pointer 1490 ; 1500 ; Here we Move MEMLO… 1510 ; we arbitrarily use 256 bytes of space 1520 ; 1530 MOVEMEMLO 1F12 A900 1540 LDA #RESETTOP&LOW 1F14 8DE702 1550 STA MEMLO 1F17 A920 1560 LDA #RESETTOP/HIGH 1F19 8DE802 1570 STA MEMLO+1 1F1C 60 1580 RTS 1590 ; 1600 ; FROMBASIC is just a second entry 1610 ; entry point into the initialization… 1620 ; for initializing from BASIC 1630 ; 1640 FROMBASIC 1F1D 68 1650 PLA ; get cnt of parms off stack 1F1E F0E0 1660 BEQ CHANGEDOSINIT ; good…no parms 1F20 D0FE 1670 OOPS BNE OOPS ; otherwise, tough! THE ACTUAL RESET TRAP 1F22 1680 .PAGE "THE ACTUAL RESET TRAP" 1690 ; 1700 ; On reset, DOS normally gets 1710 ; called to reinitialize itself … 1720 ; we use this to our advantage by 1730 ; reinit'ing both DOS and BASIC 1740 ; 1750 RESET 1F22 200000 1760 JSR 0 ; second two bytes get replaced 1770 ; by the address of real DOSINIT 1F25 A2FF 1780 LDX #$FF 1F27 9A 1790 TXS ; BASIC likes it this way 1F28 8611 1800 STX BRKFLAG ; ensure no BREAK key pending 1F2A 2041BD 1810 JSR CLOSEALL ; so everybody agrees 1F2D 2072DB 1820 JSR SETDZ 1F30 A592 1830 LDA MEOL 1F32 F003 1840 BEQ RST2 1F34 2099BD 1850 JSR FIXEOL 1860 RST2 1F37 2051DA 1870 JSR INITBUF 1880 ; 1F3A 20121F 1890 JSR MOVEMEMLO ; to protect this code 1900; 1910 ; NOW we fool BASIC into thinking 1920 ; an error occured 1930 ; 1940 ; 1F3D A9FF 1950 LDA #255 ; (or your choice of errors) 1F3F 85B9 1960 STA ERRNUM 1F41 4C40B9 1970 JMP ERROR ; do it 1980 ; 1990 ; THE FOLLOWING EQUATE IS USED TO SET 2000 ; RESETTOP on a page boundary 2010 ; 2000 2020 RESETTOP = x + $FF&*FF00 2030 ; SET UP LOAD AND GO 2040 ; lF44 2050 x = $2E0 02E0 001F 2060 .WORD CHANGEDOSINIT 02E2 2070 .END THE ACTUAL RESET TRAP = 02E7 MEMLO =00FF LOW = 0100 HIGH =BD72 SETDZ = 0092 MEOL =BD99 FIXEOL = 00B9 ERRNUM =B940 ERROR = 00BD TRAPFLAG =DA51 INITEUF = 0011 BRKFLAG =BD41 CLOSEALL = 000C DOSZNIT 1F00 CHANGEDOSINIT 1F22 RESET 1F12 MOVEMEMLO = 2000 RESETTOP 1F1D FROMBASIC 1F20 OOPS 1F37 RST2
Program 2.
10 AUDCTL = 53768:DBL = 120 20 AUDF1 = 53760 : AUDC1 = 53761 30 SOUND l,10, 10, 15 : SOUND 3, 10, 10, 15 40 POKE AUDC1, 0 : POKE AUDC1 + 4, 0 50 POKE AUDCTL, DBL 60 FOR J = 10 TO 15 : POKE AUDF1 + 2, J: POKE AUDF1 + 6, 20-J 70 FOR I = 0 TO 255 : POKE AUDF1, I: POKE AUDF1 + 4, 255-I: NEXT I 80 NEXT J …VERY SMOOTH GLIDES…
Program 3.
10 AUDCTL = 53768 : DBL = 120 12 0SC = 1789790/2 20 AUDF1 = 53760 : AUDC1 = 53761 30 SOUND 1, 10, 10, 0 40 POKE AUDC1, 0 : POKE AUDC1 + 4, 0 50 POKE AUDCTL, DBL 60 P2 = 2^(1/12) 70 NTE = 16 : REM C IN THE REAL BASS 80 FOR I = 1 TO 109 90 FREQ = INT (OSC/NIE-7 + 0.5) :F0 = INT (FREQ/256) 92 Fl = FREQ--256*F0 100 POKE AUDF1, F1 : POKE AUDF1 + 2, F0 102 POKE AUDC1 + 2, 175 103 PRINT "NOW PLAYING "; INT (NTE + 0.5);" HZ" 105 FOR J = 1 TO 100 : NEXT J 110 NTE = NTE*P2 120 NEXT I 130 GOTO 70 …9 OCTAVE CHROMATIC SCALE…
SOUND 0, 60, 10, 12 : SOUND 1, 45, 10, 12 : POKE 53768, 1 : FOR I = 1 TO 500 : NEXT I
Again, investigate De Re for even more details on some of the more complex aspects of the sound system. (Want to hear your Atari "MOO" like a cow?)
A Final Caution
I have had a couple of people write or call me complaining that, when they tried my assembly language routines, they didn't work. Honest, they do work. The listings published in the magazine are the ones I actually used. Sometimes the problem simply resolves to a typo on the part of the user. But sometimes it turns out to be an address conflict.
I do most of my work with OS/A+ and BASIC A+ (naturally. But I use Atari BASIC to check out programs for these pages), and I usually use memory addresses which are convenient to me. Since I get tired of putting everything in page six, I sometimes use $1F00 or some such as an origin. You can use these addresses in your system with the Atari Assembler/Editor if you change MEMLO to, say, $3000 (my usual choice, and achievable with the LOMEM command of EASMD). However, it may be more convenient to use SIZE to look at where your source, etc. is and then change the origin to reflect your memory configuration.
Of course, you can always assemble into the dreaded page six or assemble directly to disk (or cassette). But, in any case, don't use an origin ("* = xxxx") which conflicts with SIZE in your system. I purposely give you source listings so that you can see how things work; take the time to type them in and it will probably prove easier in the long run.