Apple II Sound
I remember that Quinti-Maze played a tune when one won, but could not remember how. This post is all about my research into Apple II sound. A follow up post will be about how I’ll produce the same sounds in Rust.
Here’s what it sounds like, played back on Virtual II .
According to Apple II sound cards:
All Apple II models (except the Apple IIGS, a significantly different, albeit backwards-compatible machine) possess a speaker, but it was limited to 1-bit output in the form of a simple voltage the user could switch on and off with software, creating clicks from the speaker each time the state was toggled. By turning the signal on and off rapidly, sounds with pitches could be produced.
This approach places extreme constraints on software design, since it requires the CPU to be available to toggle the output at specific frequencies, and all other code must be structured around that requirement. If sound generation code didn’t execute at precisely the right intervals, generating specific output frequencies would be impossible.
Nothing about Applesoft BASIC meets those requirements, so I must have done it some other way. Below is the line that detects a victory:
1220 IF X > 5 OR X < 1 OR Y > 5 OR Y < 1 OR A > 5 OR A < 1 THEN
PRINT "YOU WIN": &T100,100: &T100,50: &T100,50: &T75,66:
&T100,66: &T75,66: &T60,255: GOTO 3000
When I took another look at that line, I realized I had no idea what that &T
syntax was all about. It took some more searching, but I found
Extensions to Apple BASIC with ampersands.
It turns out that Applesoft had a feature where if it encountered an ampersand during execution of the program it would unconditionally jump to a subroutine at location $3F5.
Aha! This also explains the few lines at the beginning of the program.
1 DATA 201,84,208,15,32,177,0,32,248,230,138,72,32,183,0,
201,44,240,3,76,201,222,32,177,0,32,248,230
2 FOR I = 768 TO 833: READ P: POKE I,P: NEXT I
3 DATA 104,134,3,134,1,133,0,170,160,1,132,2,173,48,192,
136,208,4,198
4 DATA 1,240,7,202,208,246,166,0,208,239,165,3,133,1,198,2,
208,241,96
5 POKE 1013,76: POKE 1014,0: POKE 1015,3
Lines one through four cause a small amount of 6502 machine code to be written at address 0x300. According to The Big PEEKs, POKEs, and CALLs List that address is 256 bytes of free memory.
Here’s the disassembly curtesy of Norbert Landsteiner’s virtual 6502 / Disassembler, with comments added by me as I tried to understand what it is doing.
* = $0300
0300 C9 54 CMP #$54
0302 D0 0F BNE L0313
0304 20 B1 00 JSR $00B1 ; Advance TXTPTR?
0307 20 F8 E6 JSR $E6F8 ; Evaluate expression at TXTPTR, and convert it
; to single byte in X-reg.
030A 8A TXA ; transfer x register to accumulator
030B 48 PHA ; push accumulator on stack
030C 20 B7 00 JSR $00B7 ; Get next without advancing TXTPTR
030F C9 2C CMP #$2C ; compare to ','
0311 F0 03 BEQ L0316 ;
0313 4C C9 DE L0313 JMP $DEC9 ; show syntax error and exit?
0316 20 B1 00 L0316 JSR $00B1 ; Advance TXTPTR?
0319 20 F8 E6 JSR $E6F8 ; Evaluate expression at TXTPTR, and convert
; it to single byte in X-reg.
031C 68 PLA ; get accumulator from stack
031D 86 03 STX $03 ; store X into address 0x03 (V2)
031F 86 01 STX $01 ; also store X into address 0x01 (V2)
0321 85 00 STA $00 ; store accumulator in address 0x00 (V1)
0323 AA TAX ; transfer accumulator to X register
0324 A0 01 LDY #$01 ; load Y register with 1
0326 84 02 STY $02 ; store Y register into address 0x02
0328 AD 30 C0 L0328 LDA $C030 ; read from address $C030 to make the speaker click
032B 88 L032B DEY ; decrement the Y register
032C D0 04 BNE L0332 ; if it isn't zero, branch to L0322
032E C6 01 DEC $01 ; decrement the value at address 0x01
0330 F0 07 BEQ L0339 ; if it is zero, branch to L0339
0332 CA L0332 DEX ; decrement X
0333 D0 F6 BNE L032B ; if not zero, branch back decrement Y loop
0335 A6 00 LDX $00 ; load X register from address 0x00
0337 D0 EF BNE L0328 ; if not zero, loop back to speaker click
0339 A5 03 L0339 LDA $03 ; load accumulator from address 0x03
033B 85 01 STA $01 ; store accumulator in address 0x01
033D C6 02 DEC $02 ; decrement value in address 0x02
033F D0 F1 BNE L0332 ; if not zero, back to x loop
0341 60 RTS
.END
;auto-generated symbols and labels
L0313 $0313
L0316 $0316
L0332 $0332
L0339 $0339
L032B $032B
L0328 $0328
Line 5 is setting up the following at address 0x3F5.
* = $0000
0000 4C 00 03 JMP $0300
.END
So what is happening after the CPU jumps to 0x300? The first instruction is comparing the contents of the accumulator to the value 0x54 (ascii ‘T’) and, if not equal, calling an Applesoft routine to output “Syntax error”.
The next section reads two expressions with a comma between them and converts them to 8 bit values, with a syntax error if the comma is missing.
After setting up some page zero locations, the code enters the loop that plays music. This took me a long time to figure out. I do not miss assembly language.
The playback loop iterates a number of times equal to the second value, which is basically a length. Every time through the loop it decrements Y, and when it goes to zero, decrements the value stored in address $01. If that goes to zero, the routine returns. It also decrements X, and if that goes to zero it is reloaded with the first value from the & statement and re-enters the loop at the statement that clicks the speaker.
According to Lud’s Retro Computing Ressources, one trip through both parts of the inner loop is about 10 clock cycles. The Apple II was clocked at 1MHZ, so this code can run through the loop 100,000 times a second.
For the first parameter, the delay between clicks, that works out to the following frequency values, and then via Physics of Music the approximate pitch.
Value | Frequency(Hertz) | Approx Pitch |
---|---|---|
100 | 1000 | B5 |
75 | 1333 | E6 |
60 | 1667 | G#6 |
The second parameter, the length, is decremented every time the Y register goes to zero, which is once every 256 times through the loop. That means the time is 2.56 ms times the value of the parameter.
Value | Duration (ms) |
---|---|
100 | 256 |
50 | 128 |
66 | 169 |
255 | 653 |
Take those notes and timings and enter them into Garage Band, as closely as possible, and one gets this.
Close enough.
There is no way I invented this back in 1982. I must have found it in another program or in a magazine article.