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:
parent
8e1cd0855d
commit
5d82612e2a
6 changed files with 116 additions and 19 deletions
|
|
@ -134,6 +134,7 @@ const browser = await launchBrowser();
|
|||
const SPEECH_STUB = `
|
||||
window.__spoken = [];
|
||||
window.__cancelCount = 0;
|
||||
window.__droppedWhilePaused = 0;
|
||||
window.__utterances = [];
|
||||
class FakeUtterance {
|
||||
constructor(text) { this.text = String(text); this.rate = 1; this.lang = "en-US"; this.onstart = null; this.onend = null; this.onerror = null; this.onboundary = null; }
|
||||
|
|
@ -142,15 +143,24 @@ Object.defineProperty(window, "SpeechSynthesisUtterance", { configurable: true,
|
|||
Object.defineProperty(window, "speechSynthesis", {
|
||||
configurable: true,
|
||||
value: {
|
||||
_speaking: false,
|
||||
_paused: false,
|
||||
get speaking() { return this._speaking; },
|
||||
get pending() { return false; },
|
||||
get paused() { return this._paused; },
|
||||
speak(utterance) {
|
||||
/* WebKit's quirk, on purpose: a cancel leaves the synthesis paused and
|
||||
a speak() while paused is dropped on the floor. The app must resume
|
||||
before it speaks again. */
|
||||
if (this._paused) { window.__droppedWhilePaused += 1; return; }
|
||||
window.__spoken.push(utterance.text);
|
||||
window.__utterances.push(utterance);
|
||||
this._speaking = true;
|
||||
queueMicrotask(() => utterance.onstart && utterance.onstart({ name: "start", charIndex: 0 }));
|
||||
},
|
||||
cancel() { window.__cancelCount += 1; },
|
||||
cancel() { window.__cancelCount += 1; this._speaking = false; this._paused = true; },
|
||||
resume() { this._paused = false; },
|
||||
getVoices() { return []; },
|
||||
speaking: false,
|
||||
paused: false,
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
|
@ -222,6 +232,11 @@ try {
|
|||
await page.waitForTimeout(400);
|
||||
const resumed = await page.evaluate(() => window.__spoken.at(-1));
|
||||
check("player: resume starts at the current word", String(resumed).startsWith("clear day."), String(resumed).slice(0, 30));
|
||||
check(
|
||||
"player: nothing was spoken into a paused engine (the WebKit quirk)",
|
||||
(await page.evaluate(() => window.__droppedWhilePaused)) === 0,
|
||||
String(await page.evaluate(() => window.__droppedWhilePaused)),
|
||||
);
|
||||
|
||||
/* 3. chapters, speed, progress save ------------------------------ */
|
||||
await page.getByRole("button", { name: "Why", exact: true }).click();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue