A Word-Based Voice Synthesizer For The Apple II
Spring Valley, NY
Since I purchased my computer I have been interested in voice synthesis. Its applications in CAI, games, and error handling seemed extensive. I decided to apply my newly learned machine language skills to writing my own voice routines.
My routines would have to meet several requirements:
- They would have to be word based. This would keep the amount of memory per word constant. It would also provide for block memory organization. As well as this, it would simplify the program itself.
- The routines would have to be easy to use. They would be activated by a POKE and a call, or by similar means. This would enable beginners to use the programs with ease.
- To eliminate any excess costs, the routines would be hardware independent. They would make use of the Apple's cassette port and built-in speaker.
The memory used to store a vocabulary is divided into 2000-byte blocks. Each of these blocks will be used to store eight, distinct words. Each word will be stored in its own bit of the block of memory. In other words, bit 0 stores word 0, bit 1 stores word 1, and so on. I chose to store the words this way rather than sequentially to reduce the complexity of the program. If I chose the latter way, many rotate commands would be required. These tend to get confusing, and, if you are not careful, very sloppy.
Since a single word rarely contains periods of silence, no data compression is necessary. Again, this simplifies the program. In order to store data in the correct bit, a few things must be done:
- Load in the old byte.
- Get a bit from the input port.
- Move the input bit to the right position.
- Plug this bit into the old byte.
- Store the old byte.
Exactly how this is done will be explained in further detail later on.
How Speech Enters And Exits
The data enters into the program through the cassette and exits through the Apple on-board speaker. First let's talk about recording. Location $C060 is the cassette in. When a byte is read from this location, the seventh bit is affected according to the audio signal present. After this location has been sampled, the seventh bit is isolated. It is then plugged into the correct position as explained above.
When in the playback mode, your voice is produced by the on-board speaker. Because the case resonates at certain frequencies, I would recommend hooking up an external speaker, as I have. This greatly improves the quality of any sound produced by the computer, especially voice. One note: when wiring up the speaker, use shielded cable. If you do not, a tremendous amount of RF interference will occur.
The speaker is controlled by location $C030. Every time this memory location is accessed, a click is produced by the speaker. Be careful here. If you use a store instruction to toggle the speaker, it will be toggled twice. This is so because the 6502 does a read before any write. This accesses the location twice, thus producing two clicks.
Getting back to the program – once the correct data byte is loaded, the correct bit is isolated. If this bit is different than the last sample obtained, a change in state has occurred. This will result in the toggling of the speaker, producing a sound. Doing this at the proper rate reproduces the recorded word.
Here's a brief explanation of the machine language "record" and "playback" routines:
The Record routine is probably the most complex part of this program. The entry point is $9000. Here is how it works:
- All pointers are set. This includes the calculation of the position of the word and the bit that the word is located in.
- The Y register is set to zero. This will be the index of the indirect address of the word.
- A delay loop is executed. This is the start of the main program loop. The delay determines the sampling rate.
- The sample byte is taken from the cassette port. The seventh bit is then isolated via an AND instruction.
- The X register is set to $FF if the input bit was high, or $00 if the bit was low.
- This result is moved to the accumulator. There it is ANDed with the byte that contains the bit that the word is to be stored in high. This provides us with a byte that has the bit we want the word in set according to the cassette input. All other bits in the byte are zero. This value is saved.
- The accumulator is loaded with the mask byte and then inverted. This forms a byte with all bits set, except for the bit that the word will be stored in.
- The current byte is loaded and then ANDed with the previously obtained value. This leaves the byte undisturbed except for the bit that the word will be stored in. This is set to zero.
- This value is logically ORed with the byte that contained the data sample in the proper place.
- At this point we have successfully plugged the input sample into the current byte.
- The current byte is now stored. We are almost finished.
- The Y register is incremented. If it is zero, then a page has been completed. In this case the page is incremented.
- If the last page has been done, the routine ends. If not, then it jumps back to the delay routine and goes one more time.
The playback routine is far simpler than the recording routine. Its entry point is $9049.
- All pointers are set. The positions of the word and of its bit are also calculated.
- This is the beginning of the main loop. A delay is executed. This determines the sampling rate.
- The Y register is zeroed. It will be the index to the indirect address.
- The current data byte is sampled.
- This value is ANDed with the mask byte. This results in all bits being zero except for the bit containing the word data, which is unaffected.
- This is compared to the last data bit obtained.
- If the value is the same, then nothing happens.
- If there is a difference, the speaker is toggled.
- The Y register is incremented, and the program checks whether a page has been completed.
- If a page has been completed, the current page is incremented.
- If the last page was done, the program ends.
- Otherwise the program loops back until done.
Entering The Program Into Memory
Type in the BASIC Loader (Program 1) and RUN it to put the machine language program into memory. Then type CALL-151 to enter the monitor. Once this has been done, SAVE the program by typing: BSAVE VOC 1.1OBJ0, A$9000,L$C3.
The next step is to generate the table used by the mask subroutine. To do this, type the following:
* 310 : 01 02 04 08 10 20 40 80To save it, type:
Using The Program
To use the program requires only three simple steps:
- POKE 0 with the word number.
- POKE 772 with the speed.
- Call the appropriate routine.
A sample program would look something like this:
10 POKE 0,1 : REM WORD 20 POKE 772,10 : REM SPEED 30 CALL 9 * 4096 + 64 + 9 : REM PLAY 40 END : REM DONE
I have included three sample programs:
Program 2: This is a simple routine that speaks any number put in. You must enter the vocabulary from Table 1 before using it.
Program 3: This is a CAI demo. It is an addition quiz that uses Program 1 as a subroutine. This program shares a vocabulary with Program 1. Program 4: This is a vocabulary builder. It should be used to build the vocabulary in Table 1.
I hope you enjoy using these routines, as I have. They make your programs many times more pleasant and impressive.