4. Sound#
4.1 Simple beep#
This simple beep routine clicks the speaker every 656 cycles. At approximately 980 nsec per cycle, this would be a period of about 0.64 milliseconds, or a tone of 1.56 kHz. This is a short beep, playing for a little over 0.1 seconds.
ORG $86CE BEEP: SUBROUTINE LDY #$C0 .loop: ; From here to click is 651 cycles. Additional 5 cycles afterwards. LDX #$80 ; 2 cycles ; delay 640 cycles .loop2: DEX ; 2 cycles BNE .loop2 ; 3 cycles LDA ENABLE_SOUND ; 3 cycles BEQ .next ; 3 cycles LDA SPKR ; 3 cycles .next: DEY ; 2 cycles BNE .loop ; 3 cycles RTS
4.2 Sound "strings"#
A sound "string" describes a sound to play in terms of pitch and duration, ending in a 00.
Just like in the PUT_STRING routine, rather than take
an address pointing to a sound string, instead it uses the return address as the source for data.
It then has to fix up the actual return address at the end to be just after the zero-terminating
byte of the string.
Because NOTE_INDEX is not zeroed out, this actually appends to the sound data buffer.
The format of a sound string is duration, followed by pitch, although the pitch is lower for higher numbers.
One example of a sound string is 07 45 06 55 05 44 04 54 03 43 02 53, found in CHECK_FOR_GOLD_PICKED_UP_BY_PLAYER.
NOTE_INDEX EQU $54 SOUND_DURATION EQU $0E00 ; 128 bytes SOUND_PITCH EQU $0E80 ; 128 bytes
ORG $87E1 LOAD_SOUND_DATA: SUBROUTINE PLA STA SAVED_RET_ADDR PLA STA SAVED_RET_ADDR+1 BNE .next .loop: LDY #$00 LDA (SAVED_RET_ADDR),Y BEQ .end INC NOTE_INDEX LDX NOTE_INDEX STA SOUND_DURATION,X INY LDA (SAVED_RET_ADDR),Y STA SOUND_PITCH,X INC SAVED_RET_ADDR BNE .next INC SAVED_RET_ADDR+1 .next: INC SAVED_RET_ADDR BNE .loop INC SAVED_RET_ADDR+1 BNE .loop .end: LDA SAVED_RET_ADDR+1 PHA LDA SAVED_RET_ADDR PHA RTS
LOAD_SOUND_DATAThere's also a simple routine to append a single note to the sound buffer. The routine gets
called with the pitch in A and the duration in X.
ORG $87D5 APPEND_NOTE: SUBROUTINE INC NOTE_INDEX LDY NOTE_INDEX STA SOUND_PITCH,Y TXA STA SOUND_DURATION,Y RTS
4.3 Playing notes#
The PLAY_NOTE routines plays a note through the built-in speaker. The time the note is played is
based on X and Y forming a 16-bit counter (X being the most significant byte), but A controls the pitch,
which is how often the speaker is clicked. The higher A, the lower the pitch.
The ENABLE_SOUND location can also disable playing the note, but the routine still takes as long as it would have.
ENABLE_SOUND EQU $99 ; If 0, do not click speaker. SPKR EQU $C030 ; Access clicks the speaker.
4.4 Playing a sound#
The SOUND_DELAY routine delays an amount of time based on the X register. The total number of cycles is about 905 per each X.
Since the Apple //e clock cycle was 980 nsec (on an NTSC system), this routine would delay approximately 887 microseconds times X.
PAL systems were very slightly slower (by \(0.47\%\)), which corresponds to 883 microseconds times X.
ORG $86BE SOUND_DELAY_AMOUNTS: HEX 02 04 06 08 0A 0C 0E 10 12 14 16 18 1A 1C 1E 20
ORG $86B1 SOUND_DELAY: SUBROUTINE LDA SOUND_DELAY_AMOUNTS,X TAX SOUND_DELAY1: LDY #$B4 ; 180 .loop: DEY ; 2 cycles BNE .loop ; 3 cycles DEX ; 2 cycles BNE SOUND_DELAY1 ; 3 cycles RTS
Finally, the PLAY_SOUND routine plays one section of the sound string stored in the SOUND_PITCH
and SOUND_DURATION buffers. We have to break up the playing of the sound so that gameplay doesn't
pause while playing the sound, although game play does pause while playing the note.
Alternatively, if there is no sound string, we can play the note stored in location \$A4 as long as
location \$9B is zero. The duration is 2 + FRAME_PERIOD.
The routine is designed to delay approximately the same amount regardless of sound duration. The
delay is controlled by FRAME_PERIOD. This value is hardcoded to 6 initially, but the game can
be sped up, slowed down, or even paused.
FRAME_PERIOD EQU $8C ; initially 6
ORG $8811 PLAY_SOUND: SUBROUTINE LDY NOTE_INDEX BEQ .no_more_notes LDA SOUND_PITCH,Y LDX SOUND_DURATION,Y JSR PLAY_NOTE LDY NOTE_INDEX ; Y = NOTE_INDEX DEC NOTE_INDEX ; NOTE_INDEX-- LDA FRAME_PERIOD SEC SBC SOUND_DURATION,Y ; A = FRAME_PERIOD - SOUND_DURATION[Y] BEQ .done BCC .done ; If A <= 0, done. TAX JSR SOUND_DELAY1 .done: SEC RTS .no_more_notes: LDA $9B BNE .end LDA $A4 LSR ; pitch = $A4 >> 1 INC $A4 ; $A4++ LDX FRAME_PERIOD INX INX ; duration = FRAME_PERIOD + 2 JSR PLAY_NOTE CLC RTS .end: LDX FRAME_PERIOD JSR SOUND_DELAY CLC RTS
PLAY_SOUNDAnother routine is just for when a level is cleared. It appends a note based on a scratch location, and then plays it.
ORG $622A APPEND_LEVEL_CLEARED_NOTE: SUBROUTINE LDA SCRATCH_5C ASL ASL ASL ASL ; pitch = SCRATCH_5C * 16 LDX #$06 ; duration JSR APPEND_NOTE JMP PLAY_SOUND