diff --git a/.github/workflows/cmake-build.yml b/.github/workflows/cmake-build.yml index 1e1f3aa..2e247c1 100644 --- a/.github/workflows/cmake-build.yml +++ b/.github/workflows/cmake-build.yml @@ -31,7 +31,7 @@ jobs: - name: Configure CMake - run: cmake . -B build -DCMAKE_OSX_ARCHITECTURES=x86_64 -DBUILD_SHARED_LIBS=ON + run: cmake . -B build -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" -DBUILD_SHARED_LIBS=ON - name: Build with CMake run: cmake --build build --config Release -j 16 @@ -43,5 +43,3 @@ jobs: with: name: ${{ matrix.os }}-build path: build - - diff --git a/Bindings/Python/sral.py b/Bindings/Python/sral.py index 73d6732..dd6000c 100644 --- a/Bindings/Python/sral.py +++ b/Bindings/Python/sral.py @@ -31,6 +31,8 @@ class SRALEngine(IntEnum): VOICE_OVER = 1 << 8 NS_SPEECH = 1 << 9 AV_SPEECH = 1 << 10 + ANDROID_ACCESSIBILITY_MANAGER = 1 << 11 + ANDROID_TEXT_TO_SPEECH = 1 << 12 class SRALFeature(IntEnum): """ @@ -46,6 +48,18 @@ class SRALFeature(IntEnum): SPEAK_TO_MEMORY = 1 << 8 SPELLING = 1 << 9 +class SRALEngineCategory(IntEnum): + """ + Broad categories an engine can belong to. + + Unlike SRALEngine, these values are not bit flags; an engine has exactly + one category. + """ + UNKNOWN = 0 + SCREEN_READER = 1 + TEXT_TO_SPEECH_ENGINE = 2 + ACCESSIBILITY_PROVIDER = 3 + class SRALParam(IntEnum): """ Enumeration of engine parameters. @@ -217,6 +231,15 @@ def ptr(self): _sral_lib.SRAL_GetActiveEngines.argtypes = [] _sral_lib.SRAL_GetActiveEngines.restype = ctypes.c_int + _sral_lib.SRAL_GetTTSEngines.argtypes = [] + _sral_lib.SRAL_GetTTSEngines.restype = ctypes.c_int + + _sral_lib.SRAL_GetAssistiveTechEngines.argtypes = [] + _sral_lib.SRAL_GetAssistiveTechEngines.restype = ctypes.c_int + + _sral_lib.SRAL_GetEngineCategory.argtypes = [ctypes.c_int] + _sral_lib.SRAL_GetEngineCategory.restype = ctypes.c_int + _sral_lib.SRAL_GetEngineName.argtypes = [ctypes.c_int] _sral_lib.SRAL_GetEngineName.restype = ctypes.c_char_p @@ -772,6 +795,59 @@ def get_active_engines(self) -> int: if not _sral_lib: return 0 return _sral_lib.SRAL_GetActiveEngines() + def get_tts_engines(self) -> int: + """ + Get the bitmask of engines that are pure text-to-speech synthesizers. + + The mask is derived at runtime from each available engine's category, + so it reflects the engines available on the current platform and + requires the library to be initialized. + + Intended use: pass to set_engines_exclude when the application wants + to opt out of TTS output (e.g., only speak through assistive tech + unless the user has enabled an in-app TTS option). + + Returns: + A bitmask of SRALEngine enums representing TTS engines. + """ + self._check_initialized() + if not _sral_lib: return 0 + return _sral_lib.SRAL_GetTTSEngines() + + def get_assistive_tech_engines(self) -> int: + """ + Get the bitmask of engines that represent assistive technology + (screen readers and the accessibility providers that drive them). + + The mask is derived at runtime from each available engine's category, + so it reflects the engines available on the current platform and + requires the library to be initialized. + + Returns: + A bitmask of SRALEngine enums representing assistive-tech engines. + """ + self._check_initialized() + if not _sral_lib: return 0 + return _sral_lib.SRAL_GetAssistiveTechEngines() + + def get_engine_category(self, engine: SRALEngine) -> SRALEngineCategory: + """ + Get the category of the specified engine. + + The category is reported by the engine itself. The engine is resolved + against the engines available on the current platform, so an engine + that is not available here returns SRALEngineCategory.UNKNOWN. + + Args: + engine: The identifier of the engine to query. + + Returns: + The engine's SRALEngineCategory. + """ + self._check_initialized() + if not _sral_lib: return SRALEngineCategory.UNKNOWN + return SRALEngineCategory(_sral_lib.SRAL_GetEngineCategory(engine.value)) + def set_engines_exclude(self, engines_exclude: int) -> bool: """ Exclude certain engines from auto-update. diff --git a/Bindings/go/SRAL/sral.go b/Bindings/go/SRAL/sral.go index 6e07e3e..aac24f0 100644 --- a/Bindings/go/SRAL/sral.go +++ b/Bindings/go/SRAL/sral.go @@ -202,6 +202,39 @@ func GetActiveEngines() Engine { return Engine(engines) } +// GetTTSEngines returns a bitmask of engines that are pure text-to-speech +// synthesizers (e.g., SAPI, Speech Dispatcher, NSSpeech, AVSpeech, Android TTS). +// +// The mask is derived at runtime from each available engine's category, so it +// reflects the engines available on the current platform and requires the +// library to be initialized. +// +// Pass this to SetEnginesExclude when the application wants to opt out of TTS +// output (for instance, only speaking through a screen reader unless the user +// has enabled an in-app TTS option). +func GetTTSEngines() Engine { + return Engine(C.SRAL_GetTTSEngines()) +} + +// GetAssistiveTechEngines returns a bitmask of engines that represent assistive +// technology — screen readers and the accessibility providers that drive them +// (e.g., NVDA, JAWS, ZDSR, UIA, VoiceOver, Android AccessibilityManager). +// +// The mask is derived at runtime from each available engine's category, so it +// reflects the engines available on the current platform and requires the +// library to be initialized. +func GetAssistiveTechEngines() Engine { + return Engine(C.SRAL_GetAssistiveTechEngines()) +} + +// GetEngineCategory returns the category of the given engine, as reported by +// the engine itself. The engine is resolved against the engines available on +// the current platform, so an engine that is not available here returns +// UnknownCategory. +func GetEngineCategory(engine Engine) EngineCategory { + return EngineCategory(C.SRAL_GetEngineCategory(C.int(engine))) +} + // GetEngineName returns the name of the specified engine. func GetEngineName(engine Engine) string { cName := C.SRAL_GetEngineName(C.int(engine)) diff --git a/Bindings/go/SRAL/types.go b/Bindings/go/SRAL/types.go index 69a953b..08354eb 100644 --- a/Bindings/go/SRAL/types.go +++ b/Bindings/go/SRAL/types.go @@ -27,8 +27,12 @@ const ( NSSpeechEngine // AVSpeechEngine — AVFoundation Speech Synthesizer (AVSpeechSynthesizer) for Apple platforms. AVSpeechEngine + // AndroidAccessibilityManagerEngine — Android AccessibilityManager, drives the active screen reader (typically TalkBack). + AndroidAccessibilityManagerEngine + // AndroidTextToSpeechEngine — Android TextToSpeech synthesizer. + AndroidTextToSpeechEngine // AllEngines is a bitmask of all supported engines. - AllEngines Engine = NVDAEngine | JAWSEngine | ZDSREngine | NarratorEngine | UIAEngine | SAPIEngine | SpeechDispatcherEngine | NSSpeechEngine | VoiceOverEngine | AVSpeechEngine + AllEngines Engine = NVDAEngine | JAWSEngine | ZDSREngine | NarratorEngine | UIAEngine | SAPIEngine | SpeechDispatcherEngine | NSSpeechEngine | VoiceOverEngine | AVSpeechEngine | AndroidAccessibilityManagerEngine | AndroidTextToSpeechEngine // InvalidEngine represents an error or uninitialized engine state. InvalidEngine Engine = -1 // NoSpecifiedEngine is used for auto-selection of the engine. @@ -68,6 +72,24 @@ func (f Feature) IsSupported(other Feature) bool { return (f & other) != 0 } +// EngineCategory is the broad category an engine belongs to. Unlike Engine, +// these values are not bit flags; an engine has exactly one category. +type EngineCategory int + +const ( + // UnknownCategory indicates the category is unknown, or the engine is not + // available/initialized. + UnknownCategory EngineCategory = iota + // ScreenReaderCategory — a screen reader (e.g. NVDA, JAWS, ZDSR, VoiceOver). + ScreenReaderCategory + // TextToSpeechCategory — a pure text-to-speech synthesizer (e.g. SAPI, + // Speech Dispatcher, NSSpeech, AVSpeech, Android TextToSpeech). + TextToSpeechCategory + // AccessibilityProviderCategory — an accessibility provider that drives + // whatever assistive tech is consuming it (e.g. UIA, Android AccessibilityManager). + AccessibilityProviderCategory +) + // EngineParam defines parameters that can be set or retrieved from a speech engine. type EngineParam int diff --git a/Examples/C/SRALExample.c b/Examples/C/SRALExample.c index 0a4d537..15d7398 100644 --- a/Examples/C/SRALExample.c +++ b/Examples/C/SRALExample.c @@ -40,7 +40,7 @@ void PrintEngineNames(int engineBitmask, const char* title) { return; } bool found = false; - for (int engine_val = SRAL_ENGINE_NVDA; engine_val <= SRAL_ENGINE_AV_SPEECH; engine_val <<= 1) { + for (int engine_val = SRAL_ENGINE_NVDA; engine_val <= SRAL_ENGINE_ANDROID_TEXT_TO_SPEECH; engine_val <<= 1) { if (engineBitmask & engine_val) { const char* name = SRAL_GetEngineName(engine_val); printf(" - %s (0x%X)\n", name ? name : "Unknown Engine", engine_val); @@ -53,6 +53,15 @@ void PrintEngineNames(int engineBitmask, const char* title) { printf("\n"); } +const char* CategoryName(int category) { + switch (category) { + case SRAL_ENGINE_CATEGORY_SCREEN_READER: return "Screen Reader"; + case SRAL_ENGINE_CATEGORY_TEXT_TO_SPEECH_ENGINE: return "Text-To-Speech Engine"; + case SRAL_ENGINE_CATEGORY_ACCESSIBILITY_PROVIDER: return "Accessibility Provider"; + default: return "Unknown"; + } +} + void print_supported_features(int features) { printf("Supported Features (0x%X):\n", features); if (features == 0) { @@ -122,11 +131,32 @@ int main(void) { int active_engines = SRAL_GetActiveEngines(); PrintEngineNames(active_engines, "Currently Active/Usable Engines"); + int tts_engines = SRAL_GetTTSEngines(); + PrintEngineNames(tts_engines, "TTS Engines (category)"); + + int at_engines = SRAL_GetAssistiveTechEngines(); + PrintEngineNames(at_engines, "Assistive-Tech Engines (category)"); + + bool at_active = (active_engines & at_engines) != 0; + printf("Assistive tech currently active: %s\n\n", at_active ? "yes" : "no"); + + CHECK((tts_engines & at_engines) == 0, + "TTS and assistive-tech masks are disjoint.", + "TTS and assistive-tech masks overlap!"); + + printf("\nCategory of each available engine (SRAL_GetEngineCategory):\n"); + for (int e_val = SRAL_ENGINE_NVDA; e_val <= SRAL_ENGINE_ANDROID_TEXT_TO_SPEECH; e_val <<= 1) { + if (available_engines & e_val) { + printf(" - %s: %s\n", SRAL_GetEngineName(e_val), CategoryName(SRAL_GetEngineCategory(e_val))); + } + } + printf("\n"); + int current_engine_id = SRAL_GetCurrentEngine(); printf("Current Default Engine: %s (0x%X)\n", SRAL_GetEngineName(current_engine_id) ? SRAL_GetEngineName(current_engine_id) : "None/Unknown", current_engine_id); printf("\nNames of all SRAL_Engines enum members (as per SRAL_GetEngineName):\n"); - for (int e_val = SRAL_ENGINE_NVDA; e_val <= SRAL_ENGINE_AV_SPEECH; e_val <<= 1) { + for (int e_val = SRAL_ENGINE_NVDA; e_val <= SRAL_ENGINE_ANDROID_TEXT_TO_SPEECH; e_val <<= 1) { const char* name = SRAL_GetEngineName(e_val); printf(" Engine ID 0x%X: %s\n", e_val, name ? name : "(Name not defined or not a single engine ID)"); } @@ -134,14 +164,14 @@ int main(void) { int specific_engine_for_ex_tests = SRAL_ENGINE_NONE; if (active_engines != SRAL_ENGINE_NONE) { - for (int e_val = SRAL_ENGINE_NVDA; e_val <= SRAL_ENGINE_AV_SPEECH; e_val <<= 1) { + for (int e_val = SRAL_ENGINE_NVDA; e_val <= SRAL_ENGINE_ANDROID_TEXT_TO_SPEECH; e_val <<= 1) { if ((active_engines & e_val) && e_val != current_engine_id) { specific_engine_for_ex_tests = e_val; break; } } if (specific_engine_for_ex_tests == SRAL_ENGINE_NONE) { - for (int e_val = SRAL_ENGINE_NVDA; e_val <= SRAL_ENGINE_AV_SPEECH; e_val <<= 1) { + for (int e_val = SRAL_ENGINE_NVDA; e_val <= SRAL_ENGINE_ANDROID_TEXT_TO_SPEECH; e_val <<= 1) { if (active_engines & e_val) { specific_engine_for_ex_tests = e_val; break; @@ -616,6 +646,19 @@ int main(void) { CHECK(engines_to_exclude == new_engines_to_exclude, "Engines exclude set/get matches", "Engines exclude set/get mismatch"); + // Regression check: excluding the whole TTS category must never leave a TTS + // engine selected as current. Previously a sticky fallback engine (e.g. NS + // Speech) could keep speaking through TTS after it had been excluded. + CHECK_SRAL(SRAL_SetEnginesExclude(SRAL_GetTTSEngines()), "Excluded the TTS engine category."); + int current_with_tts_excluded = SRAL_GetCurrentEngine(); + printf(" Current engine with TTS excluded: %s (0x%X)\n", + SRAL_GetEngineName(current_with_tts_excluded) ? SRAL_GetEngineName(current_with_tts_excluded) : "None", + current_with_tts_excluded); + CHECK((current_with_tts_excluded & SRAL_GetTTSEngines()) == 0, + "No TTS engine is current while the TTS category is excluded.", + "A TTS engine is still current despite the TTS category being excluded!"); + SRAL_SetEnginesExclude(engines_to_exclude); // Restore the prior exclude state. + TEST_SECTION("Unregister Keyboard Hooks"); SRAL_UnregisterKeyboardHooks(); diff --git a/Include/SRAL.h b/Include/SRAL.h index 3cbafb3..045291d 100644 --- a/Include/SRAL.h +++ b/Include/SRAL.h @@ -95,6 +95,26 @@ SRAL_ENGINE_ANDROID_ACCESSIBILITY_MANAGER = 1 << 11, SRAL_ENGINE_ANDROID_TEXT_TO_SPEECH = 1 << 12 }; + /** + * @enum SRAL_EngineCategory + * @brief Broad categories an engine can belong to. + * + * Each engine reports its own category (see SRAL_GetEngineCategory). + * + * Unlike SRAL_Engines, these values are NOT bit flags; an engine has exactly + * one category. + */ + typedef enum SRAL_EngineCategory { + /** @brief Category unknown, or the engine is not available/initialized. */ + SRAL_ENGINE_CATEGORY_UNKNOWN = 0, + /** @brief A screen reader (e.g. NVDA, JAWS, ZDSR, VoiceOver). */ + SRAL_ENGINE_CATEGORY_SCREEN_READER, + /** @brief A pure text-to-speech synthesizer (e.g. SAPI, Speech Dispatcher, NSSpeech, AVSpeech, Android TextToSpeech). */ + SRAL_ENGINE_CATEGORY_TEXT_TO_SPEECH_ENGINE, + /** @brief An accessibility provider that drives whatever assistive tech is consuming it (e.g. UIA, Android AccessibilityManager). */ + SRAL_ENGINE_CATEGORY_ACCESSIBILITY_PROVIDER + } SRAL_EngineCategory; + /** * @enum SRAL_SupportedFeatures * @brief Enumeration of supported features in the engines. @@ -520,6 +540,66 @@ SRAL_ENGINE_ANDROID_TEXT_TO_SPEECH = 1 << 12 + /** +* @brief Get the category of a specific engine. +* +* The category is reported by the engine itself (see SRAL_EngineCategory). The +* library must be initialized; the engine is resolved against the engines +* instantiated on the current platform, so an engine that is not available here +* (or an unknown identifier) returns SRAL_ENGINE_CATEGORY_UNKNOWN. +* +* @param engine An SRAL_Engines identifier. +* @return The engine's SRAL_EngineCategory. +*/ + + + SRAL_API SRAL_EngineCategory SRAL_GetEngineCategory(int engine); + + + + /** +* @brief Get the bitmask of engines that are pure text-to-speech synthesizers +* (e.g., SAPI, Speech Dispatcher, NSSpeech, AVSpeech, Android TextToSpeech). +* +* The mask is derived at runtime from each available engine's category +* (SRAL_ENGINE_CATEGORY_TEXT_TO_SPEECH_ENGINE), so it reflects the engines +* instantiated on the current platform and requires the library to be +* initialized (returns 0 otherwise). +* +* Intended use: pass to SRAL_SetEnginesExclude when the application wants to +* opt out of TTS output (for instance, only speaking through a screen reader +* unless the user has enabled an in-app TTS option). +* +* @return Bitmask of TTS engines defined by the SRAL_Engines enumeration. +*/ + + + SRAL_API int SRAL_GetTTSEngines(void); + + + + /** +* @brief Get the bitmask of engines that represent assistive technology +* (screen readers and the accessibility providers that drive them, e.g., +* NVDA, JAWS, ZDSR, UIA, VoiceOver, Android AccessibilityManager). +* +* The mask is derived at runtime from each available engine's category +* (SRAL_ENGINE_CATEGORY_SCREEN_READER or SRAL_ENGINE_CATEGORY_ACCESSIBILITY_PROVIDER), +* so it reflects the engines instantiated on the current platform and requires +* the library to be initialized (returns 0 otherwise). +* +* When any of these engines is active, output is routed to the user's +* configured assistive tech (which itself handles speech and braille +* per the user's preferences). +* +* @return Bitmask of assistive-tech engines defined by the SRAL_Engines enumeration. +*/ + + + SRAL_API int SRAL_GetAssistiveTechEngines(void); + + + /** * @brief Get name of the specified engine. * @param engine The identifier of the engine to query. diff --git a/README.md b/README.md index 206ab7c..de864ce 100644 --- a/README.md +++ b/README.md @@ -122,4 +122,19 @@ bool SRAL_Braille(const char* text); // Check if an engine is currently speaking bool SRAL_IsSpeaking(void); +// Categorized engine bitmasks (use with SRAL_SetEnginesExclude) +int SRAL_GetTTSEngines(void); +int SRAL_GetAssistiveTechEngines(void); +``` + +### Routing only through assistive tech (with optional in-app TTS) + +If your application should always speak through the user's assistive +technology, but only fall back to platform TTS when the user has +explicitly enabled it, exclude the TTS engines by default: + +```c +bool tts_option_enabled = /* your app's setting */; +SRAL_SetEnginesExclude(tts_option_enabled ? 0 : SRAL_GetTTSEngines()); +SRAL_Speak("hello", true); // routes to AT always; to TTS only if opted in ``` diff --git a/SRC/AVSpeech.h b/SRC/AVSpeech.h index f93993d..821c200 100644 --- a/SRC/AVSpeech.h +++ b/SRC/AVSpeech.h @@ -22,6 +22,9 @@ namespace Sral { int GetNumber()override { return SRAL_ENGINE_AV_SPEECH; } + int GetCategory() override { + return SRAL_ENGINE_CATEGORY_TEXT_TO_SPEECH_ENGINE; + } bool GetActive()override; bool Initialize()override; bool Uninitialize()override; diff --git a/SRC/AndroidAccessibilityManager.h b/SRC/AndroidAccessibilityManager.h index 3507494..b9ce9e2 100644 --- a/SRC/AndroidAccessibilityManager.h +++ b/SRC/AndroidAccessibilityManager.h @@ -22,6 +22,9 @@ namespace Sral { int GetNumber() override { return SRAL_ENGINE_ANDROID_ACCESSIBILITY_MANAGER; } + int GetCategory() override { + return SRAL_ENGINE_CATEGORY_ACCESSIBILITY_PROVIDER; + } bool GetActive() override; bool Initialize() override; bool Uninitialize() override; diff --git a/SRC/AndroidTextToSpeech.h b/SRC/AndroidTextToSpeech.h index 5813d1b..9f88864 100644 --- a/SRC/AndroidTextToSpeech.h +++ b/SRC/AndroidTextToSpeech.h @@ -26,6 +26,9 @@ namespace Sral { int GetNumber()override { return SRAL_ENGINE_ANDROID_TEXT_TO_SPEECH; } + int GetCategory() override { + return SRAL_ENGINE_CATEGORY_TEXT_TO_SPEECH_ENGINE; + } bool GetActive()override; bool Initialize()override; bool Uninitialize()override; @@ -43,4 +46,4 @@ namespace Sral { jobject speechObj; }; } -#endif \ No newline at end of file +#endif diff --git a/SRC/Engine.cpp b/SRC/Engine.cpp index 78ddac1..a563fb6 100644 --- a/SRC/Engine.cpp +++ b/SRC/Engine.cpp @@ -32,6 +32,10 @@ namespace Sral { return SRAL_ENGINE_NONE; } + int Engine::GetCategory() { + return SRAL_ENGINE_CATEGORY_UNKNOWN; + } + bool Engine::GetParameter(int parameter, void* value) { (void)parameter; (void)value; diff --git a/SRC/Engine.h b/SRC/Engine.h index 7c3012e..61d17bc 100644 --- a/SRC/Engine.h +++ b/SRC/Engine.h @@ -26,6 +26,7 @@ namespace Sral { virtual bool ResumeSpeech(); virtual bool IsSpeaking(); virtual int GetNumber(); + virtual int GetCategory(); virtual bool GetActive(); virtual int GetFeatures(); virtual bool Initialize(); diff --git a/SRC/Jaws.h b/SRC/Jaws.h index db15cc8..ed2b6d5 100644 --- a/SRC/Jaws.h +++ b/SRC/Jaws.h @@ -16,6 +16,9 @@ namespace Sral { int GetNumber()override { return SRAL_ENGINE_JAWS; } + int GetCategory() override { + return SRAL_ENGINE_CATEGORY_SCREEN_READER; + } bool GetActive()override; bool Initialize()override; bool Uninitialize()override; @@ -28,4 +31,4 @@ namespace Sral { IJawsApi* pJawsApi = nullptr; }; } -#endif \ No newline at end of file +#endif diff --git a/SRC/NSSpeech.h b/SRC/NSSpeech.h index e489005..9fb87e8 100644 --- a/SRC/NSSpeech.h +++ b/SRC/NSSpeech.h @@ -17,6 +17,7 @@ namespace Sral { bool GetParameter(int param, void* value) override; int GetNumber() override { return SRAL_ENGINE_NS_SPEECH; } + int GetCategory() override { return SRAL_ENGINE_CATEGORY_TEXT_TO_SPEECH_ENGINE; } int GetFeatures() override { return SRAL_SUPPORTS_SPEECH | SRAL_SUPPORTS_SPEECH_RATE | SRAL_SUPPORTS_SPEECH_VOLUME; } diff --git a/SRC/NVDA.h b/SRC/NVDA.h index c19c69e..72af123 100644 --- a/SRC/NVDA.h +++ b/SRC/NVDA.h @@ -22,6 +22,9 @@ namespace Sral { int GetNumber()override { return SRAL_ENGINE_NVDA; } + int GetCategory() override { + return SRAL_ENGINE_CATEGORY_SCREEN_READER; + } bool GetActive()override; bool Initialize()override; bool Uninitialize()override; @@ -50,4 +53,4 @@ namespace Sral { }; } #endif -#endif \ No newline at end of file +#endif diff --git a/SRC/SAPI.h b/SRC/SAPI.h index 86a157f..f27dcf0 100644 --- a/SRC/SAPI.h +++ b/SRC/SAPI.h @@ -24,6 +24,9 @@ namespace Sral { int GetNumber()override { return SRAL_ENGINE_SAPI; } + int GetCategory() override { + return SRAL_ENGINE_CATEGORY_TEXT_TO_SPEECH_ENGINE; + } bool GetActive()override; bool Initialize()override; bool Uninitialize()override; @@ -43,4 +46,4 @@ namespace Sral { std::thread speechThread; }; } -#endif \ No newline at end of file +#endif diff --git a/SRC/SRAL.cpp b/SRC/SRAL.cpp index 1b6cf0f..b9d34a2 100644 --- a/SRC/SRAL.cpp +++ b/SRC/SRAL.cpp @@ -336,7 +336,17 @@ static BOOL FindProcess(const wchar_t* name) { #endif static void speech_engine_update() { - if (!g_currentEngine || !g_currentEngine->GetActive() || g_currentEngine->GetNumber() == SRAL_ENGINE_SAPI || g_currentEngine->GetNumber() == SRAL_ENGINE_UIA || g_currentEngine->GetNumber() == SRAL_ENGINE_AV_SPEECH || g_currentEngine->GetNumber() == SRAL_ENGINE_ANDROID_TEXT_TO_SPEECH) { + // Re-evaluate the current engine whenever it is unset, no longer active, or + // belongs to a lower-priority category (a text-to-speech engine or an + // accessibility provider such as UIA). Screen readers are sticky once + // chosen; the lower-priority engines must keep yielding to a screen reader + // that appears, and must also react to changes in g_excludes. Asking the + // engine its own category keeps this in sync as new engines are added. + int category = g_currentEngine ? g_currentEngine->GetCategory() : SRAL_ENGINE_CATEGORY_UNKNOWN; + if (!g_currentEngine + || !g_currentEngine->GetActive() + || category == SRAL_ENGINE_CATEGORY_TEXT_TO_SPEECH_ENGINE + || category == SRAL_ENGINE_CATEGORY_ACCESSIBILITY_PROVIDER) { #if defined(_WIN32) && !defined(SRAL_NO_UIA) if (FindProcess(L"narrator.exe") == TRUE) { g_currentEngine = get_engine(SRAL_ENGINE_UIA); @@ -344,6 +354,10 @@ static void speech_engine_update() { } else { #endif + // Clear first: if no active, non-excluded engine qualifies there is + // genuinely nothing to speak through, and g_currentEngine must not + // keep pointing at a stale (e.g. excluded) engine. + g_currentEngine = nullptr; for (const auto& [value, ptr] : g_engines) { if (ptr->GetActive() && !(g_excludes & value)) { g_currentEngine = ptr.get(); @@ -622,6 +636,35 @@ extern "C" SRAL_API int SRAL_GetActiveEngines(void) { return mask; } +extern "C" SRAL_API SRAL_EngineCategory SRAL_GetEngineCategory(int engine) { + if (!SRAL_IsInitialized()) return SRAL_ENGINE_CATEGORY_UNKNOWN; + Sral::Engine* e = get_engine(engine); + return e ? static_cast(e->GetCategory()) : SRAL_ENGINE_CATEGORY_UNKNOWN; +} + +extern "C" SRAL_API int SRAL_GetTTSEngines(void) { + if (g_engines.empty())return 0; + int mask = 0; + for (const auto& [value, ptr] : g_engines) { + if (ptr && ptr->GetCategory() == SRAL_ENGINE_CATEGORY_TEXT_TO_SPEECH_ENGINE) + mask |= value; + } + return mask; +} + +extern "C" SRAL_API int SRAL_GetAssistiveTechEngines(void) { + if (g_engines.empty())return 0; + int mask = 0; + for (const auto& [value, ptr] : g_engines) { + if (!ptr) continue; + int category = ptr->GetCategory(); + if (category == SRAL_ENGINE_CATEGORY_SCREEN_READER + || category == SRAL_ENGINE_CATEGORY_ACCESSIBILITY_PROVIDER) + mask |= value; + } + return mask; +} + extern "C" SRAL_API const char* SRAL_GetEngineName(int engine) { switch (static_cast(engine)) { diff --git a/SRC/SpeechDispatcher.h b/SRC/SpeechDispatcher.h index 2fac34e..9f18aff 100644 --- a/SRC/SpeechDispatcher.h +++ b/SRC/SpeechDispatcher.h @@ -24,6 +24,9 @@ namespace Sral { int GetNumber()override { return SRAL_ENGINE_SPEECH_DISPATCHER; } + int GetCategory() override { + return SRAL_ENGINE_CATEGORY_TEXT_TO_SPEECH_ENGINE; + } bool GetActive()override; bool Initialize()override; bool Uninitialize()override; diff --git a/SRC/UIA.h b/SRC/UIA.h index 1558d9e..b21b75f 100644 --- a/SRC/UIA.h +++ b/SRC/UIA.h @@ -14,6 +14,9 @@ namespace Sral { int GetNumber()override { return SRAL_ENGINE_UIA; } + int GetCategory() override { + return SRAL_ENGINE_CATEGORY_ACCESSIBILITY_PROVIDER; + } bool GetActive()override; bool Initialize()override; bool Uninitialize()override; @@ -29,4 +32,4 @@ namespace Sral { IUIAutomationElement* pElement = nullptr; }; } -#endif \ No newline at end of file +#endif diff --git a/SRC/VoiceOver.h b/SRC/VoiceOver.h index 3a6e4ee..2f26c9c 100644 --- a/SRC/VoiceOver.h +++ b/SRC/VoiceOver.h @@ -13,6 +13,9 @@ namespace Sral { int GetNumber()override { return SRAL_ENGINE_VOICE_OVER; } + int GetCategory() override { + return SRAL_ENGINE_CATEGORY_SCREEN_READER; + } bool GetActive()override; bool Initialize()override; bool Uninitialize()override; @@ -22,4 +25,4 @@ namespace Sral { }; } -#endif \ No newline at end of file +#endif diff --git a/SRC/ZDSR.h b/SRC/ZDSR.h index 8d35cd4..431e855 100644 --- a/SRC/ZDSR.h +++ b/SRC/ZDSR.h @@ -14,6 +14,9 @@ namespace Sral { int GetNumber()override { return SRAL_ENGINE_ZDSR; } + int GetCategory() override { + return SRAL_ENGINE_CATEGORY_SCREEN_READER; + } bool GetActive()override; bool Initialize()override; bool Uninitialize()override; @@ -35,4 +38,4 @@ namespace Sral { StopSpeak_t fStopSpeak = nullptr; }; } -#endif \ No newline at end of file +#endif