Since unit 10, a hit has made a sound, but you could miss it: twelve quick pushes of the speaker, over in about a thousandth of a second. We will turn that tick into a note you can hear, 523 Hz for a fifth of a second, which is the C an octave above middle C.
The Spectrum has no sound chip. Its speaker moves only when a program tells it to, one bit at a time, so the program makes every vibration itself. That makes sound a timing problem, and timing is something we can calculate exactly.
Source · view and edit
Companion file: assets.inc
The include line reads this editable data when you assemble. Keep it beside your source for a local build.
Edit the source, then assemble.
Edits are saved in this browser. Keep a downloaded copy of work you want to keep.
Focus the Spectrum screen to use the keyboard, or hold these buttons. Assemble again for a fresh machine.
Run the program to inspect it. Editing source does not change the running machine.
phase- —
hull- —
border- —
elapsed- —
Hear the impact
Tick Sound beside the buttons under the editor, then press Assemble and run. Sound starts unticked, because a browser plays sound only after you ask it to. Once you tick it, the other lessons remember the choice.
Launch with Space and leave the ship where it is. The first meteor reaches it about two seconds later, you hear the tone, and the run ends. R retries; Q returns to the title.
To hear it outside this page, press Download tape and load meteor-storm.tap into the site’s Spectrum player with its Sound ticked, into Emu198x on the desktop, or into any other 48K Spectrum emulator with sound turned on.
One bit moves the speaker
In Meet Assembly’s first program we turned the border red with out ($FE),a. The same port drives the speaker. Bits 0 to 2 of the value written are the border colour, bit 3 drives the MIC socket used for saving to tape, and bit 4 drives the speaker. Writing a byte with bit 4 set pushes the speaker one way; writing one with it clear lets it back.
One push and one release make a cycle. Repeat it steadily and the air carries a square wave: two levels, switched at regular intervals. The time one cycle takes is its period, and the number of cycles in a second is its frequency, measured in hertz (Hz). Shorter periods mean higher notes. The number of cycles decides how long the sound lasts.
So a tone needs two numbers: how long to wait between writes, and how many cycles to play. The new impact_sound gives each one a name at the top of the source:
IMPACT_HALF equ 255
IMPACT_CYCLES equ 105impact_sound:
; Port $FE bit 4 drives the speaker; bits 0-2 are the border colour.
; Every write ORs in the border, so the tone leaves it unchanged.
ld a,(border)
and 7
ld d,a
ld e,IMPACT_CYCLES
impact_sound_cycle:
ld a,d
or 16
out ($FE),a
ld b,IMPACT_HALF
impact_sound_high: djnz impact_sound_high
ld a,d
out ($FE),a
ld b,IMPACT_HALF
impact_sound_low: djnz impact_sound_low
dec e
jr nz,impact_sound_cycle
retEach cycle sets bit 4 and writes, counts B down from IMPACT_HALF with djnz, clears bit 4 and writes, counts down again, then decrements E. D holds the border bits throughout; the next section explains why.
Count the period in T-states
The Z80 in a 48K Spectrum runs from a 3.5 MHz clock: 3,500,000 T-states each second. Every instruction takes a fixed number of them, listed in Zilog’s manual. ld b,n takes 7, out ($FE),a takes 11 and ld a,d takes 4. djnz takes 13 when it jumps back and 8 when B reaches zero and it falls through, so a djnz that starts with n in B costs 13(n-1)+8, which is 13n-5.
Add up the time between one speaker write and the next:
| Part | Instructions | T-states |
|---|---|---|
| High half | ld b,n 7, djnz 13n-5, ld a,d 4, out 11 |
13n+17 |
| Low half | ld b,n 7, djnz 13n-5, dec e 4, jr nz 12, ld a,d 4, or 16 7, out 11 |
13n+40 |
| One cycle | both halves | 26n+57 |
With n=255 a period is 26×255+57 = 6687 T-states. Divide the clock by the period: 3,500,000 ÷ 6687 = 523.4 cycles a second, so 523 Hz. The length is 105 × 6687 = 702,135 T-states, and 702,135 ÷ 3,500,000 is 0.2006 seconds: about 200 ms. A frame of the 50 Hz picture is 69,888 T-states, so the tone lasts ten frames.
The old routine used the same idea with much shorter waits: 313 T-states a cycle, which is 3,500,000 ÷ 313 = 11,182 Hz, for only 12 cycles. That is 3756 T-states in all, 1.07 ms. A sound that short reaches the ear as a click, not a note: it ends before there is time to hear a pitch. That is why the old impact barely registered.
An Emu198x audio capture of this program measured 522 Hz for 105 cycles, 199 ms, and the site’s browser player produced the same pitch and length.
The two halves differ by 23 T-states, so the speaker spends slightly longer released than pushed. That changes the tone’s colour a little, not its pitch: pitch follows the whole period. The code runs at 32768, above the 16K at $4000 to $7FFF that the ULA, the chip that draws the picture, shares with the processor. Its instruction fetches are never held up. Only each OUT to the ULA’s port may wait a few T-states while the picture is drawn, a fraction of a percent here.
Predict: change IMPACT_HALF to 127 and leave IMPACT_CYCLES at 105. What happens to the pitch and the length?
Show the explanation
A period becomes 26×127+57 = 3359 T-states, and 3,500,000 ÷ 3359 is 1042 Hz. That is about twice the frequency, which the ear hears as the same note an octave higher. The length halves too, to 105 × 3359 = 352,695 T-states, about 101 ms, because each of the 105 cycles is now half as long. To keep 200 ms, double IMPACT_CYCLES to 210; it still fits in E’s single byte.
Try it: with Sound ticked, edit the value, press Assemble and run and let a meteor hit the ship. Compare what you hear with your prediction, then restore 255 and run again to hear the two notes one after the other.
Keep the border bits
The old routine cleared its speaker bit with xor a and out ($FE),a. That writes zero to every bit, including the three border bits, so each impact also set the border to black. The border was already black, so nobody noticed. Any program that chose another colour would lose it the moment the ship was hit.
This program keeps the border colour in a byte of its own, border, placed outside the run’s state so retry does not clear it. Start-up now loads that byte with ld a,(border) before its first out ($FE),a, where it used xor a before. impact_sound loads the same byte, keeps only the colour bits with and 7, and holds the result in D. Every speaker write starts from D: ld a,d then or 16 to push, ld a,d alone to release. The speaker moves; the colour bits are written with their existing values each time, so the border stays as it was.
Why `and 7` instead of loading `border` straight into D?
Show the explanation
and 7 keeps bits 0 to 2 and clears the rest. If border ever held a value with bit 4 set, the release write would push the speaker too. Both writes would push it, it would never move, and the tone would be silent. A value with bit 3 set would drive the MIC socket as well. Masking makes the routine depend only on the colour.
Assemble with border: defb 2 near the end of the source. The border is red from launch and stays red through the impact, where the old routine would have turned it black. Restore 0 afterwards.
Wait while it plays
The whole tone runs inside impact_sound, which returns only when the last cycle ends. For 200 ms the game does nothing else. The frame interrupt still counts frames, but the storm does not move and the result screen waits. Here that costs nothing: hull is already zero, so the run is over and the result follows as soon as the tone ends.
A sound during play would be different. Unit 25 gives the other events sounds and measures what they take from an update, and unit 26 plays them without delaying the game. Unit 27 uses that for the crash and unit 28 colours the storm. Units 29 to 35 turn the game into a voyage of five storms, and Keep and share the game packages it, sounds included, as a tape you can load on a fresh machine.
Sources
Sinclair Research, ZX Spectrum BASIC Programming, chapter 23, port 254 output: loudspeaker on D4, MIC on D3, border on D2 to D0. Zilog, Z80 CPU User Manual, UM008011-0816, T-state counts for LD, OR, AND, OUT, DEC, DJNZ and JR. Chris Smith, The ZX Spectrum ULA: How to Design a Microcomputer (ZX Design and Media), chapter 18 for the 3.5 MHz CPU clock and p. 225 for the 69,888 T-state frame. The maintained Meteor Storm source linked through this page supplies the routine; the pitch and length measurements come from Emu198x audio captures of this program.
Run it here
This unit's program, built for you.
- Program
- meteor-storm.tap
- Machine
- ZX Spectrum 48K (PAL)