Nearly twelve years ago, I made one of my best investments - I bought an Atari ST. It's hard to imagine any other computer from that era serving me as well as my Atari has done. There are many reasons why I, as you, still find these machines useful. For me, the primary one is MIDI. Whatever intrepid soul had the idea to include MIDI ports on the Atari insured that it would be used by many musicians for much longer than I am certain they could have imagined. Twelve years is a long time, however, and it shouldn't be surprising that commercial support for the Atari ST has dwindled quite a bit. In order for our computer's hunger for new software to be sated, we Atarians we'll have to produce our own. With this do-it-yourself attitude, I have begun writing programs for myself with some success. I've found that the first step in creating software is not necessarily to become an expert programmer (which I am not by any means), but to decide what kind of program I need and discover how to write it. To give you an example, I'd like to you show you the process I went through in writing one of my own programs using HiSoft BASIC.
A part of my Atari/MIDI setup is an old Kawai K5 keyboard. It uses a rather uncommon means of creating sounds called additive synthesis. While surfing the Net for information about this esoteric subject, I came across some equations for calculating various harmonic spectra. My knowledge of such things as trigonometry and logarithms is elementary at best, but I thought, why not program my Atari to do the math for me and transmit the results to my K5 via MIDI. At this stage, I had taken the first step in writing my own software by identifying the kind of program I would find useful.
The second step was to think through how I wanted the program to work, look, and interact with the user (in this case, me). After giving it some thought, I decided it would be useful to be able to chose from a menu which waveform I wanted to calculate and have my Atari show me the results. When I was satisfied with the calculations, I wanted to be able to transmit the waveform to my K5. Since I may want to create more than just one waveform, it was important to have the ability to repeat the process as many times as I wished before quitting.
Starting to write the program was the third step. This began by creating a table to list the results of each calculation on the screen and a menu listing all of the program's choices. In order to do this, I didn't have to know anything about GEM's drop down menus; All I had to do was to print my menu to the screen (using the LOCATE and PRINT statements) and have a way of interacting with it. I did this by giving each menu choice it's own key. For example, when I wanted to quite the program, I would press either the "Q" or "q" key. To chose which waveform I wanted to use, I would press one of the F keys. This is done very easily with BASIC using the INP function. Putting a=INP(2) in your program will cause the computer to wait until you press a key. When you press a key, its number from the ASCII character set is returned in variable "a". The letter "Q" has the number 81 while the letter "q" is number 113. So, if I pressed "q", the variable "a" would have the value of 113. The (2) is the number that tells the computer to read data from the keyboard rather than say from that of the MIDI port.
Now that I had printed the menu and table to the screen and had given each item in the menu its own key, all I had to do was check to see which key was pressed and have the computer respond according to the choice I made. I've found the best way to do this sort of thing is to set up a loop. This allows you to make as many choices as you want before quitting the program. In most modern versions of BASIC, there are several kinds of loops. The DO/UNTIL is probably the most powerful. You will find, though, that all the different loops come in handy at one time or another. I began my menu loop by putting DO at the beginning. On the next line, I put a=INP(2) so the program would wait until I made a choice from the menu. I then needed a way to check variable "a" to see which key was pressed. As with loops, there are several different ways to do this with BASIC. You could use the IF/THEN statement: IF a=81 OR a=113 THEN END. This would check to see if "a" equaled the ASCII number for "Q" or "q". If I had pressed either of these keys, the program would have ended. There is a better way of doing this, however, using the SELECT CASE statement. It works by allowing you to check the variable for as many possibilities as you like. The best way to explain this is by showing you:
DO a=INP(2) SELECT CASE a CASE 187 CALL saw CASE 188 CALL pulse CASE 189 CALL triangle CASE 190 CALL saw_pulse END SELECT LOOP UNTIL a=81 OR a=113 'loops until "Q" or "q" is press
This is part of my menu loop. I've actually eliminated several items from the menu for the sake of brevity. The "SELECT CASE a" line chooses "a" as the variable to check. The next two lines tells the program what to do in case "a" equals 187 (the F1 key). The two lines after that tells the program what to do if "a" equals 188 (the F2 key), and so on. Once I've finished listing all the choices for variable "a", I need to end the selection with the line END SELECT. I also need to end the loop as well. I did this by checking "a" once again to see if it equaled "Q" or "q". If it didn't, then the loop would continue allowing me to make more selections from the menu. If it did, then the loop would be completed and the program would continue on to the next lines of code. The beauty of this approach is that the program will only respond to the keys you specify. It will not respond if I press a key that is not listed. This simplifies things and makes the chances for errors much less likely.
You may have noticed the CALL statements. They tell the program to execute a subroutine. Subroutines are a very powerful way of performing tasks within your program. When the "saw" subroutine is called, for example, the program skips ahead and begins executing the lines of code within that subroutine. When it's finished, the program returns to the menu loop. This way I can chose another wave and the process starts all over again. Here is an example of a simple subroutine:
SUB main_screen CLS LOCATE 1,1 PRINT "This is the main screen" END SUB
Each time this subroutine is called the screen is cleared and "This is the main screen" is printed to the screen. Not the most useful example, I know, but I think you get the idea. Sometimes variables are passed to subroutines like this:
INPUT "Enter a number:",x CALL get_sine(x) SUB get_sine(x) SATIC y! y!=SIN(x) PRINT y! END SUB
Calling this subroutine will give you the sine ratio of the number you entered. Functions are very similar to subroutines and are good for making calculations like the one above. Here is the same as a function.
INPUT "Enter a number:",x y!=FNget_sine(x) PRINT y! DEF FNget_sine(x) FNget_sine=SIN(x) END DEF
Both of these accomplish the same thing, so the question then becomes which should you use a subroutine or a function? Generally speaking, functions are good for making single calculations that you need to make over and over. Subroutines are better suited for more complex things like interacting with the user, graphics, accessing the disk drive, etc.
Getting back to my program, when I would pick a waveform from the menu, its subroutine would be called. This subroutine in turn would call other subroutines to do things like prepare the screen to print the results and ask for me to enter a number. The subroutine would then call the function needed to make the calculation and, finally, call the subroutine to print the results. This is an important point; you can call subroutines within subroutines. You can also call functions within subroutines.
All this may seem confusing, but in practice, it's really not. If you break down each task you need to perform repeatedly and give it its own subroutine you will be simplifying your program making it much easier to edit in the process. For example, the subroutine I wrote for preparing the screen to print the results of each calculation could be called by any of the other waveform subroutines. This saved me from having to write the same code over and over again.
My K5 synth has 63 harmonics per source, so for each waveform I needed to make 63 calculations. The problem was storing all those numbers. Fortunately, arrays did the trick just fine. An array takes information and arranges it into rows and columns. A calendar is a good example of this. When you need to know when that dentist appointment you made was, you would just look at the calendar until you came to day where you had written it down. You can do this with an array by putting information into certain locations and coming back later to retrieve it. Arrays can be big or small. They can have one, two, or more dimensions. A one-dimensional array has only one column and as many rows as you specify. A two-dimensional array can have as many columns as well as rows as you want. A single month from a calendar is a good example of a two-dimensional array. A three-dimensional array is like a calendar for a whole year. You have several pages or planes of two-dimensional arrays. You can even have four or more dimensional arrays. You don't have to ask Stephen Hawking to help you imagine those; there like having several calendars to chose from at once. The size of your arrays is not unlimited, however. ST BASIC, for example, limits the total amount of memory for arrays to 32k. Just something to keep in mind. You can create arrays easily with the DIM statement. It works like this:
DIM a(30,30) This creates a two-dimensional numeric array with 30 rows and columns.
DIM a$(30,30) This creates a two-dimensional string array (an array to hold letters instead of numbers).
OK, now I have my arrays, I just have to put all those numbers into them. As with the menu, a loop is a really good way of doing this. Instead of the DO/UNTIL loop, I decided to use the FOR/NEXT loop. This is one of the FOR/NEXT loops from my program:
FOR i=1 TO 63 ampln(i)=FNsaw(i) IF ABS(ampln(i))>amax THEN amax=ABS(ampln(i) NEXT i
This FOR/NEXT loop uses the counter variable "i". When the loop begins, "i" equals 1. The next lines are then executed until the "NEXT i" line is reached. Then the loop begins again with "i" increasing by one so that it now equals 2. This continues until "i" equals 63 at which point the loop ends. My array in this loop is call "ampln()". I'm giving the variable "i" triple duty by using it to do several things at once. Besides using it as a counter variable, I've used it to set the location where my array will receive a number. As we have seen, at the beginning of the loop "i" equals 1. By putting it in the parenthesis of my array, I am causing the value from the "FNsaw" function to be put in row 1. When the loop is performed a second time, "i" equals 2 putting the next value into the second row of the array. After going through the loop 63 times, my array is filled with data I can use. The third thing I'm using "i" for is to pass a value to my function "FNsaw".
The next thing I needed to do was send all my hard-earned data to my synth. This is where the Atari really shines. With it, all I have to do to send information via MIDI is use the OUT statement. OUT 3,(64) sends the byte 64 to the MIDI port. Knowing which bytes to send is the key. I won't bore you (as if I haven't already!) with all the details of how I found out which bytes I needed to communicate with my K5. The important thing to know was that I had to send quite a few bytes to prepare the synth to receive the results of my calculations, and I had to adapt those numbers so that my K5 could understand them. I did this with several FOR/NEXT loops. Rather than show it to you, I thought I would give you a taste of using the OUT statement by showing you how to turn some notes on and off. This is probably more relevant to you than a bunch of system exclusive data for the K5. In order to do this, you need three bytes to turn the note on and three bytes to turn the note off. Here's the FOR/NEXT loop to do this:
FOR i=1 TO 13 OUT 3,(144) 'channel number 1 OUT 3,(i+59) 'note number middle C to C an octave higher OUT 3,(127) 'velocity FOR t=1 TO 10000 NEXT t OUT 3,(144) 'channel number OUT 3,(i+59) 'note number OUT 3,(0) 'velocity set to zero turns note off NEXT i
The thing you may have notice is that I have a FOR/NEXT "nested" inside the main FOR/NEXT loop. I did this to create a delay so the note wouldn't be turned off as soon as it was turned on. There are much better ways of doing this, however. This is just for an example.
The fourth and last step in writing my program (finally!) was to check for bugs. In order to do this, I threw everything I could at my program. I would enter insane numbers, press all different sorts of keys as it was making calculations, and anything else I could think of to make it crash. It's important to consider every possible situation your program could find itself in so that you can make it as immune as possible to errors. This is especially true if you are thinking of releasing it to the public. Some of my bugs were fairly minor like leaving a number on the screen after I was done with it. A few bugs were a little more serious such as not making an array big enough to handle the data. Over all the problems were pretty easy to spot and fix because I had divided everything into its own subroutine.
Well, if you've stuck with me this far then hopefully I've given you some of the tools you need to write your own programs. Sometimes programming can be frustrating, but when you succeed in making your Atari do something no one else has, it can be very satisfying as well. If you do decide to take the plunge, be sure to share (or sell!) the results with the rest of your fellow Atarians. These days, we really need to stick together. Good luck!