Fix Chrome TTS silence: cancel/speak race condition and user activation#2
Fix Chrome TTS silence: cancel/speak race condition and user activation#2
Conversation
Co-authored-by: punkouter26 <121304072+punkouter26@users.noreply.github.com>
| // Calling speak() synchronously after cancel() causes Chrome to silently | ||
| // drop the new utterance (known Chrome Web Speech API bug). | ||
| window.speechSynthesis.cancel(); | ||
| await new Promise(resolve => setTimeout(resolve, 50)); |
There was a problem hiding this comment.
Potential Race Condition in Speech Cancellation
The use of a fixed 50ms delay after window.speechSynthesis.cancel() (line 139) is intended to avoid a Chrome bug where a new utterance is dropped if spoken immediately after cancellation. However, this approach is not robust, as the actual time required for cancellation may vary depending on browser and system conditions. This could lead to intermittent failures where utterances are still dropped.
Recommended Solution:
Instead of a fixed delay, consider polling window.speechSynthesis.speaking or using the onend event of a dummy utterance to ensure cancellation is fully processed before proceeding:
window.speechSynthesis.cancel();
await new Promise(resolve => {
const check = () => {
if (!window.speechSynthesis.speaking) {
resolve();
} else {
setTimeout(check, 10);
}
};
check();
});This approach waits until speech synthesis is truly idle before starting the new utterance.
| window.speechSynthesis.resume(); | ||
| // Speak a silent dummy utterance at high rate so it ends instantly | ||
| const dummy = new SpeechSynthesisUtterance(SPEECH_ACTIVATION_TEXT); | ||
| dummy.volume = SPEECH_ACTIVATION_VOLUME; | ||
| dummy.rate = SPEECH_ACTIVATION_RATE; | ||
| window.speechSynthesis.speak(dummy); |
There was a problem hiding this comment.
Unnecessary Dummy Utterance in 'prime' Method
The prime method (lines 179-184) always speaks a dummy utterance to activate speech synthesis, regardless of whether speech synthesis is already active. This could result in unnecessary utterances and may interfere with ongoing speech.
Recommended Solution:
Check if speech synthesis is already active before speaking the dummy utterance:
if (!window.speechSynthesis.speaking) {
const dummy = new SpeechSynthesisUtterance(SPEECH_ACTIVATION_TEXT);
dummy.volume = SPEECH_ACTIVATION_VOLUME;
dummy.rate = SPEECH_ACTIVATION_RATE;
window.speechSynthesis.speak(dummy);
}This ensures the dummy utterance is only spoken when needed.
Web Speech API (
speechSynthesis) produced no audio in Chrome while Web Audio API SFX worked fine. Two Chrome-specific issues inspeech-interop.jswere the cause.Root Causes
cancel()was called synchronously beforespeak()in the same frame. Chrome silently drops the new utterance —onendfires immediately with no audio.speak()call from a direct interaction handler before permitting calls from async contexts.Changes
speech-interop.jssetTimeoutbetweencancel()andspeak()to let Chrome's engine process the cancel before accepting the next utteranceSpeechSynthesisUtteranceconstruction and configuration outside the Promise executor (after the delay), keeping everything sequentialactivateSpeechSynthesisOnce()— a self-removingclick/keydowndocument listener that speaks a silent dummy utterance on first user interaction, satisfying Chrome's activation requirement for all subsequent SignalR-driven callsSPEECH_ACTIVATION_TEXT,SPEECH_ACTIVATION_RATE,SPEECH_ACTIVATION_VOLUME) used by both the auto-activation listener and the publicprime()API methodWarning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
v2.jokeapi.dev/usr/share/dotnet/dotnet /usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/PoJoker/PoJoker/tests/Po.Joker.Tests.Integration/bin/Debug/net10.0/Po.Joker.Tests.Integration.runtimeconfig.json --depsfile /home/REDACTED/work/PoJoker/PoJoker/tests/Po.Joker.Tests.Integration/bin/Debug/net10.0/Po.Joker.Tests.Integration.deps.json /home/REDACTED/work/PoJoker/PoJoker/tests/Po.Joker.Tests.Integration/bin/Debug/net10.0/testhost.dll --port 46553 --endpoint 127.0.0.1:046553 --role client --parentprocessid 5112 --telemetryoptedin false(dns block)If you need me to access, download, or install something from one of these locations, you can either:
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.