Skip to content
Game 2 Unit 13 of 16 1 hr learning time

Footsteps and Doors

Give the keep a voice. Build a small sound-effects driver on the beeper — one tone primitive, B for duration and C for pitch — and from it a footfall under every step and a falling creak at every door. Sound as place, not as feedback.

81% of Shadowkeep

The keep looks like somewhere now — lit, furnished, composed. But put your ear to it and there's nothing. A place you believe in has sound: stone underfoot, the groan of a heavy door. This unit, the first of the Keep Has a Voice sub-arc, gives it that — and the tool is the humblest thing on the machine, the one your first game already blipped: a single bit of beeper.

What you'll see (and hear) by the end

The Hall, lit by its three sconces, the hooded thief standing in the light near the centre — unchanged on screen from the last unit, because everything new here is heard, not seen.
The same Hall as last unit — and that's the point. Nothing new is drawn; everything new is heard. A click under each footfall, a creak at every door.

The screen is the same Hall as before — that's the point. Nothing new is drawn. But now, as you walk, a soft low click falls under every footstep on the stone; and when you cross from one chamber to the next, a door groans downward on its hinges. The keep is heard as well as seen. (The stills can't carry it — load it and walk.)

One bit, one tone

The Spectrum's speaker is a single bit: bit 4 of port $FE. Write it high, then low, then high again, fast, and the cone pushes in and out — a square wave, a tone. The pitch is how fast you toggle; the volume is fixed (the Spectrum has exactly one: on).

So the whole driver is one primitive. beep takes two numbers — B, how many cycles to play (the duration), and C, how long to wait between toggles (the pitch; a larger C waits longer, so the wave is slower and the note lower):

beep:
.bp_cycle:
            ld      a, %00010000     ; speaker bit high
            out     ($FE), a
            ld      e, c             ; wait C
.bp_hi:
            dec     e
            jr      nz, .bp_hi
            xor     a                ; speaker bit low (border stays black)
            out     ($FE), a
            ld      e, c             ; wait C again
.bp_lo:
            dec     e
            jr      nz, .bp_lo
            djnz    .bp_cycle        ; B cycles in all
            ret

That's the entire engine. Everything else is composition — choosing B and C, or sweeping them, to make a sound that means something.

Two sounds about place

A footfall is short and low — a handful of cycles of a low tone, gone almost before you notice it:

sfx_step:
            ld      b, 5             ; very short
            ld      c, 90            ; low
            call    beep
            ret

A door is a creak — not one pitch but a falling one. We start the delay small (higher) and let it grow (lower), a few cycles at each step, so the note groans downward like a heavy door swinging:

sfx_door:
            ld      c, 36            ; start higher
.sd_sweep:
            ld      b, 3
            push    bc
            call    beep
            pop     bc
            inc     c                ; lower the pitch a touch
            inc     c
            ld      a, c
            cp      130              ; until it has groaned right down
            jr      c, .sd_sweep
            ret

These aren't "you pressed a key" beeps. They're about the place: the weight of stone, the heaviness of a door. That distinction — sound as atmosphere, not as button-feedback — is the whole reason to bother.

Wiring it in

Two call sites, and that's all. A footstep on every successful move, dropped in right after the thief is redrawn:

            call    draw_thief
            call    sfx_step         ; <- the footfall
            call    check_exit

And a creak the moment we pass through to another room, at the top of the room-transition path:

.enter:
            call    sfx_door         ; <- the hinges
            call    draw_room
            ; ...

Walk, and you hear yourself. Cross a threshold, and the door answers.

Milestone — give the keep a voice

One primitive does it all: beep toggles bit 4 of $FE for B cycles, waiting C between toggles — B is duration, C is pitch. sfx_step is a short low blip; sfx_door sweeps C downward for a falling creak. Two call sites wire them in — a footfall after every successful move, a creak the moment the thief passes through a doorway. The screen never changes; the diff is one driver and two hooks.

Step 1: a one-bit beeper driver, a footfall and a door creak
+54-1
11 ; Shadowkeep — Unit 13: Footsteps and Doors
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-00 = Unit 12's end: the keep lit and composed, but silent.
3+; step-01 adds a one-bit beeper driver — a footfall under each step, a creak at each door.
44
55 org 32768
66
...
365365 ld (thief_row), a
366366 call save_under
367367 call draw_thief
368+ call sfx_step
368369 call check_exit
369370 ret
370371
...
430431 ld a, 1
431432 ld (thief_row), a
432433 .enter:
434+ call sfx_door
433435 call draw_room
434436 call save_under
435437 call draw_thief
438+ ret
439+
440+; ----------------------------------------------------------------------------
441+; The sound-effects driver.
442+;
443+; beep — the one tone primitive. B = number of square-wave cycles (how long
444+; the tone lasts); C = the delay constant between speaker toggles (the pitch —
445+; a larger C waits longer between flips, so the wave is slower and the note is
446+; lower). Bit 4 of port $FE is the speaker; we write it high, wait C, write it
447+; low (and a black border with it), wait C, and repeat B times.
448+; ----------------------------------------------------------------------------
449+beep:
450+.bp_cycle:
451+ ld a, %00010000 ; speaker high (border stays black)
452+ out ($FE), a
453+ ld e, c
454+.bp_hi:
455+ dec e
456+ jr nz, .bp_hi
457+ xor a ; speaker low, border black
458+ out ($FE), a
459+ ld e, c
460+.bp_lo:
461+ dec e
462+ jr nz, .bp_lo
463+ djnz .bp_cycle
464+ ret
465+
466+; sfx_step — a footfall: short and low. A handful of cycles of a low tone,
467+; gone almost before you notice it. Played under every successful move.
468+sfx_step:
469+ ld b, 5
470+ ld c, 90
471+ call beep
472+ ret
473+
474+; sfx_door — a creak: a tone that falls in pitch as it plays. We start the
475+; delay constant small (higher) and let it grow (lower), a few cycles at each
476+; step, so the note groans downward — a heavy door swinging on its hinges.
477+sfx_door:
478+ ld c, 36 ; start higher
479+.sd_sweep:
480+ ld b, 3
481+ push bc
482+ call beep
483+ pop bc
484+ inc c ; lower the pitch a little
485+ inc c
486+ ld a, c
487+ cp 130 ; until it has groaned down low
488+ jr c, .sd_sweep
436489 ret
437490
438491 pos_bc:
The complete program
; Shadowkeep — Unit 13: Footsteps and Doors
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 adds a one-bit beeper driver — a footfall under each step, a creak at each door.

            org     32768

WALL_ATTR   equ     %01001000
FLOOR_ATTR  equ     %00001000
TORCH_ATTR  equ     %01001110
STATUE_ATTR equ     %01001111
BANNER_ATTR equ     %01001011
RUBBLE_ATTR equ     %00001000
MARK_ATTR   equ     %00001111
THIEF       equ     %01001010
WALL_BIT    equ     6

START_COL   equ     15
START_ROW   equ     11
NO_EXIT     equ     $FF
MAX_SHADE   equ     4
MAX_TORCHES equ     4

KEYS_OP     equ     $DFFE
KEYS_Q      equ     $FBFE
KEYS_A      equ     $FDFE
KEYS_SPACE  equ     $7FFE

start:
            ld      a, 0
            out     ($FE), a

            ld      hl, room0_template
            ld      de, room0_state
            ld      bc, 768
            ldir
            ld      hl, room1_template
            ld      de, room1_state
            ld      bc, 768
            ldir
            ld      hl, room2_template
            ld      de, room2_state
            ld      bc, 768
            ldir

            xor     a
            ld      (current_room), a
            ld      a, START_COL
            ld      (thief_col), a
            ld      a, START_ROW
            ld      (thief_row), a

            call    draw_room
            call    save_under
            call    draw_thief

            im      1
            ei
.loop:
            halt
            call    player_step
            call    mark_step
            jr      .loop

mark_step:
            ld      bc, KEYS_SPACE
            in      a, (c)
            bit     0, a
            ret     nz
            call    cell_state_addr
            ld      (hl), '+'
            ld      hl, mark_tile
            ld      de, under_thief
            ld      bc, 8
            ldir
            ld      a, MARK_ATTR
            ld      (under_thief + 8), a
            ret

cell_state_addr:
            call    room_entry_addr
            ld      a, (hl)
            inc     hl
            ld      h, (hl)
            ld      l, a
            push    hl
            ld      a, (thief_row)
            ld      l, a
            ld      h, 0
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            ld      a, (thief_col)
            ld      e, a
            ld      d, 0
            add     hl, de
            pop     de
            add     hl, de
            ret

room_entry_addr:
            ld      a, (current_room)
            ld      l, a
            ld      h, 0
            ld      d, h
            ld      e, l
            add     hl, hl
            add     hl, de
            add     hl, hl
            ld      de, rooms
            add     hl, de
            ret

; ----------------------------------------------------------------------------
; find_torches — collect every 'T' in the current room (up to MAX_TORCHES) into
; torch_list as (col, row) pairs; torch_count says how many.
; ----------------------------------------------------------------------------
find_torches:
            xor     a
            ld      (torch_count), a
            call    room_entry_addr
            ld      a, (hl)
            inc     hl
            ld      h, (hl)
            ld      l, a
            ld      b, 0
.fr_row:
            ld      c, 0
.fr_col:
            ld      a, (hl)
            cp      'T'
            jr      nz, .fr_skip
            ld      a, (torch_count)
            cp      MAX_TORCHES
            jr      nc, .fr_skip
            push    hl
            add     a, a            ; count * 2
            ld      e, a
            ld      d, 0
            ld      hl, torch_list
            add     hl, de
            ld      (hl), c         ; column
            inc     hl
            ld      (hl), b         ; row
            pop     hl
            ld      a, (torch_count)
            inc     a
            ld      (torch_count), a
.fr_skip:
            inc     hl
            inc     c
            ld      a, c
            cp      32
            jr      nz, .fr_col
            inc     b
            ld      a, b
            cp      24
            jr      nz, .fr_row
            ret

; ----------------------------------------------------------------------------
; shade_for_cell — row in B, column in C. Distance to the NEAREST torch,
; shifted by the room's falloff, clamped. Preserves B and C.
; ----------------------------------------------------------------------------
shade_for_cell:
            push    bc
            ld      a, b
            ld      (cell_row), a
            ld      a, c
            ld      (cell_col), a
            ld      a, (torch_count)
            or      a
            jr      z, .sf_dark
            ld      a, 255
            ld      (min_dist), a
            ld      hl, torch_list
            ld      a, (torch_count)
            ld      b, a            ; loop count
.sf_loop:
            ld      a, (cell_col)
            sub     (hl)            ; - torch col
            jr      nc, .sf_dc
            neg
.sf_dc:
            ld      d, a            ; |dc|
            inc     hl              ; -> torch row
            ld      a, (cell_row)
            sub     (hl)
            jr      nc, .sf_dr
            neg
.sf_dr:
            inc     hl              ; -> next torch pair
            cp      d               ; A = |dr|; take the larger
            jr      nc, .sf_mx
            ld      a, d
.sf_mx:
            ld      e, a            ; this torch's distance
            ld      a, (min_dist)
            cp      e
            jr      c, .sf_keep     ; min already smaller
            ld      a, e
            ld      (min_dist), a
.sf_keep:
            djnz    .sf_loop

            ld      a, (current_room)
            ld      c, a
            ld      b, 0
            ld      hl, room_falloff
            add     hl, bc
            ld      b, (hl)
            ld      a, (min_dist)
            inc     b
.sf_shift:
            dec     b
            jr      z, .sf_clamp
            srl     a
            jr      .sf_shift
.sf_clamp:
            cp      MAX_SHADE + 1
            jr      c, .sf_done
            ld      a, MAX_SHADE
.sf_done:
            pop     bc
            ret
.sf_dark:
            ld      a, MAX_SHADE
            pop     bc
            ret

draw_room:
            call    find_torches
            call    room_entry_addr
            ld      a, (hl)
            inc     hl
            ld      h, (hl)
            ld      l, a
            ld      (map_ptr), hl
            ld      b, 0
.room_row:
            ld      c, 0
.room_col:
            ld      hl, (map_ptr)
            ld      a, (hl)
            cp      '.'
            jr      nz, .not_floor
            call    shade_for_cell
            add     a, a
            ld      e, a
            ld      d, 0
            ld      hl, shade_tiles
            add     hl, de
            ld      e, (hl)
            inc     hl
            ld      d, (hl)
            ld      (tile_ptr), de
            ld      a, FLOOR_ATTR
            ld      (tile_attr), a
            call    draw_tile
            jr      .cell_done
.not_floor:
            call    lookup_tile
            call    draw_tile
.cell_done:
            ld      hl, (map_ptr)
            inc     hl
            ld      (map_ptr), hl
            inc     c
            ld      a, c
            cp      32
            jr      nz, .room_col
            inc     b
            ld      a, b
            cp      24
            jr      nz, .room_row
            ret

lookup_tile:
            ld      hl, palette
.scan:
            cp      (hl)
            jr      z, .found
            inc     hl
            inc     hl
            inc     hl
            inc     hl
            jr      .scan
.found:
            inc     hl
            ld      e, (hl)
            inc     hl
            ld      d, (hl)
            ld      (tile_ptr), de
            inc     hl
            ld      a, (hl)
            ld      (tile_attr), a
            ret

draw_tile:
            push    bc
            call    attr_addr_cr
            ld      a, (tile_attr)
            ld      (hl), a
            call    scr_addr_cr
            ld      de, (tile_ptr)
            ld      b, 8
.tile_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .tile_row
            pop     bc
            ret

player_step:
            ld      a, (thief_col)
            ld      (tcol), a
            ld      a, (thief_row)
            ld      (trow), a

            ld      bc, KEYS_OP
            in      a, (c)
            bit     1, a
            jr      z, .left
            bit     0, a
            jr      z, .right
            ld      bc, KEYS_Q
            in      a, (c)
            bit     0, a
            jr      z, .up
            ld      bc, KEYS_A
            in      a, (c)
            bit     0, a
            jr      z, .down
            ret
.left:
            ld      hl, tcol
            dec     (hl)
            jr      .move
.right:
            ld      hl, tcol
            inc     (hl)
            jr      .move
.up:
            ld      hl, trow
            dec     (hl)
            jr      .move
.down:
            ld      hl, trow
            inc     (hl)
.move:
            ld      a, (trow)
            ld      b, a
            ld      a, (tcol)
            ld      c, a
            call    wall_at
            ret     nz

            call    restore_under
            ld      a, (tcol)
            ld      (thief_col), a
            ld      a, (trow)
            ld      (thief_row), a
            call    save_under
            call    draw_thief
            call    sfx_step
            call    check_exit
            ret

wall_at:
            call    attr_addr_cr
            bit     WALL_BIT, (hl)
            ret

check_exit:
            ld      a, (thief_col)
            or      a
            jr      z, .west
            cp      31
            jr      z, .east
            ld      a, (thief_row)
            or      a
            jr      z, .north
            cp      23
            jr      z, .south
            ret
.east:
            call    room_entry_addr
            ld      de, 4
            add     hl, de
            ld      a, (hl)
            cp      NO_EXIT
            ret     z
            ld      (current_room), a
            ld      a, 1
            ld      (thief_col), a
            jr      .enter
.west:
            call    room_entry_addr
            ld      de, 5
            add     hl, de
            ld      a, (hl)
            cp      NO_EXIT
            ret     z
            ld      (current_room), a
            ld      a, 30
            ld      (thief_col), a
            jr      .enter
.north:
            call    room_entry_addr
            inc     hl
            inc     hl
            ld      a, (hl)
            cp      NO_EXIT
            ret     z
            ld      (current_room), a
            ld      a, 22
            ld      (thief_row), a
            jr      .enter
.south:
            call    room_entry_addr
            inc     hl
            inc     hl
            inc     hl
            ld      a, (hl)
            cp      NO_EXIT
            ret     z
            ld      (current_room), a
            ld      a, 1
            ld      (thief_row), a
.enter:
            call    sfx_door
            call    draw_room
            call    save_under
            call    draw_thief
            ret

; ----------------------------------------------------------------------------
; The sound-effects driver.
;
; beep — the one tone primitive. B = number of square-wave cycles (how long
; the tone lasts); C = the delay constant between speaker toggles (the pitch —
; a larger C waits longer between flips, so the wave is slower and the note is
; lower). Bit 4 of port $FE is the speaker; we write it high, wait C, write it
; low (and a black border with it), wait C, and repeat B times.
; ----------------------------------------------------------------------------
beep:
.bp_cycle:
            ld      a, %00010000     ; speaker high (border stays black)
            out     ($FE), a
            ld      e, c
.bp_hi:
            dec     e
            jr      nz, .bp_hi
            xor     a                ; speaker low, border black
            out     ($FE), a
            ld      e, c
.bp_lo:
            dec     e
            jr      nz, .bp_lo
            djnz    .bp_cycle
            ret

; sfx_step — a footfall: short and low. A handful of cycles of a low tone,
; gone almost before you notice it. Played under every successful move.
sfx_step:
            ld      b, 5
            ld      c, 90
            call    beep
            ret

; sfx_door — a creak: a tone that falls in pitch as it plays. We start the
; delay constant small (higher) and let it grow (lower), a few cycles at each
; step, so the note groans downward — a heavy door swinging on its hinges.
sfx_door:
            ld      c, 36            ; start higher
.sd_sweep:
            ld      b, 3
            push    bc
            call    beep
            pop     bc
            inc     c                ; lower the pitch a little
            inc     c
            ld      a, c
            cp      130              ; until it has groaned down low
            jr      c, .sd_sweep
            ret

pos_bc:
            ld      a, (thief_row)
            ld      b, a
            ld      a, (thief_col)
            ld      c, a
            ret

save_under:
            call    pos_bc
            call    scr_addr_cr
            ld      de, under_thief
            ld      b, 8
.save_row:
            ld      a, (hl)
            ld      (de), a
            inc     de
            inc     h
            djnz    .save_row
            call    pos_bc
            call    attr_addr_cr
            ld      a, (hl)
            ld      (under_thief + 8), a
            ret

restore_under:
            call    pos_bc
            call    scr_addr_cr
            ld      de, under_thief
            ld      b, 8
.restore_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .restore_row
            call    pos_bc
            call    attr_addr_cr
            ld      a, (under_thief + 8)
            ld      (hl), a
            ret

draw_thief:
            call    pos_bc
            call    attr_addr_cr
            ld      (hl), THIEF
            call    pos_bc
            call    scr_addr_cr
            ld      de, thief
            ld      b, 8
.thief_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .thief_row
            ret

scr_addr_cr:
            ld      a, b
            and     %00011000
            or      %01000000
            ld      h, a
            ld      a, b
            and     %00000111
            rrca
            rrca
            rrca
            or      c
            ld      l, a
            ret

attr_addr_cr:
            ld      a, b
            ld      l, a
            ld      h, 0
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            ld      de, $5800
            add     hl, de
            ld      a, c
            ld      e, a
            ld      d, 0
            add     hl, de
            ret

palette:
            defb    '.'
            defw    shade2_tile
            defb    FLOOR_ATTR
            defb    '#'
            defw    wall_tile
            defb    WALL_ATTR
            defb    'T'
            defw    torch_tile
            defb    TORCH_ATTR
            defb    'S'
            defw    statue_tile
            defb    STATUE_ATTR
            defb    'B'
            defw    banner_tile
            defb    BANNER_ATTR
            defb    'o'
            defw    rubble_tile
            defb    RUBBLE_ATTR
            defb    '+'
            defw    mark_tile
            defb    MARK_ATTR

shade_tiles:
            defw    shade0_tile
            defw    shade1_tile
            defw    shade2_tile
            defw    shade3_tile
            defw    shade4_tile

room_falloff:
            defb    2                   ; Hall — broad
            defb    1                   ; Gallery — medium
            defb    0                   ; Vault — tight

rooms:
            defw    room0_state
            defb    NO_EXIT, NO_EXIT, 1, NO_EXIT
            defw    room1_state
            defb    2, NO_EXIT, NO_EXIT, 0
            defw    room2_state
            defb    NO_EXIT, 1, NO_EXIT, NO_EXIT

; The Great Hall — three sconces (top-centre and the two side walls), broadly
; lit; banners flank a statue with rubble at its feet.
room0_template:
            defb    "###############T################"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "B..............S...............B"
            defb    "#.............ooo..............#"
            defb    "#..............................#"
            defb    "T........##..........##........T"
            defb    "#........##..........##........#"
            defb    "#..............................#"
            defb    "#..............................."
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#........##..........##........#"
            defb    "#........##..........##........#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "################################"

; The Gallery — two torches: low on the south wall, and one on the east wall.
room1_template:
            defb    "###############.################"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................T"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "###############.################"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "...............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "B..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#........................ooo...#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "###############T################"

; The Vault — one flame, on the altar. Its character is the dark.
room2_template:
            defb    "################################"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#.............#T##.............#"
            defb    "#.............####.............#"
            defb    "#.............####.............#"
            defb    "#.............####.............#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "###############.################"

shade0_tile:
            defb    %00000000
            defb    %00100010
            defb    %00000000
            defb    %00000000
            defb    %00000000
            defb    %10001000
            defb    %00000000
            defb    %00000000
shade1_tile:
            defb    %00100010
            defb    %00000000
            defb    %10001000
            defb    %00000000
            defb    %00100010
            defb    %00000000
            defb    %10001000
            defb    %00000000
shade2_tile:
            defb    %10101010
            defb    %01010101
            defb    %10101010
            defb    %01010101
            defb    %10101010
            defb    %01010101
            defb    %10101010
            defb    %01010101
shade3_tile:
            defb    %10101010
            defb    %11111111
            defb    %01010101
            defb    %11111111
            defb    %10101010
            defb    %11111111
            defb    %01010101
            defb    %11111111
shade4_tile:
            defb    %11111111
            defb    %11101110
            defb    %11111111
            defb    %10111011
            defb    %11111111
            defb    %11101110
            defb    %11111111
            defb    %10111011

wall_tile:
            defb    %00010001
            defb    %00000000
            defb    %01000100
            defb    %00000000
            defb    %00010001
            defb    %00000000
            defb    %01000100
            defb    %00000000

torch_tile:
            defb    %00010000
            defb    %00111000
            defb    %00111000
            defb    %01111100
            defb    %01111100
            defb    %01111100
            defb    %00111000
            defb    %00010000

statue_tile:
            defb    %00111100
            defb    %01111110
            defb    %00111100
            defb    %00011000
            defb    %00011000
            defb    %00111100
            defb    %01111110
            defb    %01111110

banner_tile:
            defb    %01111110
            defb    %01111110
            defb    %01011010
            defb    %01011010
            defb    %01111110
            defb    %01111110
            defb    %00111100
            defb    %00011000

rubble_tile:
            defb    %00000000
            defb    %01100000
            defb    %01100000
            defb    %00000110
            defb    %00000110
            defb    %00011000
            defb    %00011000
            defb    %00000000

mark_tile:
            defb    %00000000
            defb    %00011000
            defb    %00011000
            defb    %01111110
            defb    %01111110
            defb    %00011000
            defb    %00011000
            defb    %00000000

thief:
            defb    %00011000
            defb    %00111100
            defb    %01111110
            defb    %01111110
            defb    %01111110
            defb    %01111110
            defb    %00111100
            defb    %00100100

current_room:
            defb    0
torch_count:
            defb    0
cell_row:
            defb    0
cell_col:
            defb    0
min_dist:
            defb    0
thief_col:
            defb    START_COL
thief_row:
            defb    START_ROW
tcol:
            defb    0
trow:
            defb    0
map_ptr:
            defw    0
tile_ptr:
            defw    0
tile_attr:
            defb    0
under_thief:
            defb    0, 0, 0, 0, 0, 0, 0, 0, 0
torch_list:
            defs    MAX_TORCHES * 2

room0_state:
            defs    768
room1_state:
            defs    768
room2_state:
            defs    768

            end     start

Walk a dozen steps across the Hall and a soft low click falls under each one — the weight of stone underfoot:

ZX Spectrum beeper · port $FE bit 4
Footfalls across the Hall — a short low blip under every step

Cross from one chamber to the next and the door answers — a single pitch swept downward, groaning on its hinges:

ZX Spectrum beeper · port $FE bit 4
Crossing a threshold — the door creaks downward

Small sounds — but the keep stops being a silent diorama and starts being a place with weight.

Try this: a heavier tread

Raise sfx_step's B to 12 and its C to 120. The step gets longer and lower — a booted guard, not a barefoot thief. Sound characterises who is walking as surely as the sprite does. Drop B to 2 and the footfall becomes a light tick; you can hear a different body just by changing two numbers.

Try this: reverse the creak

Swap sfx_door to sweep the other way — start C high and dec it down to a small value. Now the door rises as it opens instead of falling. One groans shut, the other whines open; pick the one that sounds like the keep you're building. (Mind the loop test — sweeping down means jr nc / a different comparison.)

Try this: a third sound

Give the gold you'll scatter next unit its own voice now: a short, bright blip — B small, C tiny (say 12) — two or three of them in a row for a little chime. Sketching the sound before the thing that triggers it is a perfectly good way to design.

When it's wrong, see why

  • Total silence. The speaker is bit 4 — check you're writing %00010000, not another bit, and to port $FE. If you hear nothing in the emulator, make sure its speaker output is enabled.
  • A click, but no pitch — just a tick. B is too small (the tone ends before your ear registers it) or C is so small the wave is above hearing. Lengthen B; raise C.
  • The note is the same height whatever C you pass. You overwrote C inside the loop, or beep is reading a fixed value. The inner waits must both reload E from C.
  • A faint buzz or tick on top of every tone. That's the 50 Hz interrupt firing mid-tone and nudging a toggle. Harmless for short effects; if it bothers you, di before a sound and ei after — just remember the main loop needs interrupts back on for halt.
  • The creak runs forever / hangs. The sweep's exit test is wrong — C must move toward the limit and the jr must let it stop. Walk the comparison through by hand.

Before and after

You started with a keep that looked like somewhere but made no sound, and finished with one you can hear — a footfall under every step, a creak at every door. The whole instrument is one bit of beeper and one primitive: toggle the speaker at a chosen rate for a chosen length, B for duration and C for pitch. Everything else is composition — a short low blip for stone, a downward sweep for hinges — and two call sites to hang them on the moves you already had. The screen didn't change at all; the keep just stopped being a silent diorama. Atmosphere reached the ear.

What you've learnt

  • The beeper is one bit, and that's enough. Toggle bit 4 of $FE at a steady rate and you have a tone; rate is pitch, and that's the whole instrument.
  • A driver is one primitive plus composition. beep (B = duration, C = pitch) is the engine; footstep, door and chime are just different numbers fed to it.
  • Sound can be about place. A footfall and a creak aren't feedback that a key worked — they're the weight of stone and the heaviness of a door. Atmosphere reaches the ear.
  • A sweep is a sound with a shape. Move the pitch while it plays and a flat tone becomes a creak, a chime, a groan. Time is a dimension you can compose in.

What's next

The keep is heard now, but you're still only wandering it — there's nothing to do. A game needs a goal. In Unit 14, "The Keep's Gold," we scatter treasure through the chambers, let the thief lift it (with that bright chime under it), and declare the keep won when the last coin is gone. Atmosphere becomes a game.