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.
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 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.
| 1 | 1 | ; Shadowkeep — Unit 13: Footsteps and Doors | |
| 2 | 2 | ; 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. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 365 | 365 | ld (thief_row), a | |
| 366 | 366 | call save_under | |
| 367 | 367 | call draw_thief | |
| 368 | + | call sfx_step | |
| 368 | 369 | call check_exit | |
| 369 | 370 | ret | |
| 370 | 371 | | |
| ... | |||
| 430 | 431 | ld a, 1 | |
| 431 | 432 | ld (thief_row), a | |
| 432 | 433 | .enter: | |
| 434 | + | call sfx_door | |
| 433 | 435 | call draw_room | |
| 434 | 436 | call save_under | |
| 435 | 437 | 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 | |
| 436 | 489 | ret | |
| 437 | 490 | | |
| 438 | 491 | 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:
Cross from one chamber to the next and the door answers — a single pitch swept downward, groaning on its hinges:
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.
Bis too small (the tone ends before your ear registers it) orCis so small the wave is above hearing. Lengthen B; raise C. - The note is the same height whatever C you pass. You overwrote
Cinside the loop, orbeepis reading a fixed value. The inner waits must both reloadEfromC. - 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,
dibefore a sound andeiafter — just remember the main loop needs interrupts back on forhalt. - The creak runs forever / hangs. The sweep's exit test is wrong —
Cmust move toward the limit and thejrmust 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
$FEat 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.