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 |
- Ticks *FRAMES, because this ROM owns IM1
- Ticks the two keyboard-state timer pairs
- Expands the BASIC string s$ if it exists
- Speaks a new key from *LAST_K if keys is 01
- Sends the next queued allophone if the SP0256 is idle
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.
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
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
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.
- Control keys: EDIT, ENTER, cursor, DELETE
- Colour tokens, graphics and symbols, plus LET and NEW
- Digits 30-39
- "#", "$" and "%"
- Letters A-Z
- BASIC tokens from RND through COPY
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)") |
| 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) |