Skip to content

Sound Beep

Generate a simple beep or tone by toggling the speaker bit. Rising or falling pitch for feedback sounds.

Taught in Game 1, Unit 2 soundbeeperaudiospeaker

Overview

The Spectrum 16K/48K has no dedicated sound chip - just a 1-bit speaker controlled through port $FE. Toggle bit 4 on and off at different speeds to create tones. Vary the delay between toggles for pitch, and loop duration for length. Simple but effective for game feedback.

Important: port $FE writes also control the border colour (bits 0-2) and MIC out (bit 3). The naive code below clobbers both. See "Border-safe writes" further down.

The 128K Spectrum and beyond add an AY-3-8912 sound chip with three voices and proper envelopes — preferred for music when targeting 128K+ machines.

Code

; =============================================================================
; SOUND BEEP - ZX SPECTRUM
; Generate tones via speaker bit toggling
; Taught: Game 1 (Ink War), Unit 2
; CPU: Variable (blocks during playback) | Memory: ~40 bytes
; =============================================================================

KEY_PORT    equ     $fe
SPEAKER_BIT equ     $10         ; Bit 4 of port $FE

; Rising tone (success sound)
; Good for: claiming cells, collecting items, positive feedback
sound_success:
    ld      hl, 400             ; Starting pitch (higher = lower frequency)
    ld      b, 20               ; Duration (number of cycles)

.loop:
    push    bc
    push    hl

    ; Generate one cycle of tone
    ld      b, h
    ld      c, l                ; BC = delay count
.tone_loop:
    ld      a, SPEAKER_BIT      ; Speaker on
    out     (KEY_PORT), a
    call    .delay
    xor     a                   ; Speaker off
    out     (KEY_PORT), a
    call    .delay
    dec     bc
    ld      a, b
    or      c
    jr      nz, .tone_loop

    pop     hl
    pop     bc

    ; Decrease delay (increase pitch)
    ld      de, 20
    or      a
    sbc     hl, de

    djnz    .loop
    ret

.delay:
    push    bc
    ld      b, 5
.delay_loop:
    djnz    .delay_loop
    pop     bc
    ret

; Falling tone (error/failure sound)
; Good for: invalid moves, damage, negative feedback
sound_error:
    ld      hl, 100             ; Start high pitched
    ld      b, 15               ; Duration

.error_loop:
    push    bc
    push    hl

    ld      b, h
    ld      c, l
.error_tone:
    ld      a, SPEAKER_BIT
    out     (KEY_PORT), a
    call    .delay
    xor     a
    out     (KEY_PORT), a
    call    .delay
    dec     bc
    ld      a, b
    or      c
    jr      nz, .error_tone

    pop     hl
    pop     bc

    ; Increase delay (decrease pitch)
    ld      de, 15
    add     hl, de

    djnz    .error_loop
    ret

Simple single beep:

; Quick beep (for button press feedback)
sound_click:
    ld      hl, 200             ; Pitch
    ld      b, 5                ; Short duration

.click_loop:
    push    bc
    push    hl

    ld      b, h
    ld      c, l
.click_tone:
    ld      a, SPEAKER_BIT
    out     (KEY_PORT), a
    call    short_delay
    xor     a
    out     (KEY_PORT), a
    call    short_delay
    dec     bc
    ld      a, b
    or      c
    jr      nz, .click_tone

    pop     hl
    pop     bc
    djnz    .click_loop
    ret

short_delay:
    push    bc
    ld      b, 3
.sd_loop:
    djnz    .sd_loop
    pop     bc
    ret

Trade-offs

AspectCost
CPUBlocks execution during playback
Memory~40 bytes per sound routine
LimitationNo background playback, mono only

When to use: Short feedback sounds (clicks, beeps, success/error tones).

When to avoid: Background music or sounds during gameplay - CPU is blocked.

How It Works

  1. The speaker is controlled by bit 4 of port $FE
  2. Set bit 4 high ($10) to push the speaker cone out
  3. Set bit 4 low ($00) to let it return
  4. The speed of toggling determines the pitch
  5. Smaller delay = higher frequency = higher pitch

Border-safe writes

The naive code in this pattern writes $10 and $00 to port $FE, which forces the border to colour 0 (black) and MIC low on every toggle. Any game with a coloured border will see it flash to black during sound playback. The fix is to OR the speaker bit with a stored border value:

; Globally maintained border colour (bits 0-2)
current_border:
    defb    1               ; Blue border, MIC low

play_click_safe:
    ld      b, 50           ; Number of toggles
.loop:
    ld      a, (current_border)
    or      #SPEAKER_BIT    ; Speaker on, border preserved
    out     ($fe), a
    call    delay
    ld      a, (current_border)
    out     ($fe), a        ; Speaker off, border preserved
    call    delay
    djnz    .loop
    ret

To change the border colour later, update current_border and the next toggle picks it up.

Louder beeper (issue 1 hardware trick)

On issue 1 Spectrum boards, MIC out (bit 3) is connected to the speaker through a passive feedback path. Setting bits 3 and 4 together produces a noticeably louder click than bit 4 alone. Issue 2+ boards isolated MIC from EAR, so the trick has minimal effect there:

SPEAKER_LOUD equ %00011000  ; Bits 3 + 4 (MIC + EAR)

Use sparingly — the louder click is also harsher and bypasses the saturation curve of the standard beeper.

Pitch Reference

Delay ValueApproximate Note
50-100High beep
100-200Medium tone
200-400Low tone
400+Very low rumble

Patterns: Game Loop (HALT)

Vault: ULA — port $FE bit layout | AY-3-8912 — 128K sound chip | ZX Spectrum