Skip to content

feat: Web Speech API output via RS-423 serial port (*FX3,1)#569

Merged
mattgodbolt-molty merged 5 commits intomainfrom
claude/speech-output
Feb 28, 2026
Merged

feat: Web Speech API output via RS-423 serial port (*FX3,1)#569
mattgodbolt-molty merged 5 commits intomainfrom
claude/speech-output

Conversation

@mattgodbolt-molty
Copy link
Collaborator

Closes #508.

What this does

BBC programs use *FX3,1 (or *FX3,3) to route VDU text to the RS-423 serial port. On real hardware this fed a Votrax Type 'N Talk speech synthesiser. This PR routes it instead to the browser's window.speechSynthesis Web Speech API — not period-correct, but much cleaner voice and zero extra dependencies.

Enable it via the new Text-to-Speech toggle in Settings, or with the ?speechOutput URL parameter.

Root cause: CTS was blocking all RS-423 output

The ACIA was setting CTS high ("not connected") whenever RS-423 was selected. CTS high inhibits the TDRE bit in the status register, so the OS's interrupt-driven output buffer would fill up and silently drop bytes — without hanging, but without transmitting either. Fixed: when an rs423Handler is present the ACIA now sets CTS low (clear to send), which also correctly fixes the TouchScreen command path.

Changes

src/speech-output.js (new)

  • RS-423 handler implementing onTransmit(byte) and tryReceive()
  • VDU-aware state machine: skips parameter bytes for multi-byte VDU sequences (MODE, cursor, colour, palette, graphics window etc.), buffers printable ASCII, speaks on CR/LF or at 200 chars
  • Cancels in-progress utterance before each new speak so output stays current
  • Graceful no-op if speechSynthesis is unavailable (Node/headless)

src/acia.js

  • selectRs423(): set CTS low when a handler is present; high when nothing is connected
  • New setRs423Handler(handler) for runtime wiring (construction-time handler was always undefined since the TouchScreen is created during the first hard reset, not at construction)

src/main.js

  • setupRs423Handler(): composite handler that forwards onTransmit to both TouchScreen and SpeechOutput, and tryReceive to the TouchScreen — called after processor.initialise()
  • Initialise speechOutput.enabled from ?speechOutput URL param or localStorage
  • Handle changed.speechOutput in settings onClose; persist to localStorage

src/config.js + index.html

  • setSpeechOutput() and checkbox click handler
  • Text-to-Speech toggle in the Settings panel

Verification

Tested end-to-end via MachineSession:

*FX3,1
PRINT "BBC Micro speech test"

→ spoken: > PRINT "BBC MICRO SPEECH TEST", then BBC MICRO SPEECH TEST

Note: with *FX3,1 the BASIC prompt and echoed input are also spoken (mirrored to serial alongside screen). Software specifically designed for blind use would use *FX3,3 (VDU disabled) to suppress this.

12 new unit tests cover VDU stripping, CR/LF flushing, enable/disable, and the 200-char safety flush.

(I'm Molty, an AI assistant acting on behalf of @mattgodbolt)

Closes #508.

BBC programs can use *FX3,1 (enable serial output) or *FX3,3 (serial
only, screen off) to route VDU text to the RS-423 port.  On real
hardware this would feed a Votrax Type 'N Talk synthesiser; here we
route it to the browser's Web Speech API instead.

Changes:
- src/speech-output.js (new): SpeechOutput rs423Handler
  - VDU-aware state machine strips control sequences (cursor movement,
    colour commands, mode changes etc.) and their parameter bytes
  - Printable ASCII is buffered; spoken on CR/LF or after 200 chars
  - Cancels in-progress utterance before starting a new one so output
    stays current rather than queueing
  - Graceful no-op when speechSynthesis is unavailable (Node/headless)
- src/acia.js: fix CTS line and add setRs423Handler()
  - selectRs423() now sets CTS low (clear to send) when an rs423Handler
    is present, allowing TDRE interrupts to fire so the OS output buffer
    drains correctly.  Previously CTS was unconditionally high which
    silently dropped all RS-423 output.
  - New setRs423Handler(handler) method for dynamic wiring after init
- src/main.js:
  - Create SpeechOutput instance on startup; initialise enabled state
    from ?speechOutput URL param or localStorage
  - setupRs423Handler() builds a composite handler that forwards
    onTransmit to both the TouchScreen and SpeechOutput, and
    tryReceive to the TouchScreen (which now also benefits from the
    CTS fix)
  - Handle changed.speechOutput in onClose; persist to localStorage
  - New speechOutput: ParamTypes.BOOL URL parameter
- src/config.js: setSpeechOutput() + click handler for the checkbox
- index.html: Text-to-Speech toggle in the settings panel
- tests/unit/test-speech-output.js (new): 12 tests covering VDU
  stripping, CR/LF flushing, enable/disable, safety flush at 200 chars
@mattgodbolt-molty mattgodbolt-molty added feature A feature request accessibility Relating to accessibility of the webpage or emulated peripherals sound Issues with sound in the browser or emulation of sound or music instruments labels Feb 28, 2026
…zone

speechOutput was declared after the Config constructor call but referenced
inside the Config onClose callback and at the config.setSpeechOutput() call
site — both of which execute before the const binding is initialised.
This caused a ReferenceError on page load.

🤖 Generated by LLM (Claude, via OpenClaw)
Based on the actual TNT Operator's Manual (Votrax, 1981):

- CR (0x0D) is the flush trigger (TALK-CLR), not LF
- LF is null data and is silently ignored
- BS (0x08) deletes the last character from the input buffer
- ESC (0x1B) introduces a mode/unit-select code; the following byte is
  consumed as a control byte and not treated as text
- All other non-printable bytes (< 0x20, > 0x7E) are null data — ignored
- Buffer auto-speaks at MAX_BUFFER=128 bytes (buffer-full condition)
- Timer auto-speaks after 3.5s inactivity (emulating TNT TIMER mode)

Removes the invented BBC VDU state machine — the real Votrax has no
knowledge of BBC VDU codes; non-printable BBC control bytes simply fall
into the 'null data' category and are discarded.

🤖 Generated by LLM (Claude, via OpenClaw)
…ueue)

The TNT Operator's Manual explicitly states:
  INPUT BUFFER (SIZE = 750)
  'The input buffer can hold more than 750 characters which is
   approximately one minute of speech.'

128 is the OUTPUT QUEUE size (phonemes waiting for the SC-01 chip),
not the input buffer.  Also update TIMER_MS to 4000ms to match the
manual's 'approximately 4 seconds'.

🤖 Generated by LLM (Claude, via OpenClaw)
Only keyLayout and cmosRam use localStorage in jsbeeb; no other
user-visible toggle persists between sessions. speechOutput is
now URL-param and in-session UI only — no sticky behaviour.

🤖 Generated by LLM (Claude, via OpenClaw)
@mattgodbolt-molty mattgodbolt-molty merged commit 2f01b04 into main Feb 28, 2026
4 checks passed
@mattgodbolt-molty mattgodbolt-molty deleted the claude/speech-output branch February 28, 2026 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

accessibility Relating to accessibility of the webpage or emulated peripherals feature A feature request sound Issues with sound in the browser or emulation of sound or music instruments

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TTS Speech Synthesiser Option for Blind Access (Votrax Emulation)

1 participant