Podcast player: pause no longer kills the system voice

"It only plays once, and when you pause it you can't unpause."

WebKit leaves speechSynthesis paused after a cancel(), and every later
speak() is queued into that paused state - silently. Fixes:
- resume() whenever the synth is paused, before speaking and after our own
  cancel;
- a ~90ms beat before a speak() that follows a cancel (several engines drop
  it otherwise);
- no cancel when nothing is pending.

The speech engine also no longer rebuilds on rate or voice changes: both are
read live through getters, so a voiceschanged arriving after the first
utterance cannot leave a second engine speaking over the first. A rate or
voice change restarts the current part from the same word instead (the Web
Speech API cannot change either mid-utterance), making the change audible at
once; the natural engine applies speed live and resumes across a voice swap.

The suite's speech stub now models the quirk (cancel -> paused, speak while
paused is dropped and counted) and pins that nothing is ever spoken into a
paused engine. 44 checks. Version 0.7.7, SW v99.
This commit is contained in:
Martin Dvořák 2026-10-16 17:25:12 +00:00
commit 5d82612e2a
6 changed files with 116 additions and 19 deletions

View file

@ -1492,6 +1492,20 @@ Things I chose while building, with the reasoning:
into view as you type. No strings, so no i18n; the additive change sits on top of the merged
scene-heat rework unchanged.
105. **The play button that only worked once.** "It only plays once, and when you pause it you can't
unpause." WebKit's quirk, faithfully: `speechSynthesis.cancel()` leaves the synthesis **paused**,
and every later `speak()` is queued into that paused state — silently. Three fixes in the
system engine: `resume()` whenever the synth is paused before speaking (and after our own
cancel), a ~90 ms beat before a `speak()` that follows a cancel (several engines, Android
Chrome included, drop it otherwise), and no more unnecessary cancels. The engine also stopped
being rebuilt by speed or voice changes — both are read live through getters now, so a
`voiceschanged` that arrives after the first utterance can no longer leave a second engine
speaking over the first; a rate or voice change instead restarts the current part from the
same word (the Web Speech API cannot change either mid-utterance), which is what makes the
change audible at once. The suite's speech stub now *models* the quirk — a cancel pauses it
and a speak while paused is dropped and counted — and pins the fix: nothing may be spoken into
a paused engine (44 checks).
104. **Natural voices — and the crack at the end of every sentence.** Two reports, one root:
the platform's speech engine. "There's a loud cracking sound at the end of every sentence" is
the browser/OS TTS (iOS and several Linux engines pop between their internal sentence chunks,