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 0800 to 0FFF. Speech itself is produced by the SP0256 chip, fed one allophone at a time from a short workspace just below *RAMTOP.
Range Contents
00-37 Banner bitmaps
38-03AE Interrupt and helpers
03AF-075A Keys, suffixes and dictionary
075B-0792 Banner address list
0793-0797 "Anson"
0798-07FF More banner bitmaps
0800-0FFF Mirror of this ROM
1000 SP0256 allophone / status
3000 / 3001 Low / high intonation
On the first few frames InstallWorkspace lowers *RAMTOP by one page and plants the workspace (low byte 57). 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 00 as the byte at low address 57 index 01 as 56 and so on. A 48K machine uses FF57; a 16K machine uses 7F57. InstallWorkspace lowers *RAMTOP by one page so BASIC stays out of this block.
Index Address Purpose
00 57 Flags
01 56 Cleared on install
03 54 Live buffer pointer (low, then high)
06 51 Saved queue start / first allophone
The live pointer is a word, so its high byte sits at 55 (install first writes the workspace page there). The allophone buffer begins at the address of index 06 and grows down towards the new *RAMTOP. CheckMemory sets carry when the pointer has dropped into that reserved page.
Flags at index 00:
Bit Meaning
1 Key speech (set when keys is 01)
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.
1000 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 00 silences the chip.
Intonation is selected first by writing the same ID to 3000 (low) or 3001 (high). Bit 6 of the queued byte chooses the port; FeedAllophone_Write then writes the ID to 1000.
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 00DD looks that name up with FindVariable (name code 53) 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 (01) 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 80-A4 (block graphics) all use key 80. Everything else is offered to PunctuationKeys first.
ServiceSpeech_Punctuation searches the 0D 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 (08)
Cursor down Cursor (08)
Cursor up Cursor (08)
SGN SIN
AND "&"
All brackets therefore share the "(" entry (spoken as a bracket). The extra cursor keys share the spoken "cursor" for 08. 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 (80) so the scanner's "end of the previous list" step lands on the first real key, and it ends on a key of 00.
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 21) and must appear in the first 18 entries of CharacterKeys. Those values are summed; the sum is the lookup key.
Most sums are searched for in the 36 bytes from CharacterKeys, and the matching allophone is 35 bytes later. Five sums are handled separately and go through the suffix tables instead:
Sum Letter used
64 Last letter (e.g. "(RR)")
68 Last letter (e.g. "(OY)")
5C Last letter (e.g. "(NN)")
66 Last letter (e.g. "(OW)")
78 Letter before last (e.g. "(EAR)")
ParseAllophoneGroup_Suffix takes that letter, normalises it, and searches SuffixKeys. The parallel byte 08 later in SuffixValues is the allophone. A letter that is not in SuffixKeys makes the group invalid.
Suffix letter Allophone
"R" 0E (RR1)
"U" 16 (UW1)
"T" 0D (TT2)
"Y" 05 (OY)
"H" 1D (TH)
"N" 38 (NN2)
"A" 3C (YR)
"E" 2F (XR)
"W" 20 (AW)