A 48K Spectrum has one bit of sound hardware, bit 4 of port $FE, and Ant Attack makes every sound by flipping it with the processor counting in between. The machine code has four OUTs to the port, two in TONE and two in NOISE, and only two routines call those: RUN_SCRIPT, which plays the sound bytes in a script, and HANDLE_EVENTS, which clicks for a footstep. There is no other sound in a frame -- no sound for a thrown grenade in flight, an ant or a fall except through a script -- and while a script plays the game stands still. The BASIC adds four BEEP statements of its own, played by the ROM.

The same port sets the border colour (bits 0-2) and the MIC output (bit 3), so an OUT that only meant to move the speaker would turn the border black. That is what $B42C is for: both routines start from it with bits 3 and 4 set, so the border stays whatever BASIC made it. BASIC sets it to 0 for play (it is one of the variables from VIEW_ORIGIN on that line 200 POKEs from DATA, to go with line 300's BORDER 0) and to 5 before the ending (line 3600, to go with line 2000's BORDER 5); each recording below says which colour its OUTs wrote.

TONE plays one entry of TONES: a pitch delay E and a count of half-waves B. Each half-wave is E turns of a 14 T-state loop (DEC A, JP NZ) and 84 T-states more, the ten NOPs among them; E = 0 goes round 256 times. B rises as the pitch does, so every note lasts about 39 ms. The table is a chromatic scale, a semitone a step from 477 Hz (40 cents above A#4) to 1078 Hz (49 cents below C#6), tuned to itself rather than to concert pitch. TONE's first OUT turns the speaker off. After another note or a BEEP it is off already, so the note begins with a half-wave of silence; after a burst of noise, which leaves it on, that first OUT is an edge. Note 15 is never played as a note: RUN_SCRIPT takes it as noise, 256 turns of NOISE. Its speaker bit is bit 4 of what RANDOM leaves in A, not the carry RANDOM returns: that is bit 2 of the generator's first byte ($B428) as it was before the step, the carry being clear on entry as it always is in NOISE -- checked by running RANDOM for all 256 values of that byte. So a burst sets the speaker to a new random bit every 194 T-states, some 18,041 a second: a hiss.

NoteEBHalf-wavePitchSemitones upLengthMeasured
0256373,668 T477.1 Hz0.0038.8 ms477.1 Hz, 2,030 waves
1241393,458 T506.1 Hz1.0238.5 ms506.1 Hz, 1,850 waves
2227413,262 T536.5 Hz2.0338.2 ms536.5 Hz, 3,588 waves
3214443,080 T568.2 Hz3.0238.7 ms568.2 Hz, 1,134 waves
4201472,898 T603.9 Hz4.0838.9 ms603.9 Hz, 3,645 waves
5190492,744 T637.8 Hz5.0238.4 ms637.8 Hz, 3,290 waves
6179522,590 T675.7 Hz6.0238.5 ms675.7 Hz, 950 waves
7168562,436 T718.4 Hz7.0939.0 ms718.4 Hz, 918 waves
8159592,310 T757.6 Hz8.0138.9 ms757.6 Hz, 741 waves
9149632,170 T806.5 Hz9.0939.1 ms806.5 Hz, 3,172 waves
10141672,058 T850.3 Hz10.0139.4 ms850.3 Hz, 2,015 waves
11132711,932 T905.8 Hz11.1039.2 ms905.8 Hz, 1,035 waves
12124761,820 T961.5 Hz12.1339.5 ms961.5 Hz, 2,294 waves
13117801,722 T1016.3 Hz13.0939.4 ms1016.3 Hz, 3,822 waves
14110861,624 T1077.6 Hz14.1139.9 ms1077.6 Hz, 2,520 waves
15104911,540 T1136.4 Hz15.0240.0 msnever played: noise

Pitch and half-wave are worked out from the table and the loop's T-states; Measured is from the recordings of all the scripts below, every whole wave timed from the OUT that started it to the next but one.

A script's sound bytes are $80-$FE: bits 0-3 the note, bits 4-6 one less than how many times to play it. They sit among the letters of its message, so RUN_SCRIPT prints a letter, plays a note, prints the next -- and a message types itself out to its own tune. See the scripts page for what each one says.

Every recording here is the game's own code, or the ROM's, running in SkoolKit's simulator, with the T-state of each change of the speaker bit logged and rendered as 16-bit mono at 44100 Hz; nothing is synthesised. The simulated machine is uncontended; the machine code's loops are in uncontended memory anyway, so only the OUTs would be held up on a real Spectrum, by a few T-states each. Each recording says how its sound was checked against what the code implies.

The scripts

Scripts 8 and 11 have no sound bytes: they only reprint a number on the panel, and are left out.

Script 1: NASTY FALL !

The player lands after falling for five frames or more: LANDED records a bad fall in the player's event byte, and HANDLE_EVENTS plays this through PLAY_SCRIPT_ONE.

Recorded: PLAY_SCRIPT called with A = 1 on a copy of the machine at the start of play, the speaker off; to its return.

Checked: TONE made 451 OUTs (the script's notes imply 451: B + 1 each) and 440 edges (implied 440); every whole wave measured was exactly 2(14E + 84) T-states for its note's E; every OUT wrote border colour 0 (bits 0-2, from $B42C).

280 ms, 440 edges; notes 2, 4, 7, 9, 11, 13, 14: 536 to 1078 Hz from the table, 536 to 1078 Hz measured.

Script 2: "HELP! I FELL!"

The same for the rescued person: HANDLE_EVENTS.

Recorded: PLAY_SCRIPT called with A = 2 on a copy of the machine at the start of play, the speaker off; to its return.

Checked: TONE made 502 OUTs (the script's notes imply 502: B + 1 each) and 490 edges (implied 490); every whole wave measured was exactly 2(14E + 84) T-states for its note's E; every OUT wrote border colour 0 (bits 0-2, from $B42C).

321 ms, 490 edges; notes 2, 3, 4, 5, 11, 12, 13, 14: 536 to 1078 Hz from the table, 536 to 1078 Hz measured.

Script 3: GOOD SHOT!

A grenade's blast kills an ant: GRENADE_SOUNDS, from bit 2 of $B42D. Notes, noise, and the play area flashing through colours (FILL_PLAY_ATTRIBUTES) around the words.

Recorded: PLAY_SCRIPT called with A = 3 on a copy of the machine at the start of play, the speaker off; to its return.

Checked: TONE made 527 OUTs (the script's notes imply 527: B + 1 each) and 511 edges (implied 511); every whole wave measured was exactly 2(14E + 84) T-states for its note's E; NOISE made 2570 OUTs (implied 2570: 257 a burst), every random bit 194 T-states after the last and the final OUT 23 after that, as its instructions imply; its edges depend on the random bits; every OUT wrote border colour 0 (bits 0-2, from $B42C).

547 ms, 1811 edges; notes 0, 1, 4, 5, 7, 8, 12, 13: 477 to 1016 Hz from the table, 477 to 1016 Hz measured; 10 bursts of noise.

Script 4: a grenade thrown

A grenade is thrown: GRENADE_SOUNDS (bit 0 of $B42D) plays it to reprint the count. PRINT_COUNTERS plays it too, at the start of every call of PLAY -- so the same blip sounds twice as each level begins, once as the city is drawn behind the ready prompt and once as play starts.

Recorded: PLAY_SCRIPT called with A = 4 on a copy of the machine at the start of play, the speaker off; to its return.

Checked: TONE made 77 OUTs (the script's notes imply 77: B + 1 each) and 76 edges (implied 76); every whole wave measured was exactly 2(14E + 84) T-states for its note's E; and in the run the BASIC recordings come from, between the story card's key and the start of play, TONE played 2 notes (154 OUTs) with pitch delay 124 (note 12's is 124); the only other sounds were 2 footstep clicks as the run's V walked the player in, and the ROM wrote the port once with the speaker bit clear (line 300's BORDER 0); every OUT wrote border colour 0 (bits 0-2, from $B42C).

43 ms, 76 edges; notes 12: 962 Hz from the table, 962 Hz measured.

Script 5: a grenade going off

A grenade goes off: GRENADE_SOUNDS (bit 1 of $B42D). Four bursts of noise, the only sound a grenade makes.

Recorded: PLAY_SCRIPT called with A = 5 on a copy of the machine at the start of play, the speaker off; to its return.

Checked: NOISE made 1028 OUTs (implied 1028: 257 a burst), every random bit 194 T-states after the last and the final OUT 23 after that, as its instructions imply; its edges depend on the random bits; every OUT wrote border colour 0 (bits 0-2, from $B42C).

58 ms, 509 edges; 4 bursts of noise.

Script 6: SILLY! YOU BLEW / YOURSELF UP !

The player is caught in their own blast: HANDLE_EVENTS sets their energy to 0 and plays this.

Recorded: PLAY_SCRIPT called with A = 6 on a copy of the machine at the start of play, the speaker off; to its return.

Checked: TONE made 2072 OUTs (the script's notes imply 2072: B + 1 each) and 1978 edges (implied 1978); every whole wave measured was exactly 2(14E + 84) T-states for its note's E; every OUT wrote border colour 0 (bits 0-2, from $B42C).

1.82 s, 1978 edges; notes 1, 2, 4, 5: 506 to 638 Hz from the table, 506 to 638 Hz measured.

Script 7: BITTEN!

An ant bites the player, taking a point of energy: HANDLE_EVENTS.

Recorded: PLAY_SCRIPT called with A = 7 on a copy of the machine at the start of play, the speaker off; to its return.

Checked: TONE made 115 OUTs (the script's notes imply 115: B + 1 each) and 113 edges (implied 113); every whole wave measured was exactly 2(14E + 84) T-states for its note's E; NOISE made 2056 OUTs (implied 2056: 257 a burst), every random bit 194 T-states after the last and the final OUT 23 after that, as its instructions imply; its edges depend on the random bits; every OUT wrote border colour 0 (bits 0-2, from $B42C).

200 ms, 1149 edges; notes 0, 12: 477 to 962 Hz from the table, 477 to 962 Hz measured; 8 bursts of noise.

Script 9: " HOW COULD YOU ? / AFTER ALL WE'VE / BEEN THROUGH ?"

The rescued person is caught in the player's blast: HANDLE_EVENTS.

Recorded: PLAY_SCRIPT called with A = 9 on a copy of the machine at the start of play, the speaker off; to its return.

Checked: TONE made 2302 OUTs (the script's notes imply 2302: B + 1 each) and 2205 edges (implied 2205); every whole wave measured was exactly 2(14E + 84) T-states for its note's E; NOISE made 1799 OUTs (implied 1799: 257 a burst), every random bit 194 T-states after the last and the final OUT 23 after that, as its instructions imply; its edges depend on the random bits; every OUT wrote border colour 0 (bits 0-2, from $B42C).

2.05 s, 3106 edges; notes 1, 2, 4, 5, 11, 12, 13: 506 to 1016 Hz from the table, 506 to 1016 Hz measured; 7 bursts of noise.

Script 10: "THEY GOT ME"

An ant bites the rescued person: HANDLE_EVENTS.

Recorded: PLAY_SCRIPT called with A = 10 on a copy of the machine at the start of play, the speaker off; to its return.

Checked: TONE made 168 OUTs (the script's notes imply 168: B + 1 each) and 167 edges (implied 167); every whole wave measured was exactly 2(14E + 84) T-states for its note's E; NOISE made 2056 OUTs (implied 2056: 257 a burst), every random bit 194 T-states after the last and the final OUT 23 after that, as its instructions imply; its edges depend on the random bits; every OUT wrote border colour 0 (bits 0-2, from $B42C).

204 ms, 1203 edges; notes 13, 14: 1016 to 1078 Hz from the table, 1016 to 1078 Hz measured; 8 bursts of noise.

Script 12: "MY HERO!" / "TAKE ME AWAY / FROM ALL THIS !"

The player comes within three cells of the person waiting, at the same height: MOVE_RESCUEE. The letters are typed out between the notes, so the tune and the message arrive together.

Recorded: PLAY_SCRIPT called with A = 12 on a copy of the machine at the start of play, the speaker off; to its return.

Checked: TONE made 5116 OUTs (the script's notes imply 5116: B + 1 each) and 4944 edges (implied 4944); every whole wave measured was exactly 2(14E + 84) T-states for its note's E; every OUT wrote border colour 0 (bits 0-2, from $B42C).

3.91 s, 4944 edges; notes 0, 1, 2, 3, 5, 6, 8, 10, 13: 477 to 1016 Hz from the table, 477 to 1016 Hz measured.

Script 13: CONGRATULATIONS !

The player and the rescued person are both outside the walls: CHECK_RESCUED. Towards the end the play area is refilled in a new colour (FILL_PLAY_ATTRIBUTES) before each run of notes as the scale climbs.

Recorded: PLAY_SCRIPT called with A = 13 on a copy of the machine at the start of play, the speaker off; to its return.

Checked: TONE made 4697 OUTs (the script's notes imply 4697: B + 1 each) and 4534 edges (implied 4534); every whole wave measured was exactly 2(14E + 84) T-states for its note's E; every OUT wrote border colour 0 (bits 0-2, from $B42C).

3.71 s, 4534 edges; notes 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14: 477 to 1078 Hz from the table, 477 to 1078 Hz measured.

Script 14: EATEN ALIVE

An ant bites the player's last point of energy away: HANDLE_EVENTS.

Recorded: PLAY_SCRIPT called with A = 14 on a copy of the machine at the start of play, the speaker off; to its return.

Checked: TONE made 934 OUTs (the script's notes imply 934: B + 1 each) and 891 edges (implied 891); every whole wave measured was exactly 2(14E + 84) T-states for its note's E; NOISE made 1799 OUTs (implied 1799: 257 a burst), every random bit 194 T-states after the last and the final OUT 23 after that, as its instructions imply; its edges depend on the random bits; every OUT wrote border colour 0 (bits 0-2, from $B42C).

961 ms, 1792 edges; notes 0, 2, 4: 477 to 604 Hz from the table, 477 to 604 Hz measured; 7 bursts of noise.

Script 15: IVE BEEN EATEN ALIVE

An ant bites the rescued person's last point of energy away: HANDLE_EVENTS.

Recorded: PLAY_SCRIPT called with A = 15 on a copy of the machine at the start of play, the speaker off; to its return.

Checked: TONE made 934 OUTs (the script's notes imply 934: B + 1 each) and 891 edges (implied 891); every whole wave measured was exactly 2(14E + 84) T-states for its note's E; NOISE made 1799 OUTs (implied 1799: 257 a burst), every random bit 194 T-states after the last and the final OUT 23 after that, as its instructions imply; its edges depend on the random bits; every OUT wrote border colour 0 (bits 0-2, from $B42C).

966 ms, 1792 edges; notes 0, 2, 4: 477 to 604 Hz from the table, 477 to 604 Hz measured; 7 bursts of noise.

Script 16: PARALYSED AN ANT !

The player lands on top of an ant that is neither exploding nor already paralysed, and PARALYSE_ANT paralyses it for good.

Recorded: PLAY_SCRIPT called with A = 16 on a copy of the machine at the start of play, the speaker off; to its return.

Checked: TONE made 1778 OUTs (the script's notes imply 1778: B + 1 each) and 1740 edges (implied 1740); every whole wave measured was exactly 2(14E + 84) T-states for its note's E; NOISE made 1285 OUTs (implied 1285: 257 a burst), every random bit 194 T-states after the last and the final OUT 23 after that, as its instructions imply; its edges depend on the random bits; every OUT wrote border colour 0 (bits 0-2, from $B42C).

1.18 s, 2381 edges; notes 4, 7, 10, 13: 604 to 1016 Hz from the table, 604 to 1016 Hz measured; 5 bursts of noise.

Script 17: YOU ARE A REAL / AT LAST YOUR MISSION IS OVER! / YOUR NEW FRIENDS WISH TO / THANK YOU FOR SAVING THEIR / LIVES.

The tenth rescue: BASIC line 3600 clears the screen (line 2000, which also sets BORDER 5), POKEs 5 into $B42C to match, and calls FINAL_SCRIPT (USR 36594). The whole tribute types itself out a letter or two a note.

Recorded: FINAL_SCRIPT called on a copy of the machine at the start of play, with the screen cleared to PAPER 6 and $B42C set to 5, as BASIC line 3600 leaves them; to its return.

Checked: TONE made 15236 OUTs (the script's notes imply 15236: B + 1 each) and 14842 edges (implied 14842); every whole wave measured was exactly 2(14E + 84) T-states for its note's E; every OUT wrote border colour 5 (bits 0-2, from $B42C).

9.53 s, 14842 edges; notes 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14: 477 to 1078 Hz from the table, 477 to 1078 Hz measured.

Footsteps

A click a step

HANDLE_EVENTS, every frame the player or the rescued person takes a step: MOVE_OBJECT sets the object's event byte (+$0F) to 1 for a step and for a short landing, and when that is all that happened to either of them HANDLE_EVENTS calls NOISE with B = 5 -- five random speaker bits and the speaker left on, a click. A frame that also has a bite, a blast or a bad fall plays that script instead, and no click.

Recorded: A copy of the machine at the start of play, the player moved to open ground (x $8D, y $90, facing x up) and V held, run frame by frame from GAME_FRAME; recorded over 12 frames from the first step.

Checked: 12 of the 12 frames clicked, each with 6 OUTs (implied 6: B random ones and one to leave the speaker on); all 72 came from NOISE, every random bit 194 T-states after the last and the final OUT 23 after that, as its instructions imply. The player ended 12 cells further along x. The frames were 534,699 to 545,468 T-states, so 6.5 steps a second; a click itself is over in 799 T-states, 0.23 ms.

1.85 s, 30 edges.

BASIC's BEEPs

Played by the ROM's BEEP command ($03B5 is its BEEPER) from the game's own BASIC, run in the simulator: the recordings are the ROM working from the numbers BASIC gives it, not a reconstruction.

The title screen's logo

BASIC line 3110, at the title screen: the logo is printed eight times over in inks 7 down to 0, five times round, and each printing is preceded by a BEEP of 0.05 s -- pitch 1 (C#4) for the first four inks and 0 (middle C) for the last four, from INT (i/4). 40 BEEPs; the silences between are the logo being printed.

The line: 3110 PRINT AT 12,6;"© SANDY WHITE 1983";AT 16,3; INK 2;"SOFT SOLID 3-D Pat.Pending": FOR j=1 TO 5: FOR i=7 TO 0 STEP -1: BEEP .05,INT (i/4): PRINT AT 7,2;: LET ink=i: GO SUB 3000: NEXT i: NEXT j

Recorded: The game run from the end of its loader with the key presses the build's playing_machine() uses, from the first BEEP of the title to the first of the scale.

Checked: at the 40 entries to the ROM's BEEPER ($03B5), read in the simulator, DE was round(f × t) − 1 and HL was round(437500/f − 30.125) in all 40 -- f being middle C × 2p/12, t and p the BEEP's duration and pitch, the ROM's own sums. Every call made 2(DE + 1) edges, DE + 1 whole waves in all 40; and every half-wave lasted 4 × HL + 118 T-states, as BEEPER's instructions imply in all 40. That is 2.5 T-states shorter than the 4 × (HL + 30.125) the ROM's constant allows for, which, with HL rounded, left the pitches within 1.0 cents of what BASIC asked for.

7.47 s, 1080 edges (2(DE + 1) summed over the calls: 1080); asked 262 to 277 Hz, measured 262 to 277 Hz.

The title screen's scale

BASIC line 3120, straight after: 21 BEEPs of 0.02 s climbing from middle C in steps of three semitones (a diminished-seventh arpeggio) to pitch 60, five octaves up, before the girl-or-boy question appears.

The line: 3120 FOR i=0 TO 60 STEP 3: BEEP .02,i: NEXT i: PRINT AT 21,6; FLASH 1; BRIGHT 1;" Girl or Boy (g/b)? ": RETURN

Recorded: The same run, from the scale's first BEEP to the end of its last.

Checked: at the 21 entries to the ROM's BEEPER ($03B5), read in the simulator, DE was round(f × t) − 1 and HL was round(437500/f − 30.125) in all 21 -- f being middle C × 2p/12, t and p the BEEP's duration and pitch, the ROM's own sums. Every call made 2(DE + 1) edges, DE + 1 whole waves in all 21; and every half-wave lasted 4 × HL + 118 T-states, as BEEPER's instructions imply in all 21. That is 2.5 T-states shorter than the 4 × (HL + 30.125) the ROM's constant allows for, which, with HL rounded, left the pitches within 25.3 cents of what BASIC asked for -- sharpest at the top, where a half-wave is only a few hundred T-states.

1000 ms, 2048 edges (2(DE + 1) summed over the calls: 2048); asked 262 to 8372 Hz, measured 262 to 8495 Hz.

A key pressed on a card

BASIC line 960, the end of every story card and score card: WAIT_KEY waits for a key, then one BEEP of 0.01 s at pitch 40 (E7). Recorded after the key on the first story card.

The line: 960 GO SUB 100: GO SUB 200: PRINT AT 21,8; PAPER 0; INK 7;" PRESS ANY KEY ": RANDOMIZE USR 32919: BEEP .01,40: RETURN

Recorded: The same run, on through "b" for boy to the first story card, and SPACE pressed there; from the entry to BEEPER to its return.

Checked: at the entry to the ROM's BEEPER ($03B5), read in the simulator, DE was round(f × t) − 1 and HL was round(437500/f − 30.125) -- f being middle C × 2p/12, t and p the BEEP's duration and pitch, the ROM's own sums. Every call made 2(DE + 1) edges, DE + 1 whole waves; and every half-wave lasted 4 × HL + 118 T-states, as BEEPER's instructions imply. That is 2.5 T-states shorter than the 4 × (HL + 30.125) the ROM's constant allows for, which, with HL rounded, left the pitches within 4.3 cents of what BASIC asked for.

10 ms, 52 edges (2(DE + 1) summed over the calls: 52); asked 2637 Hz, measured 2644 Hz.

Out of time

BASIC line 3220, the end of the "out of time" card (lines 3200-3220, reached from line 60 when the clock at $B436 reads 0): 14 BEEPs of 0.2 s falling from pitch 40 to 1, three semitones at a time.

The line: 3220 FOR i=40 TO 0 STEP -3: BEEP .2,i: NEXT i: RETURN

Recorded: A copy of the same run once play has started, with the clock at $B436 poked down to 2 (one tick is three frames) and no key held, run on until the fourteen BEEPs have played: from the first BEEP to the end of the last. The silences are BASIC going round its FOR loop.

Checked: at the 14 entries to the ROM's BEEPER ($03B5), read in the simulator, DE was round(f × t) − 1 and HL was round(437500/f − 30.125) in all 14 -- f being middle C × 2p/12, t and p the BEEP's duration and pitch, the ROM's own sums. Every call made 2(DE + 1) edges, DE + 1 whole waves in all 14; and every half-wave lasted 4 × HL + 118 T-states, as BEEPER's instructions imply in all 14. That is 2.5 T-states shorter than the 4 × (HL + 30.125) the ROM's constant allows for, which, with HL rounded, left the pitches within 7.0 cents of what BASIC asked for -- sharpest at the top, where a half-wave is only a few hundred T-states.

3.17 s, 6042 edges (2(DE + 1) summed over the calls: 6042); asked 277 to 2637 Hz, measured 277 to 2644 Hz.