--- title: "ZTalker 1.2 Text-to-Speech Software" id: 50572 type: "computer_media" slug: "ztalker" url: "http://localhost/computer_media/ztalker/" markdown_url: "http://localhost/computer_media/ztalker.md" published_at: "2023-06-18T17:35:33+00:00" modified_at: "2026-04-01T10:41:15+00:00" author: "David Anderson" featured_image: url: "http://localhost/wp-content/uploads/2019/02/20230809-041332.jpg" excerpt: "A full phoneme-sequence editor drives dedicated speech synthesizer hardware, letting you compose, navigate, edit, and speak aloud any sequence of up to 100 phoneme codes." category: - name: "Archived Media" slug: "archived-media" taxonomy: "category" url: "http://localhost/category/archived-media/" post_tag: - name: "Downloadable" slug: "downloadable" taxonomy: "post_tag" url: "http://localhost/tag/downloadable/" - name: "TS 2068" slug: "ts2068" taxonomy: "post_tag" url: "http://localhost/tag/ts2068/" - name: "Zebra Systems" slug: "zebra-systems" taxonomy: "post_tag" url: "http://localhost/tag/zebra-systems/" model: - name: "Timex/Sinclair 2068" slug: "ts-2068" taxonomy: "model" url: "http://localhost/model/ts-2068/" genre: - name: "Speech Synthesis" slug: "speech-synthesis" taxonomy: "genre" url: "http://localhost/type/speech-synthesis/" media_type: "Program" download_url: "https://archive.org/download/timex-sinclair-software-archive/ZTalker%20%28198x%29%28Zebra%20Systems%29%28TS2068%29%28US%29%28Program%29.zip" mediadate: "198x" producer_company: - id: 11469 title: "Zebra Systems, Inc." type: "company" url: "http://localhost/company/zebra-systems/" images: - url: "http://localhost/wp-content/uploads/2023/06/SCR-20260401-gltd.png" related_products: - id: 14694 title: "ZTalker 1.2 Text-to-Speech Software" type: "product" url: "http://localhost/product/ztalker-1-2/" - id: 14679 title: "Zebra-Talker 2068" type: "product" url: "http://localhost/product/zebra-talker-voice-synthesizer-for-ts2068/" media_type_tags: "Speech Synthesis" --- ZTalker 1.2 is a phoneme-based text-to-speech editor designed to drive the Zebra-Talker 2068 speech synthesizer hardware via I/O port 191. The program maintains a buffer of up to 100 phoneme codes starting at memory address 60016, storing each as a single byte index into a 68-entry phoneme table encoded in the string P$, where each entry occupies exactly three characters (e.g., “EH3”, “AH2”, “SH “, “NG “). An operator navigates the phoneme sequence using a cursor-based interface controlled by single-key commands (5/6/7/8 for directional movement, A/D/I/K/R/X for edit operations), with the display arranged in rows of eight phonemes each. The SPEAK routine at line 3400 sends phonemes to the hardware using OUT 191 combined with an INF (inflection) value derived from phonemes with codes above 63, followed by a terminating OUT 191,3. The program supports printing the phoneme list to both screen (LIST) and printer (LPRINT/COPY), and can save itself to tape with auto-run via SAVE “ZPE” LINE 0. *** ## Program Analysis ### Program Structure The program is organized into clearly delineated subroutines and named sections, entered from a main loop at line 2000. Execution flows as follows: 1. Lines 10–50: REM headers, uppercase lock (`POKE 23658,8`), display setup, and `CLEAR 59999` to protect the phoneme buffer above that address. 2. Line 110: `GO TO 2000` — skips all subroutines to reach the main routine. 3. Lines 1230–1255: Display buffer contents on screen. 4. Lines 1400–1460: Cursor erase and redraw logic. 5. Lines 1500–1530: Key dispatch table (single-letter commands). 6. Lines 1550–1720: Cursor movement handlers (up/down/left/right, beginning/end). 7. Lines 1800–1830: On-screen help/legend display. 8. Lines 2000–2060: Main initialization and event loop. 9. Lines 2110–2200: Append (ADD) phoneme operation. 10. Lines 2300–2325: Single-cell display update subroutine. 11. Lines 2500–2540: Delete last phoneme. 12. Lines 2600–2670: Kill (clear all phonemes). 13. Lines 2700–2760: Replace phoneme at cursor. 14. Lines 2800–2865: Excise (delete at cursor, shift remaining left). 15. Lines 2900–2998: Insert phoneme at cursor (shift right). 16. Lines 3005–3025, 3050–3060: Phoneme lookup and invalid-input handler. 17. Lines 3100–3110: Print (screen copy via `COPY`). 18. Lines 3200–3265: LIST — paginated on-screen phoneme listing. 19. Lines 3300–3325: LLIST — printer output of phoneme listing. 20. Lines 3400–3440: SPEAK — send phonemes to hardware via `OUT 191`. 21. Lines 9900–9920: Tape SAVE with `LINE 0`. ### Phoneme Table Encoding The entire phoneme dictionary is packed into a single string `P$` at line 2008, initialized to a 204-character literal (68 phonemes × 3 characters each). Examples of entries include `EH3`, `PA0`, `DT`, `AH2`, `NG`, `OO1`, `TH`, `VTH`, `ER`, `PA1`, `ST`, `PIN`, `IN0`–`IN3`. Lookup is performed by iterating `I` from 0 to 67 and comparing `T$` to `P$((I*3)+1 TO (I*3)+3)` (lines 3005–3025). Display uses the same arithmetic: `P$(W*3-2 TO W*3)`, where `W` is 1-based. Note that there is a minor index inconsistency between the lookup (1-based slice starting at `I*3+1`) and the display (slice `W*3-2` where `W=I+1`); both are equivalent and correct. ### Phoneme Buffer and Memory Layout The phoneme buffer begins at address `BUF = 60016` (set at line 2003), safely above the `CLEAR 59999` boundary. Each phoneme is stored as a single byte: its 0-based index into `P$`. Up to `MAXL = 100` phonemes can be held. The buffer is accessed directly with `PEEK` and `POKE` throughout. ### Cursor and Display Model The display arranges phonemes in rows of 8, with each cell occupying 4 character columns. Screen position is computed as `AT INT(I/8)+1, 4*(I - 8*INT(I/8))`. The cursor is represented by a `">"` character printed in the cell to the left of the phoneme (the cell column is offset by one space before the 3-character phoneme code). The cursor is 1-based and initialized to `-8` before the first render, which combined with `NCURSOR=CURSOR` at line 2015 means the initial cursor draw call at 2021 references position `-8` — a potential off-screen print, though it is harmless in practice since the `AT` calculation produces a large negative column that wraps or is ignored. ### Hardware Speech Output The SPEAK routine (lines 3400–3440) iterates over the buffer and sends bytes to the Zebra-Talker 2068 via `OUT 191, value`. Bytes with value ≥ 64 are treated as inflection prefix codes: `INF = (X-64)*64`. Bytes below 64 are phoneme codes, sent as `OUT 191, INF+X`, followed immediately by `IN 191` to handshake with the hardware. The sequence is terminated by `OUT 191, 3` plus a final `IN 191`. The variable `p` (lowercase) holds the `IN` result and is otherwise unused — it serves purely as a hardware synchronization read. ### Edit Operations | Key | Operation | Entry Point | | --- | --- | --- | | A | Add (append) phoneme | 2100 | | D | Delete last phoneme | 2500 | | I | Insert at cursor (shift right) | 2900 | | K | Kill all phonemes | 2600 | | R | Replace phoneme at cursor | 2700 | | X | Excise phoneme at cursor (shift left) | 2800 | | 5/6/7/8 | Move cursor left/down/up/right | 1610/1570/1550/1590 | | B | Move to beginning | 1630 | | E | Move to end | 1645 | | L | List phonemes (screen, paginated) | 3200 | | P | Print (COPY to printer) | 3100 | | S | Speak via hardware | 3400 | | W | Save to tape | 9900 | | Q | Quit (STOP) | — | ### Key BASIC Idioms - The main event loop (lines 2030–2060) polls `INKEY$` in a tight loop, jumping back if the result is empty — a standard keypress-wait pattern. - The key dispatch at lines 1505–1521 is a chain of `IF D$="x" THEN GO TO` statements rather than a computed jump, which is typical for Sinclair BASIC where no `ON ... GO TO` with string keys is available. - `DIM P$(68*3)` at line 2007 pre-allocates the phoneme table with a computed size, keeping the code self-documenting. - The LIST routine uses a line counter `J` to pause every 18 lines, a standard pagination technique. ### Bugs and Anomalies - **Line 2300–2325 subroutine structure:** Lines 2300–2325 contain both a `FOR` loop and a `RETURN` statement. The `RETURN` at line 2325 is inside the `FOR` loop body; it exits the subroutine on the first iteration. The loop itself is never completed as a loop — this is effectively dead code for iterations beyond 1. The actual single-cell update use (from line 2195 via `GO SUB 2315`) jumps directly into line 2315, bypassing the `FOR` at 2300, so that path works correctly. However, line 2310 references an undefined variable `E`, meaning any call that enters at line 2300 would error. - **Initial cursor value of -8:**`CURSOR` is set to `-8` at line 2001, and the cursor draw at line 2021 uses this value, producing `AT INT(-8/8)+1 = AT 0,...` which prints in row 0 — likely harmless but unintentional. - **Append loop re-entry:** The ADD routine (line 2110) uses `GO TO 2120` at line 2200 to loop for additional phonemes, but the entry point from the key dispatch is `GO TO 2100` (line 1512), which falls through to line 2115 — these are consistent. - **Line 3265 GO TO 2990:** After the LIST routine finishes, it jumps to line 2990 (`CLS` / redraw), effectively re-entering the tail of the INSERT routine’s cleanup. This is a deliberate reuse of the redraw sequence rather than a bug. ## Source Code ``` 10 REM \:: COPYRIGHT (C) 1984 \:: 20 REM \:: ZEBRA SYSTEMS, INC.\:: 30 REM \:: ALL RIGHTS RESERVED\:: 40 REM \:: ZPE VER. 1.2 FEB.84\:: 45 POKE 23658,8: REM UPPERCASE 47 INK 7: PAPER 1: BORDER 1: CLS 50 CLEAR 59999 110 GO TO 2000 1230 IF LENGTH=0 THEN RETURN 1231 FOR I=1 TO LENGTH 1234 LET W=1+(PEEK (I+BUF-1)) 1235 IF W<0 THEN RETURN 1236 LET D$=P$(W*3-2 TO W*3) 1240 PRINT AT INT (I/8)+1,4*(I-8*INT (I/8));" ";D$; 1250 NEXT I 1255 RETURN 1400 REM ERASE OLD CURSOR 1405 LET D$=" " 1410 LET C=CURSOR 1415 GO SUB 1450 1430 REM SHOW NEW CURSOR 1435 LET D$=">" 1440 LET C=NCURSOR 1445 GO SUB 1450 1447 RETURN 1450 REM CURSOR 1455 PRINT AT INT (C/8)+1,4*(C-8*INT (C/8));D$; 1460 RETURN 1500 REM KEY SCAN 1505 IF D$="5" THEN GO TO 1610 1506 IF D$="6" THEN GO TO 1570 1507 IF D$="7" THEN GO TO 1550 1508 IF D$="8" THEN GO TO 1590 1509 IF D$="B" THEN GO TO 1630 1510 IF D$="E" THEN GO TO 1645 1511 IF D$="Q" THEN STOP 1512 IF D$="A" THEN GO TO 2100 1513 IF D$="D" THEN GO TO 2500 1514 IF D$="K" THEN GO TO 2600 1515 IF D$="R" THEN GO TO 2700 1516 IF D$="X" THEN GO TO 2800 1517 IF D$="I" THEN GO TO 2900 1518 IF D$="P" THEN GO TO 3100 1519 IF D$="S" THEN GO TO 3400 1520 IF D$="L" THEN GO TO 3200 1521 IF D$="W" THEN GO TO 9900 1530 RETURN 1550 REM UP 1555 IF CURSOR<9 THEN GO TO 1700 1560 LET NCURSOR=CURSOR-8 1565 GO TO 1720 1570 REM DOWN 1575 IF CURSOR>=LENGTH-8 THEN GO TO 1700 1580 LET NCURSOR=CURSOR+8 1585 GO TO 1720 1590 REM RIGHT 1595 IF CURSOR=LENGTH THEN GO TO 1700 1600 LET NCURSOR=CURSOR+1 1605 GO TO 1720 1610 REM LEFT 1615 IF CURSOR=1 THEN GO TO 1720 1620 LET NCURSOR=CURSOR-1 1625 GO TO 1720 1630 REM BEGINNING 1635 LET NCURSOR=1 1640 GO TO 1720 1645 REM END 1650 LET NCURSOR=LENGTH 1655 GO TO 1720 1700 REM INVALID REQUEST 1710 LET NCURSOR=CURSOR 1720 RETURN 1800 REM SCREEN HELP 1810 PRINT AT 15,0;"(5)LEFT (6)DOWN (7)UP (8)RIGHT" 1815 PRINT AT 16,0;"(B)EGINNING (E)ND (P)RINT"; 1816 PRINT AT 17,0;"(A)DD (D)ELETE (I)NSERT (K)ILL"; 1817 PRINT AT 18,0;"(R)EPLACE E(X)CISE (Q)UIT"; 1818 PRINT AT 19,0;"(L)IST (S)PEAK "; 1830 RETURN 1900 STOP 2000 REM MAIN 2001 LET CURSOR=-8 2002 LET LENGTH=0 2003 LET BUF=60016 2005 DIM T$(3) 2007 DIM P$(68*3) 2008 LET P$="EH3EH2EH1PA0DT A2 A1 ZH AH2I3 I2 I1 M N B V CH SH Z AW1NG AH1OO1OO L K J H G F D S A AY Y1 UH3AH P O I U Y T R E W AE AE1AW2UH2UH1UH O2 O1 IU U1 THVTH ER EH E1 AW PA1STPIN0IN1IN2IN3" 2010 REM SOFTSTART 2011 LET MAXL=100 2012 GO SUB 1230 2014 GO SUB 1800 2015 LET NCURSOR=CURSOR 2021 GO SUB 1430 2030 LET D$=INKEY$ 2035 IF D$="" THEN GO TO 2030 2040 GO SUB 1500 2043 GO SUB 1400 2050 LET CURSOR=NCURSOR 2060 GO TO 2030 2110 REM APPEND 2115 IF LENGTH=MAXL THEN GO TO 2180 2120 PRINT AT 20,0;"ENTER PHONEME CODE " 2125 INPUT T$ 2126 IF T$=" " THEN GO TO 2185 2130 GO SUB 3000 2150 IF I<68 THEN GO TO 2190 2155 GO SUB 3050 2160 GO TO 2120 2180 PRINT AT 20,0;"BUFFER FULL. HIT ENTER "; 2182 INPUT T$ 2183 RETURN 2185 PRINT AT 20,0;" "; 2186 RETURN 2190 POKE BUF+CURSOR,I 2191 LET LENGTH=LENGTH+1 2192 LET NCURSOR=LENGTH 2193 LET W=I+1 2194 LET I=NCURSOR 2195 GO SUB 2315 2196 GO SUB 1400 2197 LET CURSOR=NCURSOR 2200 GO TO 2120 2300 FOR I=1 TO LENGTH 2310 LET W=PEEK (E+I) 2315 LET D$=P$(W*3-2 TO W*3) 2320 PRINT AT INT (I/8)+1,4*(I-8*INT (I/8));" ";D$; 2325 RETURN 2500 REM DELETE 2505 IF LENGTH=0 THEN RETURN 2510 LET D$=" " 2515 LET I=LENGTH 2520 GO SUB 2320 2525 LET LENGTH=LENGTH-1 2530 LET NCURSOR=LENGTH 2535 GO SUB 1400 2540 RETURN 2600 REM KILL 2605 PRINT AT 20,0;"DELETE ALL PHONEMES (Y/N)" 2610 INPUT D$ 2615 IF D$="Y" THEN GO TO 2650 2620 GO SUB 2185 2625 RETURN 2650 LET LENGTH=0 2655 LET NCURSOR=LENGTH 2660 CLS 2665 GO SUB 1800 2670 RETURN 2700 REM REPLACE 2705 IF LENGTH=0 THEN RETURN 2710 LET W=1+PEEK (BUF-1+CURSOR) 2712 LET D$=P$(W*3-2 TO W*3) 2715 PRINT AT 20,0;"REPLACE > ";D$;" < WITH? "; 2720 INPUT T$ 2722 GO SUB 3000 2735 IF I=68 THEN GO TO 2715 2737 POKE BUF+CURSOR-1,I 2740 LET D$=T$ 2745 GO SUB 2185 2747 LET I=CURSOR 2750 GO SUB 2320 2755 GO SUB 1400 2760 RETURN 2800 REM EXCISE 2805 IF LENGTH=0 THEN RETURN 2810 LET BUF=60016 2815 FOR I=BUF+CURSOR-1 TO BUF+LENGTH-1 2820 POKE I,PEEK (I+1) 2825 NEXT I 2830 LET LENGTH=LENGTH-1 2835 CLS 2840 GO SUB 1800 2845 GO SUB 1230 2850 IF CURSOR>LENGTH THEN LET CURSOR=LENGTH 2855 LET NCURSOR=CURSOR 2860 GO SUB 1400 2865 RETURN 2900 REM INSERT 2910 IF LENGTH63 THEN LET INF=(W-65)*64 3225 LET D$=P$(W*3+1 TO W*3+3) 3227 LET W1=W: IF W<32 THEN LET W1=63 3230 PRINT I;" ";D$;" ";CHR$ W1;" ";W;" ";W+INF 3235 LET J=J+1 3240 IF J<>18 THEN GO TO 3260 3243 LET J=0 3245 PRINT "HIT ENTER TO CONTINUE."; 3250 INPUT T$ 3255 CLS 3260 NEXT I 3262 PRINT "HIT ENTER TO CONTINUE."; 3263 INPUT T$ 3265 GO TO 2990 3300 REM LLIST 3302 LPRINT "REF-SYM-CHR-DEC-+INF" 3303 LET INF=0 3305 FOR I=1 TO LENGTH 3310 LET W=PEEK (BUF+I-1) 3311 IF W>63 THEN LET INF=(W-65)*64 3312 LET D$=P$(W*3+1 TO W*3+3) 3313 LET W1=W: IF W<32 THEN LET W1=63 3315 LPRINT I;" ";D$;" ";CHR$ W1;" ";W;" ";W+INF 3320 NEXT I 3325 RETURN 3400 REM SPEAK 3402 LET INF=0 3405 FOR I=(BUF) TO (BUF+LENGTH) 3410 LET X=PEEK (I) 3415 IF X<64 THEN GO TO 3425 3420 LET INF=(X-64)*64 3422 GO TO 3430 3425 OUT 191,INF+X 3426 LET p=IN 191 3430 NEXT I 3433 OUT 191,3 3434 LET p=IN 191 3440 RETURN 9900 REM TAPE SAVE 9910 SAVE "ZPE" LINE 0 9920 RETURN ```