From c9e3ef765224e97089fc3f43913c23bfe3f64a24 Mon Sep 17 00:00:00 2001 From: David Racine Date: Wed, 16 Sep 2026 20:13:59 -0400 Subject: [PATCH] Interrupt espeak on a new utterance (#7207) The SAPI and AVSpeechSynthesizer backends both drop what they are saying when a new utterance arrives (SPF_PURGEBEFORESPEAK, stopSpeakingAtBoundary). espeak never did, so on Linux every menu item scrolled past queued another utterance with no way to skip ahead. Co-authored-by: Claude Opus 5 --- .../speechsynthesizer/ESpeakSpeechSynthesizer.cpp | 7 ++++++- .../speechsynthesizer/ESpeakSpeechSynthesizer.h | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/soh/soh/Enhancements/speechsynthesizer/ESpeakSpeechSynthesizer.cpp b/soh/soh/Enhancements/speechsynthesizer/ESpeakSpeechSynthesizer.cpp index c5af34eb60..e83cd0339b 100644 --- a/soh/soh/Enhancements/speechsynthesizer/ESpeakSpeechSynthesizer.cpp +++ b/soh/soh/Enhancements/speechsynthesizer/ESpeakSpeechSynthesizer.cpp @@ -13,9 +13,10 @@ bool ESpeakSpeechSynthesizer::DoInit() { this->Initialize = (speak_Initialize)dlsym(espeak, "espeak_Initialize"); this->SetVoiceByProperties = (speak_SetVoiceByProperties)dlsym(espeak, "espeak_SetVoiceByProperties"); this->Synth = (speak_Synth)dlsym(espeak, "espeak_Synth"); + this->Cancel = (speak_Cancel)dlsym(espeak, "espeak_Cancel"); this->Terminate = (speak_Terminate)dlsym(espeak, "espeak_Terminate"); if (this->Initialize == NULL || this->SetVoiceByProperties == NULL || this->Synth == NULL || - this->Terminate == NULL) { + this->Cancel == NULL || this->Terminate == NULL) { SPDLOG_INFO("Failed to load espeak-ng"); dlclose(espeak); return false; @@ -39,6 +40,10 @@ void ESpeakSpeechSynthesizer::Speak(const char* text, const char* language) { if (this->espeak == NULL) { SPDLOG_INFO("Spoken Text ({}): {}", language, text); } else { + // Match the SAPI and AVSpeechSynthesizer backends, which both drop what they are saying when a new + // utterance arrives. Without this, scrolling a menu queues one utterance per item with no way to skip. + this->Cancel(); + if (language != this->mLanguage) { espeak_VOICE voice = { .languages = language }; if (this->SetVoiceByProperties(&voice)) { diff --git a/soh/soh/Enhancements/speechsynthesizer/ESpeakSpeechSynthesizer.h b/soh/soh/Enhancements/speechsynthesizer/ESpeakSpeechSynthesizer.h index 0844b92935..4760cc2436 100644 --- a/soh/soh/Enhancements/speechsynthesizer/ESpeakSpeechSynthesizer.h +++ b/soh/soh/Enhancements/speechsynthesizer/ESpeakSpeechSynthesizer.h @@ -12,6 +12,7 @@ typedef ESPEAK_API espeak_ERROR (*speak_SetVoiceByProperties)(espeak_VOICE* voic typedef ESPEAK_API espeak_ERROR (*speak_Synth)(const void* text, size_t size, unsigned int position, espeak_POSITION_TYPE position_type, unsigned int end_position, unsigned int flags, unsigned int* unique_identifier, void* user_data); +typedef ESPEAK_API espeak_ERROR (*speak_Cancel)(void); } class ESpeakSpeechSynthesizer : public SpeechSynthesizer { @@ -30,5 +31,6 @@ class ESpeakSpeechSynthesizer : public SpeechSynthesizer { speak_Initialize Initialize = NULL; speak_SetVoiceByProperties SetVoiceByProperties = NULL; speak_Synth Synth = NULL; + speak_Cancel Cancel = NULL; speak_Terminate Terminate = NULL; };