ZX Spectrum Assembly, Pong – 0x0B Sound and 16K
In this ZX Spectrum Assembly chapter, we will implement the sound effects.
Translation by Felipe Monge Corbalán
Table of contents
Sound
We have implemented sound effects when the ball hits the borders, the paddles and when a point is scored.
We create the folder Step11 and copy from the folder Step10: controls.asm, game.asm, main.asm, sprite.asm and video.asm. We create sound.asm to add the necessary counters and routines for our sound effects.
We will define three different sounds:
- When a point is marked.
- When the ball hits a paddle.
- When the ball hits the border.
For each sound, we need to define the note and the frequency. The frequency defines how long the note lasts; we identify it with the suffix FQ.
; Point
C_3: EQU $0D07
C_3_FQ: EQU $0082 / $10
; Paddle
C_4: EQU $066E
C_4_FQ: EQU $0105 / $10
; Border
C_5: EQU $0326
C_5_FQ: EQU $020B / $10
All the sounds we are going to use are C, but in different scales; the larger the scale, the higher the pitch.
The frequencies given are those that make the note last one second, so we’ll divide them by 16. If we multiply them by 2, the note would last 2 seconds.
Each note in each scale has its own frequency. Appendix one contains tables of frequencies and notes, in decimal and hexadecimal.
The next constant is the memory address where the ROM BEEPER routine is located:
BEEPER: EQU $03B5
This routine receives the note in HL and the duration in DE, and changes the value of registers AF, BC, DE, HL and IX, as well as another aspect that we will see later.
Because the ROM BEEPER routine modifies so many registers, it is advisable not to call it directly; we implement a routine that does this.
This routine receives in A the type of sound to be emitted and does not change the value of any register:
- 1 = point
- 2 = paddle
- 3 = border
PUSH DE
PUSH HL
We preserve the value of the DE, PUSH DE, and HL, PUSH HL, registers.
cp $01
jr z, playSound_point
We check if the sound to be played is of type 1 (point), CP $01, and if so we skip, JR Z, playSound_point.
cp $02
jr z, playSound_paddle
If the sound is not type 1, we check if it is type 2 (paddle), CP $02, and if so we jump, JR Z, playSound_paddle.
If the sound is neither type 1 nor type 2, it is type 3 (border):
ld hl, C_5
ld de, C_5_FQ
jr beep
We load the note in HL, LD HL, C_5, the duration in DE, LD DE, C_5_FQ, and play the sound, JR beep.
If the sound is of type 1 or 2, we do the same, with the values of each sound:
playSound_point:
ld hl, C_3
ld de, C_3_FQ
jr beep
playSound_paddle:
ld hl, C_4
ld de, C_4_FQ
We are spared the last JR, as it is immediately followed by the routine that plays the sound:
beep:
push af
push bc
push ix
call BEEPER
pop ix
pop bc
pop af
pop hl
pop de
ret
We keep AF, PUSH AF, BC, PUSH BC, and IX, PUSH IX. We then call the ROM routine, CALL BEEPER, and retrieve IX, POP IX, BC, POP BC, AF, POP AF, HL, POP HL, and DE, POP DE. HL and DE are retained at the beginning of the PlaySound routine. We exit with, RET.
The final appearance of the sound.asm file is as follows:
; -------------------------------------------------------------------
; Sound.asm
; File with the sounds
; -------------------------------------------------------------------
; Point
C_3: EQU $0D07
C_3_FQ: EQU $0082 / $10
; Paddle
C_4: EQU $066E
C_4_FQ: EQU $0105 / $10
; Rebound
C_5: EQU $0326
C_5_FQ: EQU $020B / $10
; -------------------------------------------------------------------
; ROM beeper routine.
;
; Input: HL -> Note.
; DE -> Duration.
;
; Alters the value of the AF, BC, DE, HL and IX registers.
; -------------------------------------------------------------------
BEEPER: EQU $03B5
; -------------------------------------------------------------------
; Reproduces the sound of bouncing.
; Input: A -> Sound type: 1. Dot
; 2. Paddle
; 3. Border
; -------------------------------------------------------------------
PlaySound:
; Preserves the value of records
push de
push hl
cp $01 ; Evaluates sound dot
jr z, playSound_point ; Sound point? Jump
cp $02 ; Evaluates sound paddle
jr z, playSound_paddle ; Sound paddle? Jump
; The edge sound is emitted
ld hl, C_5 ; HL = note
ld de, C_5_FQ ; DE = duration (frequency)
jr beep ; Jumps to beep
; The sound of Dot is emitted
playSound_point:
ld hl, C_3 ; HL = note
ld de, C_3_FQ ; DE = duration (frequency)
jr beep ; Jumps to beep
; The paddle sound is emitted
playSound_paddle:
ld hl, C_4 ; HL = note
ld de, C_4_FQ ; DE = duration (frequency)
; Sounds the note
beep:
; Preserves registers; ROM BEEPER routine alters them.
push af
push bc
push ix
call BEEPER ; Call BEEPER from ROM
; Retrieves the value of the registers
pop ix
pop bc
pop af
pop hl
pop de
ret
Now we need to call our new routine to play the sounds of the ball bouncing.
Open game.asm and locate checkBallCross_right. We will add two lines between RET NZ and LD A, (ballSetting):
ld a, $02
call PlaySound
We load the sound type in A, LD A, $02; we play it, CALL PlaySound.
We find the tag checkBallCross_left. Let’s add the same two lines between RET NZ and LD A, (ballSetting):
ld a, $02
call PlaySound
We locate the moveBall_upChg tag. Below it, we add two lines, almost the same as above:
ld a, $03
call PlaySound
Locate the moveBall_downChg tag and add the above two lines just below it:
ld a, $03
call PlaySound
Locate the moveBall_rightChg tag below add:
ld a, $01
call PlaySound
Five lines below that is CALL SetBallLeft; below that we add:
ld a, $03
call PlaySound
Locate the moveBall_leftChg tag; below add:
ld a, $01
call PlaySound
Five lines down is CALL SetBallRight, which we add just below:
ld a, $03
call PlaySound
Finally, open main.asm, locate the Loop routine and add the following lines just above it:
ld a, $03
call PlaySound
We go to the end of the file, in the «includes» part, we include the file sound.asm:
include "sound.asm"
If all goes well, we have reached the end. We compile, load into the emulator and…
What about the border, why is it white? We have already seen that the ROM’s BEEPER routine changes a lot of things, and one of them is the colour of the border, although it has a simple solution.
Fortunately, we have a system variable where we can store the border colour. The attributes of the bottom screen are also stored in this variable. The background of the bottom screen is the border colour.
We open video.asm and declare a constant at the top with the memory address of this system variable:
BORDCR: EQU $5c48
Locate the Cls routine, and add it just before the INC HL line:
ld a, $07 ; Black background, white ink
We modify the line LD (HL), $07 and leave it as follows:
ld (hl), a
Finally, before RET, we add:
ld (BORDCR), a
Compile, load into the emulator, and you’re done – have we finished our ZX-Pong?
16K compatibility
Is our programme compatible with the 16K model? Not yet, but since we’re not using interrupts, it’s very easy to make it compatible.
We open main.asm, find the two directives ORG and END, and replace $8000 with $5dad in ORG. In END, we replace the $8000 address with Main, the program entry label.
If we compile and load in the 16K model, our programme is compatible.
If we look closely, we can see that we have lost some speed. This loss is due to the fact that the second 16K of the ZX Spectrum, where we are now loading the program, is the so-called contained memory, which is shared with the ULA. When the ULA is working, everything stops.
We are going to change the speed at which the ball moves again.
Open sprite.asm, find ballSetting, comment out the line or $21 and write just below it:
; or $21
or $19
Now the ball starts at speed 3, which is the slowest speed.
Open game.asm, find SetBallLeft, go to line 7, comment it out and write just below it:
; or $21
or $19
If we now set the ball to come out of the left side of the screen, it will start at speed 3.
Find SetBallRight, comment out line 7 and type just below it:
; db $61
db $59
If we now set the ball to come out of the right side of the screen, it will start at speed 3.
Find the tag checkCrossY_1_5, comment out line 7 and write just below it:
; or $21
or $19
Now the ball speed is 3 instead of 4.
Find the tag checkCrossY_2_5, comment out line 7 and write just below it:
; or $1a
or $12
Now the ball speed is 2 instead of 3.
Find the tag checkCrossY_3_5, comment out line 7 and write just below it:
; or $17
or $0f
Now the ball speed is 1 instead of 2.
Find the tag checkCrossY_4_5, comment out line 7 and write just below it:
; or $9a
or $92
Now the ball speed is 2 instead of 3.
Find the tag checkCrossY_5_5, comment out line 3 and write just below it:
; or $a1
or $99
Now the ball speed is 3 instead of 4.
We compiled it, tested it in the emulator and we’re almost done.
ZX Spectrum Assembly, Pong
In the next ZX Spectrum Assembly chapter, we will implement further optimisations.
Download the source code from here.
Useful links
ZX Spectrum Assembly, Pong by Juan Antonio Rubio García.
Translation by Felipe Monge Corbalán.
This work is licensed to Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).
Any comments are always welcome.