Overview
One channel plays one tone; envelopes make it move. Music is the step up: all three channels at once, each handed a fresh note on a regular beat. And here the AY earns its keep — where beeper music seizes the whole CPU, an AY tune plays from the interrupt: step the pattern once a frame, and between those steps the chip holds all three notes on its own while your game runs. Count the interrupts into rows, look up a period per channel from a note table, write it, and you have the skeleton of every AY player — and, with only the port addresses changed, of CPC and Atari ST players too.
Code
; =============================================================================
; AY MUSIC - ZX SPECTRUM 128K
; ay_music_tick, called once per 50 Hz interrupt, feeds all three channels
; =============================================================================
TEMPO equ 6 ; interrupts per row
ROWS equ 8
ay_music_init:
ld a,7 ; mixer: all three tones ON, noise off
ld e,%00111000
call ay_out
ld d,3 ; set channels A, B, C to full fixed volume
.vol:
ld a,7
add a,d ; volume registers are 8, 9, 10
ld e,15
call ay_out
dec d
jr nz,.vol
ld a,TEMPO
ld (mtimer),a
xor a
ld (mrow),a
ret
; --- Call once per interrupt; a new row lands every TEMPO frames ---
ay_music_tick:
ld hl,mtimer
dec (hl)
ret nz ; between rows — the chip holds the notes, no work
ld (hl),TEMPO
ld a,(mrow) ; channel A's note this row
ld c,a
ld b,0
ld hl,pat_a
add hl,bc
ld a,(hl)
ld d,0 ; A's tone registers are 0 / 1
call set_channel
ld a,(mrow) ; reload row (ay_out clobbers BC)
ld c,a
ld b,0
ld hl,pat_b
add hl,bc
ld a,(hl)
ld d,2 ; B -> registers 2 / 3
call set_channel
ld a,(mrow)
ld c,a
ld b,0
ld hl,pat_c
add hl,bc
ld a,(hl)
ld d,4 ; C -> registers 4 / 5
call set_channel
ld a,(mrow) ; step the pattern, loop at ROWS
inc a
cp ROWS
jr c,.save
xor a
.save:
ld (mrow),a
ret
; A = note index (0 = rest/hold), D = channel's tone-LO register (0, 2 or 4)
set_channel:
or a
ret z ; 0 = leave the channel playing its last note
add a,a ; note index * 2 (two bytes per entry)
ld l,a
ld h,0
ld bc,note_tab
add hl,bc ; HL -> {period lo, period hi}
ld a,d
ld e,(hl)
call ay_out ; period low -> register D
inc hl
ld a,d
inc a
ld e,(hl)
call ay_out ; period high -> register D+1
ret
; A = register, E = value
ay_out:
ld bc,$fffd
out (c),a
ld b,$bf
out (c),e
ret
mtimer: defb 0
mrow: defb 0
; AY tone periods (128K clock). Index 0 = rest.
; C-3 C-4 E-4 G-4
note_tab: defw 0, $034F, $01A8, $0150, $011B
pat_a: defb 2,3,4,3, 2,3,4,3 ; lead
pat_b: defb 4,4,2,2, 4,4,2,2 ; harmony
pat_c: defb 1,0,1,0, 1,0,1,0 ; bass
Trade-offs
| Aspect | Cost |
|---|---|
| CPU | A few bytes of work most interrupts; a little more on a row change |
| Memory | The period table, the patterns, two bytes of state |
| Limitation | Three channels, and one shared envelope generator across them |
When to use: Any 128K game that wants music during play — which is most of them.
When to avoid: Nothing about music; for a single reactive blip, a one-shot envelope effect is lighter.
It plays from the interrupt
This is the whole reason the AY beats the beeper for games. The Spectrum raises an interrupt 50 times a second; hook it (the HALT game loop already waits on it) and call ay_music_tick there. Each call is tiny — usually just a counter decrement — and only every TEMPO-th one writes a row. In between, the chip sustains all three notes with no help at all, so the same frame that nudges the music also moves the sprites and reads the keys. TEMPO sets the speed; 6 interrupts a row is a common default.
Three channels, one envelope
The player above gives every channel a fixed volume, so all three ring flat. Bring in the envelope generator and you meet its one hard limit: a single envelope, shared. Real AY drivers get around it by writing volumes by hand every interrupt — a software envelope table per channel, stepped alongside the note table — so each voice can pluck or swell independently. That per-frame volume work, plus ornaments (fast note wobbles that fake vibrato and chords), is most of what separates this skeleton from a tracker replay.
The same chip, other machines
The AY-3-8912 and its near-twins sit in the Amstrad CPC, the Oric, the MSX, and — as the YM2149 — the Atari ST. The register map is identical, so this player ports almost verbatim: change the two port addresses (the CPC reaches the AY through its PPI, not $FFFD/$BFFD) and rebuild the period table for that machine’s clock, and the same pattern, the same tick, the same three voices play on. It’s why AY tracker formats like PT3 and the Vortex Tracker lineage span so many machines at once.
Related
Patterns: AY Envelopes and Noise, AY Tone, Game Loop (HALT)
Vault: AY-3-8912 | ZX Spectrum