Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/cmake-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noticed that we were missing the Apple Silicone build targets, so including that here as well


- name: Build with CMake
run: cmake --build build --config Release -j 16
Expand All @@ -43,5 +43,3 @@ jobs:
with:
name: ${{ matrix.os }}-build
path: build


33 changes: 33 additions & 0 deletions Bindings/Python/sral.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +34 to +35
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, missing engines from the Android PRs


class SRALFeature(IntEnum):
"""
Expand Down Expand Up @@ -217,6 +219,12 @@ 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_GetEngineName.argtypes = [ctypes.c_int]
_sral_lib.SRAL_GetEngineName.restype = ctypes.c_char_p

Expand Down Expand Up @@ -772,6 +780,31 @@ 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.

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.
"""
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 frameworks that drive them).

Returns:
A bitmask of SRALEngine enums representing assistive-tech engines.
"""
if not _sral_lib: return 0
return _sral_lib.SRAL_GetAssistiveTechEngines()

def set_engines_exclude(self, engines_exclude: int) -> bool:
"""
Exclude certain engines from auto-update.
Expand Down
17 changes: 17 additions & 0 deletions Bindings/go/SRAL/sral.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,23 @@ 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).
//
// 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 frameworks that drive them
// (e.g., NVDA, JAWS, ZDSR, Narrator, UIA, VoiceOver, Android AccessibilityManager).
func GetAssistiveTechEngines() Engine {
return Engine(C.SRAL_GetAssistiveTechEngines())
}

// GetEngineName returns the name of the specified engine.
func GetEngineName(engine Engine) string {
cName := C.SRAL_GetEngineName(C.int(engine))
Expand Down
6 changes: 5 additions & 1 deletion Bindings/go/SRAL/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing engines from the Android PRs 😅

// InvalidEngine represents an error or uninitialized engine state.
InvalidEngine Engine = -1
// NoSpecifiedEngine is used for auto-selection of the engine.
Expand Down
34 changes: 30 additions & 4 deletions Examples/C/SRALExample.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throughout this file we reference SRAL_ENGINE_AV_SPEECH as the last engine, so we change it to now be SRAL_ENGINE_ANDROID_TEXT_TO_SPEECH (although in retrospect, we probably should have a dedicated variable that semantically references this as last_engine_value, or something like that)

if (engineBitmask & engine_val) {
const char* name = SRAL_GetEngineName(engine_val);
printf(" - %s (0x%X)\n", name ? name : "Unknown Engine", engine_val);
Expand Down Expand Up @@ -122,26 +122,39 @@ 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!");

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)");
}


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;
Expand Down Expand Up @@ -616,6 +629,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();
Expand Down
33 changes: 33 additions & 0 deletions Include/SRAL.h
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,39 @@ SRAL_ENGINE_ANDROID_TEXT_TO_SPEECH = 1 << 12



/**
* @brief Get the bitmask of engines that are pure text-to-speech synthesizers
* (e.g., SAPI, Speech Dispatcher, NSSpeech, AVSpeech, Android TextToSpeech).
*
* 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 frameworks that drive them, e.g.,
* NVDA, JAWS, ZDSR, Narrator, UIA, VoiceOver, Android AccessibilityManager).
*
* 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.
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
32 changes: 31 additions & 1 deletion SRC/SRAL.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -336,14 +336,26 @@ 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, a
// TTS engine, or UIA. Screen readers are sticky once chosen; the lower
// priority engines (TTS / UIA) must keep yielding to a screen reader that
// appears, and must also react to changes in g_excludes. Deriving the TTS
// set from SRAL_GetTTSEngines() keeps this in sync as engines are added.
if (!g_currentEngine
|| !g_currentEngine->GetActive()
|| (g_currentEngine->GetNumber() & SRAL_GetTTSEngines())
|| g_currentEngine->GetNumber() == SRAL_ENGINE_UIA) {
#if defined(_WIN32) && !defined(SRAL_NO_UIA)
if (FindProcess(L"narrator.exe") == TRUE) {
g_currentEngine = get_engine(SRAL_ENGINE_UIA);
return;
}
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();
Expand Down Expand Up @@ -622,6 +634,24 @@ extern "C" SRAL_API int SRAL_GetActiveEngines(void) {
return mask;
}

extern "C" SRAL_API int SRAL_GetTTSEngines(void) {
return SRAL_ENGINE_SAPI
| SRAL_ENGINE_SPEECH_DISPATCHER
| SRAL_ENGINE_NS_SPEECH
| SRAL_ENGINE_AV_SPEECH
| SRAL_ENGINE_ANDROID_TEXT_TO_SPEECH;
}

extern "C" SRAL_API int SRAL_GetAssistiveTechEngines(void) {
return SRAL_ENGINE_NVDA
| SRAL_ENGINE_JAWS
| SRAL_ENGINE_ZDSR
| SRAL_ENGINE_NARRATOR
| SRAL_ENGINE_UIA
| SRAL_ENGINE_VOICE_OVER
| SRAL_ENGINE_ANDROID_ACCESSIBILITY_MANAGER;
}

Comment on lines +637 to +654
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't hardcode engines here. Instead, consider making engine feature or Sral::Engine interface method with engine category (either TTS or screen reader).


extern "C" SRAL_API const char* SRAL_GetEngineName(int engine) {
switch (static_cast<SRAL_Engines>(engine)) {
Expand Down
Loading