← Back to Courses
module
27

Giving the game a voice

Why this matters

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.

The chip you already have the keys to

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:

  • Tone (registers 0-5, two per channel): a fine and coarse byte set a 12-bit period, and the period sets a pitch.
  • Noise (register 6): one period, shared by any channel that mixes it in - not a pitch, a hiss.
  • Volume (registers 8-10, one per channel): a level from silent to loud, or - set one bit - handed over to the envelope (registers 11-13), which changes the level on its own, once armed, with no further code.

Three channels exist (A, B, C). This section uses two: A for a tone, B for noise.

A tone is a number

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.

Turning it on: volume and the mixer

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.

An effect that ends itself

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.

Once per press, not once per frame

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.

The code

        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.

Change one thing

  • Change tone A's period from 30 to 200. What note is that, roughly, against S25's 625 Hz example - and which direction did it move?
  • Change the envelope shape from 0 to 8. Fire twice, a beat apart. What is different about the second shot, and why - think about what shape 8 does on its own between your presses.
  • Change 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?
  • Add a rising two-step fade to the boom instead of a hard cut: two counter stages, volume 10 then 5 then 0. Compare it against the original.
  • Give the boom its own pitch character by trying a few different noise periods (register 6). Is "a different noise" the same kind of change as "a different note" was for the tone?

When it goes wrong

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.

Summary

  • Tone: a 12-bit period sets pitch, f = 2,000,000 / (16 x period). Noise: one period, a hiss rather than a note.
  • Volume is per-channel, 0 is real silence, and the mixer's bits gate tone and noise onto a channel independently.
  • The envelope changes a channel's volume on its own, once armed - rewriting its shape register, even to the same value, restarts it. A whole retriggerable effect in one write.
  • A software countdown, S22's animation-clock idea aimed at a volume register, is the other way to end a sound - full control, no envelope needed.
  • Trigger effects on the edge of a keypress, not its level, or a held key turns one shot into a stutter.

Next

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.

Get the Newsletter

New guides, disk images and community finds, roughly once a quarter. No spam, we promise, this isn't Tatung's marketing department.
Your subscription could not be saved. Please try again.
Your subscription has been successful.

Newsletter

Subscribe to our newsletter and stay updated.