.png)
Every part of the game is on screen. None of it makes a sound. A shot that only looks like a shot, a hit with no impact, a rocket that flies in absolute silence - games have trained you to expect noise, and its absence is felt even by someone who couldn't say what's missing.
The chip that reads your keyboard is the same chip that can fix this. S25 used two of its sixteen registers. The other fourteen are what it was built for.
Register select at port 2, write at port 3 - S25's protocol, unchanged. What differs is which registers you reach for. Three groups matter for a first sound:
Three channels exist (A, B, C). This section uses two: A for a tone, B for noise.
Set a channel's period, and it sings:
LD A,0
OUT (2),A
LD A,30 ; tone A period, fine byte
OUT (3),A
LD A,1
OUT (2),A
LD A,0 ; tone A period, coarse byte
OUT (3),A
The chip runs at 2,000,000 Hz, and a channel's frequency is
f = 2,000,000 / (16 x period)
Small periods are high notes; large periods are low ones. Nothing plays yet - a period with no volume and no mixer permission is a silent number.
Two more switches, and the tone is audible:
LD A,8
OUT (2),A
LD A,15 ; channel A volume: loud
OUT (3),A
LD A,7
OUT (2),A
LD A,126 ; mixer: bit 0 low - tone A permitted through
OUT (3),A
Volume 0 is true silence, not merely quiet - a reliable off switch. The mixer's bit 0 gates tone A specifically: clear it and the tone plays; set it and nothing does, whatever the period or volume say. This is also why S25's keyboard setup (register 7 = 127) never made a sound: 127 has every tone and noise bit set, all channels muted, by design or by accident of a value chosen for its row/column bits alone.
A held tone is an instrument. A game wants events - short, and over. Two ways to end one, and this section uses both:
Let the hardware do it. The envelope generator drives a channel's volume up or down on a schedule you set once, with zero further code:
LD A,11
OUT (2),A
LD A,144 ; envelope period, fine
OUT (3),A
LD A,12
OUT (2),A
LD A,1
OUT (3),A ; envelope period, coarse
LD A,13
OUT (2),A
LD A,0
OUT (3),A ; shape 0: decay once, then hold silent
LD A,8
OUT (2),A
LD A,16
OUT (3),A ; channel A volume: envelope-driven, not fixed
Shape 0 falls once from loud to silent and stops - a laser, a coin, a blip, entirely hardware-timed. And firing it again later needs nothing but one more write:
fire: LD A,13
OUT (2),A
LD A,0
OUT (3),A
RET
Rewriting the shape register - even to the value it already holds - restarts the decay from the top. A whole sound effect, replayed, in two port writes.
Or count it yourself. Sound needn't touch the envelope at all. Set a fixed volume, and end it later exactly as S22 ended an animation frame - with a counter:
boom: LD A,9
OUT (2),A
LD A,10
OUT (3),A ; channel B volume: fixed, on
LD A,6
LD (boomTimer),A
RET
boomtick:
LD A,(boomTimer)
OR A
RET Z
DEC A
LD (boomTimer),A
RET NZ
LD A,9
OUT (2),A
LD A,0
OUT (3),A ; time's up - off
RET
Six frames of noise, then silence - the same "count down, act at zero" shape as a flame's flicker or a flash of red, just aimed at a register instead of the screen.
The matrix (S25) reports whether a key is down, every frame, for as
long as it is held. Call fire from that reading directly and a half-second
press retriggers the envelope roughly twenty-five times - a stutter, not a
shot. A sound effect wants the moment a key goes down, not its whole
held duration.
One byte per key remembers last frame's answer, and the effect fires only when the answer changes from up to down:
input: LD C,254 ; row 0: SPACE, bit 6
CALL readrow
BIT 6,A
LD B,0
JR NZ,spcalc ; not pressed - B stays 0
LD B,1
spcalc: LD A,(spaceWas)
CP B
LD A,B
LD (spaceWas),A
JR Z,spskip ; no change since last frame
OR A
CALL NZ,fire ; changed, and now down - the edge
spskip:
This is a small, general trick worth keeping: anything that should happen once on a press - not once a frame while held - reads the same way.
ORG 256
DI
; --- tone A period (laser pitch)
LD A,0
OUT (2),A
LD A,30
OUT (3),A
LD A,1
OUT (2),A
LD A,0
OUT (3),A
; --- envelope period and one-shot shape
LD A,11
OUT (2),A
LD A,144
OUT (3),A
LD A,12
OUT (2),A
LD A,1
OUT (3),A
LD A,13
OUT (2),A
LD A,0
OUT (3),A ; shape 0: single decay, held silent after
; --- channel A volume: envelope mode
LD A,8
OUT (2),A
LD A,16
OUT (3),A
; --- noise period (boom texture)
LD A,6
OUT (2),A
LD A,20
OUT (3),A
; --- channel B volume: silent until a boom fires
LD A,9
OUT (2),A
LD A,0
OUT (3),A
; --- mixer: tone A on, noise B on, row/column direction as usual
LD A,7
OUT (2),A
LD A,110
OUT (3),A
main: CALL waitframe
CALL input
CALL boomtick
JR main
waitframe:
IN A,(9)
BIT 7,A
JR Z,waitframe
RET
; --- edge-detect SPACE and X; fire on the down-transition only
input: LD C,254 ; row 0: SPACE, bit 6
CALL readrow
BIT 6,A
LD B,0
JR NZ,spcalc ; not pressed - B stays 0
LD B,1
spcalc: LD A,(spaceWas)
CP B
LD A,B
LD (spaceWas),A
JR Z,spskip ; no change - nothing to do
OR A
CALL NZ,fire ; changed, and now down - the edge
spskip:
LD C,127 ; row 7: X, bit 5
CALL readrow
BIT 5,A
LD B,0
JR NZ,xcalc
LD B,1
xcalc: LD A,(xWas)
CP B
LD A,B
LD (xWas),A
JR Z,xskip
OR A
CALL NZ,boom
xskip: RET
readrow:
LD A,14
OUT (2),A
LD A,C
OUT (3),A
LD A,15
OUT (2),A
IN A,(2)
RET
; --- fire: retrigger the envelope, nothing else to touch
fire: LD A,13
OUT (2),A
LD A,0
OUT (3),A
RET
; --- boom: fixed volume, arm the countdown
boom: LD A,9
OUT (2),A
LD A,10
OUT (3),A
LD A,6
LD (boomTimer),A
RET
; --- the countdown: when it reaches zero, cut channel B
boomtick:
LD A,(boomTimer)
OR A
RET Z
DEC A
LD (boomTimer),A
RET NZ
LD A,9
OUT (2),A
LD A,0
OUT (3),A
RET
spaceWas: DEFB 0
xWas: DEFB 0
boomTimer: DEFB 0
SPACE fires; X booms.
What you should see
A rising two-note handshake as the program starts is not a bug: writing the envelope's shape register arms it immediately, so configuring the one-shot plays it once, quietly, before you have touched a key. After that: SPACE gives a short, sharp, decaying blip. X gives a flat burst of noise that stops cleanly. Hold either key down - one sound each, not a machine-gun stutter.
boomTimer's starting value from 6 to 40. Is the result a longer
boom, or something that sounds broken - and if the latter, why doesn't a
bigger number simply mean "longer" here?| Symptom | Cause |
|---|---|
| Nothing plays at all | The mixer bit for that channel is set, not clear - active-low. Or volume is 0. Or wrong channel's registers were written for the mixer bit that's actually enabled. |
| A key held down machine-guns the sound | The effect is called straight from the level read, not from an edge. Add the "was it down last frame" byte. |
| The sound never stops | For a fixed-volume effect: the countdown never reaches zero, or nothing writes the volume back to 0 when it does. |
| A tiny click at the very start | Expected - see "What you should see". Not a fault. |
| Two sounds fire when only one key was touched | Both channels share the one envelope generator (sound.md's caveat) - if both are in envelope mode at once, retriggering one can disturb the other. Keep only one channel on the envelope at a time. |
S28. A clock of your own. Sound and sprites both still run on the screen's fifty-times-a-second heartbeat. The machine has a second, entirely independent clock - a chip that ticks at whatever rate you choose, whether or not the main loop is watching - and taking it over is what separates a sound effect from a tune.