Introduction
Whenever there was speech in a Spectrum game, it was a very special thing. It didn’t ever really happen! I can only imagine it must have cost too much, both in real-money terms, and also in data! And for it to work on a 48K ZX Spectrum with its “beeper” is simply an absolute miracle!
180 says “ONE HUNDRED AND EIGHTY!” when you score 180 - and yeah sure, it’s not the cleanest sample in the world - but they stored it in 2048 bytes. Consider quite how crazy that is for a second!!! It’s a 1-bit PCM sample in 2KB from 1986, this was an incredible achievement!
It’s not just any old digitised speech either, it has character too! You can clearly hear that it’s in the style of a darts commentator!
Routine: “180” Speech
Remarkably the “player” isn’t especially complex. It’s 2048 bytes, and a 1-bit sample - so each bit represents whether the speaker is ON ($01) or OFF ($00).
In order to achieve this, each byte of the data is rotated in-place through the carry flag, then the A register is set to either $10 or $00 (speaker ON or OFF) jumping based on the carry flag. The A register is then sent to the speaker to play the sample- and there’s a small delay loop. This is repeated eight times, for each of the 8 bits in a byte.
Once this is done, the routine moves onto the next speech data byte/ checks to see if we’re done. Simple!
; Sound: "180"
;
; Plays 1-bit PCM speech for the "ONE HUNDRED AND EIGHTY!" announcement.
;
@label=Sound_180Speech
c$CA7C LD HL,$D500 ; Set a pointer to the speech data in HL ($D500).
$CA7F LD DE,$0800 ; Set the data length counter in DE to $0800 bytes.
@label=SpeechProcessByte
*$CA82 LD B,$08 ; Set a counter in the B register for $08 bits per byte.
@label=SpeechProcessBit
*$CA84 RLC (HL) ; Rotate *HL left one position to set the next bit to the carry
; flag.
; Set the speaker bit ON or OFF based on if the currently processed bit
; is set or not.
$CA86 LD A,%00010000 ; Set the speaker bit to ON.
$CA88 JR C,$CA8B ; Jump to SpeechOutputSample if the carry bit is set.
$CA8A XOR A ; Set the speaker bit to OFF.
; Output the sample.
@label=SpeechOutputSample
*$CA8B OUT ($FE),A ; Send the A register to the speaker.
; Introduce a tiny delay.
$CA8D LD C,$12 ; Set a delay counter in the C register.
@label=SpeechDelay_Loop
*$CA8F DEC C ; Decrease the delay counter by one.
$CA90 JR NZ,$CA8F ; Jump back to SpeechDelay_Loop until the delay counter is zero.
$CA92 DJNZ $CA9C ; Decrease the bit counter by one and jump to SpeechWasteCycles until all
; bits in the current byte have been processed.
; Finished with this byte, move onto the next.
$CA94 INC HL ; Move to the next byte of speech data.
$CA95 DEC DE ; Decrease the data length counter by one.
$CA96 LD A,D ; {Jump back to SpeechProcessByte until all the bytes have been
$CA97 OR E ; processed.
$CA98 JR NZ,$CA82 ; }
; All done, re-enable the interrupts and return.
$CA9A EI ; Enable interrupts.
$CA9B RET ; Return.
; Minor timing tweaks (probably).
@label=SpeechWasteCycles
*$CA9C NOP ; {No operation.
$CA9D NOP ;
$CA9E NOP ;
$CA9F NOP ;
$CAA0 NOP ; }
$CAA1 JR $CA84 ; Jump to SpeechProcessBit.