Theory Of Operation
The Currah μSpeech is a 2K ROM that occupies the same addresses as the Spectrum ROM. Any access to HandleInterrupt (including the opcode fetch that lands there) pages one or the other in. The Spectrum IM1 handler always jumps to HandleInterrupt, so this ROM runs first on every interrupt; every exit jumps back to HandleInterrupt to page the Spectrum ROM in and let IM1 finish.
The ROM is mirrored from 2048 to 4095. Speech itself is produced by the SP0256 chip, fed one allophone at a time from a short workspace just below *RAMTOP.
Range Contents
0-55 Banner bitmaps
56-942 Interrupt and helpers
943-1882 Keys, suffixes and dictionary
1883-1938 Banner address list
1939-1943 "Anson"
1944-2047 More banner bitmaps
2048-4095 Mirror of this ROM
4096 SP0256 allophone / status
12288 / 12289 Low / high intonation
On the first few frames InstallWorkspace lowers *RAMTOP by one page and plants the workspace (low byte 87). After that, each interrupt at ServiceSpeech:
Letters, tokens and symbols become allophone IDs through AllophoneDictionary. A parenthesised name such as "(SH)" picks an allophone directly (see ParseAllophoneGroup). The queued IDs sit in the workspace buffer and grow downwards; when the SP0256 is ready, FeedAllophone writes one ID and shuffles the rest down.
Speech Workspace
The workspace is a few bytes in the page of *P_RAMT. ReadWorkspace reads index 0 as the byte at low address 87 index 1 as 86 and so on. A 48K machine uses 65367; a 16K machine uses 32599. InstallWorkspace lowers *RAMTOP by one page so BASIC stays out of this block.
Index Address Purpose
0 87 Flags
1 86 Cleared on install
3 84 Live buffer pointer (low, then high)
6 81 Saved queue start / first allophone
The live pointer is a word, so its high byte sits at 85 (install first writes the workspace page there). The allophone buffer begins at the address of index 6 and grows down towards the new *RAMTOP. CheckMemory sets carry when the pointer has dropped into that reserved page.
Flags at index 0:
Bit Meaning
1 Key speech (set when keys is 1)
2 Workspace live (set while installing, reset when ready)
4 A key is already being spoken
6 High intonation (set for uppercase, reset for lowercase)
7 Banner not yet drawn
SP0256 Speech Synthesiser
The μSpeech speaks through a General Instrument SP0256-AL2 chip. FeedAllophone feeds it one allophone at a time from the workspace queue.
4096 is the allophone port. Reading it returns status; bit 0 set means the chip is still speaking. Writing an allophone ID starts that sound; writing 0 silences the chip.
Intonation is selected first by writing the same ID to 12288 (low) or 12289 (high). Bit 6 of the queued byte chooses the port; FeedAllophone_Write then writes the ID to 4096.
The IDs themselves are the AL2 allophone set stored in AllophoneDictionary. Hear every ID on the Allophones page.
BASIC Speech String
Programs speak by assigning the BASIC string variable s$. On each interrupt 221 looks that name up with FindVariable (name code 83) and, if it exists, calls ParseSpeechString.
ParseSpeechString ignores an empty string, and also ignores a string whose first character is "*" or "?". Those two markers mean the text has already been spoken, or could not be parsed.
Anything else is expanded into the allophone buffer. A full stop queues a short pause. A "(" starts a parenthesised allophone name (see ParseAllophoneGroup). Other characters are mapped through CharacterKeys.
After a successful pass the first character of s$ is overwritten with "*". If a character cannot be mapped, it is overwritten with "?" instead. Either way the next interrupt will skip the string until a program assigns s$ again.
The separate numeric variable keys (1) enables speaking each key press from *LAST_K; that path does not use s$.
The Try s$ page runs this parser in the browser and plays the resulting SP0256 allophones.
Key Speech Remap
When a new key arrives in *LAST_K, ServiceSpeech_Key turns it into a dictionary key and looks it up in AllophoneDictionary. Letters are uppercased, and codes 128-164 (block graphics) all use key 128. Everything else is offered to PunctuationKeys first.
ServiceSpeech_Punctuation searches the 13 keys at PunctuationKeys. A miss keeps the original code. A hit takes the byte the same distance into PunctuationValues and uses that as the dictionary key instead.
The table covers symbols and tokens that either share a spoken word with another key, or have no entry of their own in AllophoneDictionary (POINT, CLS, SGN and AND).
LAST_K Dictionary key
")" "("
"[" "("
"]" "("
"{" "("
"}" "("
POINT "."
CLS CLEAR
"@" AT
Cursor right Cursor (8)
Cursor down Cursor (8)
Cursor up Cursor (8)
SGN SIN
AND "&"
All brackets therefore share the "(" entry (spoken as a bracket). The extra cursor keys share the spoken "cursor" for 8. POINT is spoken as a full stop, CLS as CLEAR, "@" as AT, SGN as SIN, and AND as "&".
Allophone Dictionary
AllophoneDictionary is the character-to-allophone table used by ServiceSpeech_Lookup. Each entry is a key byte followed by one or more SP0256-AL2 allophone IDs. Bit 6 of an ID selects high intonation; bit 7 marks the last ID of that entry.
The table starts with a dummy terminator (128) so the scanner's "end of the previous list" step lands on the first real key, and it ends on a key of 0.
Each comment line on AllophoneDictionary has an audio player for that spoken list.
A key that is not in the table is skipped; the interrupt then continues to FeedAllophone. That is why POINT, CLS, SGN and AND are rewritten by PunctuationKeys before the lookup.
Allophone Name Suffixes
A parenthesised name in s$ such as "(AA)" or "(SH)" is read by ParseAllophoneGroup. Each letter is normalised by NormaliseCharacter (so "A" and "a" both become 33) and must appear in the first 24 entries of CharacterKeys. Those values are summed; the sum is the lookup key.
Most sums are searched for in the 54 bytes from CharacterKeys, and the matching allophone is 53 bytes later. Five sums are handled separately and go through the suffix tables instead:
Sum Letter used
100 Last letter (e.g. "(RR)")
104 Last letter (e.g. "(OY)")
92 Last letter (e.g. "(NN)")
102 Last letter (e.g. "(OW)")
120 Letter before last (e.g. "(EAR)")
ParseAllophoneGroup_Suffix takes that letter, normalises it, and searches SuffixKeys. The parallel byte 8 later in SuffixValues is the allophone. A letter that is not in SuffixKeys makes the group invalid.
Suffix letter Allophone
"R" 14 (RR1)
"U" 22 (UW1)
"T" 13 (TT2)
"Y" 5 (OY)
"H" 29 (TH)
"N" 56 (NN2)
"A" 60 (YR)
"E" 47 (XR)
"W" 32 (AW)