Skip to content

Fix a speech crash on a missing utterance id, and the braille keyboard characters echo - #104

Open
thebetterfarhan wants to merge 2 commits into
trypsynth:masterfrom
thebetterfarhan:pr/speech-crash-and-braille-echo
Open

thebetterfarhan wants to merge 2 commits into
trypsynth:masterfrom
thebetterfarhan:pr/speech-crash-and-braille-echo

Conversation

@thebetterfarhan

Copy link
Copy Markdown
Contributor

Two fixes from a log review of today's debug build.

Fix a crash when speech has no utterance id
FailoverTextToSpeech.speak() adds the utterance id to recentUtteranceIds, a ConcurrentLinkedDeque that rejects nulls, and the id came from a shared speech-parameters map that processNextFragmentInternal clears and refills while speech also runs on the low-latency audio callback thread. A fragment spoken during a rebuild got a null id and threw NullPointerException on the speech thread. Now each fragment builds its own map, and addRecentUtteranceId ignores a null id (as allowDeviceSleep and the failure path in speak() already do).

Stop the braille keyboard speaking whole words with the characters echo
In contracted braille the keyboard holds the whole word and commits it in one text event, so with the "characters" echo Backtalk echoed the finished word as added text too, making "characters" sound the same as "characters and words". The keyboard already announces each character itself whenever the echo includes characters, so with "characters" and contracted mode on it now reports "none" to Backtalk and leaves those announcements as the echo. Uncontracted typing commits a character at a time and still uses Backtalk's echo, so it is unchanged.

FailoverTextToSpeech.speak() reads the utterance id from the shared
speech-parameters map and adds it to recentUtteranceIds, a
ConcurrentLinkedDeque that rejects nulls. That map is a single field that
processNextFragmentInternal clears and refills, and speech runs from both
the handler thread and the low-latency audio callback pool, so a fragment
could be spoken while the map was being rebuilt; the id then came back
null and the deque threw NullPointerException on the speech thread
(fyi.quin.backtalk, thread "LowLatencyAudio callbacks").

Build one parameters map per fragment, and guard addRecentUtteranceId
against a null id, matching allowDeviceSleep and the failure path in
speak(), which already tolerate a missing id.
In contracted braille the keyboard holds the whole word and commits it in
one text event, so with the "characters" echo Backtalk echoed the committed
word as added text as well, making "characters" sound the same as
"characters and words". The keyboard already announces each character
itself whenever the echo includes characters.

Report "none" to Backtalk for the on-screen braille keyboard when the echo
is "characters" and contracted mode is on, leaving the keyboard's own
character announcements as the echo. Uncontracted typing commits a
character at a time and relies on Backtalk's echo, so it is unchanged.
@aaron-gh

aaron-gh commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

The two changes are unrelated to each other and should be separate PRs, so each can be reviewed and merged on its own.

Braille keyboard "characters" echo

The fix is correct. In contracted braille the keyboard commits the whole word in one go and already announces each character itself whenever the echo includes characters, so Backtalk's echo of the committed text made "characters" sound like "characters and words". Reporting "none" to Backtalk in that one case fixes it without touching uncontracted typing or the other echo settings.

Side effect: while the braille keyboard reports "none", Backtalk speaks no text added to the field at all, not just the committed word. This includes, for example, an app filling in a suggestion while the braille keyboard is up. Also check on a device that committing a word with space still gives feedback in contracted mode.

Crash on a missing utterance id

The cause is right, but the race is wider than the params map. The whole speech queue was used from more than one thread: UtteranceProgressCallback.onDone, onStop and onError call handleUtteranceCompleted directly, so processNextFragmentInternal() runs on the engine's callback thread or the "LowLatencyAudio callbacks" thread. Screen off also clears the queues and speaks from a background thread. #107 moves both to the main thread, which fixes the cause, so neither change here is needed. A per-fragment map would only remove one symptom, and the null guard would hide the next one.

@thebetterfarhan

Copy link
Copy Markdown
Contributor Author

sorry about that. I thought maybe bundling them together would save time. I will try and recreate these in seperated pr's tomorrow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants