SOUND On The Sinclair/Timex
Arthur B. Hunkins
School of Music, UNCG, Greensboro, NC
Sound on the Sinclair/Timex? Beethoven symphonies, no; simple melodies, yes! All you need is one of the programs below, and a high-gain amplifier/speaker connected to the computer's mike output. (Radio Shack's battery-operated mini- or telephone-amplifier/speakers at $10-12 work fine.)
The software secret is a short Z-80 machine language (ML) routine buried in a REM statement at the beginning of a BASIC program. It is important that the routine be the FIRST program statement; if it isn't, all the USR and POKE addresses that follow must be changed. We'll make our opening REMark statement number 10, and leave it!
After typing 10 REM, press the following sequence of CHARACTERS (ignore commas, periods, and spaces): NEXT, A, /, I, =, :, COPY, INKEY$, PEEK, COPY, (, RETURN, INKEY$, < =, RETURN, (, RETURN, H, 4, LET, 9, 4, GOTO, TAN. Remember, these are Characters – single keystrokes. Spaces will appear in the display, but you don't type any in.
Some of the characters are FUNCTIONS; to register them, you first hit the function key. Others are KEYWORDS, a bit trickier. First hit THEN, which causes the K cursor to appear; then press the keyword; finally go back and delete THEN. Presto, a keyword in a REM statement! When you are finished entering characters, be sure to hit ENTER.
Now we'll add a second statement: 30 LET A = USR 16514. Attach the amplifier to the mike output, turn up the volume, and we're ready for a test. (You might want to save the program first – machine language crashes make you start over.) Be sure you are in FAST mode; sound doesn't work in SLOW. (In your own applications, you may switch back and forth from SLOW to FAST whenever you wish.)
RUN the short program. If all is well, you should hear a slightly low B above middle C for approximately one second. The screen display goes berserk during the note, showing horizontal black streaks similar to a LOAD. When the sound is finished, the previous display returns.
Note that the computer hum, which is quite audible except during the note itself, is caused by the screen display. When the screen is "off," including black during computer calculations, there is no hum. This program produces one note per subroutine call – a square wave, limited in range to pitches from B above middle C on up. (More sophisticated routines that extend the range into the bass register, and permit a variety of tone colors, are discussed later.)
Frequency And Duration
Let's construct a simple BASIC program to play our choice and length of note:
10 REM (as above) 12 LET B = 255 14 LET D = 5 17 POKE 16520, B 22 POKE 16518, D*1000/B 30 LET A = USR 16514
B is the frequency value, POKEd into location 16520; D is the duration value, which – converted to be constant for varying frequencies – is placed in 16518. B must be between 1 and 255, and D must be greater than 0. For higher frequencies, it is possible to ask for too long a duration; one may receive a report code of B in statement 22 here (an attempt to POKE a number larger than 255 into location 16518).
For longer tones, adjust tempo location 16516, which normally contains value 24. POKEing a smaller value speeds up the tempo, while a larger value makes everything last longer. Note that these three memory locations – for frequency, duration, and tempo – are the same for all programs in this article. So is the USR subroutine call address.
The single-byte frequency values for pitches from the B above middle C up through two octaves are given in the table. These are the values with zero as the high byte (ignore the zero and use only the low byte). For this particular program, add three to each value listed; thus a low B (the first pitch with high byte = 0) should be 250 instead of 247. Experiment with different frequency and duration values.
Now let's explore a routine that permits a full range of pitches, down to two octaves below middle C or lower, if you wish. Here we require a two-byte frequency value. The machine language is more extensive. Again begin with 10 REM, adding the following sequence of character strokes: NEXT, A, /, I, =, upper left quarter square graphic, upper left quarter square graphic, COPY, VAL, PEEK, COPY, lower left quarter square graphic, left half square graphic, C, upper right quarter square graphic, (, RETURN, £, $, C, upper half square graphic, $, /, RUN, AT, < =, RETURN, lower left quarter square graphic, left half square graphic, C, upper right quarter square graphic, (, RETURN, £, $, C, upper half square graphic, $, /, RUN, H, 4, AND, 9, 4, STR$, TAN:
Whew! Again conclude by hitting ENTER, and double or triple-check the entry. Be sure to identify the correct graphics characters, preceding and following them by pressing the GRAPHICS key to obtain or cancel the G cursor. To get the graphics character rather than a reverse field letter, you must use the shift key.
This routine may be quick-checked also by adding the statement: 30 LET A = USR 16514. This time you should hear a slightly low B below middle C, for about two seconds. The complete BASIC program is listed below.
10 REM (as above) 12 LET B = 1 13 LET C = 255 14 LET D = 10 15 LET X = B * 256 + C 17 POKE 16520, B 18 POKE 16521, C 22 POKE 16518, D * 1000/X 23 IF PEEK 16518 = 0 THEN POKE 16518, 1 30 LET A = USR 16514
Here B and C are the two-byte frequency values (high-low order). X sums the two values for purposes of calculating the duration to be POKEd into 16518. Statement 23 may seem problematic. It is needed because very low notes (high B and C values) may cause 16518 to contain zero, which will produce a very long tone in combination with a small D value. Statement 23 protects against this possibility. To obtain accurate rhythms on low short tones, speed up the tempo (POKE a value less than 24 into 16516) so that you can work with larger values of D. Again, try different frequencies and durations, using the frequency table at the end of the article. B is the high byte, C, the low. Use the values straight out of the table (do not add 3).
Next we have a modification of the above ML routine that permits variation in tone color. It is more complex and difficult to use, but permits any width of pulse (rectangular) wave by changing a single variable. While requiring considerable additional overhead, only one more variable is specified (pulse width). Frequency values are the same as in the previous program. It will be easiest to enter this routine as a modification of the previous one.
Start, as usual, with 10 REM then: NEXT, A,/,I, =, upper left quarter square graphic, upper left quarter square graphic, COPY, PEEK, COPY, lower left quarter square graphic, left half square graphic, C, upper right quarter square graphic, (, RETURN, £, $, C, upper half square graphic, $, /, RUN, upper left quarter square graphic, upper left quarter square graphic, COPY, < =, RETURN, lower left quarter square graphic, left half square graphic, C, upper right quarter square graphic, (, RETURN, £, $, C, upper half square graphic, $, /, RUN, H, 4, OR, 9, 4, USR, TAN. Conclude with ENTER. Adding the statement 30 LET A = USR 16514 should result in the same tone as previously, since a square wave (50 percent pulse width) is specified.
As the BASIC overhead for this routine is fairly extensive, it may not run on the 1K ZX-81. All other programs should.
11 LET P = 50 12 LET B = 1 13 LET C = 255 14 LET D = 10 15 LET X = B * 256 + C 16 LET Y = P * .02 * X 17 POKE 16520, INT (Y/256) 18 POKE 16521, Y - PEEK 16520 * 256 19 LET Y = (100 - P) * .02 * X 20 POKE 16538, INT (Y/256) 21 POKE 16539, Y - PEEK 16538 * 256 22 POKE 16518, D * 1000/X 23 IF PEEK 16518 = 0 THEN POKE 16518, 1 30 LET A = USR 16514
P is the new variable, representing pulse width expressed as a percentage (greater than zero and less than 100). The other statements are needed to calculate, from a single two-byte frequency number, the two sets of timing loop values for the top and bottom portions of the pulse wave. Suffice it to say that the two pairs of loop values go into locations 16520-16521 and 16538-16539, and that if the two pairs are identical, you get a square wave; otherwise, a variable-width pulse results. Incidentally, these variable pulse widths may be monitored on the screen, where the thicker streaks of white represent greater positive pulse widths at the same frequency. Note, too, that spacing of the streaks is proportional to frequency.
A Short Melody
Finally, let's return to our first sound routine – the one with one-byte frequency values, square waves, and high pitches only – and attempt a short melodic phrase. To do this, we define a series of frequency and duration variables in arrays, inserting them in order during a FOR/NEXT loop that calls the notes one at a time.
Observe that this program is not designed to perform entire compositions (though, given enough memory, it could). I will review two commercial programs in an upcoming issue of COMPUTE! which allow you to code or perform, then play back, extended melodies.
10 REM (as above, first sound routine) 12 DIM B(6) 13 DIM D(6) 14 LET B(1) = 157 15 LET D(1) = 3 16 LET B(2) = 186 17 LET D(2) = 1 18 LET B(3) = 235 19 LET D(3) = 4 20 LET B(4) = 186 21 LET D(4) = 4 22 LET B(5) = 157 23 LET D(5) = 4 24 LET B(6) = 117 25 LET D(6) = 8 26 FOR I = 1 TO 6 27 POKE 16520, B(I) 28 POKE 16518, D(I)*1000/B(I) 35 LET A = USR 16514 40 NEXT I
After SAVEing the program to prevent possible catastrophe, RUN it. Do you recognize the tune? If the tempo is too slow, you can always POKE the tempo location, 16516 – insert the statement, 11 POKE 16516,15 (or POKE any other number below 24). Experiment with different speeds between 1 and 255.
You may have noticed that there is no provision for rests. Rests are a bit awkward. Perhaps you might want to work out something inside the play loop that checked for a B (frequency) array variable of zero, and converted the D (duration) array variable into an index for a "do nothing" FOR/NEXT loop. Yes, it sounds complicated. Perhaps the following suggestion is some improvement.
Add two statements to the program above: 30 IF B(I) = 255 THEN POKE 16528, 255, and 32 IF B(I)<>255 THEN POKE 16528, 254. Now, if you code a FREQUENCY value of 255, you'll get a rest of the specified duration rather than a pitch. Make sure to reserve the "pitch" of 255 for a rest. Or you may choose, and reserve, any other value greater than zero to 255 for this purpose. Unfortunately, zero won't work. After inserting this code, try substituting 255 for one of the B array values in the melody. You should get a note hole.
Comparable additions to the BASIC code for the other sound routines are also possible. I suggest reserving the value of 255 for the lower frequency byte. In the routine that deals with square waves throughout the frequency range, add the statements IF C (I) = 255 THEN POKE 16540, 255 and IF C(I)<>255 THEN POKE 16540, 254 during the play loop. Note the change in memory location. The C array represents the lower frequency byte. For the routine with variable pulse width, use the same two statements, but POKE 16541 instead of 16540.
What follows is a list of delay loop (frequency) POKE values for equally tempered pitches through five octaves around middle C. The first pitch is two octaves below middle C, the last, three octaves above middle C. Be sure to add three to these values when using the one-byte, simple sound routine. This routine handles only pitches with a high byte of zero, which is omitted.
|PITCH||HIGH BYTE||LOW BYTE|
|c1 (middle c)||1||210|