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.