From 5d82612e2a87021aba00e081ade1eabc7f038a3c Mon Sep 17 00:00:00 2001 From: Martin Dvorak Date: Fri, 16 Oct 2026 17:25:12 +0000 Subject: [PATCH] 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. --- README.md | 14 ++++++++ components/podcast/PodcastPlayer.tsx | 43 ++++++++++++++++++++-- lib/podcast-speech.ts | 53 ++++++++++++++++++++++------ package.json | 2 +- public/sw.js | 2 +- tests/podcast-test.mjs | 21 +++++++++-- 6 files changed, 116 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 78decb6..533c87a 100644 --- a/README.md +++ b/README.md @@ -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, diff --git a/components/podcast/PodcastPlayer.tsx b/components/podcast/PodcastPlayer.tsx index 1f92777..f165b71 100644 --- a/components/podcast/PodcastPlayer.tsx +++ b/components/podcast/PodcastPlayer.tsx @@ -112,6 +112,7 @@ export function PodcastPlayer({ episode }: { episode: Podcast }) { natural?.setRate(rate); }, [natural, rate]); + /* The engine owns an