Skip to content

Beeper Music

Play an actual melody on the Spectrum's one-bit speaker — a note table for pitch, a length per note — and coax two or more voices from that single bit with phase accumulators.

beepermusicspeakeraudio

Overview

A beep is one tone; music is a sequence of them, each held for a beat. On the Spectrum’s one-bit speaker there’s no oscillator to hand a frequency to — the CPU is the oscillator, toggling bit 4 of port $FE by hand. So a tune is a table of periods (one per note) and lengths (how long to hold each), stepped through in a loop. And the Spectrum’s party trick, the thing that made its best music astonishing, is that with the right loop you can toggle fast enough to interleave several tones at once — real chords and basslines from a single bit.

Code

; =============================================================================
; BEEPER MUSIC - ZX SPECTRUM
; Play a melody from a note stream: (period, length) pairs, 0 = end
; =============================================================================

SPEAKER  equ  $10          ; bit 4 of port $FE
BORDER   equ  $00          ; keep bits 0-2 as whatever border you want

; DE -> the tune. Plays it once and returns.
play_tune:
.next:
    ld   a,(de)            ; period = pitch; 0 marks the end of the tune
    or   a
    ret  z
    ld   l,a               ; L = period
    inc  de
    ld   a,(de)            ; length = how many half-cycles to hold the note
    ld   b,a
    inc  de
    call play_note
    jr   .next

; L = period (bigger -> lower note), B = length in half-cycles
play_note:
.cycle:
    ld   a,BORDER|SPEAKER  ; speaker high, border preserved
    out  ($fe),a
    ld   c,l
    call delay
    ld   a,BORDER          ; speaker low
    out  ($fe),a
    ld   c,l
    call delay
    djnz .cycle
    ret

delay:                     ; C = period; a constant-cost busy-wait
.d: dec  c
    jr   nz,.d
    ret

; period, length pairs. Periods roughly halve each octave.
;             C-4       E-4       G-4       C-5
tune:
    defb 239,90,  190,90,  159,90,  119,140,  0

Trade-offs

Aspect Cost
CPU All of it — the routine blocks completely while a note sounds
Memory The player, a note table, the tune stream
Limitation One bit: naive playback is one voice, and nothing else can run

When to use: Title screens, jingles, and any 48K game happy to pause for its music.

When to avoid: Music during play — the CPU can’t drive the beeper and run the game at once. That’s what the AY chip is for on 128K machines.

Pitch is a period, and the loop is the clock

There’s no frequency register — pitch is however long you wait between toggles. A note of frequency f needs a half-period of 3500000 / (2f) T-states (the Spectrum runs at 3.5 MHz), so the delay count is that figure divided by the loop’s cost per iteration. Because a bigger period is a longer wait, bigger means lower: halve a period and the note jumps an octave. The one rule that can’t bend is that every path through the loop must take the same number of T-states — a branch that sometimes costs a few cycles more bends the pitch and the note sours. Constant-time code is the whole discipline of beeper sound.

Faking channels on one bit

A single bit can be either up or down — it can’t add two square waves together. The engines that made the Spectrum sing get around that with phase accumulators and a pin pulse: give each voice a 16-bit counter and a 16-bit “speed”, and in one tight, fixed-rate loop add every voice’s speed to its counter. Whenever a counter overflows, fire the speaker with a single short click. The ear reconstructs each voice from the rate of its clicks, so several tones ring out at once:

; --- Two voices from one bit (the heart of a beeper engine) ---
; hl = phase 1, bc = speed 1     ; hl' = phase 2, de' = speed 2
mix2:
    add  hl,bc              ; advance voice 1; carry set on overflow
    ; ... remember voice 1's carry ...
    exx
    add  hl,de             ; advance voice 2
    exx
    ; ... if either voice overflowed, emit ONE short speaker pulse ...
    ld   a,BORDER|SPEAKER
    out  ($fe),a
    ; ... a fixed number of T-states later, drop it low again ...
    ld   a,BORDER
    out  ($fe),a
    ; ... loop, keeping every iteration exactly the same length ...

The catch is that same iron rule: the mixing loop must be perfectly constant-time, because its length is the sample rate. This is the technique behind tools like Beepola’s Tritone engine and the multi-voice arrangements Tim Follin wrung out of the 48K — as many as four voices, plus drums, from a chip that was never meant to play a chord.

The louder side, and the 128K way out

Two follow-ons worth knowing:

  • Volume. The beeper has no volume control — a bit is on or off. What little dynamic range exists comes from pulse width: a very short pin pulse sounds quieter than a long one, which is how beeper engines fake note envelopes and drums.
  • The AY. All of this steals the whole CPU. The 128K, +2 and +3 add the AY-3-8912 — three voices with real envelopes that play in the background while your game runs. It’s the natural next step when a tune has to share the machine with a moving game.

Patterns: Sound Beep, Game Loop (HALT)

Vault: ULA — port $FE and the speaker | AY-3-8912 — the 128K sound chip | ZX Spectrum